I asked about StampedLock in the interview, but I don’t know anything...

Like it and look again, develop a habit, search on WeChat [ San Taizi Ao Bing ] to follow this fool who seems to have something

This article has been included on GitHub https://github.com/JavaFamily , with complete test sites, materials, resume templates, and my program life.

Preface

In multi-threaded development, in order to control thread synchronization, the most commonly used keyword is the synchronized keyword and reentrant lock. In JDK8, a new weapon, StampedLock, was introduced. What is this thing? The English word Stamp means postmark. What is the meaning of it here? Fool, please see the breakdown below.

In the face of the problem of resource management in critical regions, there are generally two sets of ideas:

The first is to use a pessimistic strategy. The pessimist thinks that: every time I access the shared variable in the critical section, someone will always conflict with me. Therefore, every time I access the shared variable, I must first lock the entire object, and then unlock it after the access is completed.

On the contrary, the optimists believe that although the shared variables in the critical section will conflict, the conflict should be a small probability event. In most cases, it should not happen. Therefore, I can visit first. If I wait for it to use If there is no conflict after the data is over, then my operation is successful; if I find that there is a conflict after I finish using it, then I will either try again or switch to a pessimistic strategy.

It is not difficult to see from here that reentrant locking and synchronized are a typical pessimistic strategy. Smart, you must have guessed it. StampedLock provides a tool for optimistic locking. Therefore, it is an important supplement to reentrant locks.

Basic use of StampedLock

A very good example is provided in the StampedLock documentation, so that we can quickly understand the use of StampedLock. Let me take a look at this example below. The explanation about it is written in the comments.

Here is another explanation of the meaning of the validate() method. The function signature looks like this:

public boolean validate(long stamp)

Its acceptance parameter is the postmark returned by the last lock operation. If the lock has not been applied for a write lock before calling validate(), it returns true, which also means that the shared data protected by the lock has not been modified, so the previous The read operation is sure to ensure data integrity and consistency.

Conversely, if the lock is successfully applied for a write lock before validate(), it means that the previous data read and write operations conflict, and the program needs to retry or upgrade to a pessimistic lock.

Comparison with reentrant lock

From the above example, it is not difficult to see that in terms of programming complexity, StampedLock is actually much more complicated than reentry locks, and the code is not as concise as before.

So why should we use it?

The most essential reason is to improve performance! Generally speaking, the performance of this optimistic lock is several times faster than the ordinary reentrant lock, and as the number of threads continues to increase, the performance gap will become larger and larger.

In short, in a large number of concurrent scenarios, the performance of StampedLock is to crush reentrant locks and read-write locks.

But after all, there is no perfect thing in the world, and StampedLock is not omnipotent. Its disadvantages are as follows:

  1. Coding is more troublesome, if you use optimistic reading, then the conflicting scene should be handled by yourself
  2. It is not reentrant. If you accidentally call it twice in the same thread, your world will be clean. . . . .
  3. It does not support wait/notify mechanism

If the above 3 points are not a problem for you, then I believe StampedLock should be your first choice.

Internal data structure

In order to help you better understand StampedLock, here is a brief introduction to its internal implementation and data structure.

In StampedLock, there is a queue in which threads waiting on the lock are stored. The queue is a linked list, and the element in the linked list is an object called WNode:

When there are several threads waiting in the queue, the entire queue may look like this:

In addition to this waiting queue, another particularly important field in StampedLock is long state, which is a 64-bit integer. The use of StampedLock is very clever.

The initial value of state is:

private static final int LG_READERS = 7;
private static final long WBIT  = 1L << LG_READERS;
private static final long ORIGIN = WBIT << 1;

That is...0001 0000 0000 (there are too many zeros in front, so I won’t write them, let’s make up 64~), why not use 0 as the initial value here? Because 0 has a special meaning, in order to avoid conflicts, a non-zero number was chosen.

If there is a write lock occupied, then set the 7th bit to 1 …0001 1 000 0000, that is, add WBIT.

Each time the write lock is released, 1 is added, but instead of adding state directly, the last byte is removed, and only the first 7 bytes are used for statistics. Therefore, after the write lock is released, the state becomes: …0010 0000 0000, and the lock is added again, and it becomes: …0010 1000 0000, and so on.

Why is the number of write lock releases recorded here?

This is because the state of the entire state is judged based on CAS operations. Ordinary CAS operations may encounter ABA problems. If the number of times is not recorded, then when the write lock is released, applied for, and released again, we will not be able to determine whether the data has been written. The number of releases is recorded here, so when "release->application->release" occurs, the CAS operation can check the data change, so as to determine that the write operation has occurred. As an optimistic lock, it can be accurate It is judged that the conflict has occurred, and the rest is left to the application to resolve the conflict. Therefore, the number of lock releases recorded here is to accurately monitor thread conflicts.

The 7 bits of the remaining byte of the state are used to record the number of threads that read the lock. Since there are only 7 bits, only 126 can be recorded. Look at the RFULL in the code below, which is the number of read threads that are fully loaded. . What to do if it exceeds, the extra part is recorded in the readerOverflow field.

    private static final long WBIT  = 1L << LG_READERS;
    private static final long RBITS = WBIT - 1L;
    private static final long RFULL = RBITS - 1L;
    private transient int readerOverflow;

To summarize, the structure of the state variable is as follows:

Application and release of write lock

After understanding the internal data structure of StampedLock, let's take a look at the application and release of write locks! The first is to apply for a write lock:

    public long writeLock() {
    
    
        long s, next;  
        return ((((s = state) & ABITS) == 0L &&  //有没有读写锁被占用,如果没有,就设置上写锁标记
                 U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?
                //如果写锁占用成功范围next,如果失败就进入acquireWrite()进行锁的占用。
                next : acquireWrite(false, 0L));
    }

If CAS fails to set the state, it means that the write lock application has failed. At this time, acquireWrite() will be called to apply or wait. acquireWrite() basically does the following things:

  1. Join the team
    1. If the head node is equal to the tail node wtail == whead, it means it’s my turn, so I’ll spin and wait, and if I get it, it’s over.
    2. If the wtail==nullqueue is not initialized, just initialize the queue
    3. If there are other waiting nodes in the queue, you can only join the queue honestly and wait
  2. Block and wait
    1. If the head node is equal to the front node (h = whead) == p), it means it’s my turn to spin and wait for contention.
    2. Otherwise wake up the reader thread in the head node
    3. If the lock cannot be preempted, then park() the current thread

Simply put, the acquireWrite() function is used to compete for the lock, and its return value is the postmark representing the current lock state. At the same time, in order to improve the performance of the lock, acquireWrite() uses a large number of spin retries. Therefore, it The code looks a bit obscure.

The release of the write lock is as follows, the incoming parameter of unlockWrite() is the postmark obtained when applying for the lock:

    public void unlockWrite(long stamp) {
    
    
        WNode h;
        //检查锁的状态是否正常
        if (state != stamp || (stamp & WBIT) == 0L)
            throw new IllegalMonitorStateException();
        // 设置state中标志位为0,同时也起到了增加释放锁次数的作用
        state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
        // 头结点不为空,尝试唤醒后续的线程
        if ((h = whead) != null && h.status != 0)
            //唤醒(unpark)后续的一个线程
            release(h);
    }

Application and release of read lock

The code to acquire the read lock is as follows:

    public long readLock() {
    
    
        long s = state, next;  
        //如果队列中没有写锁,并且读线程个数没有超过126,直接获得锁,并且读线程数量加1
        return ((whead == wtail && (s & ABITS) < RFULL &&
                 U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
                //如果争抢失败,进入acquireRead()争抢或者等待
                next : acquireRead(false, 0L));
    }

The implementation of acquireRead() is quite complicated, and is roughly divided into these steps:

In short, it is spin, spin and spin, and use continuous spinning to avoid the thread being really suspended as much as possible. Only when the spin fails sufficiently, will the thread be really allowed to wait.

The following is the process of releasing the read lock:

StampedLock pessimistic reading is full of CPU problem

StampedLock is certainly a good thing, but because of its complexity, it is inevitable that there will be some minor problems. The following example demonstrates the problem of StampedLock's pessimistic lock crazily occupying the CPU:

public class StampedLockTest {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        final StampedLock lock = new StampedLock();
        Thread t1 = new Thread(() -> {
    
    
            // 获取写锁
            lock.writeLock();
            // 模拟程序阻塞等待其他资源
            LockSupport.park();
        });
        t1.start();
        // 保证t1获取写锁
        Thread.sleep(100);
        Thread t2 = new Thread(() -> {
    
    
            // 阻塞在悲观读锁
            lock.readLock();
        });
        t2.start();
        // 保证t2阻塞在读锁
        Thread.sleep(100);
        // 中断线程t2,会导致线程t2所在CPU飙升
        t2.interrupt();
        t2.join();
    }
}

In the above code, after interrupting t2, the CPU occupancy rate of t2 will be 100%. At this time, t2 is blocking the readLock() function. In other words, after being interrupted, the read lock of StampedLock may fill up the CPU. What is the reason for this? The little fool of the mechanism must have thought about it, this is caused by too many spins in StampedLock! Yes, your guess is correct.

The specific reasons are as follows:

If there is no interruption, the thread blocked on readLock() will enter park() waiting after several spins. Once it enters park() waiting, it will not occupy the CPU. But there is a feature of the park() function, that is, once the thread is interrupted, park() will return immediately. If the return is not counted, it will not throw you an exception or something, which is embarrassing. Originally, you wanted to unpark() the thread when the lock was ready, but now the lock is not good, you interrupt it directly, and park() also returns, but after all, the lock is not good, so you go to Spin.

Turning around, I turned to the park() function, but sadly, the thread's interrupt flag is always on, and park() can't block, so the next spin starts again, endless The spinning can't stop, so the CPU is full.

To solve this problem, in essence, it needs to be inside StampedLock. When park() returns, it needs to judge the interrupt mark and make the correct treatment, such as exiting, throwing an exception, or clearing the interrupt bit, all of which can solve the problem. .

Unfortunately, at least in JDK8, there is no such treatment. Therefore, the above problem occurs when the CPU is full after interrupting readLock(). Please pay attention.

Write at the end

Today, we have introduced the use and main realization ideas of StampedLock more carefully. StampedLock is an important supplement to reentrant locks and read-write locks.

It provides an optimistic locking strategy, which is a unique lock implementation. Of course, in terms of programming difficulty, StampedLock is a bit more cumbersome than reentry locks and read-write locks, but it brings a double performance increase.

Here are some tips for everyone. If the number of our application threads is controllable, and there are not many, and the competition is not too fierce, then we can directly use simple synchronized, re-entry locks, read-write locks, and just fine; if the number of application threads is Many, fierce competition, and sensitive to performance, then we still need to work hard and use the more complicated StampedLock to improve the throughput of the program.

There are also two points to pay special attention to when using StampedLock: First, StampedLock is not reentrant. Don’t single-thread yourself and deadlock yourself. Second, StampedLock does not have a wait/notify mechanism. If you must need this function, it will only I can go around!

Do little fools have a deeper understanding of this unpopular category? Understand that there can be a wave in the comment area: become stronger

This is Ao Bing. The more you know, the more you don’t know. See you next time.


Ao Bing compiled his interview essays into an e-book with 1,630 pages!

Full of dry goods, the essence of every word. The content is as follows, there are interview questions and resume templates that I summarized during the review, and I now give them to everyone for free.

Link: https://pan.baidu.com/s/1ZQEKJBgtYle3v-1LimcSwg Password:wjk6


The article is continuously updated. You can search for " San Tai Zi Ao Bing " on WeChat to read it for the first time, and reply to [ Information ] I have prepared the interview information and resume template of the first-line manufacturers. This article has been included on GitHub https://github.com/JavaFamily , There are complete test sites for interviews with major factories, welcome to Star.

Guess you like

Origin blog.csdn.net/qq_35190492/article/details/115290027