Java concurrent programming 5-Java storage mode

Source: Internet
Author: User

J: hi, T.
T: hi, J.
J: today we should talk about the Java storage mode, right?
T: Yes. I will introduce the Java storage mode and its functions, introduce Happens-before, and explain how to use it through some examples.
J (can't wait): Let's get started.

Introduction to the Java storage mode and its Functions

T: Okay. Java Memory Model (JMM) determines whether the value written by the program can be correctly read by other programs according to specific rules. For example, assign a value to the variable value in a thread:
Value = 3;
The storage mode tells you under what circumstances the thread reading the value can correctly see the value 3.
In simple terms, some basic rules are defined in the Java storage mode to ensure that the program will get specific results under specific circumstances.
J: Oh, but what are the advantages?
T: JMM specifies a minimum JVM guarantee, which provides a lot of freedom for JVM implementation. JVM can optimize code execution without violating JMM. Such a design balances the predictable needs with the simplicity of the development program. If you do not understand this, you will be confused about some of your program's behavior.
In general, after learning about JMM, you can better determine when to use synchronization to coordinate thread Activities and Use JMM to implement some high-performance and thread-Safe Containers. Otherwise, you may encounter some amazing behavior in the program due to incorrect use of synchronization, such as the visibility we mentioned in java concurrent programming 1. Next I will introduce two other optimization strategies of JVM: Re-sorting and address reuse.

Reorder

Let's start with an example:

public class Test {private static int r1 = 0;private static int r2 = 0;private static int a = 0;private static int b = 0;public static void main(String[] args) {Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {r2 = a;b = 1;}});Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {r1 = b;a = 2;}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("(" + r1 + ", " + r2 + ")");}}


So, can the output result of this program be (1, 2?
J: Oh, let me think about it. If r2 is equal to 2, it must wait until thread 2 executes a = 2 before thread 1 can start execution. r1 must wait for 1, it must wait until thread 1 executes B = 1 before thread 2 can start execution. This... It is unlikely.
T: Actually, this is possible because the compiler allows the order of commands executed in the re-Sort thread. This is also called re-sorting as long as this does not affect the execution results of that thread. Therefore, the order of commands in the two threads in the preceding example may be reordered as follows:
Thread t1: B = 1; r2 =;
Thread t2: r1 = B; a = 2;
In this way, the final result may be: r2 = 2 and r1 = 1.
J: Oh, it turns out to be like this.

Address multiplexing

T: The above program has problems, mainly because of data competition between thread t1 and thread t2, and unexpected results often occur in data competition. Let's look at the next example:

public class Test {private static Point r1;private static int r2;private static Point r3;private static int r4;private static int r5;private static Point r6;private static Point p = new Point();private static Point q = p;public static void main(String[] args) {Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {r1 = p;r2 = r1.x;r3 = q;r4 = r3.x;r5 = r1.x;}});Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {r6 = p;r6.x = 3;}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("(" + r2 + ", " + r4 + ", " + r5 + ")");}}class Point {public int x = 0;public int y = 0;}


This program will lead to a compiler optimization, that is, address reuse. Since both r2 and r5 read the value of r1.x, and there is no modification to the value of r1.x between the replication operations of r2 and r5, the compiler points r2 and r5 to the same address, the result after the compiler optimization is as follows:
Thread t1: r1 = p; thread t2: r6 = p;
R2 = r1.x; r6.x = 3;
R3 = q;
R4 = r3.x;
R5 = r2;
The direct consequence of this optimization is: if the p modification in thread t2 is executed after the r2 value of thread t1 and before the r4 value, the execution result will be:

R1 = p;
R2 = 0;
R3 = q;
R4 = 3;
R5 = 0;
This gives people the feeling that the p. x value starts to 0, then changes to 3, and then changes to 0 again, which is inconsistent with the actual situation.
J: Wow, this is indeed a very strange result.
T: Yes. Since JMM does not have requirements for this, the compiler can do this.

Summary

In short, it is reasonable for the compiler to follow JMM and ensure that no optimization execution results are the same as those after optimization in the case of a single thread. In addition, in the case of a single thread, these optimizations are hidden for us. They do not affect the execution speed of the program.
J: that is to say, these optimizations can improve the execution efficiency of programs in a single-threaded environment, but they bring problems to the execution under multiple threads.
T: Yes. By studying JMM, we can recognize which operations are insecure under multiple threads.
J: Let's get started;

Java Storage Model

The Java storage model is described in the form of actions, that is, it defines a set of partial virtual relationships for the actions in all programs, called happens-before. We will explain it in detail below.

Happens-before

Happens-before ensures that if behavior A happens-before acts B, behavior A must be visible (whether A and B occur in the same thread) before behavior B executes ). If the two operations are not sorted by the happens-before relationship, JVM can reorder them at will.
Let's take a look at the happens-before rule defined by JMM:

Program order rules: Each action A in the thread is Happens-before in every action B in the thread. in the program, all action B appears after action;
Monitor lock rules: Unlock a monitor lock. Happens-before locks each subsequent lock on the same monitor lock;
Volatile variable rules: Write operations to the volatile domain Happens-before for each subsequent read operation to the same domain;
Thread startup rules: In a Thread, the call to Thread. start will Happens-before the action in each startup Thread;
Thread termination rule: Any action in the Thread is Happens-before. When other threads detect that the Thread has ended (or the Thread is successfully returned from the Thread. join call, or Thread. isAlive returns false );
Interrupt rule: One thread calls the interrupt Happens-before of another thread to discover the interruption (by throwing InterruptedException or calling isInterrupted and interrupted );
Termination rule: The end of an object's constructor. Happens-before begins with finalizer;
Transfer Rule: If A Happens-before is in B and B Happens-before is in C, A Happens-before is in C.

J: How do I feel there is a conflict between the procedural order rule and the reordering rule?
T: The procedural order rule does not imply that the two actions must occur in the procedural order. If the re-ordered results and the legal execution results are consistent, then the re-ordered results are also legal. This is too abstract. For example:

public class Test {private static int x = 0;private static int y = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {x = 5;y = 6;}});Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {if (y == 6) {System.out.println(x);}}});t1.start();t2.start();t1.join();t2.join();}}


Based on the re-sorting, the result of t2 thread printing may be 5 or 0. According to the program order rule, x = 5 happens-before y = 6, but there is no happens-before relationship between execution of t2 and execution of t1, therefore, in t2, if y = 6, you may see x = 5 or x = 0. However, if y is a volatile variable, we can see that:
1) according to the procedural order rule: x = 5 happens-before y = 6;
2) read y according to volatile variable rules: y = 6 happens-before;
3) read y happens-before to read x according to the program order rule.
Finally, according to the pass-through rule, we can ensure that the t2 printing result is always 5.
J: This example shows that it is much better. I think I have almost understood happens-before.
T: I will use a positive example and a negative example to illustrate the use of Happens-before. We will start from the positive example:

FutureTask

FutureTask is located under the java. util. concurrent package, which implements the Runnable and Future interfaces. The Runnable interface must be implemented for classes that intend to run their instances through threads, while the Future interface is used to represent the interface for asynchronous computing. It mainly includes the following methods:
Cancel: attempts to cancel the execution of this task.
Get: if necessary, wait until the calculation is complete and obtain the result. You can specify the time.
IsCancelled: returns true if the task is canceled before it is completed.
IsDone: returns true if the task is completed.

An example of using FutureTask is as follows:

public class Test {public static void main(String[] args) throws InterruptedException,ExecutionException {ExecutorService executor = Executors.newFixedThreadPool(10);try {Future
 
   future = executor.submit(new Callable
  
   () {@Overridepublic String call() throws Exception {Thread.sleep(2000);return "It is done!";}});System.out.println(future.get());} finally {executor.shutdown();}}}
  
 


Here we will focus on the use of the happens-before rule by FutureTask. To help us analyze the source code of FutureTask, now I have implemented a simplified version of FutureTask (excluding the cancel function and exception handling. The Code has defects. For the complete code, see the JDK source code: java. util. concurrent. futureTask. java ):

public class FutureTask
 
   implements Runnable {private final Sync sync;public FutureTask(Callable
  
    callable) {sync = new Sync(callable);}@Overridepublic void run() {sync.innerRun();}public V get() throws InterruptedException, ExecutionException {return sync.innerGet();}protected void set(V v) {sync.innerSet(v);}private final class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = -7828117401763700385L;/** State value representing that task is ready to run */private static final int READY = 0;/** State value representing that task is running */private static final int RUNNING = 1;/** State value representing that task ran */private static final int RAN = 2;private final Callable
   
     callable;private V result;private volatile Thread runner;Sync(Callable
    
      callable) {this.callable = callable;}protected int tryAcquireShared(int ignore) {return innerIsDone() ? 1 : -1;}boolean innerIsDone() {return isRan(getState()) && runner == null;}private boolean isRan(int state) {return (state & RAN) != 0;}protected boolean tryReleaseShared(int ignore) {runner = null;return true;}V innerGet() throws InterruptedException, ExecutionException {acquireSharedInterruptibly(0);return result;}void innerSet(V v) {for (;;) {int s = getState();if (s == RAN)return;if (compareAndSetState(s, RAN)) {result = v;releaseShared(0);return;}}}void innerRun() {if (!compareAndSetState(READY, RUNNING))return;runner = Thread.currentThread();if (getState() == RUNNING) { // recheck after setting threadV result;try {result = callable.call();} catch (Throwable ex) {return;}set(result);} else {releaseShared(0); // cancel}}}}
    
   
  
 


The following is AQS. Here we will only list the relevant code. I will introduce AQS in detail in subsequent articles. Here we will only briefly introduce the processes we are concerned about.

Public abstract class AbstractQueuedSynchronizer extendsAbstractOwnableSynchronizer implements java. io. serializable {....... // determine whether tryAcquireShared meets the requirement. if not, enter doAcquireSharedInterruptiblypublic final void acquireSharedInterruptibly (int arg) throws InterruptedException {if (Thread. interrupted () throw new InterruptedException (); if (tryAcquireShared (arg) <0) doAcquireSharedInterruptibly (arg);} // The thread will wait here until the entry Return private void doAcquireSharedInterruptibly (int arg) throws InterruptedException {final Node node = addWaiter (Node. SHARED); boolean failed = true; try {for (;) {final Node p = node. predecessor (); if (p = head) {// call tryAcquireShared after each wakeup to check whether the condition meets int r = tryAcquireShared (arg); if (r> = 0) {setHeadAndPropagate (node, r); p. next = null; // help GC failed = false; return ;}// parkAndCheckInte The thread in rrupt enters the wait if (else (p, node) & parkAndCheckInterrupt () throw new InterruptedException () ;}} finally {if (failed) cancelAcquire (node );}} // set the runner to null in tryReleaseShared, and then wake up the waiting thread public final boolean releaseShared (int arg) {if (tryReleaseShared (arg) {doReleaseShared (); return true;} return false;} private void doReleaseShared () {for (;) {Node h = hea D; if (h! = Null & h! = Tail) {int ws = h. waitStatus; if (ws = Node. SIGNAL) {if (! CompareAndSetWaitStatus (h, Node. SIGNAL, 0) continue; // loop to recheck cases // wake up Wait thread unparkSuccessor (h);} else if (ws = 0 &&! CompareAndSetWaitStatus (h, 0, Node. PROPAGATE) continue; // loop on failed CAS} if (h = head) // loop if head changed break ;}}......}


Take a closer look at the above code and then answer the following two questions:
1) Why should the runner variable be declared as a volatile variable;
2) Why does the result not need to be declared as a volatile variable.
J: this is a bit difficult. It takes some time.
... An hour later...
J: I understand why the runner needs to declare a volatile variable, but I still don't understand why the result does not need to be declared as a volatile variable.
T: Okay. Let's start with the runner: the runner will leave it blank in the tryReleaseShared method, and tryReleaseShared will be called in the running thread; the runner will judge whether it is null in the innerIsDone method, innerIsDone is called in the thread that calls the get method. Therefore, the runner is modified in one thread and read from the other. To ensure visibility, the runner must be declared as a volatile variable.
Why does the result not need to be declared as a volatile variable? What does it rely on to ensure its visibility? Next I will use the happens-before rule to prove that the result is visible. By analyzing the program, we can conclude that:
1) Call the releaseShared operation happens-before to assign values to results in innerSet (Program order rule );
2) Call releaseShared in releaseShared;
3) successful call of releaseShared happens-before tryAcquireShared (volatile variable law );
4) Call tryAcquireShared in acquireSharedInterruptibly;
5) The successful call of acquireSharedInterruptibly in innerGet will return the result of happens-before result (Program order rule ).
From the above analysis, and then according to the pass-through rule: innerSet's operation to assign values to the result will return the result in happens-before innerGet. This proves that the result is complete and true. That is to say, the get thread can always obtain the correct result value.
J (happy): I have thoroughly understood happens-before.
T: Congratulations. The example of FutureTask shows us how to use the happens-before principle "slaves" to synchronize. The advantage of this is the performance improvement, and the disadvantage is that it is very error-prone, therefore, you must be very careful when using it.
J: Okay. Let's continue with the counterexample.
T: Okay.

Double-Checked Locking (DCL)

This is a classic example. In earlier versions of JVM, synchronization, or even non-competing synchronization, all have amazing performance overhead, many clever tips have been invented to reduce the impact of synchronization, DCL is one of them, but it is the ugly one, look at the following code (this code comes from the http://www.iteye.com/topic/260515 here ):

public class LazySingleton {private int someField;private static LazySingleton instance;private LazySingleton() {this.someField = new Random().nextInt(200) + 1; // (1)}public static LazySingleton getInstance() {if (instance == null) { // (2)synchronized (LazySingleton.class) { // (3)if (instance == null) { // (4)instance = new LazySingleton(); // (5)}}}return instance; // (6)}public int getSomeField() {return this.someField; // (7)}}


DCL considers that the worst case of the program is to see the expiration value of the instance (that is, null). At this time, DCL will make another judgment when locking, so as to avoid risks, make sure that the latest instance value is obtained. But the actual situation is worse than this, because when the thread can obtain the current value of the instance, the status of the internal variables of the instance still expires. Why? The someField variable will be initialized in the constructor. When Statement 2 determines that the instance is not null, it returns the instance because there is no happens-before relationship between writing and reading someField, therefore, getSomeField cannot obtain the latest value of someField (according to the visibility principle ).
J: It's easy. We can solve this problem by adding synchronization for the getSomeField method:
Public synchronized int getSomeField (){
Return this. someField; // (7)
}
T: But this is still incorrect.
J: Why?
T: If you observe it carefully, Statement 5 and 7 do not use the same lock. If you want to lock it, you need:

public int getSomeField() {synchronized(LazySingleton.class){return this.someField; // (7)}}


However, if you do this, there will be a synchronization efficiency problem. In fact, after JDK 5, you can declare the instance as volatile, as shown below:
Private volatile static LazySingleton instance;
J: volatile can also make the variables inside the object visible?
T: No. This is correct because the following happens-before relationship exists in the code after volatile is added:
1) Statement 1 Write someField value happens-before Statement 5 Write to instance (Program order rule );
2) Statement 5: Write the instance into happens-before Statement 2 to obtain the instance (volatile variable law );
3) Statement 2 obtains instance happens-bufore. Statement 7 returns someField (Program order rule ).
According to the pass-through rule, Statement 1 writes the someField value to happens-before Statement 7 and returns someField, which ensures the correctness of DCL and does not cause too much performance degradation.
J: complicated.
T: In any case, DCL is no longer used. Here we just use it as a learning example. If you need to use delayed initialization, there are better methods:

public class LazySingleton {private int someField;private LazySingleton() {this.someField = new Random().nextInt(200) + 1; }private static class LazySingletonHolder{public static LazySingleton instance = new LazySingleton();}public static LazySingleton getInstance() {return LazySingletonHolder.instance;}public int getSomeField() {return this.someField; }}


JVM will delay the initialization of LazySingletonHolder to the time when it is actually used [JLS 12.4.1]. Because the instance is initialized in the static initial stage, no additional synchronization is required. When the thread calls getInstance for the first time, LazySingletonHolder is loaded and initialized. At this time, the instance is initialized.
Here, the DCL example is over. I hope you have a comprehensive understanding of JMM, which is very important for the analysis of concurrent containers. Thank you very much for your persistence until now.
J: Okay. See you next time.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.