java multithreading - CAS

This is the fifth part of the multi-threading series, please pay attention to the following:

Java multithreading - how do threads come from?

java multithreading - memory model

java multithreading - volatile

java multithreading - lock

Regarding the lock-free queue, there are many introductions on the Internet. I will make a comb, from what it is to what features and applications to make a summary, so that it is convenient for myself to record and use.

The main content of this article:

  1. What is non-blocking synchronization
  2. what is CAS
  3. CAS characteristics
  4. non-blocking queue
  5. ABA problem

 

1. Non-blocking synchronization

 

Mutual exclusion synchronization belongs to a pessimistic concurrency strategy. It is always believed that as long as correct synchronization measures are not taken, problems will definitely occur. No matter whether there is competition for shared data, it must be locked.

The optimistic concurrency strategy based on conflict detection is to operate first. If there is no competition, the operation is successful. If there is competition and a conflict occurs, remedial measures are adopted. The common one is to retry continuously.

CAS is an optimistic concurrency strategy. In addition to CAS, there are:

  • Test-and-Set,

  • Fetch-and-Increment,

  • Swap,

  • LL/SC (Load Link/Conditional Store)

All of the above require the support of the hardware instruction set to be atomic. For example, under the IA64 and x86 CPU architectures, the CAS function can be completed through the cmpxchg instruction, while under the ARM and PowerPC architectures, the ldrex/strex instruction is required to complete the LL/SC function.

 

2. What is CAS

 

The full name of cas is Compare-and-Swap (Compare and Swap), and it has 3 operands, namely the memory location V, the old expected value A, and the new value B, then if and only if V meets the old expected value A , the processor updates the value of V to B with the new value B. And these processes are supported by the instruction set, so it seems that the read-write-modify operation is just an atomic operation, so there is no thread safety problem. Let's look at the pseudo code of the operation process of cas:

int value;

int compareAndSwap(int oldValue,int newValue){



    int old_reg_value =value;

    if (old_reg_value==old_reg_value)

        value=newValue;

    return old_reg_value;

}

 

When multiple threads try to use CAS to update the same variable at the same time, only one of them can update the value of the variable. When other threads fail, they will not be suspended like acquiring the lock, but can try again, or do nothing, this flexibility greatly reduces the risk of lock liveness.

 

jvm supports CAS

 

JDK did not support cas before 1.5, and the bottom-level support has been introduced since JDK 1.5. Currently, cas operations are exposed on int, long and object references, mainly with sum.misc.Unsafe to wrap, and then by The virtual machine handles these methods specially. On platforms that support cas, the runtime compiles it into the corresponding machine instructions, and in the worst case, if cas instructions are not supported, the jvm uses a spinlock instead.

At present, the classes supported by java for cas are mainly under the util.concurrent.atomic package, and the specific use is not introduced. For example, our common AtomicInteger uses the cas operation to ensure the security of int value changes.

 

3. CAS characteristics

 

We know that if locks are used to process shared data, when multiple threads compete, they all need to be locked. Threads that do not get the lock will be blocked and awakened. These require the conversion from user mode to core state. This The cost is quite high for blocking threads. The CAS uses a lock-free optimistic method to compete, and the performance is higher than that of the lock. Why not replace the lock competition method?

To answer this question, let's take an example.

When you are driving during the rush hour, if you control the traffic flow through traffic lights, you can achieve higher throughput, and although there are no traffic lights on the roundabout to make you wait, you may not be able to go around the intersection you left first. , sometimes it may take a few more laps, and when there is low congestion, the roundabout can achieve higher throughput, you can succeed at one time, but the red street light is inefficient, even if there are not many people, you still need to wait .

This example is still suitable for the comparison of locks and cas. In the case of high competition, the performance of locks will exceed the performance of cas, but in the case of low-level competition, the performance of cas will exceed the performance of locks. In most cases, competition for resources is generally not that intense.

 

4. Non-blocking and non-locking linked list

 

We refer to a source code implementation of ConcurrentLinkedQueue  to see the application of cas.

ConcurrentLinkedQueue is an unbounded thread-safe queue based on linked nodes. It is a singly linked list. Each linked node has an element of the current node and a pointer to the next node.

 Node<E> {

    volatile E item;

    volatile Node<E> next;

}

It sorts the nodes using a first-in, first-out rule, when we add an element, it is added to the tail of the queue, and when we get an element, it returns the element at the head of the queue. The tail and head nodes allow us to quickly locate the last and first elements.

 

Let's look at the source code implementation of adding an element:

public boolean offer(E e) {

    checkNotNull(e);

    final Node<E> newNode = new Node<E>(e);

   

   //从tail执向的节点开始循环,查找尾部节点,然后插入,直到插入成功。

    for (Node<E> t = tail, p = t;;) {

        Node<E> q = p.next;

        if (q == null) {

            // p 是最后一个节点

            if (p.casNext(null, newNode)) {

                // 添加下一个节点成功之后,如果当前tail节点的next节点!=null,更新tail的指针指向newNode

                // 如果==null,则不更新t  

                if (p != t)

                    casTail(t, newNode);  // 更新tail节点指针指向newNode

                return true;

            }

            // 没有竞争上cas的线程,继续循环

        }

        else if (p == q)

            // 如果next节点指向自己,表示tail指针自引用了,当前只有head一个节点,下一个节点从head开始循环

            p = (t != (t = tail)) ? t : head;

        else

            // 如果tail节点的next节点不为null,继续查找尾部节点(尾部节点的next==null)

            p = (p != t && t != (t = tail)) ? t : q;

    }

}

 

The above code mainly does the following functions:

1. Start the loop from the node pointed to by the tail pointer, and search for the tail node. The characteristic of the tail node is that next is null.

2. If the nextNode of the current node!=null, continue to find the nextNode of the nextNode.

2. If the nextNode==null of the current node indicates that the tail node is found, add newNode to the nextNode of the tail node.

3. Update the tail pointer, generally pointing to the latest tail node

4. If the tail node nextNode==null, it will not be updated, indicating that it has pointed to the latest tail node last time.

5. If !=null, update to newNode, and update once for 2 insert operations

Diagram:

 

The above code algorithm is too complicated and can be simplified as follows:

while (true){

    //添加尾节点的next节点,成功之后,更新tail的指针指向最新尾节点

    if (tail.casNext(null, newNode)) {

        casTail(tail, newNode); 

        return true;

    }

}

 

Why do we need to update the tail pointer after inserting the node twice?

1. Reduce the number of tail writes, thereby reducing the write overhead

2. The increase in the number of reads of tail will not affect the performance. Although the overhead of a loop is increased, it is not large compared to the write.

3. The tail speeds up the team entry efficiency, and does not start looking for the tail node from the head every time you enter the team.

 

There are two CAS operations, how to ensure consistency?

1. If the first cas update succeeds and the second fails, then the tail will be inconsistent. And even if the update is successful, there may still be another thread accessing the queue between the execution of two cas, so how to ensure that there will be no errors in this multi-threaded situation.

2. For the first question, even if the tail update fails, the above code will loop to find the real tail node. Here, it is not mandatory to use tail as the tail node, it is just a pointer close to the tail node.

3. In the second case, if thread B arrives and finds that thread A is performing an update, then thread B will check the status of the queue through repeated loops until A completes the update, and thread B gets the latest information of nextNode and adds a new one. node, so that the two threads do not interfere with each other.

The above is the basic idea of ​​ConcurrentLinkedQueue non-blocking linked list, we can see how to use cas to update shared data, and how to improve efficiency. The source code adopts jdk1.8.

 

5. ABA problem

 

As soon as CAS looks perfect, it is not semantically perfect, there is such a logical hole:

If a variable V has the value A when it is first read, and checks that it is still the value A when it is ready to be assigned, then we assume that it has not changed. If during this time its value is changed to B and then to A, then CAS will mistake it for it has never been changed, and this vulnerability is also known as the "ABA" problem.

In the memory reuse mechanism widely used in the memory management mechanism of c, if the pointer is updated by cas, there may be some pointer confusion problems. The common solution to the ABA problem is to add a version number when updating, and the version number is +1 after each update to ensure data consistency.

However, in most cases, the ABA problem will not affect the correctness of the program. If it needs to be solved, it can be specially considered, or it will be better to use the traditional mutual exclusion synchronization.

 

-----------------------------------------------------------------------------

If you want to see more interesting and original technical articles, scan and follow the official account.

Focus on personal growth and game development, and promote the growth and progress of the domestic game community.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324430675&siteId=291194637