synchronized / Lock / CAS
- synchronized和Lock实现的同步锁机制,都属于悲观锁,而CAS属于乐观锁
- 悲观锁在高并发的场景下,激烈的锁竞争会形成线程阻塞,而大量阻塞线程会致使系统的上下文切换,增长系统的性能开销
乐观锁
- 乐观锁:在操做共享资源时,老是抱着乐观的态度进行,认为本身可以完成操做
- 但实际上,当多个线程同时操做一个共享资源时,只有一个线程会成功,失败的线程不会被挂起,仅仅只是返回
- 乐观锁相比于悲观锁来讲,不会带来死锁、饥饿等活性故障问题,线程间的相互影响也远远比悲观锁要小
- 乐观锁没有因竞争而形成的系统上下文切换,因此在性能上更胜一筹
实现原理
- CAS是实现乐观锁的核心算法,包含3个参数:V(须要更新的变量),E(预期值)、N(最新值)
- 只有V等于E时,V才会被设置为N
- 若是V不等于E了,说明其它线程已经更新了V,此时该线程不作操做,返回V的真实值
CAS实现原子操做
AtomicInteger是基于CAS实现的一个线程安全的整型类,Unsafe调用CPU底层指令实现原子操做java
复制
1 2 3 4 5 6 7 8
|
// java.util.concurrent.atomic.AtomicInteger public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
public final int getAndDecrement() { return unsafe.getAndAddInt(this, valueOffset, -1); }
|
复制
1 2 3 4 5 6 7 8 9 10 11 12
|
// sun.misc.Unsafe public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, v + delta)); return v; }
public native int getIntVolatile(Object o, long offset);
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
|
处理器实现原子操做
- CAS是调用处理器底层指令来实现原子操做的
- 处理器和物理内存之间的通讯速度要远低于处理器间的处理速度,因此处理器有本身的内部缓存(L1/L2/L3)
- 服务器一般为多处理器,而且处理器是多核的,每一个处理器维护了一块字节的缓存存,每一个内核也维护了一块字节的缓存
- 此时在多线程并发就会存在缓存不一致的问题,从而致使数据不一致
- 处理器提供了总线锁定和缓存锁定两种机制来保证复杂内存操做的原子性
- 总线锁定
- 当处理器要操做一个共享变量时,会在总线上会发出一个Lock信号,此时其它处理器就不能操做共享变量了
- 总线锁定在阻塞其余处理器获取该共享变量的操做请求时,也可能会致使大量阻塞,从而增长系统的性能开销
- 缓存锁定(后来出现)
- 当某个处理器对缓存中的共享变量进行了操做,就会通知其余处理器放弃存储或者从新读取该共享变量
- 目前最新的处理器都支持缓存锁定机制
优化CAS乐观锁
- 乐观锁在并发性能上要优于悲观锁
- 但在写大于读的操做场景下,CAS失败的可能性增大,若是循环CAS,会长时间占用CPU
- 例如上面的AtomicInteger#getAndIncrement
- JDK 1.8中,提供了新的原子类LongAdder
- LongAdder在高并发场景下会比AtomicInteger和AtomicLong的性能更好,代价是消耗更多的内存空间
- 核心思想:空间换时间
- 实现原理:下降操做共享变量的并发数
- LongAdder内部由一个base变量和一个cell[]数组组成
- 当只有一个写线程(没有竞争)
- LongAdder会直接使用base变量做为原子操做变量,经过CAS操做修改base变量
- 当有多个写线程(存在竞争)
- 除了占用base变量的一个写线程外,其余写线程的value值会分散到cell数组中
- 不一样线程会命中到数组的不一样槽中,各个线程只对本身槽中的value进行CAS操做
- value=base+∑ni=0Cell[i]value=base+∑i=0nCell[i]
- LongAdder在操做后的返回值只是一个近似准确的值,但最终返回的是一个准确的值
性能对比
- 读大于写,读写锁ReentrantReadWriteLock、读写锁StampedLock、乐观锁LongAdder的性能最好
- 写大于读,乐观锁的性能最好,其余四种锁的性能差很少
- 读约等于写,两种读写锁和乐观锁的性能要优于synchronized和Lock
小结
- 乐观锁的常见使用场景:数据库更新
- 为每条数据定义一个版本号,在更新前获取版本号,在更新数据时,再判断版本号是否被更新过,若是没有才更新数据
- CAS乐观锁的使用比较受限,由于乐观锁只能保证单个变量操做的原子性
- CAS乐观锁在高并发写大于读的场景下
- 大部分线程的原子操做会失败,失败后的线程将不断重试CAS原子操做,致使大量线程长时间占用CPU资源
- JDK 1.8中,新增了原子类LongAdder,采用空间换时间的思路解决了这个问题,但实时性不高