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.