[Java Study Notes - Concurrent Programming] Detailed Explanation of Treiber stack

foreword

Recently, I am looking at the relevant source code of Java concurrent programming. Found a word - Treiber stack. This article introduces what is Treiber stack and its core algorithm CAS.

For example, in FutureTask:

/** Treiber stack of waiting threads */
private volatile WaitNode waiters;

1. The origin of Treiber stack

The Treiber Stack first appeared in R. Kent Treiber's 1986 paper Systems Programming: Coping with Parallelism (Systems Programming: Coping with Parallelism). It is a lock-free concurrent stack, and its lock-free feature is based on the atomic operation implemented by the CAS (Compare and swap: compare and exchange) algorithm.

2. CAS

1. What is CAS?

CAS: Compare and Swap, that is, compare and exchange. It is a lock -free implementation algorithm of optimistic locking.

The algorithm involves three operands:

  • The memory location V that needs to be read and written
  • The expected value A that needs to be compared
  • The new value U that needs to be written

CAS algorithm analysis: When CAS is specifically executed, if and only when the expected value A matches the value stored in the memory address V, the old value is replaced with the new value U and written into the memory address V. Otherwise do not update.

The process is as follows:
insert image description here

2. CAS in Java

In Java15 concurrent programming, there are some CAS implementations based on basic data types, under java.util.concurrent.atomic.*. Here we take AtomicBoolean as an example:

public class AtomicBoolean implements java.io.Serializable {
    
    
    private static final long serialVersionUID = 4654671469794556979L;
    //VarHandle 之后会写一篇文章对比 Unsafe 的访问方法
    //可以先粗浅的理解为,获取到了 AtomicBoolean 实例的变量。
    private static final VarHandle VALUE;
    static {
    
    
        try {
    
    
            MethodHandles.Lookup l = MethodHandles.lookup();
            VALUE = l.findVarHandle(AtomicBoolean.class, "value", int.class);
        } catch (ReflectiveOperationException e) {
    
    
            throw new ExceptionInInitializerError(e);
        }
    }

    public final boolean compareAndSet(boolean expectedValue, boolean newValue) {
    
    
        return VALUE.compareAndSet(this,
                                   (expectedValue ? 1 : 0),
                                   (newValue ? 1 : 0));
    }
}

Jump to the VarHandle class:

    public final native
    @MethodHandle.PolymorphicSignature
    @HotSpotIntrinsicCandidate
    boolean compareAndSet(Object... args);
     
    public final native
    @MethodHandle.PolymorphicSignature
    @HotSpotIntrinsicCandidate
    boolean weakCompareAndSet(Object... args);

We found that it is a native method, that is to say, the underlying operation implemented by Java externally. The specific c implementation will be explored later.

3. Problems with CAS
  • ABA questions
  1. Thread 1 and thread 2 obtain the main memory value A, respectively copy it to their own memory segments, and perform calculations;
  2. During the calculation process of thread 2, thread 1 completes the calculation first and updates the updated value B to the main memory;
  3. During the calculation process of thread 2, join thread 3;
  4. During the calculation process of thread 2, thread 3 copies the main memory value B, and completes the calculation and updates the updated value A to the main memory;
  5. At this point, thread 2 completes the calculation, and according to the CAS algorithm, thread 2 updates the updated value C to the main memory;

This problem will lead to data errors in business scenarios with strong data meaning.
For example: two users A and B grabbed an iPhone on the e-commerce platform. User A got it, but user B didn't get it due to poor internet connection, but at this time user C returned one. The final result is that users A and B both grab the iPhone, but the actual business meaning and data deviate.

  • Long-term thread spin is expensive
    If the CPU (single core) does not have a preemptive scheduler (that is, a thread is interrupted by the clock to run other threads), thread spin will not release CPU resources, and will always access the target memory segment. Spinning threads for a short time is a pretty cost-effective choice, but if multiple threads spin for a long time, it will burden the CPU.

3. Lock-free concurrent stack implementation of FutureTask

Back to the beginning:

/** Treiber stack of waiting threads */
private volatile WaitNode waiters;

Let's take a look at what WaitNode is:

    static final class WaitNode {
    
    
        volatile Thread thread;
        volatile WaitNode next;
        WaitNode() {
    
     thread = Thread.currentThread(); }
    }

WaitNode is an internal class in FutureTask, and its structure is a one-way linked list .

Let's search again, the use function of the waiters variable, let's not worry about its business significance for the time being, since it is a stack, let's take a look at the most critical steps:

  • The node is pushed into the stack, and the pointer points to the top node:
    When q is pushed into the stack, it is necessary to check whether the next pointer points to waiters. If it is another node, it proves that the thread is not safe (tampered), queued = false, and try again in the next cycle.
   private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
    
    
			//……………………

            else if (!queued)
                queued = WAITERS.weakCompareAndSet(this, q.next = waiters, q);
                
			//……………………
    }
  • The node is popped out of the stack, and the pointer points to the top node:
    When q is popped out of the stack, check whether the current waiters is q, if it is not, it proves that the thread is not safe (tampered with), and the spin block waits.
	 private void removeWaiter(WaitNode node) {
    
    
	 //……………………
            s = q.next;
            //……………………
            else if (!WAITERS.compareAndSet(this, q, s))
                continue retry;
     //……………………
    }
  • The stack is cleared:
    the direct pointer points to null, and the gc will reclaim the waiter list floating in the memory.
    private void finishCompletion() {
    
    
    //……………………
            if (WAITERS.weakCompareAndSet(this, q, null)) {
    
    
            	//……………………
            }
    //……………………
    }

The above is the implementation of the lock-free concurrent stack. The core is the implementation of the stack operated by the CAS algorithm. Please experience it carefully.

For the difference between compareAndSet and weakCompareAndSet, you can read my next article.

Guess you like

Origin blog.csdn.net/weixin_43742184/article/details/113567022