《面试专题》第四部分 并发编程进阶
ReentrantLock 源码分析
ReentrantLock 与 synchronized 实现机制有什么区别?
ReentrantLock和synchronized都是独占锁
synchronized:
1、是悲观锁会引起其他线程阻塞,java内置关键字,
2、无法判断是否获取锁的状态,锁可重入、不可中断、只能是非公平
3、加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单但显得不够灵活
4、一般并发场景使用足够、可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁
5、synchronized 操作的应该是对象头中mark word,参考原先原理图片
ReentrantLock:
1、是个Lock接口的实现类,是悲观锁,
2、可以判断是否获取到锁,可重入、可判断、可公平可不公平
3、需要手动加锁和解锁,且 解锁的操作尽量要放在finally代码块中,保证线程正确释放锁
4、在复杂的并发场景中使用在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致 其他线程无法获得该锁。
5、创建的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁
6、底层不同是AQS的state和FIFO队列来控制加锁
ReentrantLock 加锁,阻塞、唤醒理论说明
如何加锁?
所谓上锁在ReentrantLock就是给state变量+1,state声明如下,注意是volatile的,也就是在多线程环境下对每个线程都是可见的
private volatile int state;
那么很多线程都在抢这把锁,只有一个线程能抢到(即能执行state+1成功),怎么保证线程安全?答案是CAS,CAS是啥?简单来说就是Compare And Swap,即比较并替换:给一个预期值E和一个更新值U,如果当前值A和预期值E相等,则更新A为U。感觉是不是有点像乐观锁?
int c = getState();//c是state
if (c == 0) {//锁还没被别人抢
if (compareAndSetState(0, acquires)) {//重点是这句,CAS方式设置state
setExclusiveOwnerThread(current);
return true;
}
}
继续跟下去,调用了unsafe的compareAndSwapInt,在往下就是native方法了
如何阻塞?
首先是调用了LockSupport.park(),park直译为停车,线程停车即阻塞,线程就阻塞在这里一动不动了,不会网下执行, LockSupport.park调用了Unsafe.park,这又是一个native方法
如何唤醒
ReentrantLock中调用了LockSupport.unpark,同样unpark也是一个native实现
ReentrantLock lock方法源码追踪
1、ReentrantLock 内部有一个抽象内部类 Sync,Sync 继承了 AQS 抽象类,调用ReentrantLock 的 lock/unlock 方法实际上是调用了 Sync 的 lock/unlock 方法
2、ReentrantLock 通过构造器创建公平/非公平锁,具体实现是内部类 NofairSync 和 FairSync ,并且这两个类都继承子 Sync 抽象内部类
3、公平锁和非公平锁的区别有两点
- 非公平锁的 lock 方法,首先会通过 CAS 操作抢占锁,不会考虑阻塞队列中是否有等待解锁的线程
- 公平锁的 tryAcquire 方法中会有一个 hasQueuedPredecessors 判断,hasQueuedPredecessors 判断当前是否有等待执行的队列,如果有则放弃抢占锁,执行 addWait 方法将当前线程变为 Node 节点,加入到CLH双向队列的尾部,变成队列的一部分
4、整个 ReentrantLock 加锁的过程分为三个阶段,分别是 tryAcquire 、addWaiter 、 acquireQueued , 加锁过程中大量使用 自旋锁
和 CAS
相关知识
5、ReentrantLock 加锁第一阶段 tryAcquire
- 再次判断 state 是否为0,为0则表示当前没有其他线程占用锁
current == getExclusiveOwnerThread()
判断,是否是当前线程占有锁,如果是则将 state + 1,这是可重入锁的体现- 如果上面两条都不满足,则抢占锁失败,返回false,则
!tryAcquire(arg) = true
6、ReentrantLock 加锁第二阶段 addWaiter
- 我们发现 addWaiter 方法是 AQS 这个顶层抽象类的方法,表示所有子类都会使用这个方法
- 第一步,将当前线程包装为一个Node节点
- 第二步,通过判断
tail
是否为 null,来判断当前双向链表是否为空 - 第三步,为了更好的了解 AQS 是如何初始化双向链表的我们先看
enq
方法是如何实现的
6.1、AQS 初始化双向链表 enq 方法
- for 循环构成自旋锁,该自旋锁的出口是,node 节点加入到双向链表尾部
- 如果当前链表为空,则使用 CAS 将一个虚拟空节点
new Node()
置为链表头节点,所以大家注意了 AQS 链表的第一个节点不是第一个等待线程的Node节点,而是一个空节点 - 第一次 for 循环之后已经创建了一个双向链表,第二次 for 循环将 node 节点加入到双向链表尾部,最后结束自旋锁
6.2 如果队列不为空
如果队列不为空,则将当前 Node 节点加入到队列尾部
7、ReentrantLock 加锁第三阶段 acquireQueued
acquireQueued
属于顶层抽象类 AQS 的方法,是所有子类共有的- 首先使用 for 循环构建一个自旋锁,该自旋锁的出口是链表的第一个等待线程获取锁成功
- 链表的第一个节点尝试抢占锁,再次执行 tryAcquire 方法
- 如果抢占锁失败则第一个等待线程继续等待,并且执行 parkAndCheckInterrupt 方法,通过 LockSupport 的 park 方法将当前线程阻塞,等待执行 LockSupport 的 unpark方法唤起
ReentrantLock unlock 方法源码追踪
- ReentranLock 的 unlock 方法实际上调用的是 Sync 的 release 方法
- release 方法的核心是使用 tryRelease 解锁,解锁成功后,唤醒队列中第一个有效 Node 节点
2、tryRelease 核心解锁方法
3、unparkSuccessor
唤醒队列中正在等待的第一个有效 Node
- unparkSuccessor 方法的入参 node,是等待队列的头节点
- 默认使用 head.next 节点,作为待解锁的节点
- 如果 head.next 节点为 null 或者 node 节点状态不是等待状态,则从队列尾部向首部开始遍历,直到找到最后一个可用的 node 节点,并返回
《面试专题》第四部分 并发编程进阶
内容待补充。。。