Java Thread Programming: Lesson 3

Posted on by

Categories:         

Photo Photo by Wonderlane on Unsplash

Previous articles :

  1. Java Thread Programming: Lesson 1
  2. Java Thread Programming: Lesson 2

When we write a program, we test its correctness. If the program works according to our expectations, it builds confidence. If the same program runs in a multithread environment and produces the correct result, we call that piece of code thread-safe. Thread safety is critical; otherwise, we will end up having the wrong output, which is not desirable. In this article, we will discuss another problem towards ensuring thread safety.

In the previous article, we have discussed race condition. However, that is not the only problem we deal with when we work with java threads. Let us see an example-

package com.bazlur;

public class Day021 {
	static boolean running = true;

	public static void main(String[] args) {
		var t1 = new Thread(() -> {
			while (running) {
			}
			System.out.println("from the other side");
		});

		var t2 = new Thread(() -> {
			running = false;
			System.out.println("Hello");
		});
		t1.start();
		t2.start();
	}
}

Look at the above code, it used two threads, and they share data, which is a Boolean, running.

Note that the first thread has a while loop, and this loop will keep running until the variable running is true. After the loop, we have a print statement that prints a text. In the second thread, the first statement changes the running variable from true to false, and then it prints another text.

If you look at the program, at first sight, we would think it will print the following –

Hello
from the other side.

Nevertheless, in reality, it will not be the only case. It will produce a different result in different machines, even in the same machine, if we run repeatedly. Let me explain why-

In a single-threaded environment, the execution order of the code remains the same as the way it is written. However, it does not remain the same in a multithreaded environment. It depends on the thread scheduler, the processor and the interaction between two threads.

There are three possible outputs of this program-

  1. In the first thread, the while loop will keep running. Until then, the following line will not be executed. However, when the second thread starts, the first thing it does is, change the value of the running variable. It breaks the while loop of the first thread, thus, it prints the “Hello.” Meanwhile, the second thread prints the “from the other side.”
Hello
from the other side
  1. The loop inside the first thread will keep running, and the second thread will change the value and print the text, “from the other side,” and then the loop of the first thread will break and print the “Hello”.
from the other side
Hello      
  1. Most of us can imagine this far, but there is another possibility that is also trickiest. The first thread will keep running the loop forever, and although the second thread will change the value of running, making it false and print the text, the first thread will not see the changes. In such a case, the program’s output will be only- “from the other side.”

Now in which case it could happen that the first thread will not read the changes? Usually, we have multiple processors in our modern computer. Each processor has some caches along with them. They are called L1, L2, and L2 cache. The processor uses these caches to execute the program faster without going to the main memory each time. It might well happen that the two threads in the above program can run in two different processors, and each of them can cache the variable, running inside their L caches. If the second thread changes the value, the first thread will not see it because it keeps reading it from the caches. This is called a visibility problem. Although the value was updated, it was not visible to the other thread. This often called data race.

L Caches

Now the question is, how do we fix it?

If we could prevent the thread from not reading from the caches, the fix is to read them constantly from the main memory. There is a keyword for that in java, which is volatile.

The above code will fix if we add this keyword before running.

package com.bazlur;

public class Day021 {
	static volatile boolean running = true;

	public static void main(String[] args) {
		var t1 = new Thread(() -> {
			while (running) {
			}
			System.out.println("from the other side");
		});

		var t2 = new Thread(() -> {
			running = false;
			System.out.println("Hello");
		});
		t1.start();
		t2.start();
	}
}

The above code will always produce the correct result.

Reading from and writing to cache is always cheap compare to the main memory. Thus, we should only use a volatile variable when we need to enforce the visibility of variables.

That is for today! Cheers.

             

Share on:

Author: A N M Bazlur Rahman

Java Champion | Software Engineer | JUG Leader | Book Author | InfoQ & Foojay.IO Editor | Jakarta EE Ambassadors| Helping Java Developers to improve their coding & collaboration skills so that they can meet great people & collaborate

100daysofcode 100daysofjava access advance-java agile algorithm arraylist article bangla-book becoming-expert biginteger book calculator checked checked-exceptions cloning code-readability code-review coding coding-convention collection-framework compact-strings completablefuture concatenation concurrency concurrentmodificationexception concurrentskiplistmap counting countingcollections critical-section daemon-thread data-race data-structure datetime day002 deliberate-practice deserialization design-pattern developers duration execute-around executors export fibonacci file file-copy fork/join-common-pool functional future-java-developers groupby hash-function hashmap history history-of-java how-java-performs-better how-java-works http-client image import inspiration io itext-pdf java java-10 java-11 java-17 java-8 java-9 java-developers java-performance java-programming java-thread java-thread-programming java11 java16 java8 lambda-expression learning learning-and-development linkedlist list local-type-inference localdatetime map methodology microservices nio non-blockingio null-pointer-exception object-cloning optional packaging parallel pass-by-reference pass-by-value pdf performance prime-number programming project-loom race-condition readable-code record refactoring review scheduler scrum serialization serversocket simple-calculator socket software-development softwarearchitecture softwareengineering sorting source-code stack string string-pool stringbuilder swing thread threads tutorial unchecked vector virtual-thread volatile why-java zoneid