Introduction to CLH lock

Reposted from CLH lock introduction- gaob2001's personal space- OSCHINA - Chinese Open Source Technology Exchange Community

overview

When I was learning the Java AQS framework, I found that the locking logic was very strange. Later, I learned that the locking logic is a variant of the CLH lock, so knowing it is good for understanding the AQS framework.

Introduction

CLH lock is a lock invented by three people, Craig, Landin, and Hagersten. The initials of the names of the three people are taken, so it is called CLH Lock.

The CLH lock mainly has a QNode class. The QNode class maintains a boolean type variable inside. Each thread has a precursor node (myPred) and its own node (myNode), and a tail node is used to store the last acquired lock. The state of the thread. CLH logically forms a lock waiting queue to achieve locking. CLH locks only support sequential locking and unlocking, and do not support reentry or interruption.

Java implementation

public class CLHLock {
    private final AtomicReference<QNode> tail;
    private final ThreadLocal<QNode> myPred;
    private final ThreadLocal<QNode> myNode;

    private static class QNode {
        volatile boolean locked = false;
    }

    public CLHLock() {
        tail = new AtomicReference<QNode>(new QNode());
        myNode = new ThreadLocal<QNode>() {
            @Override
            protected QNode initialValue() {
                return new QNode();
            }
        };
        myPred = new ThreadLocal<QNode>() {
            @Override
            protected QNode initialValue() {
                return null;
            }
        };
    }

    public void lock() {
        QNode node = myNode.get();
        node.locked = true;
        QNode pred = tail.getAndSet(node);
        myPred.set(pred);
        while (pred.locked) {}
    }

    public void unlock() {
        QNode qnode = myNode.get();
        qnode.locked = false;
        myNode.set(myPred.get());
        // myNode.set(new QNode());
    }
}

The code is very simple. The type of tail variable is AtomicReference to ensure atomic operation. myNode is a thread local variable of type ThreadLocal, which saves the state of the current node. myPred is a thread local variable of type ThreadLocal, which saves the state of waiting nodes.

test

Take a look at the effect through a simple test

public static void main(String[] args) {
    Runnable runnable = new Runnable() {
        private int a;

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                a++;
            }
            System.out.println(Thread.currentThread().getName() + " a = " + a);
        }
    };

    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
}

Declare a runnable object, execute it in the thread and accumulate from 1 to 10000, and finally print a result. In a multi-threaded environment, this a++ is not an atomic operation, so the final calculation result must be incorrect.

Thread-0 a = 11758
Thread-1 a = 15091
Thread-2 a = 18309
Thread-3 a = 18831
Thread-4 a = 23398
Thread-5 a = 23686
Thread-6 a = 33686

This is the result after running it once, as expected. Then add the lock to see

public static void main(String[] args) {
    CLHLock lock = new CLHLock();

    Runnable runnable = new Runnable() {
        private int a;

        @Override
        public void run() {
            lock.lock();
            for (int i = 0; i < 10000; i++) {
                a++;
            }
            System.out.println(Thread.currentThread().getName() + " a = " + a);
            lock.unlock();
        }
    };

    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
}

A CLHLock object is created and lock.lock() and lock.unlock() are called. Lock the content in the entire run method, that is, wait for one thread to finish running the accumulation before the next thread can continue to execute, otherwise it can only wait.

Thread-0 a = 10000
Thread-1 a = 20000
Thread-2 a = 30000
Thread-3 a = 40000
Thread-4 a = 50000
Thread-5 a = 60000
Thread-6 a = 70000

Now after running it many times, it is the same result, and locking has an effect.

Principle analysis

Let's carefully analyze the code of lock and unlock

public void lock() {
    QNode node = myNode.get();
    node.locked = true;
    QNode pred = tail.getAndSet(node);
    myPred.set(pred);
    while (pred.locked) {}
}

public void unlock() {
    QNode qnode = myNode.get();
    qnode.locked = false;
    myNode.set(myPred.get());
}

The lock code is very simple, just a few lines

Combining this picture from top to bottom, the scene is that there are 2 threads (Thread1, Thread2) who want to acquire locks to execute tasks at the same time. The execution status of Thread1 is on the left, and the execution status of Thread2 is on the right. Here myNode myPred is of threadlocal type, and the state of myNode myPred mentioned below refers to the state of QNode in myNode myPred.

The first line is the state after initialization, and each QNode is false.

The second line and the third line start to execute the lock operation, first change the state of myNode to true, and then assign the reference of myNode to tail (tail.getAndSet(node) means to set tail to node and return the original value of tail , here tail stores a QNode object), then assign the original value of tail to myPred, and judge whether the state of myPred is true through a while loop. If it is true, it means that the lock is being occupied and needs to wait. Once myPred becomes false, it means the lock is released and can be executed. Then combined with the situation of two threads, thread1 calls the lock method to obtain the lock successfully, and thread2 also calls the lock method to obtain the lock. When executing tail.getAndSet(node), set tail to thread2.myNode, and then obtain The old value of tail is set to thread2.myPred. At this time, the old value of tail is the myNode of thread1 just now. That is to say, when thread2 is executing while(pred.locked){}, it is actually waiting for the state of thread1.myNode to change false. The tail stores only the QNode of the last thread that acquired the lock. myNode has been waiting on myPred, and an exclusive lock is realized through a while loop.

The fourth line starts to execute the unlock operation. After the thread1 task is executed, the state of myNode is set to false. At this time, thread2.myPred also becomes false because it holds the reference of thread1.myNode and exits the loop. Thread2 can perform the following tasks .

The fifth line sets the value of myNode as a reference to myPred.

It seems that there is no need for the fifth line. There are many talks about this on the Internet. Let me explain my understanding. If there is no such line of code, thread2 thread is waiting for the state of thread1.myNode in the above figure, assuming that the execution speed of thread1 task is very fast, after the first judgment of thread2's while judgment, before the next judgment starts, thread1 executes the task and calls unlock Unlock, and then immediately apply for a lock to call lock, and set the status of thread1.myNode to true, and thread1 sets the tail value to thread1.myPred (at this time, the tail node stores the reference of thread2.myNode), so The two threads become a situation of waiting for each other, that is, deadlock. Then execute myNode.set(myPred.get()); when unlocking, the current myNode and thread2's myPred are no longer an object, so thread2.myPred will exit because of the fourth line of qnode.locked=false; Loop waits. In my humble opinion, here myNode.set(myPred.get()); is replaced by myNode.set(new QNode()); the effect is the same.

        In Gao Shan's personal opinion, the deadlock occurs because the thread quickly acquires the lock after releasing the lock, and the action of releasing the lock and acquiring the lock is completed between a while loop of the next thread. So the fundamental way to solve the problem is to avoid the situation where multiple threads pay attention to the same piece of memory. Whether it is myNode.set(myPred.get()); or myNode.set(new QNode()), it is to let two threads focus on different memory addresses. Using myNode.set(myPred.get()), you can reuse objects, which may be slightly better than myNode.set(new QNode()).

CLHLock diagram

Refer to the explanation of the principle of CLH lock queue and its implementation in Java . (Craig, Landin, a https://www.cnblogs.com/mrcharleshu/p/13338957.html

The situation where the deadlock occurs here has certain particularities. myNode myPred is of ThreadLocall type, and in the thread pool scenario, for thread reuse, Thread will not be destroyed once created, so ThreadLocal type variables must be used manually. Clean up (if you do not manually clean up before the next execution, the ThreadLocal type variable is still the result of the last execution), the fifth line of code above is actually the meaning of cleaning up the variable after ThreadLocal is used up, if you do not use the thread pool, even if there is no fifth line There will be no deadlock.

This code will cause a deadlock (stuck), reproducible by running it multiple times

public class CLHLock {
    ...
    public void unlock() {
        QNode qnode = myNode.get();
        qnode.locked = false;
        //myNode.set(myPred.get());
    }
    ...
}

public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    CLHLock lock = new CLHLock();

    Runnable runnable = new Runnable() {
        private int a;

        @Override
        public void run() {
            lock.lock();
            for (int i = 0; i < 100; i++) {
                a++;
            }
            System.out.println(Thread.currentThread().getName() + " a = " + a);
            lock.unlock();
        }
    };

    executorService.execute(runnable);
    executorService.execute(runnable);
    executorService.execute(runnable);
    executorService.execute(runnable);
    executorService.execute(runnable);
    executorService.execute(runnable);

    executorService.shutdown();
}

Guess you like

Origin blog.csdn.net/gaoshan12345678910/article/details/124172788