Three ways of thread-safe plus one (+1) operation in Java

1. Locks are divided into optimistic locks and pessimistic locks. Pessimistic locks always assume that each critical section operation will conflict. If multiple threads need to access critical section resources at the same time, it is better to sacrifice performance and let the threads wait . As for optimistic locking, it will assume that there is no conflict in access to resources , and all threads can continue to execute without stopping. If conflicts are encountered, optimistic locking is called CAS Compare And Swap. Identify thread conflicts, and once a conflict is detected, the current operation is attempted until there is no conflict .

2. The need for locks:

Example : variable i = 1, thread A performs the i+1 operation, and thread B also performs the i+1 operation. After two thread additions, i may be equal to 2 , not necessarily equal to 3 as imagined.

1) Direct concurrent operations

Analysis: The memory model of Java in the figure below, assuming i=1 in the main memory, assuming that thread A executes first, thread A reads i = 1 from the main memory to the local memory A, and adds one operation, in thread A Before writing the value added by one from local memory A back to main memory A, thread B reads the value of i from the main memory to local memory B, and it is still 1 at this time, and thread B compares i in local memory B = 1 to add one operation. Then thread A and thread B respectively write the 2 in the local memory back to the main memory, so the final result is that the value of i is both 2.

2) In order to solve this problem, first think of variable i using volatile modification

If i is defined as volatile, the visibility of i is guaranteed at this time, that is, when thread A modifies the variable i, the new value is immediately visible to other threads. But there will still be problems. The problem is that the i+1 statement is not an atomic operation , i+1 contains 3 operations: read the value of i from the working memory, increment it by one through the operand stack, and write the value back to the working memory . Let’s analyze the above process again. Suppose i = 1 in the main memory. Suppose thread A executes first, and thread A reads i = 1 from the main memory to the local memory A. When performing the i+1 operation, first set i = 1 is taken to the top of the operand stack. At this time, thread 2 obtains the execution right of the CPU. Thread B takes i = 1 from the main memory, and adds one operation. The value of i is 2, and the value of i in the local memory A is also refreshed to 2 (visibility), and then thread A obtains the execution right of the CPU and continues the i++ operation, but note that the i=1 just now has been fetched The operand stack ( the operand stack will not re-fetch data from the local memory A ), so after adding one, the value of i is 2, and 2 is written back to the local memory A, and the final result is also 2. Therefore, it is not enough to only satisfy the visibility, and also need to satisfy the atomicity .

3) Solution 1: Through the synchronized keyword

Using synchronized guarantees visible rows and atomicity

Two operations, lock and unlock, are defined in the Java memory model:

lock (lock): a variable that acts on the main memory, which identifies a variable as the exclusive state of one thread ( it can be simply understood as: the variable can only be used by one thread at this time ).

unlock (unlock): a variable in the main memory of the scope, it releases a variable in a locked state, and the released variable can be locked by other threads.

Atomic line guarantee : The variables between lock and unlock are exclusive to the thread . These two instructions cannot be used directly in Java, but the bytecode instructions monitorenter and monitorexit can be used to implicitly use these two operations. The bytecode instruction reflected in the Java code is the synchronized block - the synchronized keyword, so the operations on variables between the synchronized blocks are atomic .

Visibility Guarantee: Before performing an unlock operation on a variable, the variable must be synchronized back to main memory .

3) Solution 2: Use CAS operation to solve

First introduce the basic principle of CAS: it contains three parameters CAS (V, E, N). V represents the variable to be followed, E represents the expected value, and N represents the new value. Only when the value of V is equal to the value of E, will the value of V be set to N. If the value of V and the value of E are different, it means that other threads have already made updates, and the current thread does nothing.

Define i as an AtomicInteger type: 

 static AtomicInteger i = new AtomicInteger(1);

At this time, the following method can ensure thread safety, and the result is 3 after both the A thread and the B thread perform the plus one operation.

i.incrementAndGet();        //当前值加一

Principle analysis:

A core field is saved in AtomicInteger:

private volatile int value;

IncrementAndGet source code:

public final int incrementAndGet() {
    for (;;) {
        int current = get();                    //第三句                   
        int next = current + 1;               
        if (compareAndSet(current, next))       //第五句
            return next;
    }
}
public final int get() {
        return value;
}

incrementAndGet() guarantees atomicity. As mentioned above, int next = current + 1 includes multi-step operations. First, take current from working memory, add one to it, and then assign it to next (including at least these three steps, in fact, transfer into machine instructions, there are more operations), now the value of value is 1, so current = get() = 1, execute next = current + 1 = 2, at this time compare the expected value of current (1) with the value to be updated The variable value (that is, the value value), if the value of the value is changed by another thread from the third sentence to the fifth sentence, then the expected value is not equal to the value of the variable to be updated, the operation fails , and continue to execute for(;;) , if the operation is successful, replace the value value with the value of next (add one operation).

For the implementation of the compareAndSet function.

public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);

The compareAndSwapInt() method is a native method. The first parameter o is the given object, and the offset is the offset within the object (actually, it is the offset from a field to the object header, through which you can quickly locate to value), expected represents the expected value, if the value to be updated is equal to the expected value, then assign x to value. Note that the comparison and assignment here are all done using CAS atomic instructions (by calling CPU low-level instructions) , because atomic rows of these two steps are guaranteed.

My own understanding: When comparing here, the value to be updated is not to obtain the value of the working memory, but to obtain the value from the main memory, so use the offset to directly locate the field address in the object.

refer to:

http://zl198751.iteye.com/blog/1848575

http://www.cnblogs.com/xrq730/p/4976007.html

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=324064752&siteId=291194637