我们知道悲观锁在高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统上下文切换,增加系统的性能开销。那么有没有可能实现一种非阻塞的锁机制来保证线程的安全呢?答案是肯定的。今天我就带你学习下乐观锁的优化方法,看看怎么使用才能发挥它最大的价值。
一 什么是乐观锁
乐观锁,顾名思义,就是说在操作共享资源时,它总是抱着乐观的态度进行,它认为自己可以成功的完成操作。但实际上,当多个线程同时操作一个共享资源时,只有一个线程会成功,那么失败的线程呢?它们不会像悲观锁一样,在操作系统中挂起,而仅仅是返回,并且系统允许失败的线程重试,也允许自动放弃退出操作。
所以,乐观锁相比悲观锁来说,不会带来死锁,饥饿等活性故障问题,线程间的相互影响也远远比悲观锁要小。更为重要的是,乐观锁没有因锁竞争造成的系统开销,所以在性能上也是更胜一筹。
二 乐观锁的实现原理
CAS是实现乐观锁的核心算法,它包含了3个参数:V(需要更新的变量),E(预期值)和N(最新值)。
1.CAS如何实现原子操作
在JDK中的concurrent包中,atomic路径下的类都是基于CAS实现的。AtomicInteger就是基于CAS实现的一个线程安全的整型类。下面我们通过源码来了解下如何使用CAS实现原子操作。
我们可以看到AtomicInteger的自增方法是使用了Unsafe的getAndAddInt方法,显然AtomicInteger依赖于本地方法Unsafe类,Unsafe类中的操作方法会调用CPU底层指令实现原子操作。
2.处理器如何实现原子操作
CAS是调用处理器底层指令来实现原子操作,那么处理器底层又是如何实现原子操作的呢?
处理器和物理内存之间的通信速度要远慢于处理器间的处理速度,所以处理器有自己的内部缓存。如下图所示,在执行操作时,频繁使用的内存数据会缓存在处理器的L1,L2和L3高速缓存中,以加快频繁读取的速度。
三 优化CAS乐观锁
虽然乐观锁在并发性能上要比悲观苏优越,但是在于写大于读的操作场景下,CAS失败的可能性会增大,如果不放弃此次CAS操作,就需要循环做CAS重试,这无疑会长时间地占用CPU。
在JAVA1.7中,通过以下代码我们可以看到:AtomicInteger的getAndSet方法中使用了for循环不断重试CAS操作,如果长时间不成功,就会给CPU带来非常大的执行开销。到了Java8,for循环虽然被去掉了,但是我们反编译Unsafe类时就可以发现该循环其实是被封装在了Unsafe类中,CPU的执行开销依然存在。
JDK1.8中,Java提供了一个新的原子类LongAdder,LongAdder在高并发的场景下比AtomicInteger和AtomicLong的性能更好,代价就是会消耗更多的内存空间。
LongAdder内部由一个base变量和一个cell[]数组组成。当只有一个写线程,没有竞争的情况下,LongAdder会直接使用base变量作为原子操作变量,通过CAS操作修改变量;当有多个写线程竞争的情况下,除了占用base变量的一个写线程之外,其它各个线程会将修改的变量写入到自己的槽cell[]数组中,最终结果可通过公式计算得出:
四 总结
在日常开发中,使用乐观锁最常见的场景就是数据库的更新操作了。为了保证操作数据库的原子性,我们常常会为每一条数据定义一个版本号,并在更新前获取到它,到了更新数据库的时候,还要判断下已经获取的版本号是否被更新过,如果没有,则执行该操。
CAS乐观锁在平常使用时比较受限,它只能保证单个变量操作的原子性,当涉及到多个变量时,CAS就无能为力了,但前两讲讲到的悲观锁可以通过对整个代码块加锁来做到这点。
CAS乐观锁在高并发写大于读的场景下,大部分线程的原子操作会失败,失败后的线程将会不断重试CAS原子操作,这样就会导致大量线程长时间地占用CPU资源,给系统带来很大的性能开销。在JDK1.8中,Java新增了一个原子类LongAdder,它使用了空间换时间的方法,解决了上诉问题。
最近的这几讲中,我详细的讲解了基于JVM实现的同步锁Sychronized,AQS实现的同步锁Lock以及CAS实现的乐观锁。相信你也很好奇,这三种锁,到底哪一种的性能最好,现在我们来对比一下不同实现方式下的锁的性能。
鉴于脱离实际业务场景的性能对比测试结果没有意义,我们可以分别在“读多写少”,“读少写多”,“读写差不多”这三种场景下进行测试。又因为锁的性能还与竞争的激烈程度有关,所以除此之外,我们还将做三种锁在不同竞争级别下的性能测试。
综合上述条件,我将对四种模式下的五个锁Sychronized,ReentrantLock,ReentrantReadWriteLock,StampedLock以及乐观锁LongAdder进行压测。
通过以上结果,我们可以发现:在读大于写的场景下,读写锁ReentrantReadWriteLock,StampedLock以及乐观锁的读写性能是最好的;在写大于读的场景下,乐观锁的性能是最好的,其它4种锁的性能则差不多;在读和写差不多的场景下,两种读写锁以及乐观锁的性能要优于Sychronized和ReentrantLock。