1. Introduction
The Dining Philosophers problem is one of the classic problems used to describe synchronization issues in a multi-threaded environment and illustrate techniques for solving them. Dijkstra first formulated this problem and presented it regarding computers accessing tape drive peripherals.
The present formulation was given by Tony Hoare, who is also known for inventing the quicksort sorting algorithm. In this article, we analyze this well-known problem and code a popular solution.
2. The Problem
The diagram above represents the problem. There are five silent philosophers (P1 – P5) sitting around a circular table, spending their lives eating and thinking.
There are five forks for them to share (1 – 5) and to be able to eat, a philosopher needs to have forks in both his hands. After eating, he puts both of them down and then they can be picked by another philosopher who repeats the same cycle.
The goal is to come up with a scheme/protocol that helps the philosophers achieve their goal of eating and thinking without getting starved to death.
3. A Solution
An initial solution would be to make each of the philosophers follow the following protocol:
while(true) { // Initially, thinking about life, universe and everything think(); // Take a break from thinking, hungry now pick_up_left_fork(); pick_up_right_fork(); eat(); put_down_right_fork(); put_down_left_fork(); // Not hungry anymore. Back to thinking! }
As the above pseudo code describes, each philosopher is initially thinking. After a certain amount of time, the philosopher gets hungry and wishes to eat.
At this point, he reaches for the forks on his either side and once he’s got both of them, proceeds to eat. Once the eating is done, the philosopher then puts the forks down, so that they’re available for his neighbor.
4. Implementation
We model each of our philosophers as classes that implement the Runnable interface so that we can run them as separate threads. Each Philosopher has access to two forks on his left and right sides:
public class Philosopher implements Runnable { // The forks on either side of this Philosopher private Object leftFork; private Object rightFork; public Philosopher(Object leftFork, Object rightFork) { this.leftFork = leftFork; this.rightFork = rightFork; } @Override public void run() { // Yet to populate this method } }
We also have a method that instructs a Philosopher to perform an action – eat, think, or acquire forks in preparation for eating:
public class Philosopher implements Runnable { // Member variables, standard constructor private void doAction(String action) throws InterruptedException { System.out.println( Thread.currentThread().getName() + " " + action); Thread.sleep(((int) (Math.random() * 100))); } // Rest of the methods written earlier }
As shown in the code above, each action is simulated by suspending the invoking thread for a random amount of time, so that the execution order isn’t enforced by time alone.
Now, let’s implement the core logic of a Philosopher. To simulate acquiring a fork, we need to lock it so that no two
To simulate acquiring a fork, we need to lock it so that no two Philosopher threads acquire it at the same time. To achieve this, we use the
To achieve this, we use the synchronized keyword to acquire the internal monitor of the fork object and prevent other threads from doing the same. A guide to the synchronized keyword in Java can be found here. We proceed with implementing the run() method in the Philosopher class now:
public class Philosopher implements Runnable { // Member variables, methods defined earlier @Override public void run() { try { while (true) { // thinking doAction(System.nanoTime() + ": Thinking"); synchronized (leftFork) { doAction( System.nanoTime() + ": Picked up left fork"); synchronized (rightFork) { // eating doAction( System.nanoTime() + ": Picked up right fork - eating"); doAction( System.nanoTime() + ": Put down right fork"); } // Back to thinking doAction( System.nanoTime() + ": Put down left fork. Back to thinking"); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } }
This scheme exactly implements the one described earlier: a Philosopher thinks for a while and then decides to eat. After this, he acquires the forks to his left and right and starts eating. When done, he places the forks down. We also add timestamps to each action, which would help us understand the order in which events occur.
After this, he acquires the forks to his left and right and starts eating. When done, he places the forks down. We also add timestamps to each action, which would help us understand the order in which events occur.
To kick start the whole process, we write a client that creates 5 Philosophers as threads and starts all of them:
public class DiningPhilosophers { public static void main(String[] args) throws Exception { Philosopher[] philosophers = new Philosopher[5]; Object[] forks = new Object[philosophers.length]; for (int i = 0; i < forks.length; i++) { forks[i] = new Object(); } for (int i = 0; i < philosophers.length; i++) { Object leftFork = forks[i]; Object rightFork = forks[(i + 1) % forks.length]; philosophers[i] = new Philosopher(leftFork, rightFork); Thread t = new Thread(philosophers[i], "Philosopher " + (i + 1)); t.start(); } } }
We model each of the forks as generic Java objects and make as many of them as there are philosophers. We pass each Philosopher his left and right forks that he attempts to lock using the synchronized keyword. Running this code results in an output similar to the following.
Running this code results in an output similar to the following. Your output will most likely differ from the one given below, mostly because the sleep() method is invoked for a different interval:
Philosopher 1 8038014601251: Thinking Philosopher 2 8038014828862: Thinking Philosopher 3 8038015066722: Thinking Philosopher 4 8038015284511: Thinking Philosopher 5 8038015468564: Thinking Philosopher 1 8038016857288: Picked up left fork Philosopher 1 8038022332758: Picked up right fork - eating Philosopher 3 8038028886069: Picked up left fork Philosopher 4 8038063952219: Picked up left fork Philosopher 1 8038067505168: Put down right fork Philosopher 2 8038089505264: Picked up left fork Philosopher 1 8038089505264: Put down left fork. Back to thinking Philosopher 5 8038111040317: Picked up left fork
All the Philosophers initially start off thinking, and we see that Philosopher 1 proceeds to pick up the left and right fork, then eats and proceeds to place both of them down, after which `Philosopher 5` picks it up.
5. The Problem with the Solution: Deadlock
Though it seems that the above solution is correct, there’s an issue of a deadlock arising.
A deadlock is a situation where the progress of a system is halted as each process is waiting to acquire a resource held by some other process.
We can confirm the same by running the above code a few times and checking that some times, the code just hangs. Here’s a sample output that demonstrates the above issue:
Philosopher 1 8487540546530: Thinking Philosopher 2 8487542012975: Thinking Philosopher 3 8487543057508: Thinking Philosopher 4 8487543318428: Thinking Philosopher 5 8487544590144: Thinking Philosopher 3 8487589069046: Picked up left fork Philosopher 1 8487596641267: Picked up left fork Philosopher 5 8487597646086: Picked up left fork Philosopher 4 8487617680958: Picked up left fork Philosopher 2 8487631148853: Picked up left fork
In this situation, each of the Philosophers has acquired his left fork, but can’t acquire his right fork, because his neighbor has already acquired it. This situation is commonly known as the circular wait and is one of the conditions that results in a deadlock and prevents the progress of the system.
6. Resolving the Deadlock
As we saw above, the primary reason for a deadlock is the circular wait condition where each process waits upon a resource that’s being held by some other process. Hence, to avoid a deadlock situation we need to make sure that the circular wait condition is broken. There are several ways to achieve this, the simplest one being the follows:
All Philosophers reach for their left fork first, except one who first reaches for his right fork.
We implement this in our existing code by making a relatively minor change in code:
public class DiningPhilosophers { public static void main(String[] args) throws Exception { final Philosopher[] philosophers = new Philosopher[5]; Object[] forks = new Object[philosophers.length]; for (int i = 0; i < forks.length; i++) { forks[i] = new Object(); } for (int i = 0; i < philosophers.length; i++) { Object leftFork = forks[i]; Object rightFork = forks[(i + 1) % forks.length]; if (i == philosophers.length - 1) { // The last philosopher picks up the right fork first philosophers[i] = new Philosopher(rightFork, leftFork); } else { philosophers[i] = new Philosopher(leftFork, rightFork); } Thread t = new Thread(philosophers[i], "Philosopher " + (i + 1)); t.start(); } } }
The change comes in lines 17-19 of the above code, where we introduce the condition that makes the last philosopher reach for his right fork first, instead of the left. This breaks the circular wait condition and we can avert the deadlock.
Following output shows one of the cases where all the Philosophers get their chance to think and eat, without causing a deadlock:
Philosopher 1 88519839556188: Thinking Philosopher 2 88519840186495: Thinking Philosopher 3 88519840647695: Thinking Philosopher 4 88519840870182: Thinking Philosopher 5 88519840956443: Thinking Philosopher 3 88519864404195: Picked up left fork Philosopher 5 88519871990082: Picked up left fork Philosopher 4 88519874059504: Picked up left fork Philosopher 5 88519876989405: Picked up right fork - eating Philosopher 2 88519935045524: Picked up left fork Philosopher 5 88519951109805: Put down right fork Philosopher 4 88519997119634: Picked up right fork - eating Philosopher 5 88519997113229: Put down left fork. Back to thinking Philosopher 5 88520011135846: Thinking Philosopher 1 88520011129013: Picked up left fork Philosopher 4 88520028194269: Put down right fork Philosopher 4 88520057160194: Put down left fork. Back to thinking Philosopher 3 88520067162257: Picked up right fork - eating Philosopher 4 88520067158414: Thinking Philosopher 3 88520160247801: Put down right fork Philosopher 4 88520249049308: Picked up left fork Philosopher 3 88520249119769: Put down left fork. Back to thinking
It can be verified by running the code several times, that the system is free from the deadlock situation that occurred before.
7. Conclusion
In this article, we explored the famous Dining Philosophers problem and the concepts of circular wait and deadlock. We coded a simple solution that caused a deadlock and made a simple change to break the circular wait and avoid a deadlock. This is just a start, and more sophisticated solutions do exist.
The code for this article can be found over on GitHub.