Java Thread Programming (Part 4)

Posted on by

Categories:         

This article was first published in Foojay.io

In the previous article, we discussed the visibility problem while working with multiple threads.

We will discuss another similar situation in this article. However, we will use symbols and pseudocode to explain this.

Symbols

Let’s define the symbols first.

S.X - here X is a field of an object where S is the reference of that Object, e.g., S1.X.

In pseudocode, we will use thread number and line number as well. For example, 1.1, here, 1 is the thread id, and the number after the dot is the line number.

1.2 - thread 1, line number 2.

The local variables will have defaults unless they are initialized with a value. For example, for Boolean, the default value is false; for integer, the default value is 0.

Pseudocode

If we turn the program that we discussed in the last article into symbols and pseudocode, we have the following:

Thread 1 Thread 2
1.1 WHILE (!S1){} 2.1 S1= TRUE
1.2 PRINT “Foojay.io” 2.2 PRINT “I love ”

Since we already discussed the above problem in our previous article, the execution order would be:

Execution order 1 # 1.1, 2.1, 1.2, 2.2
Execution order 2 # 1.1, 2.1, 2.2, 1.2
Execution order 3 # 1.1, 2.1, 2.2

Now that we are familiar with the symbol and pseudocode let’s see another problem:

Thread 1 Thread 2
1.1 L1 = S1 2.1 L2 = S2
1.2 S2 = 2 2.2 S1 = 1
1.3 PRINT “Thread1: ” + L1 2.3 PRINT “Thread2: ”+ L2

What can be the possible output of this program?

The possible execution order could be:

Execution order 1 # 1.1, 1.2. 1.3, 2.1, 2.2, 2.3
Execution order 2 # 2.1, 2.2, 2.3, 1.1, 1.2. 1.3

If the first execution order succeeds, then the output would be:

Thread1: 0
Thread2: 2

And if the second execution order succeeds, then the output would be:

Thread1: 0
Thread2: 1

However, apart from the above two, there is another possible execution order:

Execution order 3:  1.1, 2.1, 1.2, 2.2, 1.3, 2.3

If the above execution order succeeds, then the output would be:

Thread1: 0
Thread2: 0

The above output doesn’t depend on the last two executions, 1.3. or 2.3.

So the output will remain the same if though 2.3 executes first.

Execution Order Optimization

So far, we have three execution orders, and it seems only the three outputs mentioned above are possible.

However, in reality, we can have the following output as well:

Thread1: 1
Thread2: 1

…or…

Thread1: 2
Thread2: 2

These outputs may not seem logical, however, they are possible. And the execution order could be:

Execution order 4: 2.1, 1.1, 1.2, 2.1, 1.3, 2.3

…or…

Execution order 5: 1.2, 2.1,2.2, 1.1, 1.3, 2.3

Now the question is how this is even possible?

The answer is that we write our code in a particular order; however, when executing it, it doesn’t mean the Java compiler and virtual machine maintain that order. The Java compiler may change the execution order to optimize it if it can determine that the output won’t change in single-threaded code. For example, just look at the order of the code in the first thread. If we interchange the execution order, 1.1 and 1.2, the output won’t change in that thread.

These sorts of changes happen for various reasons. For example, the intelligent algorithm of the Java compiler may find a way to optimize particular code to run faster. The bottom line is, the program order and execution order may vary. It doesn’t always match. And the execution order also depends on the computer, hardware architecture, etc. So this is a possible source of having a programming bug. This sort of bug may not be easily detectable in a development environment but can very likely appear in a production environment. However, when you go to find it, it may disappear. This sort of bug has a unique name; they are called Heisenbugs.

Let’s look at another example:

Thread 1 Thread 2
1.1 L1 = S1 2.1 L6 = S1
1.2 L2 = L1.X 2.2 L6.X = 3
1.3 L3 = S2 2.3 PRINT “Thread2: ” + L6.X
1.4 L4 = L3.X
1.5 L5 = L1.X
1.6 PRINT “Thread1: ” + L2, L4, L5

In this program, we have used an object which has a field X.

Here, S1 and S2 are the references of the same Object.

If the second thread runs first, what would be the output of thread 2?

Thread2: 3

The reason is, in 2.3 we have set L6.X = 3. However, if execution order is different than the program order, the output would be different. That’s why here the Java compiler won’t change it.

Now let’s look at the first thread. What would be the output?

Thread1: 000

In this case, 1.2, 1.4, and 1.5 must have run before 2.2. if 2.2 executes first, then the output would be:

Thread1: 333

If 1.2 execute before 2.2 and then 1.4 and 1.5 execute, the output would be:

Thread1: 033

If 1.2 and 1.4 executes before 2.2 and then 1.5 executes, the output would be:

Thread1: 003

Now, look at the following output:

Thread1: 030

Do you think the above output is possible? The reason is if 1.2 executes first and then 2.2 executes, and then it doesn’t matter whatever the execution order for the rest of the exception, the output should be:

Thread1: 033

And we know S1 and S2 refer to the same Object.

This is only possible if the compiler changes the program order while compiling.

Note that, line 1.2 and 1.5 assign the same value. And L2 and L5 are just used to print the value.

To optimize the above code, the compiler can remove the L5 altogether. Instead of L5, it can useL2. In a single-threaded environment, this change won’t reflect the output. In our code, although we have used L2, L4, and L5 to print the value, the compiler can print L2, L4, and L2 again.

In such a case, if 1.2 executes first and then 2.2, since the compiler removed L5, instead of in the print statement, it will print the value of L2 in place of L5, which was assigned in line 1.2.

The above example can be found in the Java language specification.

From the above discussion, we have understood that the execution order can be different to the program order. The execution order depends on the compiler’s optimization technique; it can further rely on the Java virtual machine and the CPU itself. Thus the output of a program becomes uncertain. In a multithread environment, we call this a data race.

Benefits and Drawbacks of Volatility

Now the question is, what can be the solution to this problem. Well, the solution is relatively straightforward: we simply use the keyword “volatile”.

This keyword can only be used in the field of an Object, not in a local variable. This is because we don’t share local variables. Also, if a field is final, we don’t need to use volatile in it. This is because the final fields never get to change, and thus, they don’t create any problems either.

We have to keep in mind that if a reference to an Object is used as a field and we then make it volatile, that doesn’t mean the content of the Object is also volatile. The reference is the only thing that is volatile in this case.

The benefits of using the volatile keyword are:

Although the volatile keyword may be a solution, using it too much may cause problems. Since it prevents the CPU from caching data, that certainly reduces the performance of a program a bit. Besides, it prevents further optimization.

Therefore, we need to be very careful when using the “volatile” keyword, and we should use it only where it’s required, and definitely not everywhere.

That’s all for today!

     

Share on:

Author: A N M Bazlur Rahman

Java enthusiastic | Book author | Mentor | 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