Based on the theoretical knowledge of the Java memory model learned above, let's explain the reasons for common thread-unsafe scenarios.
Example 1, race condition
public class UnsafeCounting { public int count; public void add() { for (int i = 0; i < 10000; i++) { count++; } } }
public void test() throws InterruptedException { UnsafeCounting unsafeCounting = new UnsafeCounting(); Thread t1 = new Thread(() -> unsafeCounting.add()); Thread t2 = new Thread(() -> unsafeCounting.add()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(unsafeCounting.count); }
Execute 10 times result: 17784 14845 15600 18361 19201 20000 19417 20000 19861 15236
First of all, count++ is not an atomic operation, but contains three independent operations of "read-modify-write", which destroys the atomicity mentioned above. Suppose that threads A and B simultaneously read count= 100, and performing the +1 operation at the same time will result in a deviation of 1. This situation of incorrect results due to improper execution timing is formally known as a race condition, which can be solved by synchronizing the method using the synchronized keyword, ensuring memory visibility and atomicity.
Example 2, memory visibility
public class VisibilityThread extends Thread { private boolean stop; @Override public void run() { int i = 0; System.out.println("start loop."); while (!getStop()) { i++; } System.out.println("finish loop,i=" + i); } public void stopIt() { stop = true; } public boolean getStop() { return stop; } }
public void test2() throws InterruptedException { VisibilityThread v = new VisibilityThread(); v.start(); //Pause for 1 second and wait for the newly started thread to execute Thread.sleep(1000); System.out.println("about to be set stop value is true"); v.stopIt(); Thread.sleep(1000); System.out.println("finish main"); System.out.println("main passed getStop acquired stop value:" + v.getStop()); }
start loop. about to be set stop value is true finish main main passed getStop acquired stop value:true
What this example wants to express is that stop = true; is modified in the main thread, but v.start(); does not stop and does not output the finish loop,i= statement. i.e. memory visibility issues. This problem can be solved by declaring private volatile boolean stop; stop as volatile.
Example three, reordering
public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread{ public void run(){ while (!ready) { Thread.yield(); } System.out.println(number); } } public static void main(String args[]) throws Exception{ new ReaderThread().start(); number=42; ready=true; } }
The thread may keep looping because the value of ready may not be seen. Another possibility is that the output number is 0, because the thread may read ready=true, but does not see the value of the number. This phenomenon occurs because the reordering destroys the order. Declare ready as volatile, first exclude the influence of visibility, and then the actual operation of this code may not output 0, but this does not mean that the code is correct, the thread safety problem is inherently uncertain and difficult to reproduce question.
Example 4, singleton double-checked locking mechanism
This is the double-checked locking mechanism of the singleton mode. Why declare volatile objects and use synchronized at the same time?
public class Singleton { private volatile Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { // 1 synchronized (Singleton.class) { if (singleton == null) { // 2 singleton = new Singleton(); } } } return singleton; } }
When the singleton is null for the first time, when multiple threads call getInstance(), it is assumed that threads A and B both execute code 1. Since the synchronization locks the Class object of the current class, only one thread A and B can enter 2, and the other thread can enter code 1. wait. Suppose A thread enters 2, executes instantiation, and unlocks. Thread B then acquires the lock and executes two codes. At this time, it checks again whether the singleton is null. If the singleton object is not declared as volatile, it may not see that the singleton has been instantiated by thread A, and then continues to execute the instantiation, resulting in two instances appear.