「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」
引言
谈到并发编程,就不得不谈ReentrantLock
,谈到ReentrantLock
就会问实现原理,谈到原理就引出AQS(AbstractQueuedSynchronized),然后就被按在地上无情的摩擦。这篇文章主要讲解加锁过程,下一篇写释放锁过程。
ReentrantLock使用
1 | java复制代码Lock lock = new ReentrantLock(); |
lock.lock()显示的获取锁,并在finally块中显示的释放锁,目的是保证在获取到锁之后,最终能够被释放。
lock()方法调用过程(默认非公平锁)
非公平锁调用lock方法的源码如下:
1 | java复制代码static final class NonfairSync extends Sync { |
下图是ReentrantLock.lock的调用过程图。
AQS加锁实现
AQS(AbstractQueuedSynchronizer
):是JDK提供的同步框架,内部维护了一个FIFO双向队列(即CLH同步队列)。通过此队列实现同步状态的管理(volatile修饰的state状态,用于标志是否持有锁)。
Node
先了解AQS维护的队列节点结构,下面是队列节点Node的源码:
1 | java复制代码static final class Node { |
FIFO队列(CLH队列)
队列用head,tail和state三个变量来维护,源码如下:
1 | java复制代码/** 头节点 */ |
结构示意图如下:
compareAndSetState调用
首先尝试获取锁,调用compareAndSetState
方法,期待值为0,新值为1。使用unsafe
的compareAndSwapInt
方法,通过一次CAS操作来修改state属性。
CAS操作即内存拿到volatile修饰的state属性值,与期望值0对比,如果取到的值为0,则执行+1操作,将state修改为1。其中还涉及知识点volatile修饰变量保证线程间可见,以及CAS操作的经典ABA问题。
源码如下:
1 | java复制代码/** |
如果此方法执行成功,则调用setExclusiveOwnerThread方法将让线程占有锁,此时state已经置为1。
acquire调用
进入此方法说明,当前已经有其他线程占有锁了。由于此种加锁方式是非公平锁,进入方法后,首先尝试获取锁,如果获取不到锁,那么再将当前线程置于队列中,让当前线程中断执行。
非公平锁在此方法中首先展示不公平,这种不公平是对在队列中的线程来说的。就像我们去银行办业务,如果我是VIP用户,我可以越过等待的用户先办理,这对于其他等待用户不公平。
1 | java复制代码public final void acquire(int arg) { |
tryAcquire
调用此方法来尝试获取锁。源码如下:
1 | java复制代码final boolean nonfairTryAcquire(int acquires) { |
此处setState(nextc),只是单纯让state+1,而没有用CAS操作。
addWaiter
负责把当前无法获得锁的线程包装为一个Node
添加到队尾。
1 | java复制代码private Node addWaiter(Node mode) { |
其中参数mode是独占锁还是共享锁,默认为null,独占锁。追加到队尾的动作分两步:
1.如果当前队尾已经存在(tail!=null),则使用CAS把当前线程追加到队尾。
2.如果当前Tail为null或则线程调用CAS设置队尾失败,则通过enq方法继续追加。
enq
通过for循环和CAS操作自旋过程,将当前线程加入队列中,源码如下:
1 | java复制代码private Node enq(final Node node) { |
acquireQueued
此方法是对addWaiter
的补充,将加入队列的线程中断执行,源码如下:
1 | java复制代码final boolean acquireQueued(final Node node, int arg) { |
p == head && tryAcquire(arg),这里的判断也显示了非公平的意义。队里中有等待线程还要尝试获取锁。
shouldParkAfterFailedAcquire
此方法是阻塞线程前最后的检查操作,通过prev节点的等待状态判断当前线程是否应该被阻塞,
1 | java复制代码private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { |
parkAndCheckInterrupt
如果程序走到这个位置,那么就说明已经将当前线程加入队列中,可以让线程中断了。线程阻塞通过调用LockSupport.park()
完成,而LockSupport.park()则调用sun.misc.Unsafe.park()
本地方法,再进一步,HotSpot在Linux中中通过调用pthread_mutex_lock
函数把线程交给系统内核进行阻塞。
1 | java复制代码private final boolean parkAndCheckInterrupt() { |
至此加锁过程完成。
总结
ReentrantLock加锁是通过AQS实现,AQS中维护了一个FIFO的队列,当存在锁竞争时构建队列,构建过程中使用CAS和自旋,保证线程能够进入队列。已经进入队列的线程需要阻塞,使用LockSupport.park()方法完成,阻塞线程能够让CPU更专注于执行持有锁的线程,而不是将资源浪费在尝试获取锁的自旋过程中。
以上是对ReentrantLock加锁的过程分析,希望大佬多提意见。
本文转载自: 掘金