2020-04-05

3: Java Threads Synchronization with 'wait'/'notify'/'notifyAll'

<The previous article in this series | The table of contents of this series | The next article in this series>

These are some points about threads synchronization with 'wait', 'notify', and 'notifyAll' in Java, which may spare one some slips.

Topics


About: Java

The table of contents of this article


Starting Context


  • The reader has a basic knowledge on the Java programming language.

Target Context


  • The reader will know some points to note about threads synchronization with 'wait', 'notify', and 'notifyAll' in Java.

Orientation


Hypothesizer 7
I tend to slip on threads synchronization with 'wait', 'notify', and 'notifyAll' in Java.

Is that my problem you are not concerned with? . . . Congratulations! . . . However, I guess that if so, you will not be reading this.

I assert that threads synchronization without 'wait' is not any problem: it is just that only a single thread can enter the guarded blocks.

However, when 'wait' is involved, the situation becomes more complicated, on which I tend to slip.

Basically, it is just a matter of carelessness, but vowing to be more careful next time is not any effective strategy, according to my experiences.

So, I have decided that I should record some points to note as a reference to turn to for threads synchronization.


Main Body


1: The Relation Between Lock and Monitor


Hypothesizer 7
First, let me clarify the relation between lock and monitor.

As a note, I talk only about Java lock and Java monitor. As more general concepts, lock and monitor may not be as are described in this article.

I clarify it not because I enjoy pointing out wrong usages of those terms by some people, but because it helps in clarifying the behaviors of threads synchronization.

Lock is a binary semaphore, which can be owned by only a single thread.

So, why is monitor required? . . . It is because just a bare lock is not enough for synchronizing threads.

For one thing, there has to be a list in which the threads anticipating to acquire the lock have to stay (note that I have not used the term, 'waiting'). I will call the list 'anticipating threads list' (is not any widely-accepted term).

For another thing, there has to be another list in which the threads waiting to acquire the lock have to stay (I use the term, 'wait', in a specific meaning, not in the general meaning). I will call the list 'waiting threads list' (is not any widely-accepted term).

'wait' here means to have called the 'wait' method without the waiting time (if specified) expired. I mean, any waiting thread is not really anticipating to acquire the lock, but anticipating to be notified to move to the anticipating threads list.

Here, I have assumed that any waiting thread first moves to the anticipating threads list and then acquires the lock. In fact, implementation-wise, one waiting thread may directly acquire the lock without taking the trouble of moving to the anticipating threads list, but at least the others of all the waiting threads notified by 'notifyAll' have to move to the anticipating threads list (because only one of all the waiting threads can directly acquire the lock), and concept-wise, it will be good to think that all the waiting threads first move to the anticipating threads list, even if one of them really skip the part.

So, it is important to know that there are 3 states for threads: owning the lock (only one thread can be owning the lock at the same time), anticipating to acquire the lock, and waiting to become able to anticipate to acquire the lock.

To return to what monitor is, monitor is a structure that manages the concerned threads in the 3 states.

In fact, I am not sure whether I should say "I use a monitor, which uses a lock" or should say "I use a monitor and a lock", but the difference will be a matter of the Java standard libraries implementation that has no consequence for users either way.


2: The Relation Between a Monitor and an Object


Hypothesizer 7
A monitor (with a lock) is built-in in the 'Object' class.

However, what are guarded in the guarded blocks are not necessarily the state of the object of the monitor.

Enigmatic? Let me look at an example.

@Java Source Code
public class ClassA {
	private ClassB i_classB = new ClassB ();
	private int i_integer = 0;
	
	public void synchronizedNoWaitingNoNotifyingMethod (int a_threadIdentification, int a_sleepTime) throws InterruptedException {
		System.out.println (String.format ("### the thread -> '%d' before the guarded block               : 'i_integer' -> '%d'", a_threadIdentification, i_integer));
		System.out.flush ();
		synchronized (i_classB) {
			i_integer ++;
			System.out.println (String.format ("### the thread -> '%d' in     the guarded block               : 'i_integer' -> '%d'", a_threadIdentification, i_integer));
			System.out.flush ();
			Thread.sleep (a_sleepTime);
		}
		System.out.println (String.format ("### the thread -> '%d' after  the guarded block               : 'i_integer' -> '%d'", a_threadIdentification, i_integer));
		System.out.flush ();
	}
	
	public void synchronizedWaitingMethod (int a_threadIdentification, int a_sleepTime) throws InterruptedException {
		System.out.println (String.format ("### the thread -> '%d' before the guarded block               : 'i_integer' -> '%d'", a_threadIdentification, i_integer));
		System.out.flush ();
		synchronized (i_classB) {
			i_integer ++;
			System.out.println (String.format ("### the thread -> '%d' in     the guarded block before waiting: 'i_integer' -> '%d'", a_threadIdentification, i_integer));
			System.out.flush ();
			i_classB.wait ();
			System.out.println (String.format ("### the thread -> '%d' in     the guarded block after  waiting: 'i_integer' -> '%d'", a_threadIdentification, i_integer));
			System.out.flush ();
			Thread.sleep (a_sleepTime);
		}
		System.out.println (String.format ("### the thread -> '%d' after  the guarded block               : 'i_integer' -> '%d'", a_threadIdentification, i_integer));
		System.out.flush ();
	}
	
	public void synchronizedNotifyingMethod (int a_threadIdentification, int a_sleepTime) throws InterruptedException {
		System.out.println (String.format ("### the thread -> '%d' before the guarded block               : 'i_integer' -> '%d'", a_threadIdentification, i_integer));
		System.out.flush ();
		synchronized (i_classB) {
			i_integer ++;
			System.out.println (String.format ("### the thread -> '%d' in     the guarded block               : 'i_integer' -> '%d'", a_threadIdentification, i_integer));
			System.out.flush ();
			i_classB.notifyAll ();
			Thread.sleep (a_sleepTime);
		}
		System.out.println (String.format ("### the thread -> '%d' after  the guarded block               : 'i_integer' -> '%d'", a_threadIdentification, i_integer));
		System.out.flush ();
	}
}

public class ClassB {
}

public class Test1Test {
	private Test1Test () {
	}
	
	public static void main (String [] a_arguments) throws Exception {
		Test1Test.test ();
	}
	
	public static void test () throws Exception {
		ClassA l_classA = new ClassA ();
		Thread l_subThread0 = new Thread (() -> {
			try {
				l_classA.synchronizedNoWaitingNoNotifyingMethod (0, 10000);
			}
			catch (Exception l_exception) {
				l_exception.printStackTrace ();
			}
		});
		Thread l_subThread1 = new Thread (() -> {
			try {
				l_classA.synchronizedNoWaitingNoNotifyingMethod (1, 0);
			}
			catch (Exception l_exception) {
				l_exception.printStackTrace ();
			}
		});
		Thread l_subThread2 = new Thread (() -> {
			try {
				l_classA.synchronizedWaitingMethod (2, 0);
			}
			catch (Exception l_exception) {
				l_exception.printStackTrace ();
			}
		});
		Thread l_subThread3 = new Thread (() -> {
			try {
				l_classA.synchronizedWaitingMethod (3, 0);
			}
			catch (Exception l_exception) {
				l_exception.printStackTrace ();
			}
		});
		Thread l_subThread4 = new Thread (() -> {
			try {
				l_classA.synchronizedNotifyingMethod (4, 0);
			}
			catch (Exception l_exception) {
				l_exception.printStackTrace ();
			}
		});
		Thread l_subThread5 = new Thread (() -> {
			try {
				l_classA.synchronizedNoWaitingNoNotifyingMethod (5, 0);
			}
			catch (Exception l_exception) {
				l_exception.printStackTrace ();
			}
		});
		l_subThread0.start ();
		Thread.sleep (1000);
		l_subThread5.start ();
		Thread.sleep (1000);
		l_subThread4.start ();
		Thread.sleep (1000);
		l_subThread3.start ();
		Thread.sleep (1000);
		l_subThread2.start ();
		Thread.sleep (1000);
		l_subThread1.start ();
		l_subThread0.join ();
		l_subThread1.join ();
		l_subThread2.join ();
		l_subThread3.join ();
		l_subThread4.join ();
		l_subThread5.join ();
	}
}

Certainly, that is unnatural: why do not I use 'this' instead of 'i_classB'? But anyway, what is guarded in the guarded blocks is the state of a 'ClassA' instance, not the state of 'i_classB', whose monitor is used: 'i_classB' is used solely for monitoring purpose.

And I have to use 'i_classB.wait ()', etc., not just 'wait ()', which means 'this.wait ()', in the above example.


3: The Number of Threads Inside the Guarded Blocks


Hypothesizer 7
I have to get rid of the notion, "only one thread can enter the guarded blocks at the same time".

Instead, I have to adopt this: "only one thread can be running in the guarded blocks at the same time".

In fact, if a thread enters a guarded block and waits inside it, another thread can enter any guarded block and also may wait inside it. So, any number of threads can be inside the guarded blocks.

Howeve, I can be sure that only one thread is really running inside the guarded blocks.


4: The State Change After the Waiting


Hypothesizer 7
This is obvious in careful thought, but seems a usual stumbling block for me: the state recognition before going into waiting is no longer valid after the waiting.

After the thread had gone into waiting, some other threads may have entered the guarded blocks and changed the status (in fact, usually, that is the purpose of waiting).

So, the resuming thread has to assume that it is in a new world order now, and look around the world in the unprejudiced eyes.


5: What 'notify' or 'notifyAll' does


Hypothesizer 7
I cannot assume that 'notify' or 'notifyAll' makes a notified thread begin to run.

First, 'notify' or 'notifyAll' does not release the lock, so, until the notifying thread leaves the guarded blocks, there is no possibility that any notified thread begins to run.

Second, even after the notifying thread leaves the guarded blocks, the notifying thread may just keep running in a single core single CPU system, because why not? Or a non-notified anticipating thread, instead of a notified thread, may acquire the lock.

I understand that notifying means just moving a waiting thread or all the waiting threads into the anticipating threads list; which thread is given the lock among the anticipating threads including the non-notified ones is another story.

In fact, this is a result of the above code in a single core single CPU Linux computer, which, of course, may be different in another environment or timing.

@Output
### the thread -> '0' before the guarded block               : 'i_integer' -> 0.
### the thread -> '0' in     the guarded block               : 'i_integer' -> 1.
### the thread -> '5' before the guarded block               : 'i_integer' -> 1.
### the thread -> '4' before the guarded block               : 'i_integer' -> 1.
### the thread -> '3' before the guarded block               : 'i_integer' -> 1.
### the thread -> '2' before the guarded block               : 'i_integer' -> 1.
### the thread -> '1' before the guarded block               : 'i_integer' -> 1.
### the thread -> '0' after  the guarded block               : 'i_integer' -> 1.
### the thread -> '1' in     the guarded block               : 'i_integer' -> 2.
### the thread -> '2' in     the guarded block before waiting: 'i_integer' -> 3.
### the thread -> '3' in     the guarded block before waiting: 'i_integer' -> 4.
### the thread -> '4' in     the guarded block               : 'i_integer' -> 5.
### the thread -> '1' after  the guarded block               : 'i_integer' -> 2.
### the thread -> '4' after  the guarded block               : 'i_integer' -> 5.
### the thread -> '5' in     the guarded block               : 'i_integer' -> 6.
### the thread -> '5' after  the guarded block               : 'i_integer' -> 6.
### the thread -> '3' in     the guarded block after  waiting: 'i_integer' -> 6.
### the thread -> '2' in     the guarded block after  waiting: 'i_integer' -> 6.
### the thread -> '3' after  the guarded block               : 'i_integer' -> 6.
### the thread -> '2' after  the guarded block               : 'i_integer' -> 6.

That result means that 1) the thread, '0', entered the guarded blocks and stayed there for a long time; 2) meanwhile, the threads, '5', '4', '3', '2', '1', entered the anticipating threads list, in that order; 3) the thread, '0', left the guarded blocks; 4) the thread, '1', entered and left the guarded blocks (although the "after the guarded block" message for the thread, '1', comes later, the thread, '1', should have left the guarded blocks immediately, which is proven by the value of 'i_integer'); 5) the thread, '2', entered the guarded blocks and began to wait; 6) the thread, '3', entered the guarded blocks and began to wait; 7) the thread, '4', entered and left the guarded blocks, while notifying the threads, '2' and '3'; 8) the thread, '5', entered and left the guarded blocks; 9) the thread, '3', left the guarded blocks; 10) the thread, '2', left the guarded blocks.

Well, the relation between the messaging order and the displayed messages order is uncertain (as "the thread -> '1' after the guarded block" comes later), it is certain that the thread, '5', resumed to run earlier than the notified threads , '2' and '3', did, because the notified threads saw the state changed by the thread, '5'.

As an aside, unexpectedly, threads seem to tend to be given the lock in the LIFO order than in the FIFO order, in my environment.


6: The Conclusion and Beyond


Hypothesizer 7
Now, I have some points to turn to when I code threads synchronization.

As some bugs related with threads synchronization tend to cause time-consuming debugging, writing the code right from the begining will be very beneficial.

I will record such points to note also for each of some other Java features in some future articles.


References


<The previous article in this series | The table of contents of this series | The next article in this series>