When studying multithreading in Java, I encountered a phenomenon that I did not understand when rearranging two methods in places within a single if block.

Below is an example in which 10 threads are launched (link to sample code in GitHub ). Their task is to do some work in ascending order of Id, in this case, display a message containing the stream Id and the value of the variable in the auxiliary class.

The example contains three classes:

Main class.
It creates 10 instances of the class ThreadExample , which, in turn, are array elements. Id first instance of the thread, before its start, is entered into the variable of the auxiliary class. After that, the start method of all threads is called in a loop.

 package lan.example.thread_example; public class Main { static HelperSingletonBillPugh INSTANCE_TEST_THREAD = HelperSingletonBillPugh.getInstance(); static ThreadExample[] myThreads = new ThreadExample[10]; static final int COUNT = 10; public static void main(String[] args) { for (int i = 0; i < COUNT; i++) { myThreads[i] = new ThreadExample(); if (i == 0) { INSTANCE_TEST_THREAD.setCurentThreadId(myThreads[i].getId()); } myThreads[i].start(); } } } 

Class HelperSingletonBillPugh.
This is a helper helper class, implemented as a Bill Pugh singleton. It seems that this type of singleton implementation is considered thread-safe. The class contains the curentThreadId variable of the long type with the thread Id and the public methods setCurentThreadId(long threadId) , getCurentThreadId() , incremenCurentThreadId() for changing, reading and incrementing the value of this variable.

 package lan.example.thread_example; public class HelperSingletonBillPugh { private HelperSingletonBillPugh() { } private static class SingletonHandler { private static final HelperSingletonBillPugh INSTANCE = new HelperSingletonBillPugh(); } public static HelperSingletonBillPugh getInstance() { return SingletonHandler.INSTANCE; } private long curentThreadId = 0L; // Id ΠΏΠΎΡ‚ΠΎΠΊΠ°, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π΄ΠΎΠ»ΠΆΠ΅Π½ Π²Ρ‹ΠΏΠΎΠ»Π½ΠΈΡ‚ΡŒ Π²Ρ‹Π²ΠΎΠ΄ сообщСния public long getCurentThreadId() { return this.curentThreadId; } public void setCurentThreadId(long threadId) { this.curentThreadId = threadId; } public void incremenCurentThreadId() { this.curentThreadId++; } } 

Class ThreadExample.
Class heir Thread . It has an eternal loop, with pauses and constant comparison of the getId() value of the stream with the variable curentThreadId instance of the auxiliary class. If the equality is curentThreadId , a message is displayed, the variable curentThreadId incremented by 1, and the thread exits.

 package lan.example.thread_example; public final class ThreadExample extends Thread { static HelperSingletonBillPugh INSTANCE_TEST_THREAD = HelperSingletonBillPugh.getInstance(); @Override public void run() { while (true) { if (INSTANCE_TEST_THREAD.getCurentThreadId() == this.getId()) { // РаскоммСнтируй ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΡƒΡŽ строку //INSTANCE_TEST_THREAD.incremenCurentThreadId(); System.out.println("Print " + this.getName() + " ### ID:" + this.getId() + " ### getCurentThreadId: " + INSTANCE_TEST_THREAD.getCurentThreadId()); // Π—Π°ΠΊΠΎΠΌΠ΅Π½Ρ‚ΠΈΡ€ΡƒΠΉ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΡƒΡŽ строку INSTANCE_TEST_THREAD.incremenCurentThreadId(); break; } } } } 

In this example, the code of the message is displayed sequentially and the threads complete their work, everything is stable from launch to launch, this is the conclusion:

 Print Thread-1 ### ID:11 ### getCurentThreadId: 11 Print Thread-2 ### ID:12 ### getCurentThreadId: 12 Print Thread-3 ### ID:13 ### getCurentThreadId: 13 Print Thread-4 ### ID:14 ### getCurentThreadId: 14 Print Thread-5 ### ID:15 ### getCurentThreadId: 15 Print Thread-6 ### ID:16 ### getCurentThreadId: 16 Print Thread-7 ### ID:17 ### getCurentThreadId: 17 Print Thread-8 ### ID:18 ### getCurentThreadId: 18 Print Thread-9 ### ID:19 ### getCurentThreadId: 19 Print Thread-10 ### ID:20 ### getCurentThreadId: 20 

But it is only necessary to rearrange the INSTANCE_TEST_THREAD.incremenCurentThreadId() method and the System.out.println() method (in the code it is marked where to remove and add comments) and the result becomes unstable and for me is not clear, although the rearrangement is performed inside the if block. To be more precise, it is not always stable, that is, several launches can go quite correctly. Here is the output of one of the launches after rearranging methods:

 Print Thread-1 ### ID:11 ### getCurentThreadId: 12 Print Thread-2 ### ID:12 ### getCurentThreadId: 15 Print Thread-6 ### ID:16 ### getCurentThreadId: 17 Print Thread-5 ### ID:15 ### getCurentThreadId: 16 Print Thread-4 ### ID:14 ### getCurentThreadId: 15 Print Thread-7 ### ID:17 ### getCurentThreadId: 21 Print Thread-3 ### ID:13 ### getCurentThreadId: 21 Print Thread-8 ### ID:18 ### getCurentThreadId: 21 Print Thread-9 ### ID:19 ### getCurentThreadId: 21 Print Thread-10 ### ID:20 ### getCurentThreadId: 21 

From the example it can be seen that the threads display messages randomly and the value of the variable getCurentThreadId sometimes differs from the current thread Id more than 1. What happens when the two methods change? The fact is that when System.out.println() is in front of INSTANCE_TEST_THREAD.incremenCurentThreadId() , then I do not have a single unpredictable result. Can this be considered a stable job, or is it still deceptive and under certain circumstances the example will not work correctly?

P / S. Just in case, I will clarify. I have a little idea about the Java Memory Model, the volatile operator, the synchronize block, data caches and atomicity, although there is no deep knowledge yet, but I can achieve guaranteed guaranteed work of the example. It is precisely the understanding of what such serious changes occur when rearranging the places of these two methods, so affecting the result of the example. And, as a result, can the example work in the first case be considered stable, and why? Checked the example on different operating systems (Linux Mint 32 and 64 bit, Windows 10 64 bit), but only with Oracle JDK.

    2 answers 2

    The fact is that the increment operation is not atomic, but consists of two operations: reading the current value and writing the increased value. If you expand it, the loop in the first script will look like this:

     while (...) { <Ρ‡Ρ‚Π΅Π½ΠΈΠ΅ id> // Π²Ρ‹Π²ΠΎΠ΄ Π½Π° консоль <Ρ‡Ρ‚Π΅Π½ΠΈΠ΅ id> // ΠΈΠ½ΠΊΡ€Π΅ΠΌΠ΅Π½Ρ‚, шаг 1 <запись id> // ΠΈΠ½ΠΊΡ€Π΅ΠΌΠ΅Π½Ρ‚, шаг 2 } 

    Such an arrangement of operations, coupled with the tricky condition while , essentially serves as a lock that gives access to the body of the loop to only one thread. At first, only the first thread goes inside the loop, and the rest just scroll through it ("blocked"). When the first thread writes an enlarged id and leaves the loop, it already begins to scroll through the loop ("blocked"), and the second thread goes inside. And so in turn for all threads. Due to the increment, all threads get this access sequentially, according to their numbers. The important thing here is that the id record essentially releases the "lok" of the current thread and "allows" the execution of another thread. As long as it comes last in the body of the cycle, there are no problems.

    In the second scenario, the loop looks like this:

     while (...) { <Ρ‡Ρ‚Π΅Π½ΠΈΠ΅ id> // ΠΈΠ½ΠΊΡ€Π΅ΠΌΠ΅Π½Ρ‚, шаг 1 <запись id> // ΠΈΠ½ΠΊΡ€Π΅ΠΌΠ΅Π½Ρ‚, шаг 2 <Ρ‡Ρ‚Π΅Π½ΠΈΠ΅ id> // Π²Ρ‹Π²ΠΎΠ΄ Π½Π° консоль } 

    Now we see that the β€œlok” is released a little earlier. What can this lead to? To the fact that between the record id and the second reading id other threads can start to run , since the lok is free and another thread can get access to the loop body. Those. a classic race arises, which may result in the following:

    • the thread on the second reading id can get an already updated value (this explains, for example, why Thread-2 prints 15, not 13)
    • output to the console may be confused due to the fact that other threads "meet" immediately after recording id

       while (...) { <Ρ‡Ρ‚Π΅Π½ΠΈΠ΅ id> // ΠΈΠ½ΠΊΡ€Π΅ΠΌΠ΅Π½Ρ‚, шаг 1 <запись id> // ΠΈΠ½ΠΊΡ€Π΅ΠΌΠ΅Π½Ρ‚, шаг 2 <Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΠ΅ исполнСниС Π΄Ρ€ΡƒΠ³ΠΈΡ… ΠΏΠΎΡ‚ΠΎΠΊΠΎΠ²> <Ρ‡Ρ‚Π΅Π½ΠΈΠ΅ id> // Π²Ρ‹Π²ΠΎΠ΄ Π½Π° консоль (ΠΏΠΎΡ‚Π΅Π½Ρ†ΠΈΠ°Π»ΡŒΠ½ΠΎ ΡƒΠΆΠ΅ Π½ΠΎΠ²ΠΎΠ³ΠΎ значСния) } 

    The execution script for the log you quoted in the question might look like this:

     всС ΠΏΠΎΡ‚ΠΎΠΊΠΈ Π½Π°Ρ‡ΠΈΠ½Π°ΡŽΡ‚ Π²Ρ‹ΠΏΠΎΠ»Π½Π΅Π½ΠΈΠ΅ всС ΠΏΠΎΡ‚ΠΎΠΊΠΈ ΠΊΡ€ΠΎΠΌΠ΅ ΠΏΠΎΡ‚ΠΎΠΊΠ° 1 "Π±Π»ΠΎΠΊΠΈΡ€ΡƒΡŽΡ‚ΡΡ" Π½Π° условии while ΠΏΠΎΡ‚ΠΎΠΊ 1 Ρ‡ΠΈΡ‚Π°Π΅Ρ‚ id (=11) ΠΏΠΎΡ‚ΠΎΠΊ 1 ΡƒΠ²Π΅Π»ΠΈΡ‡ΠΈΠ²Π°Π΅Ρ‚ id (=12) ΠΏΠΎΡ‚ΠΎΠΊ 2 "разблокируСтся" ΠΏΠΎΡ‚ΠΎΠΊ 2 Ρ‡ΠΈΡ‚Π°Π΅Ρ‚ id (=12) ΠΏΠΎΡ‚ΠΎΠΊ 1 Ρ‡ΠΈΡ‚Π°Π΅Ρ‚ id ΠΈ Π²Ρ‹Π²ΠΎΠ΄ΠΈΡ‚ Π½Π° консоль (=12) ΠΏΠΎΡ‚ΠΎΠΊ 1 "засыпаСт" ΠΏΠΎΡ‚ΠΎΠΊ 2 ΡƒΠ²Π΅Π»ΠΈΡ‡ΠΈΠ²Π°Π΅Ρ‚ id (=13) ΠΏΠΎΡ‚ΠΎΠΊ 3 "разблокируСтся" ΠΏΠΎΡ‚ΠΎΠΊ 3 Ρ‡ΠΈΡ‚Π°Π΅Ρ‚ id (=13) ΠΏΠΎΡ‚ΠΎΠΊ 3 ΡƒΠ²Π΅Π»ΠΈΡ‡ΠΈΠ²Π°Π΅Ρ‚ id (=14) ΠΏΠΎΡ‚ΠΎΠΊ 4 "разблокируСтся" ΠΏΠΎΡ‚ΠΎΠΊ 4 Ρ‡ΠΈΡ‚Π°Π΅Ρ‚ id (=14) ΠΏΠΎΡ‚ΠΎΠΊ 4 ΡƒΠ²Π΅Π»ΠΈΡ‡ΠΈΠ²Π°Π΅Ρ‚ id (=15) ΠΏΠΎΡ‚ΠΎΠΊ 2 Ρ‡ΠΈΡ‚Π°Π΅Ρ‚ id ΠΈ Π²Ρ‹Π²ΠΎΠ΄ΠΈΡ‚ Π½Π° консоль (=15) ΠΏΠΎΡ‚ΠΎΠΊ 2 "засыпаСт" ΠΏΠΎΡ‚ΠΎΠΊ 5 "разблокируСтся" ΠΏΠΎΡ‚ΠΎΠΊ 5 Ρ‡ΠΈΡ‚Π°Π΅Ρ‚ id (=15) ΠΏΠΎΡ‚ΠΎΠΊ 5 ΡƒΠ²Π΅Π»ΠΈΡ‡ΠΈΠ²Π°Π΅Ρ‚ id (=16) ΠΏΠΎΡ‚ΠΎΠΊ 6 "разблокируСтся" ΠΏΠΎΡ‚ΠΎΠΊ 6 Ρ‡ΠΈΡ‚Π°Π΅Ρ‚ id (=16) ΠΏΠΎΡ‚ΠΎΠΊ 6 ΡƒΠ²Π΅Π»ΠΈΡ‡ΠΈΠ²Π°Π΅Ρ‚ id (=17) ΠΏΠΎΡ‚ΠΎΠΊ 6 Ρ‡ΠΈΡ‚Π°Π΅Ρ‚ id ΠΈ Π²Ρ‹Π²ΠΎΠ΄ΠΈΡ‚ Π½Π° консоль (=17) ΠΏΠΎΡ‚ΠΎΠΊ 6 "засыпаСт" ...ΠΈ Ρ‚Π°ΠΊ Π΄Π°Π»Π΅Π΅ 

      In fact, everything is quite simple. You create several threads, and this method allows only 1 to be executed with the desired id. This id is contained in the variable INSTANCE_TEST_THREAD.getCurentThreadId() . Now you change places. Maybe this situation:

       1 ΠΏΠΎΡ‚ΠΎΠΊ Π·Π°ΡˆΡ‘Π», Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ 1 1 ΠΏΠΎΡ‚ΠΎΠΊ ΡƒΠ²Π΅Π»ΠΈΡ‡ΠΈΠ» Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ Π΄ΠΎ 2 2 ΠΏΠΎΡ‚ΠΎΠΊ Π·Π°ΡˆΡ‘Π» Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ 2 2 ΠΏΠΎΡ‚ΠΎΠΊ ΡƒΠ²Π΅Π»ΠΈΡ‡ΠΈΠ» Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ Π΄ΠΎ 3 1 ΠΏΠΎΡ‚ΠΎΠΊ Π²Ρ‹Π²Π΅Π» 3 (!!) 2 ΠΏΠΎΡ‚ΠΎΠΊ Π²Ρ‹Π²Π΅Π» 3 (!!) 3 ΠΏΠΎΡ‚ΠΎΠΊ ΡƒΠ²Π΅Π»ΠΈΡ‡ΠΈΠ» Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ Π΄ΠΎ 4 3 ΠΏΠΎΡ‚ΠΎΠΊ Π²Ρ‹Π²Π΅Π» 4. 

      I think you get the idea. By swapping places we make the race flow with an unpredictable order of execution, essentially making the entire synchronization code inoperative.