[Concurrent Programming] Talk about your understanding of locks

 

0. Preface

This article hopes to make an easy-to-understand integration for Java newcomers, aiming to eliminate the fear of various lock terms, to have a taste of the underlying implementation of each lock, but to know what to check when needed. First, we must dispel the idea that a lock can only belong to one category. In fact, this is not the case. For example, a lock can be a pessimistic lock, a reentrant lock, a fair lock, an interruptible lock, etc. at the same time, just as a person can be a man, a doctor, a fitness enthusiast, or a game player. This is not a contradiction. OK, international practice, dry goods.

0.1 synchronized与Lock

  • There are two ways to lock in Java: one is to use the synchronized keyword , and the other is to use the implementation class of the Lock interface .

So if you just want to simply add a lock without any special requirements on performance, using the synchronized keyword is enough. Since Java 5, there is another way to implement locks under the java.util.concurrent.locks package, which is Lock. In other words, synchronized is a built-in keyword in the Java language, and Lock is an interface . The implementation class of this interface implements the lock function at the code level. The specific details are not discussed in this article. If you are interested, you can study the AbstractQueuedSynchronizer class, and it can be written. Said it is awesome.

In fact, you only need to pay attention to three classes: ReentrantLock class, ReadLock class, WriteLock class.

ReentrantLock, ReadLock, and WriteLock  are the three most important implementation classes of the Lock interface. Corresponds to "reentrant locks", "read locks" and "write locks", and their use will be discussed later.

ReadWriteLock is actually a factory interface, and ReentrantReadWriteLock is the implementation class of ReadWriteLock, which contains two static internal classes, ReadLock and WriteLock. These two static inner classes respectively implement the Lock interface.

We stop delving into the source code, only from the perspective of use, what is the difference between Lock and synchronized? In the next few sections, I will sort out the concepts of various lock classifications, as well as the differences and connections between the synchronized keyword and various Lock implementation classes.

1.  Reentrant lock (recursive lock)

  • Reentrant lock: refers to the inner method of the same thread after obtaining the lock, and then entering the inner method of the thread will automatically obtain the lock  ( 前提,锁对象是同一个对象) is  similar to the door inside the home, after entering, you can enter the toilet, kitchen, etc.

  • ReentranLock (display lock) and synchronized (implicit lock) in Java are both reentrant locks. An advantage of reentrant locks is that deadlocks can be avoided to a certain extent. 

  • 隐式锁:(The lock used by the synchronized keyword) The default is a reentrant lock (synchronized block, synchronized method)

1.1  Implicit lock synchronized

  1. Each lock object has a lock counter and a pointer to the thread holding the lock
  2. When the monitorenter is executed, if the counter of the target lock object is zero, it means that it is not held by other threads. The Java virtual machine sets the holding thread of the lock object as the current thread and increments its counter by 1, otherwise it needs to wait. Until the holding thread releases the lock
  3. When monitorexit is executed, the counter of the lock object is decremented by the Java virtual machine. A counter of zero means the lock has been released

å¨è¿éæå ¥ å¾çæè¿ °

public class DemoSynchronized {

    Object object = new Object();

    public void sychronizedMethod() {
        new Thread(() -> {
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + "\t" + "外层....");
                synchronized (object) {
                    System.out.println(Thread.currentThread().getName() + "\t" + "中层....");
                    synchronized (object) {
                        System.out.println(Thread.currentThread().getName() + "\t" + "内层....");
                    }
                }
            }
        }, "A").start();
    }

    public static void main(String[] args) {
        new DemoSynchronized().sychronizedMethod();
        /**
         *  输出结果:
         *    A	外层....
         *    A	中层....
         *    A	内层....
         * */
    }
}

1.2 显示锁ReentrantLock

Note: There are as many unlocks as there are locks, and they are used in pairs; if one more or one less, other threads will be in a waiting state

public class DemoReentrantLock {
    static ReentrantLock reentrantLock=new ReentrantLock();

    public static void sendSms(){
        reentrantLock.lock();
        /*
        //reentrantLock.lock();
        注意有多少个lock,就有多少个unlock,他们是配对使用的
        如果多了一个lock(),那么会出现线程B一直处于等待状态
        * */
        reentrantLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"\t"+"sendSms");
            sendEmails();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            reentrantLock.unlock();
        }
    }

    private static void sendEmails() {
        reentrantLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"\t"+"sendEmails...");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            reentrantLock.unlock();
        }
    }

    public static void main(String[] args) {
        DemoReentrantLock phone2=new DemoReentrantLock();
        new Thread(()->{phone2.sendSms();},"A").start();
        new Thread(()->{phone2.sendSms();},"B").start();
    }
}

2. Pessimistic Locking and Optimistic Locking

A macro classification of locks is pessimistic locking and optimistic locking . Pessimistic lock and optimistic lock do not specifically refer to a certain lock (there is no Lock implementation class in Java called PessimisticLock or OptimisticLock), but two different strategies in concurrent situations.

  • Pessimistic Lock (Pessimistic Lock) is very pessimistic. Every time I get data, I think that others will modify it. So every time you get data, it will be locked. In this way, others who want to get the data are blocked until the pessimistic lock is released.
  • Optimistic Lock (Optimistic Lock) is very optimistic. Every time I get data, I think that others will not modify it. So it will not be locked, it will not be locked! But if you want to update the data, you will check before the update whether anyone else has modified the data during the period from reading to update . If it has been modified, read again, try to update again, and loop the above steps until the update is successful (of course, the thread that failed to update is also allowed to abandon the operation).

Pessimistic locks block transactions, optimistic locks roll back and retry . They have their own advantages and disadvantages. Don't think that one is necessarily better than the other.

  • Optimistic locking is suitable for the case of relatively few writes, that is, when conflicts really rarely occur, which can save the overhead of locking and increase the overall throughput of the system.
  • However, if conflicts occur frequently, the upper-level application will continue to retry, which will actually reduce the performance, so in this case it is more appropriate to use a pessimistic lock .

2.1 The basis of optimistic locking (CAS)

Speaking of optimistic locking, we must mention a concept: CAS

What is CAS?

Compare-and-Swap, that is, compare and replace, also called Compare-and-Set, compare and set .

  1. Comparison: A value A is read, before updating it to B, check whether the original value is still A (not changed by other threads).
  2. Setting: If yes, update A to B and end. [1] If not, do nothing.

The above two-step operation is atomic and can be simply understood as an instant completion, which is a one-step operation from the CPU's point of view.

With CAS, an optimistic lock can be implemented :

data = 123; // 共享数据

/* 更新数据的线程会进行如下操作 */
flag = true;
while (flag) {
    oldValue = data; // 保存原始数据
    newValue = doSomething(oldValue); 

    // 下面的部分为CAS操作,尝试更新data的值
    if (data == oldValue) { // 比较
        data = newValue; // 设置
        flag = false; // 结束
    } else {
	// 啥也不干,循环重试
    }
}
/* 
   很明显,这样的代码根本不是原子性的,
   因为真正的CAS利用了CPU指令,
   这里只是为了展示执行流程,本意是一样的。
*/

This is a simple and intuitive implementation of optimistic locking, which allows multiple threads to read at the same time (because there is no lock operation at all), but only one thread can successfully update the data and cause other threads to update the data to roll back and retry. CAS uses CPU instructions to ensure the atomicity of operations from the hardware level to achieve a lock-like effect.

Because there are no "locking" and "unlocking" operations in the entire process, the optimistic locking strategy is also called lock-free programming . In other words, optimistic locking is not actually a "lock", it is just an algorithm that retry CAS in a loop!

In fact, although optimistic lock CAS consumes CPU resources, it also avoids the switching of online text

2.2 Detailed description of pessimistic locking and optimistic locking

Almost all the locks we use in Java are pessimistic locks . Synchronized from biased locks, lightweight locks to heavyweight locks, all pessimistic locks. The Lock implementation classes provided by the JDK are all pessimistic locks. In fact, as long as there is a "lock object", it must be a pessimistic lock. Because optimistic locking is not a lock, but an algorithm that tries CAS in a loop.

  • Is there any optimistic lock in the JDK concurrency package?

Have. The atomic classes in the java.util.concurrent.atomic package are all implemented using optimistic locking.

  • The auto-increment method of the atomic class AtomicInteger is an optimistic locking strategy

  • Why do some information on the Internet believe that biased locks and lightweight locks are optimistic locks

The reason is that they use CAS at the bottom? Or is it confusing "optimistic/pessimistic" with "lightweight/weight"? In fact, when the thread preempts these locks, it is indeed a loop + CAS operation, which feels like an optimistic lock. But the crux of the problem is that when we say whether a lock is a pessimistic lock or an optimistic lock, we should always stand at the application layer to see how they lock application data, rather than stand at the bottom to see the process of preempting the lock. If a thread tries to acquire the lock and finds that it is already occupied, will it continue to read the data, and then decide whether to retry it when it needs to be updated later? For biased locks and lightweight locks, the answer is obviously no. Regardless of whether it is suspended or busy, the read operation of the application data is "blocked". From this perspective, they are indeed pessimistic locks.

related articles

  1. There is a problem here, that is, a value changes from A to B, and then from B back to A. In this case, CAS may think that the value has not changed, but it has actually changed. In this regard, there is AtomicStampedReference under the concurrent package to provide an implementation based on the version number.
  2. Java interrupt mechanism:  https://www.cnblogs.com/jiangzhaowei/p/7209949.html

3. Spin lock

There is a type of lock called a spin lock . The so-called spin, to put it bluntly, is a while(true) infinite loop.

  • Spin lock: Spin lock is an infinite loop to acquire the lock, which is different from the infinite loop of optimistic lock to set the value;

The optimistic lock just now has a similar infinite loop operation, so is it a spin lock?

It's not. Although the operation of spin and while(true) is the same, the two terms should be separated. The word "spin" specifically refers to the spin of a spin lock.

However, there is no spin lock (SpinLock) in the JDK, so what is a spin lock? You'll know after reading the next section.

  • The spin lock is an infinite loop to acquire the lock. This difference is different from the infinite loop of optimistic lock to set the value;

4. Synchronized lock upgrade: bias lock→lightweight lock→heavyweight lock

synchronized from the lock escalation will not lock upgrade is biased locking , upgrade to a lightweight lock , and finally upgraded to heavyweight lock , so where is the spin lock it? The lightweight lock here is a spin lock .

      

When the synchronized code block is executed for the first time, the lock object becomes a biased lock (the lock flag in the object header is modified through CAS), which literally means "biased to the first thread to obtain it" lock. After executing the synchronization code block, the thread does not actively release the bias lock . When the synchronization code block is reached for the second time, the thread will determine whether the thread holding the lock is itself (the thread ID holding the lock is also in the object header), and if it is, it will execute normally. Since the lock has not been released before, there is no need to re-enable the lock here. If there is only one thread that uses the lock from beginning to end, it is obvious that there is almost no additional overhead in favor of the lock, and the performance is extremely high.

Once a second thread joins the lock competition , the bias lock is upgraded to a lightweight lock (spin lock) . Here we need to clarify what is lock competition: if multiple threads acquire a lock in turn, but each time the lock is acquired, it goes smoothly and no blocking occurs, then there is no lock competition. Only when a thread tries to acquire a lock and finds that the lock is already occupied and can only wait for it to be released, then lock contention occurs.

In the light-weight lock state, the lock competition continues, and the thread that has not grabbed the lock will spin , that is, it will continue to loop to determine whether the lock can be successfully acquired. The operation of acquiring the lock is actually to modify the lock flag in the object header through CAS. First compare whether the current lock flag is "released", and if it is, set it to "locked". The comparison and setting occur atomically . This is considered to have grabbed the lock, and then the thread modifies the current lock holder information to itself.

Long-term spin operations are very resource intensive. If a thread holds a lock, other threads can only consume the CPU in place and cannot perform any effective tasks. This phenomenon is called busy-waiting . If multiple threads use a lock, but no lock contention occurs, or a very slight lock contention occurs, then synchronized uses a lightweight lock to allow short-term busyness. This is a compromise idea, short-term busyness, etc., in exchange for the overhead of thread switching between user mode and kernel mode.

Obviously, this busy wait is limited (there is a counter to record the number of spins, and 10 cycles are allowed by default, which can be changed by virtual machine parameters). If the lock competition is serious, a thread that reaches the maximum number of spins will upgrade the lightweight lock to a heavyweight lock (the CAS still modifies the lock flag, but does not modify the thread ID that holds the lock). When the subsequent thread tries to acquire the lock and finds that the occupied lock is a heavyweight lock, it directly suspends itself (instead of busy waiting) and waits for it to be awakened in the future. Before JDK1.6,

Synchronized directly adds heavyweight locks, obviously now it has been well optimized.

A lock can only be gradually upgraded in the order of biased locks, lightweight locks, and heavyweight locks (also called lock expansion ), and downgrades are not allowed.

A feature of biased locks is that the thread holding the lock will not release the lock when it finishes executing the synchronized code block. So when the second thread executes to this synchronized code block, will lock contention occur and then upgrade to a lightweight lock?
After thread A executes the synchronization code block for the first time, when thread B tries to acquire the lock, it is found to be a biased lock, and it will be judged whether thread A is still alive. If thread A is still alive, thread A is suspended. At this time, the bias lock is upgraded to a lightweight lock. After that, thread A continues to execute and thread B spins. But if the judgment result is that thread A does not exist , thread B holds this biased lock, and the lock is not upgraded.
There are still people who have doubts about this. I did not describe it clearly before, but if there are too many new concepts to be expanded, I can open a new article. What's more, there are some things that are too low-level, I have not read the source code, and I am not confident that I must be right. In fact, before upgrading to a lightweight lock, the virtual machine allows thread A to suspend at a safe point as soon as possible, and then “forge” some information in its stack, so that thread A will think that it has been holding light after being awakened. Magnitude lock. If thread A was in a synchronized code block before, then thread B can spin and wait. If thread A was not in the synchronized code block before, it will check this situation after being awakened and immediately release the lock so that thread B can get it. I haven't studied this part of the content in depth before. If anything is wrong, please advise!

5. Fair locks and unfair locks

If multiple threads apply for a fair lock , when the lock is released, the one that applies first gets the first one, which is very fair. Obviously if it is unfair lock , the thread after the application is likely to first obtain the lock, random or in accordance with other prioritized.

For the ReentrantLock class, passing parameters through the constructor can specify whether the lock is a fair lock, and the default is an unfair lock . In general, the throughput of unfair locks is larger than that of fair locks. If there is no special requirement, unfair locks are preferred.

  • The ReentrantLock constructor can be specified as fair or unfair
  • For synchronized, it is also an unfair lock , but there is no way to make it a fair lock.

6. Interruptible lock

Interruptible lock, literally means "a lock that can respond to interruption ".

The key here is to understand what an interrupt is . Java does not provide any method to directly interrupt a thread, only an interrupt mechanism . What is "interrupt mechanism"? Thread A sends a "please stop running" request to thread B (thread B can also send this request to itself), but thread B does not stop running immediately, but chooses the right time to respond to the interrupt in its own way , You can also ignore this interrupt directly. In other words, Java's interruption cannot directly terminate the thread , but the interrupted thread needs to decide how to deal with it. This is like the parents telling their children to pay attention to the body, but whether the children pay attention to the body or how to pay attention to the body is entirely up to them. [2]

Back to the topic of locks, if thread A holds a lock, thread B waits to acquire the lock. Because thread A has held the lock for too long, thread B does not want to wait anymore. We can let thread B interrupt itself or interrupt it in other threads. This is an interruptible lock .

In Java, synchronized is an uninterruptible lock , and the implementation classes of Lock are all interruptible locks. You can simply look at the Lock interface.

/* Lock接口 */
public interface Lock {

    void lock(); // 拿不到锁就一直等,拿到马上返回。

    // 拿不到锁就一直等,如果等待时收到中断请求,则需要处理InterruptedException。
    void lockInterruptibly() throws InterruptedException;  

    // 无论拿不拿得到锁,都马上返回。拿到返回true,拿不到返回false。
    boolean tryLock(); 

    // 同上,可以自定义等待的时间。
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();

    Condition newCondition();
}

7. Read-write locks, shared locks, mutex locks

The read-write lock is actually a pair of locks, a read lock (shared lock) and a write lock (mutual exclusion lock, exclusive lock).

Look at the ReadWriteLock interface in Java. It only specifies two methods, one returns a read lock and the other returns a write lock.

Remember the optimistic locking strategy from before? All threads can read at any time, and only judge whether the value has been changed before writing.

The read-write lock actually does the same thing, but the strategy is slightly different. In many cases, the thread knows whether it is to update it after reading the data. So why not clarify this directly when locking? If I read the value to update it (SQL for update is what it means), then when I lock the lock , I directly add the write lock . When I hold the write lock, other threads need to wait for both reading and writing; if I The read data is only for front-end display, then a read lock is explicitly added when the lock is locked. If other threads also need to add a read lock, they can directly acquire it without waiting (read lock counter + 1).

Although read-write locks feel a bit like optimistic locks, read-write locks are a pessimistic locking strategy . Because the read-write lock does not determine whether the value has been modified before the update , but decides whether to use a read lock or a write lock before locking. Optimistic locking specifically refers to lock-free programming. If you still have doubts, you can go back to the first and second sections to see what "optimistic locking" is.

The only implementation class of the ReadWriteLock interface provided by the JDK is ReentrantReadWriteLock. Just look at the name, it not only provides read-write locks, but also reentrant locks. In addition to the two interface methods, ReentrantReadWriteLock also provides some methods to facilitate the outside world to monitor its internal working status, which will not be expanded here.

 

 

 

Guess you like

Origin blog.csdn.net/qq_41893274/article/details/113801002