美文网首页程序员干货分享
Java并发编程——深入理解自旋锁

Java并发编程——深入理解自旋锁

作者: 小白菜aaa | 来源:发表于2020-10-22 20:46 被阅读0次

    1.什么是自旋锁

    自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
    获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting

    2.Java如何实现自旋锁?

    先看一个实现自旋锁的例子,java.util.concurrent包里提供了很多面向并发编程的类. 使用这些类在多核CPU的机器上会有比较好的性能.主要原因是这些类里面大多使用(失败-重试方式的)乐观锁而不是synchronized方式的悲观锁.

    
    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);
        }
    }
    

    lock()方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。

    自旋锁验证代码

    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();
        }
    }
    

    通过之前的AtomicReference类创建了一个自旋锁cas,然后创建两个线程,分别执行,结果如下:

    
    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
    
    

    通过对输出结果的分析我们可以得知,首先假定线程一在执行lock方法的时候获得了锁,通过方法

    cas.compareAndSet(null, current)

    将引用改为线程一的引用,跳过while循环,执行打印函数

    而线程二此时也进入lock方法,在执行比较操作的时候发现,expect value != update value,于是进入while循环,打印

    i am spinning。由以下红字可以得出结论,Java中的一个线程并不是总是占着cpu时间片不放,一直执行完的,而是采用抢占式调度,所以出现了上面两个线程交替执行的现象

    Java线程的实现是通过映射到系统的轻量级线程上,轻量级线程有对应系统的内核线程,内核线程的调度由系统调度器来调度的,所以Java的线程调度方式取决于系统内核调度器,只不过刚好目前主流操作系统的线程实现都是抢占式的。

    3.自旋锁存在的问题

    使用自旋锁会有以下一个问题:
    1. 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
    2. 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

    4.自旋锁的优点

    1. 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
    2. 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

    5.可重入的自旋锁和不可重入的自旋锁

    文章开始的时候的那段代码,仔细分析一下就可以看出,它是不支持重入的,即当一个线程第一次已经获取到了该锁,在锁释放之前又一次重新获取该锁,第二次就不能成功获取到。由于不满足CAS,所以第二次获取会进入while循环等待,而如果是可重入锁,第二次也是应该能够成功获取到的。
    而且,即使第二次能够成功获取,那么当第一次释放锁的时候,第二次获取到的锁也会被释放,而这是不合理的。

    例如将代码改成如下:

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

    则运行结果将会无限打印,陷入无终止的循环!

    为了实现可重入锁,我们需要引入一个计数器,用来记录获取锁的线程数。

    
    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);
                }
            }
        }
    }
    

    同样lock方法会先判断是否当前线程已经拿到了锁,拿到了就让count加一,可重入,然后直接返回!而unlock方法则会首先判断当前线程是否拿到了锁,如果拿到了,就会先判断计数器,不断减一,不断解锁!

    可重入自旋锁代码验证

    
     
    //可重入自旋锁验证
    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.自旋锁与互斥锁异同点

    • 自旋锁与互斥锁都是为了实现保护资源共享的机制。
    • 无论是自旋锁还是互斥锁,在任意时刻,都最多只能有一个保持者。
    • 获取互斥锁的线程,如果锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放。

    7.总结

    • 自旋锁:线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。
    • 自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)。
    • 自旋锁如果持有锁的时间太长,则会导致其它等待获取锁的线程耗尽CPU。
    • 自旋锁本身无法保证公平性,同时也无法保证可重入性。
    • 基于自旋锁,可以实现具备公平性和可重入性质的锁

    结尾

    本文到这里就结束了,感谢看到最后的朋友,都看到最后了,点个赞再走啊,如有不对之处还请多多指正。

    相关文章

      网友评论

        本文标题:Java并发编程——深入理解自旋锁

        本文链接:https://www.haomeiwen.com/subject/ufglyktx.html