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

1.什么是自旋锁

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

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

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

经过对输出结果的分析咱们能够得知,首先假定线程一在执行lock方法的时候得到了锁,经过方法ide

cas.compareAndSet(null, current)函数

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

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

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

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。
  • 自旋锁自己没法保证公平性,同时也没法保证可重入性。
  • 基于自旋锁,能够实现具有公平性和可重入性质的锁

结尾

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

相关文章
相关标签/搜索