偏心锁和非偏心锁的区别?
之前剖析AQS的时候,理解到AQS依赖于外部的两个FIFO队列来实现同步状态的治理,当线程获取锁失败的时候,会将以后线程以及期待状态等信息结构成Node对象并将其退出同步队列中,同时会阻塞以后线程。当开释锁的时候,会将首节点的next节点唤醒(head节点是虚构节点),使其再次尝试获取锁。
同样的,如果线程因为某个条件不满足,而进行期待,则会将线程阻塞,同时将线程退出到期待队列中。当其余线程进行唤醒的时候,则会将期待队列中的线程出队退出到同步队列中使其再次取得执行权。
依照咱们的剖析,无论是同步队列还是期待队列都是FIFO,看起来就很偏心呀?为什么ReentrankLock还分偏心锁和不偏心锁呢?
还是间接看源码吧,看看它是怎么做的?
首先看看锁的创立
// 默认是不偏心锁 public ReentrantLock() { sync = new NonfairSync();}// true示意偏心锁,false示意不偏心锁public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();}
能够看到对应不同的锁,只是代表他们外部的Sync变量不同而已。
其中NonfairSync和FairSync两个类是Sync的子类,Sync又继承自AbstractQueuedSynchronizer
当咱们应用ReentrantLock加锁的时候实际上调用的是sync.lock()办法,也就是说,咱们须要看看他们加锁的时候有什么不同之处?
能够看到在lock办法外部,非偏心锁会先间接通过CAS批改state变量的值,如果批改胜利则示意获取到了锁,而偏心锁则是间接调用AQS的acquire办法来获取锁。
也就是说有可能当其余线程开释锁的时候,非偏心锁能率先批改state的值胜利,从而获取到锁。这样就比其余期待的线程率先获取到锁了,这就是不偏心。
之前也有提到过,子类会依据本人的需要以实现tryAcquire办法,同样的非偏心锁和偏心锁的实现也实现了这个办法,咱们能够来看看,两个的实现有什么不同
能够看到偏心锁比非偏心锁的实现多了一个判断条件(!hasQueuedPredecessors()),咱们来看看这个办法的实现
public final boolean hasQueuedPredecessors() { Node t = tail; Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());}
这个办法很简略,它的意思是如果以后线程之前有排队的线程,则返回true;如果以后线程位于队列的结尾或队列为空,则返回false。
也就是说偏心锁在获取锁的时候会判断队列中是否曾经有排队的线程,如果有则进行阻塞,如果没有则去通过CAS申请锁。
这就实现了偏心锁,先来的先获取到锁,起初的后获取到锁。
所以咱们能够总结下偏心锁和非偏心锁实现上的两点区别:
- 非偏心锁在调用lock()办法后,首先会通过CAS抢占锁,如果凑巧这个时候锁没有被占用,则获取锁胜利
- 非偏心锁在CAS失败后,和偏心锁一样会调用tryAcquire()办法,在tryAcquire()办法中,如果发现锁被开释了(state=0),非偏心锁会间接CAS进行抢占,而偏心锁会判断同步队列中是否有线程处于期待状态,如果有则不去抢占,而是排队获取。
这就是两者将轻微的区别,如果这非偏心锁两次CAS都失败了,那么会和偏心锁一样,乖乖的在同步队列中排队。
相对而言,非偏心锁的吞吐量更大,然而让获取锁的工夫变得不确定,可能会导致同步队列中的线程长期处于饥饿状态。
ReentrantLock靠什么保障可见性?
synchronized 之所以可能保障可见性,是因为有一条happens-before准则,那Java SDK 外面 ReentrantLock 靠什么保障可见性呢?
它是利用了 volatile 相干的 Happens-Before 规定。AQS外部有一个 volatile 的成员变量 state,当获取锁的时候,会读写state 的值;解锁的时候,也会读写 state 的值。
对一个volatile变量的写操作happens-before 于前面对这个变量的读操作。这里的happens-before是工夫上的先后顺序
这样说起来挺形象的,咱们间接去看JVM中对volatile是否有非凡的解决,在src/hotspot/share/interpreter/bytecodeinterpreter.cpp
中,咱们找到getfield和getstatic字节码执行的地位
当初这个执行器根本不再应用了,根本都会应用模板解释器,然而模板解释器的代码根本都是汇编,而咱们只是想要疾速理解其原理,所以能够看这个,对模板解释器感兴趣的能够去看templateTable_x86.cpp::getfield查看相干细节
...CASE(_getfield):CASE(_getstatic):{ ... ConstantPoolCacheEntry* cache; ... if (cache->is_volatile()) { if (support_IRIW_for_not_multiple_copy_atomic_cpu) { OrderAccess::fence(); } ... } ...}
能够看到在拜访对象字段的时候,会判断它是不是volatile的,如果是,且以后CPU平台反对多核atomic操作(当初大多数CPU都反对),就调用OrderAccess::fence()
。
JDK中的Unsafe也提供了内存屏障的办法,在JVM层面也是通过OrderAccess实现
接下来来看下Linux x86下的实现是怎么的(src/hotspot/os_cpu/linux_x86/orderAccess_linux_x86.cpp)
inline void OrderAccess::fence() {// always use locked addl since mfence is sometimes expensive#ifdef AMD64 __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");#else __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");#endif compiler_barrier();}
指令中的"addl $0,0(%%esp)"(把ESP寄存器的值加0)是一个空操作,采纳这个空操作而不是空操作指令nop是因为IA32手册规定lock前缀不容许配合nop指令应用,所以才采纳加0这个空操作。
而lock有如下作用
- lock锁定的时候,如果操作某个数据,那么其余CPU核不能同时操作
- lock 锁定的指令,不能上下文随便排序执行,必须依照程序高低程序执行
- 在 lock 锁定操作结束之后,如果某个数据被批改了,那么须要立刻通知其余 CPU 这个值被批改了,是它们的缓存数据立刻生效,须要从新到内存获取
对于lock的实现有两种,一种是锁总线,一种是锁缓存。锁缓存就波及到CPU Cache,缓存行以及MESI了,所以这里就不开展了,有趣味的童鞋咱们能够私下交换下。