前言

在AQS系列(一)中我们一起看了ReentrantLock加锁的过程,今天我们看释放锁,看看老Lea那冷峻的思维是如何在代码中笔走龙蛇的。

正文

追踪unlock方法:

 public void unlock() {
sync.release(1);
}

很简单的一行,调用了release方法,参数为1,继续跟踪发现不管是公平锁还是非公平锁调用的都是AbstractQueuedSynchronizer中的release方法:

 public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

此方法看起来简单,却暗含杀机。

1、首先看if中的判断方法tryRelease

 protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 计算出释放锁之后的state值
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // c==0说明要释放锁了
free = true;
setExclusiveOwnerThread(null); //在释放之前将独占线程置为空
             } 
setState(c); // 将state置为0,此处没用cas操作,因为没必要,反正在此之前state都大于0,不会被其他线程操作,只有当前线程能操作
return free;
}

此方法的实现逻辑在ReentrantLock类的Sync内部类中,即公平锁和非公平锁公用,相信理解起来比较轻松。

2、再看里面的if判断条件 h != null && h.waitStatus != 0

注意此时h是head,队列头。我们先要搞清楚这两个判断条件所表示的意思,h!=null说明队列不是空的,而h.waitStatus != 0又是什么意思呢?回顾一下上一篇的最后第二个方法 shouldParkAfterFailedAcquire,当时讲这个方法时其实描述的不是很清楚,这次重新结合释放锁的场景回顾一下。下面先将该方法粘贴出来(注释中的两个2表示执行一次这个方法只会走一个2的逻辑):

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 1、正常情况进到这里ws是0,pred可能是head,也可能只是node前面另一个排队的任务
if (ws == Node.SIGNAL)
// 3、如果是-1了,就返回true,进入后面park当前线程
return true;
if (ws > 0) {
do {
// 2、如果是大于0,说明pred线程已经被取消,则继续往前遍历,直到从后往前找到第一个不大于0的节点,然后互相设置指针
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 2、是0的话进这里,设置成-1,注意是将pred(即当前node的前一个节点)设置成-1。即如果一个节点ws是-1,那么它后面一定至少还有一个node(就是这个方法中的node)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

waitStatus>0只有一种情况-线程被取消了(状态值为1)。当线程被取消时就要舍弃掉它,继续往前遍历。

回顾完上述的方法,再看h.waitStatus != 0,我们可以知道,waitStatus != 0表示等待后面还有排队的node(可能是正常状态也可能是已取消的状态),这时就要去唤醒下一个正常状态的线程,进入unparkSuccessor方法。

3、unparkSuccessor 方法代码

 private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 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);
}

该方法用于唤醒当前线程的下一个有效任务,入参node为head节点。首先如果ws为-1则通过CAS设置为0;然后判断node的下一个节点是不是空,或者是不是已经被取消(ws大于0表示已经被取消);如果满足条件,则从后往前遍历找到从前往后数的第一个ws小于等于0的node节点,唤醒这个节点的线程。此处的for循环用的比较有意思,用了一种类似于while循环的格式来用for循环,可见老Lea不拘一格的思维方式。

此处最后一行unpark方法执行之后,就会进入系列(一)中的最后一个方法的第3行代码(如下所示),继续执行下一个线程的加锁过程,进入下一次轮回。

 private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}

附加:公平锁与非公平锁的源码理解

在上一篇文章中未讲到公平锁和非公平锁的区别,在这里统一进行一下总结:

在释放锁的过程中,公平锁和非公平锁的处理流程是一样的,都是从队列的头往后遍历挨个唤醒等待的线程。

在加锁的过程中,有两个不同的地方。第一个是在lock方法中,公平锁代码:

 final void lock() {
acquire(1);
}

非公平锁代码:

 final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

可以看到非公平锁直接先用CAS尝试获取一下锁,不用排队。这就是第一个非公平的地方。

第二个不同的地方,是acquire方法中的tryAcquire方法实现不同,公平锁的tryAcquire方法:

 protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

可以看到当c==0时公平锁会先通过hasQueuedPredecessors方法判断队列前面有没有排队的。

非公平锁的实现:

 protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
} final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
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);
return true;
}
return false;
}

当c==0时,非公平锁是直接用CAS尝试获取加锁。这是第二个非公平的地方。

好了,ReentrantLock的加锁和释放锁过程基本就这些了,这周末继续搞JUC!

最新文章

  1. String.Format用法
  2. 图——拓扑排序(uva10305)
  3. 关于window的resize事件
  4. 关于前后台交互生成json区别
  5. python深复制和浅复制
  6. 怎样使用AutoLayOut为UIScrollView添加约束
  7. bitmag
  8. iOS block简单传值
  9. Asp.net绑定带层次下拉框(select控件)
  10. 偷偷mark下一个
  11. tableView Crash
  12. 关于httpservletrequest的获取真实的ip
  13. 安装python模块
  14. groovy install,gvm,groovysh简述(转)
  15. PAM - 可插拔认证模块
  16. docker 安装mysql数据库 &lt;二&gt;
  17. MDB数据类型注意事项
  18. python2,python3同时安装时,python3可以安装并升级pip库,python2报错的解决办法
  19. 4、申请开发(Development)证书和描述文件
  20. cf463d

热门文章

  1. Redis中的键值过期操作
  2. Java 8 Streams API 详解
  3. docker-compose搭建zookeeper集群
  4. .net 上传文件:超过了最大请求长度
  5. ASP.NET Core 1.0: Using Entity Framework Core
  6. .NET Core3.0 EF 连接 MySql
  7. 【Spring】Spring的定时任务注解@Scheduled原来如此简单
  8. DAL
  9. C语言|博客作业03
  10. 通过django 速成 blog