关于java:好烦面试官逮着我问ReentrantLock的这几个问题

5次阅读

共计 3007 个字符,预计需要花费 8 分钟才能阅读完成。

偏心锁和非偏心锁的区别?

之前剖析 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 申请锁。

这就实现了偏心锁,先来的先获取到锁,起初的后获取到锁。

所以咱们能够总结下偏心锁和非偏心锁实现上的两点区别:

  1. 非偏心锁在调用 lock()办法后,首先会通过 CAS 抢占锁,如果凑巧这个时候锁没有被占用,则获取锁胜利
  2. 非偏心锁在 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 有如下作用

  1. lock 锁定的时候,如果操作某个数据,那么其余 CPU 核不能同时操作
  2. lock 锁定的指令,不能上下文随便排序执行,必须依照程序高低程序执行
  3. 在 lock 锁定操作结束之后,如果某个数据被批改了,那么须要立刻通知其余 CPU 这个值被批改了,是它们的缓存数据立刻生效,须要从新到内存获取

对于 lock 的实现有两种,一种是锁总线,一种是锁缓存。锁缓存就波及到 CPU Cache, 缓存行以及 MESI 了,所以这里就不开展了,有趣味的童鞋咱们能够私下交换下。

正文完
 0