It's quite obvious where the deadlock here is.
z1
spawns two new threads; let's call those T1 and T2. It also creates an instance of Object called lock
, let's call this L1.
z2
does the same; let's call the threads T3 and T4 and the lock L2.
Now, let's assume*, T1 starts first.
- T1 calls the send method on the Z instance z2.
- This method causes T1 to acquire the lock L2 then print out "[Z] Send". The method then calls the receive method on the z1 instance.
- The receive method on the z1 instance causes T1 to acquire the lock L1, then print out "[Z] Receive".
- T1 releases the lock L1 then exits the receive method on z1.
- T1 releases the lock L2 then exits the send method on z2.
Now, let's say that in between steps 2 and 3, T3 is starts and does the following:
- T3 calls the send method on the Z instance z1.
- This method causes T3 to acquire the lock L1 then print out "[Z] Send". The method then calls the receive method on the z2 instance.
- The receive method on the z2 instance causes T3 to try to acquire the lock L2...
- L2 is already held by T1 and has not been released. So T3 waits.
Switch back to T1, step 3 now becomes this:
- The receive method on the z1 instance causes T1 to try to acquire the lock L1.
- L1 is already held by T3 and has not been released. So T1 waits.
Context switch to T3, L2 is still held by T1, T3 waits.
Context switch to T1, L1 is still held by T3, T1 waits.
...and so on.
This is the explanation of where the deadlock may occur. To solve it, you may want to move the call to z.receive() outside of the synchronized block in send, so that the lock in the current instance gets released before calling the received method of the other instance:
public void send()
{
synchronized(lock)
{
System.out.println("[Z] Send");
}
z.receive();
}
EDIT
If the lock is meant to protect all instances from executing concurrently, then you can use one lock shared across all threads, thus:
class Z extends Thread
{
static final Object lock=new Object();
...
}
Now we only have one lock instance, let's call it L0 and take another look at how the steps above would run:
- T1 calls the send method on the Z instance z2.
- This method causes T1 to acquire the lock L0 then print out "[Z] Send". The method then calls the receive method on the z1 instance.
- The receive method on the z1 instance causes T1 to again acquire the lock L0; because it already holds this lock, it can continue. It prints out "[Z] Receive".
- T1 exits the receive method on z1.
- T1 releases the lock L0 then exits the send method on z2.
Again, let's say that in between steps 2 and 3, T3 is starts and does the following:
- T3 calls the send method on the Z instance z1.
- This method causes T3 to try to acquire the lock L0.
- L0 is already held by T1 and has not been released. So T3 waits.
This time step 3 in T1 is unaffected. This means that it can still continue and eventually release the lock L0, which will eventually let T3 acquire that lock and itself continue.
No more deadlock.
__
*Thread start order is never guaranteed to be the same as the order in which the start()
methods are called, but in this case, there is a risk of deadlock in all possible scenarios.