CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,已达到锁的效果。自旋是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁, 当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
什么是自旋?字面意思是 “自我旋转” 。在 Java 中就是循环的意思,比如 for 循环,while 循环等等。那自旋锁顾名思义就是「线程循环地去获取锁」。
非自旋锁,也就是普通锁。获取不到锁,线程就进入阻塞状态。等待 CPU 唤醒,再去获取。
某线程去获取锁(可能是自旋锁 or 非自旋锁),然而锁现在被其他线程占用了。它两获取锁的执行流程就如下图所示:
自旋锁有啥好处?
自旋锁的好处就是能减少线程切换状态的开销;坏处就是如果一直旋下去,自旋开销会比线程切换状态的开销大得多。知道优缺点,那我们的适用场景就很简单了:
在 Java 1.5 版本及以上的并发包中,也就是 java.util.concurrent
的包中,里面的原子类基本都是自旋锁的实现。我们看看做常用的 AtomicInteger
类,它里面有个 getAndIncrement
方法,源码如下:
getAndIncrement()
:返回当前的值,并将当前值+1
getAndIncrement
也是直接调用 usafe
的 getAndAddInt
方法,从下面源码可以看出这个方法直接就是做了一个 do-while
的循环。「这个循环就是一个自旋操作,如果在修改过程中遇到了其他线程竞争导致没修改成功的情况,就会 while 循环里进行死循环,直到修改成功为止」。
为了引入自旋特性,我们使用 AtomicReference
类提供一个可以原子读写的对象引用变量。
定义一个加锁方法,如果有其他线程已经获取锁,当前线程将进入自旋,如果还是已经持有锁的线程获取锁,那就是重入。
定义一个解锁方法,解锁的话,只有持有锁的线程才能解锁,解锁的逻辑思维将 count-1,如果 count == 0,则是把当前持有锁线程设置为 null,彻底释放锁。
源码如下:
public class ReentrantSpinLock {private AtomicReference owner = new AtomicReference<>();//重入次数private int count = 0;public void lock() {Thread t = Thread.currentThread();if (t == owner.get()) {++count;return;}//自旋获取锁while (!owner.compareAndSet(null, t)) {System.out.println("自旋了");}}public void unlock() {Thread t = Thread.currentThread();//只有持有锁的线程才能解锁if (t == owner.get()) {if (count > 0) {--count;} else {//此处无需CAS操作,因为没有竞争,因为只有线程持有者才能解锁owner.set(null);}}}public static void main(String[] args) {ReentrantSpinLock spinLock = new ReentrantSpinLock();Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");spinLock.lock();try {System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");Thread.sleep(4000);} catch (InterruptedException e) {e.printStackTrace();} finally {spinLock.unlock();System.out.println(Thread.currentThread().getName() + "释放了了自旋锁");}}};Thread thread1 = new Thread(runnable);Thread thread2 = new Thread(runnable);thread1.start();thread2.start();}
}
从结果我们可以看出,前面一直打印 “自旋了”,说明 CPU 一直在尝试获取锁。
PS:如果电脑不好的话,在这期间风扇会加速的,因为 CPU 一直在工作。