Java concurrent programming-deep understanding of spin lock

1. What is a spin lock

Spinlock: refers to when a thread is acquiring a lock, if the lock has been acquired by other threads, then the thread will wait in a loop, and then constantly determine whether the lock can be acquired successfully, until the lock is acquired Exit the loop.
The thread that acquires the lock has been active, but has not performed any effective tasks. Using this lock will cause busy-waiting .

2. How does Java implement spin lock?

Let’s take a look at an example of implementing a spin lock. The java.util.concurrent package provides many classes for concurrent programming. Using these classes will have better performance on a multi-core CPU machine. The main reason is that most of these classes are used. (Failure-retry mode) Optimistic locking instead of pessimistic locking in synchronized mode.


class spinlock {
    private AtomicReference<Thread> cas;
    spinlock(AtomicReference<Thread> cas){
        this.cas = cas;
    }
    public void lock() {
        Thread current = Thread.currentThread();
        // 利用CAS
        while (!cas.compareAndSet(null, current)) { //为什么预期是null??
            // DO nothing
            System.out.println("I am spinning");
        }
    }

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

The CAS used by the lock() method. When the first thread A acquires the lock, it can successfully acquire it without entering the while loop. If thread A does not release the lock at this time, another thread B acquires the lock again. Since the CAS is not satisfied, it will enter the while loop, and continue to determine whether the CAS is satisfied, until the A thread calls the unlock method to release the lock.

Spinlock verification code

package ddx.多线程;

import java.util.concurrent.atomic.AtomicReference;

public class 自旋锁 {
    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();
    }

}

//自旋锁验证
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(); //上锁
        for (int i = 0; i < 10; i++) {
            //Thread.yield();
            System.out.println(i);
        }
        slock.unlock();
    }
}

A spin lock cas was created through the previous AtomicReference class, and then two threads were created and executed separately. 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-deep understanding of spin lock

Through the analysis of the output results, we can know that, first of all, assume that the thread has acquired the lock when executing the lock method.

cas.compareAndSet(null, current)

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

And thread two also enters the lock method at this time, and found that expect value! = update value, then enter the while loop and print

i am spinning. It can be concluded from the following red letters that a thread in Java does not always occupy the cpu time slice and has been executed all the time, but uses preemptive scheduling, so the above two threads alternate execution phenomenon

The realization of Java threads is mapped to the lightweight threads of the system. The lightweight threads have corresponding kernel threads of the system. The scheduling of the kernel threads is scheduled by the system scheduler, so the thread scheduling method of Java depends on the system kernel scheduling It just happens that the thread implementations of mainstream operating systems are preemptive.

3. Problems with spin locks

The use of spin locks has the following problems:
1. If a thread holds the lock for too long, it will cause other threads waiting to acquire the lock to enter the loop waiting, consuming CPU. Improper use will cause extremely high CPU usage.
2. The spin lock implemented by Java above is not fair, that is, it cannot satisfy the thread with the longest waiting time to obtain the lock first. Unfair locks will have "thread starvation" problems.

4. Advantages of spin lock

  1. The spin lock will not cause the thread state to switch, and it will always be in the user state, that is, the thread will always be active; it will not cause the thread to enter the blocking state, which reduces unnecessary context switching, and the execution speed is fast
  2. The non-spin lock enters the blocking state when the lock is not acquired, and thus enters the kernel state. When the lock is acquired, it needs to recover from the kernel state and requires a thread context switch. (After the thread is blocked, it enters the kernel (Linux) scheduling state. This will cause the system to switch back and forth between the user mode and the kernel mode, which will seriously affect 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 reentrancy, that is, when a thread has acquired the lock for the first time, it will acquire the lock again before the lock is released. It cannot be successfully obtained the second time. Since CAS is not satisfied, the second acquisition will enter the while loop and wait, and if it is a reentrant lock, the second acquisition should be successful.
Moreover, even if it can be successfully acquired the second time, when the lock is released for the first time, the lock acquired the second time will be released, which is unreasonable.

For example, change the code to the following:

@Override
    public void run() {
        slock.lock(); //上锁
        slock.lock(); //再次获取自己的锁!由于不可重入,则会陷入循环
        for (int i = 0; i < 10; i++) {
            //Thread.yield();
            System.out.println(i);
        }
        slock.unlock();
    }

The running result will be printed indefinitely, falling into an endless loop! 

In order to achieve reentrant locks, we need to introduce a counter to record the number of threads acquiring 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()) { // 如果当前线程已经获取到了锁,线程数增加一,然后返回
            count++;
            return;
        }
        // 如果没获取到锁,则通过CAS自旋
        while (!cas.compareAndSet(null, current)) {
            // DO nothing
        }
    }
    public void unlock() {
        Thread cur = Thread.currentThread();
        if (cur == cas.get()) {
            if (count > 0) {// 如果大于0,表示当前线程多次获取了该锁,释放锁通过count减一来模拟
                count--;
            } else {// 如果count==0,可以将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。
                cas.compareAndSet(cur, null);
            }
        }
    }
}

In the same way, the lock method will first determine whether the current thread has acquired the lock, and then increment the count by one, reentrant, and then return directly! The unlock method will first determine whether the current thread has obtained the lock. If it does, it will first determine the counter, keep decrementing by one, and keep unlocking!

 Reentrant spinlock code 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(); //上锁
        slock.lock(); //再次获取自己的锁!没问题!
        for (int i = 0; i < 10; i++) {
            //Thread.yield();
            System.out.println(i);
        }
        slock.unlock(); //释放一层,但此时count为1,不为零,导致另一个线程依然处于忙循环状态,所以加锁和解锁一定要对应上,避免出现另一个线程永远拿不到锁的情况
        slock.unlock();
    }
}

6. The similarities and differences between spin locks and mutex locks

  • Both spin locks and mutual exclusion locks are mechanisms for protecting resource sharing.
  • Whether it is a spin lock or a mutex lock, there can be at most one holder at any time.
  • If the thread acquiring the mutex lock is already occupied, the thread will enter the sleep state; the thread acquiring the spin lock will not sleep, but will wait for the lock to be released in a loop.

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, and the thread will always be in user mode and active.
  • If the spin lock holds the lock for too long, it will cause other threads waiting to acquire the lock to exhaust the CPU.
  • Spinlock itself cannot guarantee fairness, nor can it guarantee reentrancy.
  • Based on spin locks, a fair and reentrant lock can be realized

end

This is the end of this article. Thank you for seeing the last friends. They are all seen at the end. Just give me a thumbs up before leaving. If there is anything wrong, please correct me.

Guess you like

Origin blog.51cto.com/14969174/2542996