准备知识

理想情况下,线程A拿到锁,执行完后释放锁,线程B恰好到来顺手接下这把锁,一切都那么完美的话,也没必要加锁了,问题就在于在A没释放锁时,后续线程也想得到这把锁,所以只好让这些等待的线程进行排队,进而需要一套数据结构来组织这个队伍:

Node结点:作为获取锁失败线程的包装类, 组合了Thread引用, 实现为FIFO双向队列。 下图为Node结点的属性描述-

下图为用Node节点构成的双向链表图示:-

链表初始化的头节点其实是一个虚拟节点,英文名称之为dummy header, 因为它不会像后继节点一样真实的存放线程,并且这个节点只会在真正产生竞争排队情况下才会延迟初始化,避免性能浪费,下面看代码的时候,我会再次提到。-
AbstractQueuedSynchronizer 类是一个模版类,维护了着一个同步队列(双向链表),提供着同步队列一些操作的公共方法,JUC并发包里基于此类实现了很多常用的并发工具类,如 Semaphore, CountDownLatch等。

1
2
3
4
/**
* The synchronization state.
*/
private volatile int state;

AbstractQueuedSynchronizer维护了一个state变量,来表示同步器的状态,state可以称为AQS的灵魂,基于AQS实现的好多JUC工具,都是通过操作state来实现的,state为0表示没有任何线程持有锁;state为1表示某一个线程拿到了一次锁,state为n(n > 1),表示这个线程获取了n次这把锁,用来表达所谓的“可重入锁”的概念。

锁的创建

非公平锁(默认)

1
2
final ReentrantLock lock = new ReentrantLock();
final ReentrantLock lock = new ReentrantLock(false);

公平锁

1
final ReentrantLock lock = new ReentrantLock(true);

非公平锁加锁过程

lock()方法的逻辑: 多个线程调用lock()方法, 如果当前state为0, 说明当前没有线程占有锁, 那么只有一个线程会CAS获得锁, 并设置此线程为独占锁线程。那么其它线程会调用acquire方法来竞争锁(后续会全部加入同步队列中自旋或挂起)。当有其它线程A又进来想要获取锁时, 恰好此前的某一线程恰好释放锁, 那么A会恰好在同步队列中所有等待获取锁的线程之前抢先获取锁。也就是说所有已经在同步队列中的尚未被 取消获取锁 的线程是绝对保证串行获取锁,而其它新来的却可能抢先获取锁。后面代码解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;

final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
/**
* 此为AQS的protected方法,允许子类重写, 在这里被NonfairSync类重写
*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}

先看acquire方法:逻辑:tryAcquire方法仍然尝试获取锁(快速获取锁机制),成功返回false,如果没有成功, 那么就将此线程包装成Node加入同步队列尾部。。Node.EXCLUSIVE 为null表示这是独占锁,如果为读写锁,那就是 共享模式(shared)。

1
2
3
4
5
6
7
8
   public final void acquire(int arg) {
// tryAcquire()方法也是让新来的线程进行第二次插队的机会!!
//如果再次获取锁还不成功才会放到队列
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

再看addWaiter 逻辑:

  1. Node包装当前线程
  2. pred 尾指针不为null,即队列不为空, 则快速CAS将自己设为新的tail
  3. 如果队列为空, 则调用enq强制入队
  4. 如果CAS设置失败,说明在其它线程入队节点争抢了tail,则此线程只能调用enq强制入队

注意这里在链表尾部添加节点时,先设置的prev,我们先记住这个事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}




Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}

看Node的构造方法,属性 nextWaiter 值在此被赋予EXCLUSIVE表示独占模式。

下面是enq方法,方法内是一个for(;;),看来退出的条件只能是当前线程入队成功。之前也提到过,只有在产生锁竞争了,才会去初始化链表头节点。如果队列为空,初始化头尾节点,然后后续循环会走到else,else的逻辑和上线CAS入队的逻辑一样,只不过这里套在for循环里,直到入队成功才退出循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
    private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 判断队列是否为空,空队列应该先初始化头节点
if (compareAndSetHead(new Node()))
tail = head;//头尾共同指向头结点
} else {//CAS添加并允许失败, 走for(;;)
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;//返回新插入节点的前置节点
}
}
}
}
```


入队完成后看下`acquireQueued` 逻辑:

`failed` 标记最终acquire是否成功, `interrupted`标记是否曾被挂起过。注意到for(;;) 跳出的唯一条件就是`if (p == head && tryAcquire(arg))` 即当前线程结点是头结点且获取锁成功。从这里我们应该看到,这是一个线程第三次又想着尝试快速获取锁:虽然此时该节点已被加入等待队列,在进行睡眠之前又通过`p == head && tryAcquire(arg)`方法看看能否获取锁。也就是说只有该线程结点的所有 有效的前置结点都拿到过锁了,当前结点才有机会争夺锁,如果失败了那就通过`shouldParkAfterFailedAcquire`方法判断是否应该挂起当前结点,等待响应中断。观察 每次调用`tryAcquire`方法的时机,可以看出作者优化意图:

1. 尽量在没入队的时候拿到锁,避免过多队列操作维护成本
2. 尽量在睡眠前拿到锁,避免过多上下文切换
```java
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//如果第一次循环就获取成功那么返回的interrupted是false,不需要自我中断。
//否则 说明在获取到同步状态之前经历过挂起(返回true)。
for (;;) {// 获取前置结点
final Node p = node.predecessor();
//如果刚入队的尚未被挂起的节点的前置节点是头节点,那么此节点线程有必要尝试一下获取锁,因为head很可能是 刚初始化的 dummy head,或者 会预设head很快释放锁
if (p == head && tryAcquire(arg)) {
setHead(node); //设置新的head
p.next = null; // help GC
failed = false;
return interrupted; // fase 因为刚入队还没挂起就拿到锁了
}
//走到这里说明前置节点不为head或者抢锁失败了
//判断当前node线程是否可挂起,是,就调用parkAndCheckInterrupt挂起 ,interrupted设置为true,表示曾经被中断过
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
//如果tryAcquire出现异常那么取消当前结点的获取,毕竟tryAcquire是留给子类实现的,谁知道弄出啥幺蛾子
cancelAcquire(node);
}
}
```


看下`shouldParkAfterFailedAcquire` 方法的逻辑:
```java
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
/*
如果前置结点是SIGNAL状态, 那么当前置结点执行完成后是可以唤醒后续结点的,
此时可以安全的挂起当前结点, 不需要进行不必要的for(;;),前置结点自然会通知。
*/
return true;
if (ws > 0) {
// 如果ws>0说明前置结点是被自己取消获取同步的结点(只有线程本身可以取消自己)。
//那么do while循环一直往头结点方向找waitStatus < 0的节点;
//含义就是去除了FIFO队列中的已经自我取消申请同步状态的线程。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node; // 终于找到一个“正常”的节点,赶紧将它设为自己的后继节点
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
//如果是其它状态waitStatus要不是0或着PROPAGATE,意味着当前结点需要一个signal但是尚未park,
// 所以调用者必须确保在挂起之前不能获取同步状态。
// 并强行设置前置结点为SIGNAL。之所以CAS设置是因为,pred线程也会操作cancelAcquire来取消自己和node线程对pred的操作产生竞争条件。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

上面整个流程是线程通过addWaiter方法入队后,在挂起之前尝试获取锁,如果失败,通过shouldParkAfterFailedAcquire 判断是否应该挂起,因为刚入队的node的前置节点也是刚初始化没多久的节点,默认的waitStatus=0, 所以第一次走shouldParkAfterFailedAcquire 方法if (ws == Node.SIGNAL)分支进不去,并在else逻辑里CAS设置前置节点waitStatus=SIGNAL, 注意这里可能会CAS失败,最终方法应该会返回false。对于shouldParkAfterFailedAcquire 方法的调用上层别忘了有个for(;;),再次进入此方法就满足了if (ws == Node.SIGNAL)分支(预设之前CAS设置SIGNAL成功,即使失败,再次进来时do while 也会干掉cancel的节点),总之,总之,for(;;) 一定会控制在当前线程挂起之前设置好了前置节点能够将来SIGNAL自己。-
这个获取锁的流程我整理了一个流程图,方便理解:-

接下来看ReentrantLock中的NonfairSync对同步器方法

1
2
3
4
5
6
`tryAcquire` 的重写:

protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}

下面是nonfairTryAcquire

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
    final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { //如果没有线程获取锁,则当前线程CAS获取锁。并设置自己为当前锁的独占线程
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 如果存在锁竞争,判断获取锁的线程是否是当前线程, 因为ReentrantLock是可重入锁,
//用state变量表示重入次数,即:使当前state+1;
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//如果不是当前线程,则不能获取同步状态
return false;
}
```



## 公平锁的加锁过程

`lock`方法对比非公平锁, 没有了if else 也就意味着新来的线程没有插队的机会, 所有来的线程必须扔到队列尾部, acquire方法也会像非公平锁一样首先调用tryAcquire插队试试,但是只有队列为空或着本身就是head,那么才可能成功,如果 队列非空那么肯定被扔到队列尾部去了, 插个毛线。
```java
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;

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

//只有队列为空或着当前tryAcquire的node线程是head
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();//拿到当前的同步状态, 如果是无锁状态, 则进行hasQueuedPredecessors方法逻辑
//逻辑含义是:当前队列为空或本身是同步队列中的头结点。如果满足条件CAS获取同步状态,并设置当前独占线程。
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;
}
}
```

看`hasQueuedPredecessors()`-
和非公平锁对比多了这个方法逻辑, 也就意味着没有了新来线程插队的情况,保证了公平锁的获取串行化。
```java
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

有个有趣的地方: 既然h != t 还能h.next==null ? ,我这里能想到的一处地方,在于头节点初始化时先CAS的head,第二步才将tail=head,大概只有在这两步的间隙,这个条件才会成立吧!

公平锁和非公平锁的释放逻辑

公平锁和非公平锁的释放逻辑是一致的 都是通过sync.release(1);-

1
2
3
4
5
`ReentrantLock`中的`unlock()`

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

AbstractQueuedSynchronizer中的release方法:-
release之后还要调用unparkSuccessor() 方法唤醒后继结点

1
2
3
4
5
6
7
8
9
10
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;// 当前这个头结点是同步队列中的头结点
//并且不一定当前线程是要释放锁的执行线程, 因为可能是非公平锁的释放,压根就没插入队列就特么插队获得同步了, 总之就是拿到头结点了。
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

释放锁的逻辑tryRelease:-
只有拥有锁的线程才有资格tryRelease,所以这个方法内不需要任何同步机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 只有获得锁的线程自己才能释放锁
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {//c==0说明已经无锁
free = true;
setExclusiveOwnerThread(null);
}
setState(c);//这里就没有CAS,好像reactJs里的 state状态管理~
//否则更新state为state-1, 也就是加锁多少次,就得释放多少次, lock unlock配对使用。

return free;
}
```


看`unparkSuccessor` 方法逻辑:-
某线程释放锁后,要唤醒队列中的首结点或者后继结点来偷锁,当然唤醒后可能会参与和一些新来的线程pk下。会发现其可能会存在一个从tail向前查找的流程,假如刚好这时执行这个流程,从tail向head查找节点显然就会存在问题,所以12对调的流程在并发下也是存在问题的。`unparkSuccessor` 在查找head的下一个有效节点的时候,没有从head到tail方向查找,而是反方向从tail向head查找,正常逻辑肯定是从head向tail方向查找速度更快

```java
private void unparkSuccessor(Node node) {

int ws = node.waitStatus;
if (ws < 0)//同样是因为node线程和当前线程有竞争,node本身也可以修改自身状态嘛,因此CAS
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
//发现这里竟然从tail结点往前找最靠近head结点的且处于非取消状态的结点?这不增加了遍历复杂度么? 留个疑问下面解释!
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//s是后继结点 因为后继结点很可能是满足条件的, 如果满足即s!=NULL 直接unpark后继结点。
if (s != null)
LockSupport.unpark(s.thread);
}

为什么在unparkSuccessor 方法中对于需要unpark的线程结点进行从tail 往前查找?再来看一下enq 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
    private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
```


将上面代码梳理一下,大致分为三个步骤:

1、首先`node.prev = t` ,将当前node的prev指向tail节点;

2、CAS将`tail` 指向新node结点;

3、将之前tail节点的next指向当前节点


示意图如下:

[![](https://cubox.pro/c/filters:no_upscale()?imageUrl=https%3A%2F%2Fimgconvert.csdnimg.cn%2FaHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8xMi8zLzE2ZWNhZjRlNTA3YzQ2MTg%3Fx-oss-process%3Dimage%2Fformat%2Cpng)](https://blog.reactor.top/2018/01/31/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E9%94%81%E4%B9%8BReentrantLock%E6%80%BB%E7%BB%93/images/%E8%BF%BD%E5%8A%A0%E8%8A%82%E7%82%B9.jpg)

假如将追加节点的三个步骤顺序调换下,先将tail节点的next指向当前节点,然后cas原子修改tail指向,最后再来修改当前节点的prev指向,即将上面的13对调一下,会出现上面情况呢?

将tail节点的next指向当前节点操作后,紧接着会执行cas操作修改tail指向当前节点,由于存在多线程并发问题,即可能会存在多个线程同时申请锁资源,假如现在t1、t2两个线程都同时做上面两个步骤:

1、t1线程修改next后,紧接着t2线程也修改next指向,导致会把t1修改的指向覆盖;

2、这时t1线程做cas替换tail指向成功后,t2也来做cas操作就会失败;

3、t1由于cas操作成功,最后修改prev指向
[
![](https://cubox.pro/c/filters:no_upscale()?imageUrl=https%3A%2F%2Fimgconvert.csdnimg.cn%2FaHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8xMi8zLzE2ZWNhZjRlNzMxNDA1NGY%3Fx-oss-process%3Dimage%2Fformat%2Cpng)](https://blog.reactor.top/2018/01/31/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E9%94%81%E4%B9%8BReentrantLock%E6%80%BB%E7%BB%93/images/%E8%BF%BD%E5%8A%A0%E8%8A%82%E7%82%B92.jpg)

可以发现,由于并发导致追加的t1节点是存在问题的,正常情况下Node1的next应该指向t1节点,但是却被t2节点覆盖了。所以,13对调是在并发下是存在问题的。

假如12对调,先进行cas操作,然后修改prev,最后再来修改next又会怎么样呢?首先通过cas原子操作将tail指向当前节点,示意图如下:-
![](https://cubox.pro/c/filters:no_upscale()?imageUrl=https%3A%2F%2Fimgconvert.csdnimg.cn%2FaHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8xMi8zLzE2ZWNhZjRlNzRmYmIxOTk%3Fx-oss-process%3Dimage%2Fformat%2Cpng)

tail节点这时还是孤立的节点,prev和next都还没有指向,tail节点和其它节点之间没有关联了,这时如果其它线程需要遍历这个双向链表就比较危险了

现在我们来分析下为什么[源码](https://so.csdn.net/so/search?q=%E6%BA%90%E7%A0%81&spm=1001.2101.3001.7020)中这个顺序执行在并发下就不会存在问题。现在假设两个线程同一时间都没有获取到锁,都需要追加到Sync Queue队列尾部,大致流程如下:

1. 线程t1的节点设置prev指向tail,线程t2节点同时也设置prev指向tail,这时就不会出现上面如果先设置next就会导致后设置把之前设置覆盖情况,因为如果先设置next是对Node1进行操作,存在多个线程对Node1同时操作导致状态不一致问题,而如果这里先设置prev,操作对象是线程本身的节点,是不存在多线程并发问题,示意图如下:

[![](https://cubox.pro/c/filters:no_upscale()?imageUrl=https%3A%2F%2Fimgconvert.csdnimg.cn%2FaHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8xMi8zLzE2ZWNhZjRlNzY3MDEyNzg%3Fx-oss-process%3Dimage%2Fformat%2Cpng)](https://blog.reactor.top/2018/01/31/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E9%94%81%E4%B9%8BReentrantLock%E6%80%BB%E7%BB%93/images/%E8%BF%BD%E5%8A%A0%E8%8A%82%E7%82%B95.png)

2. 这时t1和t2都进行cas原子操作,反正会有一个线程会操作成功,假如是t1线程操作成功,然后就可以顺利的设置Node1节点的next指向t1,因为只会存在一个线程操作成功,所以对Node1的操作此时也不会存在并发问题,由于t1的cas操作成功导致t2线程进行cas操作必然失败,此刻示意图如下:

[![](https://cubox.pro/c/filters:no_upscale()?imageUrl=https%3A%2F%2Fimgconvert.csdnimg.cn%2FaHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8xMi8zLzE2ZWNhZjRlNzgwYzcxZjE%3Fx-oss-process%3Dimage%2Fformat%2Cpng)](https://blog.reactor.top/2018/01/31/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E9%94%81%E4%B9%8BReentrantLock%E6%80%BB%E7%BB%93/images/%E8%BF%BD%E5%8A%A0%E8%8A%82%E7%82%B96.png)

3. 由于t2线程cas操作失败,因此不再继续操作Node1的next指向自己,而是进入enq()方法中,其源码逻辑是enq方法中通过cas+无限循环方式保证t2节点一定会被追加到Sync Queue尾部的,每次循环都是重新获取最新的tail,然后将t2的prev指向这个最新的tail,然后通过cas操作将tail指向自己,最后在将之前tail节点的next指向t2节点,这个案例中获取的最新tail就是t1节点了,所以t2节点会被追加到t1节点后,这样就能保证即使在高并发下依然可以实现节点正常添加,而不会像之前出现状态不一致情况,示意图如下:-
![](https://cubox.pro/c/filters:no_upscale()?imageUrl=https%3A%2F%2Fimgconvert.csdnimg.cn%2FaHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8xMi8zLzE2ZWNhZjRlN2M5YzUxYzU%3Fx-oss-process%3Dimage%2Fformat%2Cpng)

4. 上面分析unparkSuccessor()在查找head的下一个有效节点的时候,没有从head到tail方向查找,而是反方向从tail向head查找,如果你对我刚才分析得到逻辑理解透彻的话,就比较好解释了。比如:t1设置prev指向Node1,然后cas操作将tail指向了t1,这时Queue的结构如下,假如这时候执行unparkSuccessor(),Node0查找它的后驱节点为Node1,假如Node1是无效节点,Node1需要继续查找它的后驱节点,但是这时Node1的next并没有设置,是无法查找到的,所以必须从tail向head方向查找才行。


[![](https://cubox.pro/c/filters:no_upscale()?imageUrl=https%3A%2F%2Fimgconvert.csdnimg.cn%2FaHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOS8xMi8zLzE2ZWNhZjRlODVkNTEzMjc%3Fx-oss-process%3Dimage%2Fformat%2Cpng)](https://blog.reactor.top/2018/01/31/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E9%94%81%E4%B9%8BReentrantLock%E6%80%BB%E7%BB%93/images/%E8%BF%BD%E5%8A%A0%E8%8A%82%E7%82%B98.png)

## 取消获取锁`cancelAcquire()` 方法

取消获取锁意味着结点的出队。包括三个场景下的出队:

1. node是tail
2. node既不是tail,也不是head的后继节点
3. node是head的后继节点

```java

private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;

node.thread = null;

// 跳过所有被取消的前置结点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;

Node predNext = pred.next;

node.waitStatus = Node.CANCELLED;

// `` 如果node是tail,更新tail为pred,并使pred.next指向null``
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// 如果node既不是tail,又不是head的后继节点
int ws;
// 如果前置结点状态是SIGNAL || 如果不是CAS设为SIGNAL 那么接下来的执行自然就会唤醒下一个结点
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
//设置next-link 使node的通过上面while循环找到的有效的前继结点指向node的下一结点
//之所以还是CAS,使下一个结点还是会可能取消自己的
compareAndSetNext(pred, predNext, next);
} else { // 如果node是head的后继节点,则直接唤醒node的后继节点
unparkSuccessor(node);
}

node.next = node; // help GC
// 最终把取消acquire的 node的next指针指向的它自己
}
}

场景1. node是tail

node出队的过程如下图所示。

  1. cancelAcquire()调用compareAndSetTail()方法将tail指向pred

  2. cancelAcquire()调用compareAndSetNext()方法将pred的next指向空

场景2. node既不是tail,也不是head的后继节点

node出队过程如下图所示:-

cancelAcquire()调用了compareAndSetNext()方法将pred指向successor。将successor指向pred是谁干的?是别的线程做的。当别的线程在调用cancelAcquire()或者shouldParkAfterFailedAcquire()时,会根据prev指针跳过被cancel掉的前继节点,同时,会调整其遍历过的prev指针。代码类似这样;

cancelAcquire()中:
 // Skip cancelled predecessors
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

shouldParkAfterFailedAcquire中:
  do {
       node.prev = pred = pred.prev;
    } while (pred.waitStatus > 0);
            pred.next = node;

场景3 node是head的后继节点

node出队的过程如下图所示(图中用node*表示前继节点)

cancelAcquire()调用了unparkSuccessor(),不过,unparkSuccessor()中并没有对队列做任何调整呀。-
比场景2还糟糕,这次,cancelAcquire()对于出队这件事情可以说是啥都没干。-
出队操作实际上是由unparkSuccessor()唤醒的线程执行的。-
unparkSuccessor()会唤醒successor结点,当被唤醒后将会实际执行出队操作。-
现在需要搞清楚successor是从什么地方恢复执行的呢?这要看successor是在哪里被挂起的。

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)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

successor线程是从parkAndCheckInterrupt被挂起的,恢复执行时,也从此处开始重新执行。successor将会重新执行for循环,此时,node尚未出队,successor的前继节点依然是node,而不是head。所以,successor会执行到shouldParkAfterFailedAcquire()处。而从场景2中可以得知,shouldParkAfterFailedAcquire()中将会调整successor的prev指针(同时也调整head的next指针),从而完成了node的出队操作-

总结

悲观锁在Java中就是我们所熟知的锁,实现方式主要分为两种:synchronized和Lock,而乐观锁的实现主要通过CAS操作实现。这里我们来比较下synchronized和Lock方式的大致差别:

  • synchronized主要依赖JVM底层实现,而Lock是通过编码方式实现,其实现方式差别还是比较大,可以参考之前的一个文章https://blog.csdn.net/lsgqjh/article/details/61915074
  • synchronized由于其简单方便,只需要声明在方法、代码块上即可,主要是不需要关心锁释放问题,在一般的编程中使用量还是比较大的,但是在真正的并发编程系统中,Lock方式明显优于synchronized:
    •   在高版本JDK中,已经对synchronized进行了优化,synchronized和Lock方式在性能方面差别已不太明显
*   synchronized最致命的缺陷是:synchronized不支持中断和超时,也就是说通过synchronized一旦被阻塞住,如果一直无法获取到所资源就会一直被阻塞,即使中断也没用,这对并发系统的性能影响太大了;Lock支持中断和超时、还支持尝试机制获取锁,对synchronized进行了很好的扩展,所以从灵活性上Lock是明显优于synchronized的

参考连接:

[1] Java并发编程的艺术-

[2] https://www.cnblogs.com/yangming1996/p/7612653.html-

[3]https://blog.csdn.net/lsgqjh/article/details/61915074-

[4]https://blog.reactor.top/2018/01/31/

[5]https://www.jianshu.com/p/01f2046aab64