ReentrantLock
AbstractQueuedSynchronizer(一般简称AQS)是并发编程中的一个核心类,多线程的锁管理也主要在这个类中得到完美实现。Sync
作为ReentrantLock的内部类,继承了AbstractQueuedSynchronizer。
说明:
- 为了精简代码,能够简化的地方都是用了Java8的Lamda表达式进行了简化。
2.对于try-catch需要捕获的异常,使用lombok插件的@SneakyThrows
进行处理。
3.代码中使用的注释为行尾注释,属于不优雅的注释,阿里代码规范插件会检测这种“不文明现象”,并非是我懒得调整,而是注释均是从本人自己构建的JDK8源码的阅读环境中添加的注释,如果改变了代码的行结构,会导致Java
什么是可重入
ReentrantLock从单词的命名上,意为可重入锁,那么可重入的意思是什么呢?
public class App {
private static ReentrantLock lock = new ReentrantLock();
public static void main( String[] args ) {
Thread t1 = new Thread(App::testReentreant,"t1");
Thread t2 = new Thread(App::testReentreant,"t2");
Thread t3 = new Thread(App::testReentreant,"t3");
t1.start();
t2.start();
t3.start();
}
@SneakyThrows
private static void testReentreant() {
try {
System.out.println(Thread.currentThread().getName() + 1);
lock.lock();
System.out.println(Thread.currentThread().getName() + 2);
lock.lock();
// Thread.sleep(5000);
}finally{
int count = lock.getHoldCount();
while(count > 0){
lock.unlock();
--count;
}
}
}
}
代码挺简单,就是创建三个线程去获取锁,分别命名为t1,t2,t3
,在testReentreant
方法中,还没有释放锁的情况下,再次去执行lock.lock()
方法,不会造成死锁或者报错,这就是可重入的定义。
另外,synchronized关键字控制的锁也是属于可重锁。
public class SychronizedTest {
public static void main(String[] args) {
Thread t1 = new Thread(SychronizedTest::testSychronized);
Thread t2 = new Thread(SychronizedTest::testSychronized);
t1.start();
t2.start();
}
private static void testSychronized() {
synchronized (SychronizedTest.class) {
System.out.println(Thread.currentThread().getName() + "-----1");
synchronized (SychronizedTest.class) {
System.out.println(Thread.currentThread().getName() + "-----2");
}
}
}
}
是可以正常运行的。言归正传继续回到主题上来。
原理概览
留意上边代码的关于锁释放部分:
try{
...
}finally{
int count = lock.getHoldCount();
while(count > 0){
lock.unlock();
--count;
}
}
第一点,unlock要放在finally里面,因为finally是必定会执行的,用来保证哪怕是抛出异常后,锁也一定会被释放,如果idea中使用了阿里代码规约的插件,同样也会要求这样做。另外,既然锁支持重入,那么如何区分锁的重入次数呢,必然是计数器,ReentrantLock中对锁的重入进行了次数的计数。
初始值为0,当第一次有线程获得锁之后,会对state进行+1操作。采用volatile
进行修饰,解决了指令重排的问题。
下边这段代码的目的是为了完全释放锁,因为所可能被多次重入,因此要判断锁的重入次数来对应释放的次数。
int count = lock.getHoldCount();
while(count > 0){
lock.unlock();
--count;
}
ReentrantLock的类型
ReentrantLock的构造方法有两个,分别是无参构造和一个带有boolean值得构造:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
注意到NonfairSync
和FairSync
,上边说Sync
是继承与AQS的一个类,同时也是ReentrantLock的内部类,从类图中可以看到Sync中包含了两个子类刚好对对应了NonfairSync
和FairSync
。这就是ReentrantLock的公平锁和非公平锁的实现。
避免再次回到顶部去看图,此处冗余粘贴。
因为默认实现是非公平锁
,因此先从非公平锁进行切入。
非公平锁
本次探索,笔者使用Demo进行猜测和Debug验证,关于Idea中使用Debug,我会另写一篇文章详细总结我使用到的比较适用的功能,虽然可能不华丽,但是一定会很实用,届时会附上传送门。
public class App {
private static ReentrantLock lock = new ReentrantLock();
public static void main( String[] args ) {
Thread t1 = new Thread(App::testReentreant,"t1");
Thread t2 = new Thread(App::testReentreant,"t2");
Thread t3 = new Thread(App::testReentreant,"t3");
t1.start();
t2.start();
t3.start();
}
@SneakyThrows
private static void testReentreant() {
try {
System.out.println(Thread.currentThread().getName() + 1);
lock.lock();
// System.out.println(Thread.currentThread().getName() + 2);
// lock.lock();
// Thread.sleep(5000);
}finally{
int count = lock.getHoldCount();
while(count > 0){
lock.unlock();
--count;
}
}
}
}
还是之前的例子,注释掉了重入部分。
通过Debug,我们设置线程抢到锁的顺序为t1->t2->t3
,并从抢占锁和释放锁两个方向去探索。
抢占锁
源码中的代码相对较多而且较为复杂,我按照蓝色序号进行分析系,只分析关键部分代码。
① t1获取到锁
线程进入到ReentrantLock后立即开始执行CAS操作来设置state,如果设置成功,则成功获得锁。获得锁之后,将当前锁的独占线程设置为当前线程此处为t1
:
CAS:参数分别为:
int expect
:代表当前的数,假设为0
int update
:代表我需要数,假设为1
类似于SQL中的:
update ReentrantLock set state = 1 where state = 0;
这个方法调用了Unsafe
类的方法,采用C语言来实现,能够保证方法执行的原子性,而且只有一个线程能够执行成功。compareAndSetState(0, 1)
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
源码中经常会出现if语句不添加大括号的情况,这是很不优雅的。
因为t1是第一个线程,因此成功获得了锁,并将state设置为
1
。
线程t1获取到锁后,线程t2
也执行到了CAS操作,理所应当t2执行CAS未成功,因此往else
方向执行acquire(1)
方法。
②t2获取锁失败
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
留意
if语句中的两个条件都是函数,而且使用了&&进行连接
,因此只有tryAcquire
方法返回false的时候,acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
才会执行,tryAcquire方法实现的功能就是抢占锁,调用了内部类Sync的nonfairTryAcquire
方法:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();// 获取当前的state值
if (c == 0) {// 侥幸情况下,如果正好在重试获取锁的时候,刚好上个线程释放了锁,则可以获取到锁(非公平的特点)
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);// 如果在重试的时候获取锁成功了,就设置独占
return true;
}
}
else if (current == getExclusiveOwnerThread()) {// 如果当前线程就是这把锁的持有者
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);// 执行state+1(state就是重入次数)
return true;
}
return false;// 此处对应上个线程还没有释放锁,侥幸方式获取锁失败
}
大部分逻辑可以从注释中理解,主要逻辑就是:
判断state的值是不是0,如果是0,则说明锁被释放了,那么我就执行CAS,如果执行成功旧设置为独占;
否则判断自身是不是本来就有这把锁,如果是自己的锁,就执行重入——state+1。
如果也不是自己持有的锁,那就返回false
.
③将t2放入到队列中去
当上边返回false的时候,acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
方法就可以被执行。首先addWaiter(Node.EXCLUSIVE)
会将t2
放入到阻塞队列中去。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);// 将当前线程封装成一个Node
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {// 如果尾结点不是null
node.prev = pred;// 将节点放在尾结点后边
if (compareAndSetTail(pred, node)) {// 把当前节点设置为尾结点
pred.next = node;
return node;
}
}
enq(node);// 如果尾结点为空,则需要初始化链表
return node;
}
t2是第一个放入队列中的节点,此时队列还不存在,因此会执行
enq(node)
方法,enq方法的主要功能就是如果队列存在,就将当前线程的节点从尾部插入,否则会初始化队列:创建一个空的节点作为头节点,将当前线程(t2)插入尾部。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize 如果尾结点是null
if (compareAndSetHead(new Node()))// 尝试new一个新节点设置为头节点(如果头节点为null的话)
tail = head;// 如果设置头节点成功了,说明现在是一个初始化阶段,因此头节点也是尾结点(这段代码没有return)
} else {// 如果尾结点不为null,将当前节点作为尾结点插入
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这部分执行完毕后,代码回到
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
④t2自旋获取锁
开始执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
,从方法名看出意思为在已经在队列中的情况下再去获取锁
。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { // 自旋
final Node p = node.predecessor();// 获取上一个节点
if (p == head && tryAcquire(arg)) {// 自旋的时候再次尝试去获取锁,tryAcquire
setHead(node);// 获取成功
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&// 获取失败,为了节省开销,不再获取锁
parkAndCheckInterrupt())// 获取锁失败,则执行LockSupport.park(this)进行阻塞
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
其中
for (;;)
不带循环条件表示无条件循环,需要在方法体内实现跳出循环,否则会无限循环下去。这部分主要为两个if判断。
第二个if是实现线程的阻塞,表示在多次获取锁都没获取成功,说明上一个线程还在使用,为了节省性能开支,暂时不去获取锁了。第一个if主要是去获取锁,
tryAcquire(arg)
方法上边已经介绍过,是获取锁的逻辑;
第一个if主要是去获取锁,
tryAcquire(arg)
方法上边已经介绍过;而上一行代码是一个关键的地方。final Node p = node.predecessor()
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
可以看到返回的是上一个节点,这里想必大家都会有疑问,为什么不是使用当前线程的节点取抢占锁呢,其实只要看拿到这个节点后,用来做什么,这样就能明白了。
if (p == head && tryAcquire(arg))
,和上边一样,是用&&
连接的,也就是说只有当p是头节点的时候,才会取获取锁回顾这个tryAcquire(arg)
方法是调用了Sync的nonfairTryAcquire
方法,方法中有这么一段:final Thread current = Thread.currentThread()
,就是获取锁成功了之后,设置的独占线程还是当前线程(t2),所以至于你用谁去获取锁都无所谓,只要最后独占线程还是当前线程就好了。但是必须明确一个点:如果当前线程的上一个节点不是头节点,是不会去获取锁的,也就是说只有第二个节点的线程才能有权利拿到锁。。
⑤t1上尉释放锁,t2进入阻塞
截止目前,t1还没有执行释放锁的操作,因此t2获取锁也失败,因此进入了阻塞状态。
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
。(放在if语句中,判断结束,方法也执行结束了)其中parkAndCheckInterrupt()
是调用了LockSupport.park(this)
来完成阻塞。
t2进入阻塞状态后,让t3执行,逻辑基本和t2一致无法获取锁,最后进入队列进行阻塞。只有当t1释放了锁之后,t2才有机会去抢占锁,t3是不能够抢占锁的。
此时队列的结构是这样的:
头节点是一个new出来的节点,next指向t2,t2的next执行t3.
释放锁
t1释放锁并唤醒t2
sync.release(1);
执行释放锁,且传入的参数为1。
public final boolean release(int arg) {
if (tryRelease(arg)) {// 尝试释放锁
Node h = head;// 如果锁完全释放了,则可以唤醒队列中的线程去抢占锁
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;// 代表未完全释放锁
}
依然是执行if中的
tryRelease(arg)
方法。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;//每次执行unlock只会释放一次,因此重入需要释放多次
if (Thread.currentThread() != getExclusiveOwnerThread())// 判断当前线程是否是占有该锁的线程
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {// 判断c=0则是完全释放了否则还没有完全释放
free = true;
setExclusiveOwnerThread(null);// 如果完全释放了就将独占的线程设置为null
}
setState(c);// 将新的state赋值给state
return free;
}
因为传入的参数是1,因此每次都是对state执行减1,如果减到了0,则设置独占线程为null,返回true,否则返回false。
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
当
tryRelease
返回true的时候,锁就已经完全释放了,这段代码就会执行,当h.waitStatus != 0
判断为true的时候,执行unparkSuccessor(h)
。那么h.waitStatus代表什么呢?
/**
* Status field, taking on only the values:
* SIGNAL: The successor of this node is (or will soon be)
* blocked (via park), so the current node must
* unpark its successor when it releases or
* cancels. To avoid races, acquire methods must
* first indicate they need a signal,
* then retry the atomic acquire, and then,
* on failure, block.
* CANCELLED: This node is cancelled due to timeout or interrupt.
* Nodes never leave this state. In particular,
* a thread with cancelled node never again blocks.
* CONDITION: This node is currently on a condition queue.
* It will not be used as a sync queue node
* until transferred, at which time the status
* will be set to 0. (Use of this value here has
* nothing to do with the other uses of the
* field, but simplifies mechanics.)
* PROPAGATE: A releaseShared should be propagated to other
* nodes. This is set (for head node only) in
* doReleaseShared to ensure propagation
* continues, even if other operations have
* since intervened.
* 0: None of the above
*
* The values are arranged numerically to simplify use.
* Non-negative values mean that a node doesn't need to
* signal. So, most code doesn't need to check for particular
* values, just for sign.
*
* The field is initialized to 0 for normal sync nodes, and
* CONDITION for condition nodes. It is modified using CAS
* (or when possible, unconditional volatile writes).
*/
这是JDK对于h.waitStatus的注释说明:
SIGNAL: 这个节点的后继节点被(或即将)阻塞(通过park),所以当前节点在释放或取消时必须取消其后继节点的park。为了避免竞争,acquire方法必须首先表明它们需要一个信号,然后重试原子获取,然后在失败时阻塞。
CANCELLED: 由于超时或中断,该节点被取消。节点永远不会离开这个状态。带有取消节点的线程不会再阻塞。
CONDITION: 此节点当前位于条件队列中。在传输之前,它不会被用作同步队列节点,此时状态将被设置为0。(此值的使用与领域的其他用途无关,但简化了机制。)。
PROPAGATE: releaseShared应该传播到其他节点。这是在doreleasshared中设置的(仅针对头部节点),以确保传播继续,即使其他操作已经介入。
0: 除上述之外。
回到上边代码,当waitStatus不为0的时候,就会执行
unparkSuccessor(h)
方法,唤醒线程(唤醒的是当前节点的下一个节点的线程t2
)。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;// 当锁完全释放之后,该值会变为-1
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);// 将watiStatus从-1修改为0
Node s = node.next;// 获取当前节点的下一个节点
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//执行下一个节点的线程唤醒
}
t2唤醒t3
唤醒
t2
后,t1执行exit退出了,此时t2执行自旋获取锁,获取锁完成后将自己节点设置为头节点
,同时删除thread
属性和prev
指针,然后将原来的头节点指向null,便于GC回收。
setHead(node);
p.next = null; // help GC
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
那么为什么
不直接将原来的head指向t3的节点
呢?如果按这样,那么需要操作的步骤如下:
如果是直接直接删除head,需要操作的步骤如下:
很明显直接删除head会更省事。
此时的节点信息变成这样:
之后的释放逻辑变重复之前的步骤即可。
公平锁
公平锁的实现大部分和非公平锁一致,较为明显的差异是公平锁不会进行插队,而是老老实实进行排队,只会在队列无线程在等待的时候,才会取执行CAS操作去设置state
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {// state=0表示当前无锁,可以直接CAS,更改状态并设置独占
if (!hasQueuedPredecessors() && // 只有当前不存在等待线程的时候才会取CAS替换
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {// 如果当前线程和独占线程(即重入)
int nextc = c + acquires;// state+1
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
单独看这段代码的对比:
公平锁:
if (!hasQueuedPredecessors() && // 只有当前不存在等待线程的时候才会取CAS替换
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
非公平锁:
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);// 如果在重试的时候获取锁成功了,就设置独占
return true;
}
区别就在!hasQueuedPredecessors() &&
,公平锁会进行公平排队,只有头节点的下一个节点才能获取锁。
总结
非公平锁会有三次机会获取锁,公平锁在第一次获取锁时有两次机会获取锁,后续节点只有第二个线程可以有一次机会获取锁。释放锁的时候都是唤醒头节点的下一个节点。