Java Concurrent Programming — a deep understanding of spin lock

Time:2021-2-27

1. What is spin lock

Spinlock: when a thread is acquiring a lock, if the lock has been acquired by other threads, the thread will wait in a loop, and then continuously judge whether the lock can be acquired successfully. It will not exit the loop until the lock is acquired.

The thread that gets the lock is always active, but it doesn’t perform any effective task. Using this lock will cause a lot of problemsbusy-waiting

2. How does Java implement spin lock?

Let’s look at an example of spin locking, java.util.concurrent The package provides many classes for concurrent programming. Using these classes will have better performance on multi-core CPU machines. The main reason is that most of these classes use optimistic locks (fail and retry mode) rather than pessimistic locks (synchronized mode)


class spinlock {
    private AtomicReference<Thread> cas;
    spinlock(AtomicReference<Thread> cas){
        this.cas = cas;
    }
    public void lock() {
        Thread current = Thread.currentThread();
        //Using CAS
        while (!cas.compareAndSet(null, current)) { //Why is expectation null??
            // DO nothing
            System.out.println("I am spinning");
        }
    }

    public void unlock() {
        Thread current = Thread.currentThread();
        cas.compareAndSet(current, null);
    }
}

Lock() method uses CAS. When the first thread a acquires the lock, it can acquire it successfully and will not enter the while loop. If thread a does not release the lock at this time, another thread B acquires the lock again. At this time, because CAS is not satisfied, it will enter the while loop and judge whether CAS is satisfied until thread a calls the unlock method to release the lock.

Spin lock verification code

package ddx.Multithreading;

import java.util.concurrent.atomic.AtomicReference;

public classSpin lock{
    public static void main(String[] args) {
        AtomicReference<Thread> cas = new AtomicReference<Thread>();
        Thread thread1 = new Thread(new Task(cas));
        Thread thread2 = new Thread(new Task(cas));
        thread1.start();
        thread2.start();
    }


}

//Spin lock verification
class Task implements Runnable {
    private AtomicReference<Thread> cas;
    private spinlock slock ;

    public Task(AtomicReference<Thread> cas) {
        this.cas = cas;
        this.slock = new spinlock(cas);
    }

    @Override
    public void run() {
        slock.lock(); //Lock
        for (int i = 0; i < 10; i++) {
            //Thread.yield();
            System.out.println(i);
        }
        slock.unlock();
    }
}

A spin lock CAS is created through the previous atomicreference class, and then two threads are created and executed respectively. The results are as follows:

0
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
I am spin
1
I am spin
I am spin
I am spin
I am spin
I am spin
2
3
4
5
6
7
8
9
I am spin
0
1
2
3
4
5
6
7
8
9

Java Concurrent Programming -- a deep understanding of spin lock

Through the analysis of the output results, we can know that, first of all, we assume that thread one obtains the lock when executing the lock method

cas.compareAndSet(null, current)

Change the reference to thread one, skip the while loop, and execute the print function

Thread 2 also enters the lock method at this time. When performing the comparison operation, it is found that expect value! =Update value, then enter the while loop and print

i am spinning。 From the following scarlet letter, we can draw a conclusion that a thread in Java does not always occupy the CPU time slice and finish execution all the time, but adopts preemptive scheduling, so the above two threads execute alternately

The implementation of java thread is mapped to the lightweight thread of the system. The lightweight thread has the corresponding kernel thread of the system, and the scheduling of kernel thread is scheduled by the system scheduler. Therefore, the scheduling mode of java thread depends on the kernel scheduler of the system, but the thread implementation of the mainstream operating system is preemptive.

3. The problems of spin lock

There is one problem when using spin lock

1. If a thread holds the lock for a long time, it will cause other threads waiting for the lock to enter the cycle waiting and consume CPU. Improper use will result in high CPU utilization.

2. The spin lock implemented by Java above is not fair, that is, it can not satisfy the priority of the thread with the longest waiting time to obtain the lock. Unfair locks can lead to “thread starvation”.

4. Advantages of spin lock

  1. Spin lock will not make the thread state switch, always in the user state, that is, the thread is always active; it will not make the thread into the blocking state, reduce unnecessary context switching, and the execution speed is fast
  2. When the lock cannot be acquired, the non spin lock will enter the blocking state and enter the kernel state. When the lock is acquired, it needs to recover from the kernel state and need to switch the thread context. (when the thread is blocked, it will enter the kernel (Linux) scheduling state, which will cause the system to switch back and forth between the user state and the kernel state, seriously affecting the performance of the lock.)

5. Reentrant spin lock and non reentrant spin lock

A careful analysis of the code at the beginning of the article shows that it does not support reentry. That is, when a thread has acquired the lock for the first time, it reacquires the lock again before the lock is released, and it cannot acquire it successfully for the second time. Because CAS is not satisfied, the second acquisition will enter the while loop waiting. If it is a re entrant lock, the second acquisition should be successful.

Moreover, even if it can be acquired successfully for the second time, when the lock is released for the first time, the lock acquired for the second time will also be released, which is unreasonable.

For example, change the code as follows:

@Override
    public void run() {
        slock.lock(); //Lock
        slock.lock(); //Get your lock again! Because it is not reentrant, it will fall into a cycle
        for (int i = 0; i < 10; i++) {
            //Thread.yield();
            System.out.println(i);
        }
        slock.unlock();
    }

Then the running result will be printed infinitely and fall into endless cycle!

In order to implement the reentrant lock, we need to introduce a counter to record the number of threads obtaining the lock.

public class ReentrantSpinLock {
    private AtomicReference<Thread> cas = new AtomicReference<Thread>();
    private int count;
    public void lock() {
        Thread current = Thread.currentThread();
        if (current == cas.get()) { //If the current thread has acquired the lock, the number of threads is increased by one, and then returns
            count++;
            return;
        }
        //If the lock is not acquired, it can be spun through CAS
        while (!cas.compareAndSet(null, current)) {
            // DO nothing
        }
    }
    public void unlock() {
        Thread cur = Thread.currentThread();
        if (cur == cas.get()) {
            if (count > 0) {//If it is greater than 0, it means that the current thread has acquired the lock many times, and releasing the lock is simulated by count subtraction
                count--;
            } else {//If count = = 0, the lock can be released, so that the number of times to acquire the lock is consistent with the number of times to release the lock.
                cas.compareAndSet(cur, null);
            }
        }
    }
}

Similarly, the lock method will first determine whether the current thread has obtained the lock. Once it has obtained the lock, it will let count add one, which can be re entered, and then return directly! The unlock method will first determine whether the current thread has got the lock. If it has, it will first determine the counter, continuously decrease one, and continuously unlock!

Code verification of reentrant spin lock


//Reentrant spin lock verification
class Task1 implements Runnable{
    private AtomicReference<Thread> cas;
    private ReentrantSpinLock slock ;

    public Task1(AtomicReference<Thread> cas) {
        this.cas = cas;
        this.slock = new ReentrantSpinLock(cas);
    }

    @Override
    public void run() {
        slock.lock(); //Lock
        slock.lock(); //Get your lock again! no problem!
        for (int i = 0; i < 10; i++) {
            //Thread.yield();
            System.out.println(i);
        }
        slock.unlock(); //Release a layer, but at this time, count is 1, which is not zero, resulting in another thread still in the busy cycle state, so locking and unlocking must be corresponding to avoid another thread never getting the lock
        slock.unlock();
    }
}

6. Similarities and differences between spin lock and mutex

  • Both spin lock and mutex lock are mechanisms to protect resource sharing.
  • No matter spin lock or mutex lock, there can only be one holder at most at any time.
  • If the lock has been occupied, the thread that acquires the mutex will go to sleep; the thread that acquires the spin lock will not sleep, but will wait for the lock to be released.

7. Summary

  • Spin lock: when a thread acquires a lock, if the lock is held by another thread, the current thread will wait in a loop until the lock is acquired.
  • During the spin lock waiting period, the state of the thread will not change. The thread is always in user state and active.
  • If the lock is held for too long, other threads waiting for the lock will run out of CPU.
  • Spin lock itself can not guarantee fairness, but also can not guarantee reentry.
  • Based on spin lock, the lock with fairness and reentrant property can be realized.

This work adoptsCC agreementReprint must indicate the author and the link of this article

Recommended Today

Large scale distributed storage system: Principle Analysis and architecture practice.pdf

Focus on “Java back end technology stack” Reply to “interview” for full interview information Distributed storage system, which stores data in multiple independent devices. Traditional network storage system uses centralized storage server to store all data. Storage server becomes the bottleneck of system performance and the focus of reliability and security, which can not meet […]