首先声明一点: 我在分析源码的时候, 把jdk源码复制出来进行中文的注释, 有时还进行编译调试什么的, 为了避免和jdk原生的类混淆, 我在类前面加了"My". 比如把ReentrantLock改名为了MyReentrantLock, 在源码分析的章节里, 我基本不会对源码进行修改, 所以请忽视这个"My"即可.


一. 简介

锁是什么? 锁是一种标志, 或者是一种资源, 持有锁的线程才可以继续往下执行相应的内容. 未持有锁的线程需要等待这个锁资源. 直到获取到了这个锁, 才可以继续向下执行.

0. ReentrantLock的一个小demo

想自己运行这段代码的话, 把代码中的"MyReentrantLock" 改为 "ReentrantLock" 即可. (后续的代码也一样, 如果想自己运行, 还编译报错, 请把我修改的代码改回来. 也就是把"My"都去掉就好了)

public class Main {
private static MyReentrantLock lock = new MyReentrantLock(); public static void main(String[] args) throws Exception {
// 场景如下: 线程1先获得锁, 释放后, 线程2 再获得锁. new Thread(() -> {
System.out.println("线程1启动");
lock.lock();
System.out.println("线程1抢到锁");
try {
System.out.println("这里是业务逻辑1");
quietSleep(2);// 两秒后释放锁
System.out.println("两秒后");
} finally {
lock.unlock();
System.out.println("线程1释放锁");
}
}).start(); new Thread(() -> {
System.out.println("线程2启动");
quietSleep(1); // 在这里进行谦让. 确保上面的线程能先运行. 也就是让上面的线程先获得锁
lock.lock();
System.out.println("线程2抢到锁");
try {
System.out.println("这里是业务逻辑2");
} finally {
lock.unlock();
System.out.println("线程2释放锁");
}
}).start(); } public static void quietSleep(long sec) {
try {
Thread.sleep(sec * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} }

输出的结果如下:

1. sync字段

首先来看一下ReentrantLock里唯一的一个字段

Sync继承自AQS(AbstractQueuedSynchronizer, 以下简称AQS) . 公平锁和非公平锁都继承了Sync. Sync是ReentrantLock类里锁的统一声明.

2. lock/unlock依赖Sync

ReentraintLock的 lock()和unlock()方法实际上都是靠Sync来实现的:

3. 锁内部类定义

Sync 和 公平锁 和 非公平锁 都是ReentrantLock的内部类, 类的定义部分如下(细节先隐藏起来了, 后面会讲):

4. ReentrantLock构造器

ReentrantLock有两个构造器.

1. 默认构造器是直接使用了非公平锁. 非公平锁就是不一定按照"先来后到"的顺序来进行争抢.

2. 带参构造器可以传递一个bool类型. true的时候为公平锁. 公平锁就是按照"先来后到"的顺序来进行争抢.

二. 公平锁申请锁

使用锁的第一个步骤, 当然就是先申请锁了, 咱么来分析一下源码, 看看申请锁的流程吧.

1. 公平锁获取锁的流程(单线程, 没有争抢)

首先从最外层的调用lock()方法开始. 咱们在Main方法里写下这两行代码:

MyReentrantLock就是ReentrantLock, 我复制了源代码, 然后改了个名字而已.

Reentraint类的lock()方法最终还是调用的sync.lock()

由于我们现在使用的是公平锁. 所以sync现在是FairSync. 所以sync.lockI()实际上就是FairSync类里的lock()方法

发现lock()调用的是acquire(1)这个方法, 这个方法是在AQS类里实现的.代码如下:

arg当时传进来的是1, 所以首先进行的是tryAcquire(1)来进行"尝试获取锁"的操作. 这时一种乐观的想法.

tryAcquire方法的具体实现在FairSync类里, 具体代码如下:

/**
* @return 返回true: 获取到锁; 返回false: 未获取到锁
* 什么时候返回true呢? 1.没有线程在等待锁;2.重入锁,线程本来就持有锁,也就可以理所当然可以直接获取
* @implNote 尝试直接获取锁.
*/
protected final boolean tryAcquire(int acquires) {
// 获取当前线程的引用
final Thread current = Thread.currentThread(); // 当前锁的计数器. 用于计算锁被获取的次数.在重入锁中表示锁重入的次数.由于这个锁是第一次被获取, 所以c==0
int c = getState(); // c==0, 也就是 state == 0 ,重入次数是0, 表示此时没有线程持有锁.
if (c == 0) {
// 公平锁, 所以要讲究先来后到
// 因为有可能是上一个持有锁的线程刚刚释放锁, 队列里的线程还没来得及争抢, 本线程就乱入了
// 所以每次公平锁抢锁之前, 都要判断一下等待队列里是否有其他线程
if (!hasQueuedPredecessors() &&
// 执行到这里说明等待队列里没有其他线程在等待.
// 如果没有线程在等待,那就用CAS尝试一下,成功了就获取到锁了,
// 不成功的话,只能说明一个问题,就在刚刚几乎同一时刻有个线程抢先了 =_=
compareAndSetState(0, acquires)) { // 到这里就获取到锁了,标记一下,告诉大家,现在是我(当前线程)占用了锁
setExclusiveOwnerThread(current);
// 成功获取锁了, 所以返回true
return true;
} //-- 由于现在模拟的是单纯地获取一次锁, 没有重入和争抢的情况, 所以执行不到这里, 上面的cas肯定会成功, 然后返回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;
}

争抢完锁之后会返回true, 然后回到上层方法acquire :

if语句里 && 前面是false, 不会继续往下执行了. 当前线程获取到了锁, 而且执行了所有该执行的内容, 就完事儿了.

2. 公平锁进行重入的流程

重入就是一个线程获取到了锁, 然后这个线程又一次申请(进入)了这个锁.

重入用synchronized来举例就是这样:

用ReentrantLock来举例子就是这样:

同一个线程(main线程) 首先进行了lock.lock()申请并占有了锁, 随后又执行了一次lock.lock(). 还没释放锁的情况下, 又一次申请锁. 这样就是重入了.

上面一小节已经分析了第一行的lock.lock()是如何获取到锁的, 所以我们只分析 重入的部分, 也就是后面那句lock.lock()的执行流程.

前面的执行过程一直是一模一样的, 直到这里:

/**
* @return 返回true: 获取到锁; 返回false: 未获取到锁
* 什么时候返回true呢? 1.没有线程在等待锁;2.重入锁,线程本来就持有锁,也就可以理所当然可以直接获取
* @implNote 尝试直接获取锁.
*/
protected final boolean tryAcquire(int acquires) {
// 获取当前线程的引用
final Thread current = Thread.currentThread(); // 当前锁的计数器. 由于前面的那句lock已经获取到锁了, 所以这里是status==1, 也就是 c==1
int c = getState(); // c==1, 表示当前有线程持有锁, 所以这段if是进不去了
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
} // 由于 c==1 , 无法进入if语句, 所以来看看满不满足这里的 else if
// 这个锁被人占了, 但还是不死心, 于是看一下是不是当前线程自己占的这个锁.
// (人家女生说有喜欢的人, 为什么不问问是不是自己呢 = =.)
// 由于是同一个线程, 所以就是自己啦! 所以会进入这个else if分支,
} else if (current == getExclusiveOwnerThread()) {
// 代码执行到这里了, 就是所谓的 重入 了 // 这里的acquires的值是1, 所以nextc = 1 + 1 , 也就是2了
int nextc = c + acquires;
// 小于0, 说明int溢出了
if (nextc < 0) throw new Error("Maximum lock count exceeded");
// 在这里把状态更新一下, 把state更新为2, 意思就是这个锁被同一个线程获得2次了.
// (大家就可以以此类推, 下次再重入的话, 那么就会再+1, 就会变为3....)
setState(nextc);
// 重入完成, 返回true
return true;
} return false;
}

还记得上小节讲的, 获取锁的时候进入的是这段代码的if语句, 而重入就不一样了, 进入的是 else if语句. 但最终返回的还是true, 表示成功.

上面讲的是无争强的情况, 接下来讲讲有争抢的情况.

3. 公平锁cas争抢失败

场景如下:

一开始锁是空闲状态, 然后两个线程同时争抢这把锁(在cas操作处发生了争抢).

一个线程cas操作成功, 抢到了锁; 另一个线程cas失败.

代码例子如下(代码的意思到位了, 但是这段代码最后不一定会在cas处进行争抢, 大家意会就好了):

cas操作成功的线程就和上面第1小节的一样, 就不用再重复描述了.

而cas争抢失败的线程会何去何从呢? 看我给大家分析:

 /**
* @return 返回true: 获取到锁; 返回false: 未获取到锁
* 什么时候返回true呢? 1.没有线程在等待锁;2.重入锁,线程本来就持有锁,也就可以理所当然可以直接获取
* @implNote 尝试直接获取锁.
*/
protected final boolean tryAcquire(int acquires) {
// 获取当前线程的引用
final Thread current = Thread.currentThread(); // 当前锁的计数器.
int c = getState(); // state == 0 表示此时没有线程持有锁
if (c == 0) {
// 本场景中, 一开始锁是空闲的, 所以队列里没有等待的线程
if (!hasQueuedPredecessors() &&
// 两个线程在这里进行争抢
// cas抢成功的会进入到if代码块
// cas抢失败的, 就跳出整个if-else, 也就是直接到最后一行代码
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;
} // cas 操作失败后, 会这直接执行到这里. 返回false.
return false;
}

在这里返回了false, 回到上一层函数.

第一个条件是true, 所以会继续往下执行acquireQueued方法. 来准备让这个失败的线程进入队列等待.

下面继续来给大家讲解 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) .

先讲讲这个addWaiter(Node.EXCLUSIVE):

/**
* 将当前线程封装为Node, 然后根据所给的模式, 进行入队操作
*
* @param mode 有两种模式 Node.EXCLUSIVE 独占模式, Node.SHARED 共享模式
* @return 返回新节点, 这个新节点封装了当前线程.
*/
private Node addWaiter(Node mode) { // 这个mode没用上.
Node node = new Node(Thread.currentThread(), mode);
// 咱们刚才都没见到过tail被赋予了其他的值, 当然就是null了.
Node pred = tail;
// tail是null的话, pred就是null, 所以不会进入到这个if语句中.所以跳过这个if语句.
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
} // 因为锁的等待队列是懒初始化, 直到有节点插入进来, 它才初始化.
// 而现在这个挣钱失败的线程, 正好是锁建立以来, 第一个进入等待队列的线程. 所以现在才准备进行初始化.
// 初始化完了后会把当前线程的相关信息和引用封装成Node节点, 然后插入到队列当中.并且制定head 和 tail.
// tail就不等于null了, 所以下一次addWaiter方法被调用的时候, 就会执行上面的if语句了. 而不会跳过if语句, 来到这里进行初始化了.
enq(node);
// 返回这个Node节点.
return node;
}

目的就是要将这个cas失败的线程封装成节点, 然后插入到队尾中. (等待队列是懒初始化,)

如果队列已经初始化了, 那么tail就不会是null, 就会执行上面代码中的if语句, 调整一下指针的引用就好了.

但是如果队列还未初始化, 那么就应该先初始化, 再插入. 先初始化,再插入, 对应的代码是enq(node).

接下来讲解一下enq方法:

   /**
* 采用自旋的方式入队
* CAS设置tail,直到争抢成功.
*/
private Node enq(final Node node) {
for (; ; ) {
Node t = tail;
// 最开始tail肯定是null, 进入if进行初始化head和tail.
if (t == null) { // Must initialize
// 设置head 和tail. cas来防止并发.
if (compareAndSetHead(new Node())) tail = head; // if 语句执行完了后, 之后的for循环就会走else了.
} else {
// 争抢入队, 没抢到就继续for循环迭代.抢成功了就可以return了,不然一直循环.
// 为什么是用cas来争抢呢? 因为怕是多个线程一起执行到这里啊
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

刚才的addWaiter(Node.EXCLUSIVE) 分析完了, 总之就是addWaiter之后, 队列肯定是被创建完了, 而且还把node(当前线程的封装)插入到了队列的队尾. 并且返回了这个node.  acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 可以简化为 acquireQueued(node)

所以继续分析acquireQueued方法.

final boolean acquireQueued(final Node node, int arg) {
// node是刚才addWaiter方法插入到队尾的节点
// arg 是 1 boolean failed = true;
try {
boolean interrupted = false;
for (; ; ) {
// 获取node节点的前驱.
final Node p = node.predecessor(); // 如果node节点的前驱是head
if (p == head
// 那么可以再尝试着抢一下锁.
// 等待队列里的第一个节点很乐观, 因为确实很有可能会马上轮到他
&& tryAcquire(arg)) {
// 如果这个node就是那么巧合, 刚刚锁被释放了, 这回重新抢就真的抢到了
// 那么就把当前节点设为头结点.(头结点的含义就是当前持有锁的线程)
setHead(node);
// 上一个节点既然已经释放了锁, 也就该GC了. 置为null, 方便GC收集
p.next = null; // help GC
// 很明显是获取锁成功了啊, 所以failed = false
failed = false; // 这么大一段代码, 只有这一处return
return interrupted;
} //---- 如果不是队头, 那么就会执行到这里.
//---- 或者虽然作为等待队列里的第一名, 单由于持有锁的线程还是没有释放, 所以还是没抢到锁. 那么也会执行到这里 // 获取锁失败的时候是否该阻塞
if (shouldParkAfterFailedAcquire(p, node)
// 在这里阻塞, 等待唤醒
&& parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 上面那段, 如果中途异常了的话, 就会执行到这里. (一般不会到这里的)
if (failed) cancelAcquire(node);
}
}

上面这段代码中shouldParkAfterFailedAcquire方法 和 parkAndCheckInterrupt() 方法 还未解释. 一个一个来.

/**
* 当前线程没有抢到锁,是否需要挂起当前线程
*
* @param pred 前驱结点
* @param node 当前结点
* @return 如果线程需要被阻塞, 那么就返回true
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 前驱节点的 waitStatus == -1 ,说明前驱节点状态正常,当前线程需要挂起,直接可以返回true
if (ws == Node.SIGNAL)
return true;
// 大于0, 其实就是等于1, Node.CANCELLED 是 1, 因为状态中只有这个状态是大于0的...说明前驱节点取消了排队
// 所以下面这块代码说的是, 在链表中从prev结点开始, 往前删掉CANCELLED状态的结点.
// 只有CANCELLED状态值大于0
if (ws > 0) {
do {
node.prev = pred = pred.prev; // 删掉之后再往前看看, 看看前面是不是CANCELLED, 如果是, 那还得继续往前删
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 在前面的两个if语句中排除掉了waitStatus值为-1和1的情况,
// 只剩下0,-2,-3这三个状态了
// 然而在我们前面的源码中,都没有看到有设置waitStatus的,
// 所以只剩下等于0的情况了
// 下面的操作就是, 如果waitStatus等于0, 那么就用cas将前驱结点的waitStatus设置为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

shouldParkAfterFailedAcquire里的前两行是在判断前驱节点prev的状态. 但是之前咱们分析代码, 并没有发现哪里设置了waitState.

所以waitState是默认值0.

所以shouldParkAfterFailedAcquire会直接执行下面的else, 在这里吧pred的waitState设置为-1, 然后返回false.

回到刚才的acquireQueued方法. 由于外层是for循环, 会在下一次for循环在此执行到shouldParkAfterFailedAcquire方法.

由于刚才已经把前驱节点prev的waitState改为1了, 所以这次在前两行判断prev的waitState时, 直接就满足条件, 然后return true了.

shouldParkAfterFailedAcquire方法return true了, 才会往下执行parkAndCheckInterrupt方法.

下面是parkAndCheckInterrupt()方法. 最终返回Thread.interrupted(). 返回线程是否被中断. (中断和挂起不是一回事 )

    /**
* 在这里线程阻塞.
* 被唤醒的时候会返回, 如果被中断过, 那么就返回true
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
// 挂起.
MyLockSupport.park(this);
return Thread.interrupted();
}

LockSupport.park(this)会挂起当前线程. 但是LockSupport.park还有一个隐藏功能. 就是, 如果先对一个线程unpark, 再对这个线程park, 那么这次的park是失效的. 下一次park才会挂起.

原因就是, 对一个没有被park的线程进行unpark的时候, 会把标志位perm置为1. 而每次park的操作, 都是先去检查perm是否为1.

如果是1, 那么置为0, 并且这次不挂起.

如果perm为0, 那么就直接挂起这个线程.

4. 公平锁由于队列内有元素而失败

demo如下. 前两个线程, 其中一个获取锁成功, 另一个失败, 然后进入等待队列.

稍后, 第三个线程来获取锁, 但是这时由于等待队列中已经有元素在等待了. 所以会直接失败, 然后会被插入到等待队列的尾部.

上面的main方法中总共有三个线程想要占有锁. 前两个锁的争抢在上小节就已经模拟过了.

咱么现在只分析第三个线程申请锁的流程. 这个场景下的tryAcquire方法如下(会直接返回false):

/**
* @return 返回true: 获取到锁; 返回false: 未获取到锁
* 什么时候返回true呢? 1.没有线程在等待锁;2.重入锁,线程本来就持有锁,也就可以理所当然可以直接获取
* @implNote 尝试直接获取锁.
*/
protected final boolean tryAcquire ( int acquires){
// 获取当前线程的引用
final Thread current = Thread.currentThread(); // 当前锁的计数器.
int c = getState(); // 不会走这的if语句, 因为锁被其他线程占有, 肯定不是0
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;
} // 由于队列内有元素, 所以if语句不执行,
// 由于不是重入, else if 也不执行.
// 直接返回false
return false;
}

这段方法返回false, 说明需要执行这个. acquireQueued(addWaiter(Node.EXCLUSIVE), arg). 先看看addWaiter方法有什么区别.

/**
* 将当前线程封装为Node, 然后根据所给的模式, 进行入队操作
*
* @param mode 有两种模式 Node.EXCLUSIVE 独占模式, Node.SHARED 共享模式
* @return 返回新节点, 这个新节点封装了当前线程.
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 以下几行代码想把当前node加到链表的最后面去,也就是进到阻塞队列的最后
Node pred = tail;
// 如果tail不是空, 说明有头结点.说明这个队列已经被初始化了.
// 因为本小节讲的就是: 因为公平锁的等待队列中有其他线程才导致当前线程争锁失败, 所以说明等待队列不仅被初始化了, 而且里面还有元素.
if (pred != null) {
// node设置自己的前驱为pred
node.prev = pred;
// 用CAS把当前节点node设置为队尾, 如果成功后,tail指针就指向了node
if (compareAndSetTail(pred, node)) {
// 如果cas争抢成功, 那么就会在这里返回.(而cas失败的, 会跳过这个if代码块, 会执行到下面的enq方法)
// 剩下的就是整理一下链表数据结构的连接问题了
// pred调整自己的后继为node
pred.next = node;
return node;
}
} // 如果在上面的cas中设置失败, 那么还是会执行到这里.
// 然后在enq方法里靠for循环+cas的形式, 不断尝试着插入到队尾. enq(node);
return node;
}

后续执行的就和上小节的一样了.就不重复了...

当然, 场景是举不完的, 举完的话就跟笛卡尔积那样了. 我这里只是靠这四个例子来尽量完整地分析了获取锁的流程.

三. 公平锁释放锁

刚才申请锁的流程. 但是争抢失败的那些线程, 最后都进入到了等待队列里, 然后就杳无音讯了.

那当前持有锁的线程释放锁后, 是如何唤醒等待队列里的线程, 让下一个线程获取锁的呢?

咱么接下来分析一下释放锁的过程吧.

1. 申请1次锁, 执行一些业务, 然后释放

咱们只关注unlock, lock就跳过了, 前面讲过了.

ReentrantLock类的lock()方法 代码如下:

而这个release是AQS里的方法. 源码如下:

其中arg变量值是1.  首先会执行tryRelease(1) 来尝试释放锁.

如果尝试成功了, 那么tryRelease(1)就会返回true, 就会继续执行if代码块里的内容.

如果尝试失败了, 那么tryRelease(1)就会返回false. 然后就会跳过if语句, 最终本段方法(release方法)也会返回false.

咱们先分析一下tryRelease方法吧(tryRelease方法的源码在Sync抽象类里):

protected final boolean tryRelease(int releases) {
// releases == 1 // c 就是重入次数 -1 , 由于本场景下模拟的是简单的获取一次锁, 然后释放, 不涉及到重入. 所以getState() == 1
// 所以c = 1 - 1 , c现在等于0
int c = getState() - releases; // 判断当前的线程是不是持有锁的线程, 不然抛异常.
// 这是为了其他的线程捣乱.
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException(); // 用于标记是否可以完全释放锁
boolean free = false; // c等于0, 说明没有重入了, 可以完全释放了.
if (c == 0) {
// 标记一下, 准备完全释放锁
free = true;
// 把锁的持有者设置为空, 表示锁被释放.
setExclusiveOwnerThread(null);
}
// 把刚才c==0 设置为state
setState(c); // 表示是否完全释放. 本场景下返回true
return free;
}

由于返回的是true, 所以返回后还有if语句块要执行:

接下来分析一下其中的unparkSuccessor方法, 看看他是如何唤醒下一个节点的.(这个方法在AQS里)

unpark之后, 就会把之前park(挂起)的线程激活, 然后继续执行:

如果线程被中断了, 那么parkAndCheckInterrupt()方法会返回true, 然后就会执行interrupted = true 这句话.

挂起和中断不是一回事, 一般不会被中断的. 所以一般不会执行interrupted=true这句话.

外层是个for循环, 当前线程被激活后, 作为等待队列中的第一个线程, 来进行获取锁. 由于是公平锁, 所以可以放心拿到, 没有人会抢, 所以会正常获取到锁.

2. 重入锁的释放

释放重入的锁(同一个线程多次获取的锁), 执行流程唯一不同的就是tryRelease方法了, 其他的都一样, 可以直接参考上面一小节的.

咱么看看重入的时候, tryRelease是如何执行的吧.

 protected final boolean tryRelease(int releases) {
// 其实就是重入计数器 -1
// 而由于本线程获取了2次这个锁, 所以state字段的值为2
// 所以c = 2 - 1
// 所以现在c == 1
int c = getState() - releases; // 判断当前的线程是不是持有锁的线程, 不然抛异常.
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException(); // 用来标记是否完全释放锁
boolean free = false; // c现在等于1, 不会进入这个if代码块
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
} // 设置重入计数器, 也就是让 state =1
setState(c); // 返回false.
return free;
}

本小节和上小节的区别就是这段代码了.既然这段方法返回false, 那么返回后, release方法的if代码块自然也就不执行了.

四. 非公平锁的获取

刚才讲解了公平锁, 那么接下来讲讲非公平锁, 到底是怎么个不公平呢?

1. 非公平锁与公平锁获取的区别

由于非公平锁的获取与公平锁的获取, 只有一点点区别. 所以咱么只分析出区别就好了, 其他的部分都一样的.

然后会调用到NonfairSync类里的lock()方法.

这里就体现出了区别.

公平锁里的lock()方法里面, 只有acquire(1).

而非公平锁在acquire(1)之前多了一次cas操作. 一上来就尝试着抢占锁, 看看有没有机会(万一真的这个时候持有锁的线程正好把锁释放了呢). 非公平锁根本不管是否有其他人在排队.上来就是一抢.

当这次cas失败了, 才会像公平锁一样进入acquire(1)方法:

这里和公平锁一样. 只是, 非公平锁的tryAcquire方法和公平锁的tryAcquire方法内部实现不一样.

看看非公平锁的tryAcquire方法吧:

咱们继续往下看看nonfairTryAcquire方法吧:

/**
* 不公平地尝试获取锁.
* 不公平的语义就是: 不用判断队列里是否有其他线程在等待, 直接抢.
*/
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程的引用
final Thread current = Thread.currentThread();
// 当前线程的重入次数
int c = getState();
// 如果是0, 表示此时此刻锁还被被任何一个线程所占用
if (c == 0) {
// 当c==0的时候, 公平锁锁是先判断队列里是否有其他线程在等待, 如果没有, 再去cas争抢.
// 而非公平锁这里, 就是根本就不去理会等待队列, 自己抓到机会就赶紧抢
// cas来争抢, 让重入次数变1.
// 用cas是因为这个地方会发生并发.
// 多个抢占当然只有一个成功了
if (compareAndSetState(0, acquires)) {
// 设置锁的拥有者为当前线程.
setExclusiveOwnerThread(current);
return true;
} // 如果不是0, 说明锁被某一个线程占用了
// 既然被占用了, 那就有两种情况: 1. 被自己占用; 2. 被别的线程占用
// 所以先看看是不是自己占用的, 如果是自己占用的, 那就重入.
} else if (current == getExclusiveOwnerThread()) {
// 其实就是+1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 这里不会产生争抢, 不必用cas
// 因为只有占用锁的这一个线程才能进入到这个else if 里
// 一个线程不可能发生争抢
setState(nextc);
return true;
}
// 1. 如果在if里的cas争抢失败
// 2. 或者是不满足else if的条件
// 那就会直接返回false
// 不管是成功还是失败, 都不会有线程的等待阻塞之类的. 都是立即返回.
return false;
}

这里的非公平锁的nonfairTryAcquire方法 和 公平锁的tryAcquire方法很像. 区别就是:

非公平锁是, 当c==0. 也就是此时此刻, 锁是空闲状态的时候. 直接就尝试着用cas来争抢锁, 看看是否能成功, 而不管等待队列是否还有其他线程再等待.

而公平锁在c==0的时候, 也就是state==0 的时候, 先去看看队列里是否有其他线程再等待, 如果队列里没有其他线程在等待, 才会去cas争抢. 不然就会把机会让给队列里的第一个线程, 而自己会进入到等待队列的尾部.

为什么c==0了, 队列里还有可能会有其他的元素在等待呢?

因为c==0只是说明当前锁的状态是空闲状态. 只是上一个线程刚刚把锁释放, 当前线程就来争抢锁了, 还没来得及唤醒等待队列里的第一个线程呢.

其他地方就跟公平锁都一样的, 就是多了本小节讲的两处cas.

五. 非公平锁的释放

1. 非公平锁会导致饥饿

也就是说, 上一个线程释放锁后, `等待队列` 里的第一个线程就会被激活, 然后会执行tryAcquire方法. 如果这个时候有新的线程来争抢,

由于是非公平模式, 有可能新的线程会抢到这个锁. 如果新的线程抢到了锁, 那么刚刚被激活的线程(等待队列里的第一个线程)就是执行tryAcquire失败, 这个方法执行失败就意味着会被再次被挂起. 如果并发量严重, 很可能`等待队列`里的所有线程在一定时间内都无法被正常调度.也就是产生了线程饥饿的现象.

六. Condition简介

1. condition简介和demo

public class Main {
private static MyReentrantLock lock = new MyReentrantLock();
private static Condition condition = lock.newCondition(); public static void main(String[] args) {
new Thread(Main::funcA).start();
new Thread(Main::funcB).start();
} public static void funcA() {
lock.lock(); System.out.println("await之前");
try {
condition.await(); // 在这里等待被其他线程通知(signal)
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("await之后"); lock.unlock();
} public static void funcB() {
lock.lock(); System.out.println("signal之前");
condition.signal(); // 这里会通知funcA的await().让funcA()继续执行下去
System.out.println("signal之后"); lock.unlock();
}
}

运行这段代码, 输出如下:

如果还是没有体会到区别, 那么把main方法里的第二行注释掉, 然后再执行一下:

输出结果如下:

也就是说, await()会使当前线程挂起, 需要其他线程通知他, 他才能被激活(唤醒).

七. Condition实例化

1. 获取condition的例子

2. condition实例化的源码

newCondition方法在ReentrantLock类里的实现如下:

Sync类里的newCondition()方法如下:

ConditionObject是AQS里的一个内部类,实现自Condition,  类的声明如下(具体源码后面再解释):

八. condition的等待(await) 和 通知(signal)

1. 只执行一句await()后的流程

await()方法的具体实现在AQS里的内部类ConditionObject类里:

public final void await() throws InterruptedException {
if (Thread.interrupted()) throw new InterruptedException();
// 添加到 condition 的`条件队列`中
Node node = addConditionWaiter();
// 完全释放锁,返回值是释放锁之前的 state 值
int savedState = fullyRelease(node);
int interruptMode = 0;
// 这里的isOnSyncQueue就是在判断node节点是否在锁的`等待队列`里
while (!isOnSyncQueue(node)) {
// 在这里线程挂起
MyLockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//---- 程序不会执行到下面, 因为在前面就已经挂起了. if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}

这里面有几个方法之前没提到过, 在这里一一攻破.

先解决addConditionWaiter()方法:

   /**
* 将当前线程对应的节点入队,插入队尾, 并且作为本方法的返回值.
*/
private Node addConditionWaiter() {
// 本例子中的场景下, 只执行过一次await()方法, 所以是第一个进入`条件队列`的元素.
// 所以lastWaiter和firstWaiter肯定都是null.
Node t = lastWaiter;
// 本例子中t==null, 所以这段if暂时不考虑吧
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
} // 新建节点
Node node = new Node(Thread.currentThread(), Node.CONDITION); // 因为t==null, 意思是队列目前还是空的, 所以这个节点是第一个节点, 所以是firstWaiter.
if (t == null) firstWaiter = node;
else t.nextWaiter = node; // 但node同时也是最后一个节点, 也就是lastWaiter
lastWaiter = node; // 最后会返回本方法
return node;
}

接下来是fullRelease(node)方法, 来完全释放锁:

final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
// 这里使用了当前的 state 作为 release 的参数,也就是完全释放掉锁,将 state 置为 0
if (release(savedState)) {
failed = false;
// 并且把释放锁之前的state值返回出去. (本例子中是1)
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}

最后就是isOnSyncQueue(node)方法, 来判断锁的`等待队列`中有没有当前这个node:

   /**
* 这个方法就是判断 node 是否已经移动到sync queue了
* (signal 的时候会将节点从条件队列移到sync queue)
*/
final boolean isOnSyncQueue(Node node) {
// 当进入Condition队列时,waitStatus肯定为CONDITION,
// 如果同时别的线程调用signal,Node会从Condition队列中移除,
// 并且移除时会清除CONDITION状态。
// 从移除到进入sync queue队列,中间这段时间prev必然为null,所以还是返回false,即被park
if (node.waitStatus == Node.CONDITION || node.prev == null)
// 本例子中, 会在这里返回
return false; //--- 本例子中, 程序不会往下执行了. 但是下面的代码还是分析一下吧. 这样待会儿就不用再重新讲个方法了. // 当别的线程进入sync queue队列时,会和前一个Node建立前后关系,所以如果next存在,说明一定在release队列中
if (node.next != null) // If has successor, it must be on queue
return true; // 到这里还没找到, 那只能去锁的`等待队列`里一个一个找了 // 可能该Node刚刚最后一个进入release队列,所以是tail,其next必然是null,所以需要从队尾向前查找
// 这个方法的源码就不讲了, 太简单了, 就是链表从后往前找node.找到了就true.没找到就false.
return findNodeFromTail(node);
}

最终会执行到await()方法里的park()方法, 线程挂起. 等待被别的线程唤醒.

2. 只执行一句signal()后的流程

然后咱们看看signal()的源码.

由于firstWaiter==null, 所以first==null, signal方法直接就退出了.

3.一个线程await等待, 另一个线程用signal来唤醒

本场景的程序demo如下:

public class Main {
private static Scanner scanner = new Scanner(System.in); private static MyReentrantLock lock = new MyReentrantLock();
private static Condition condition = lock.newCondition(); public static void main(String[] args) throws Exception {
new Thread(Main::funcA).start();
new Thread(Main::funcB).start();
} public static void funcA() {
lock.lock(); System.out.println("await之前");
try {
condition.await(); // 在这里等待被其他线程通知(signal)
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("await之后"); lock.unlock();
} public static void funcB() {
lock.lock(); System.out.println("signal之前"); System.out.print("请输入任意内容并回车, 以执行signal方法: ");
scanner.next(); // 在这里进行阻塞, 在控制台输入任意内容后回车, 就会接触阻塞, 就会执行signal方法, 也就是通知funcA()方法.
condition.signal(); // 这里会通知funcA的await().让funcA()继续执行下去
System.out.println("signal之后"); lock.unlock();
} }

首先, await()仍然执行到park这句, 然后挂起, 这点与本章第1小节的流程是一样的(看下图, 我选中的park那行代码, await就在这里挂起):

而此时控制台如下:

此时还没有执行signal, 因为我用输入流给signal方法进行阻塞了, 需要输入内容后回车, 就可以调用到signal方法.signal通知后,await就会被唤醒.

如下:

咱们分析一下signal是如何通知await, 然后让await线程被唤醒的:

因为刚才执行过await(), 所以firstWaiter不会是null. 所以会调用到doSignal方法:

上面这段代码也比较简单, 就是将firstWaiter为头的这个链表, 把第一个元素出队, 然后让第二个元素当新的头部. 然后让刚才出队的那个元素执行tansferForSignal方法.

    /**
* 将节点从条件队列转移到锁的`等待队列`
*
* true 代表成功转移
* false 代表在 signal 之前,节点已经取消了
*/
final boolean transferForSignal(Node node) {
/*
* 在这里将 waitStatus 置为 0.
* 如果成功设置为0, 那么继续往下面执行
* 如果CAS 失败,说明此 node 的 waitStatus 已不是 Node.CONDITION,说明节点已经取消,那么直接return false.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false; /*
* enq(node): 自旋进入阻塞队列的队尾.这个在将lock()方法的时候大家见到过.就是同一个方法.
* 这里的返回值 p 是 node 在阻塞队列的前驱节点
*/
Node p = enq(node);
int ws = p.waitStatus;
// ws > 0 说明 node 在阻塞队列中的前驱节点取消了等待锁,直接唤醒 node 对应的线程。
// 如果 ws <= 0, 那么 compareAndSetWaitStatus 将会被调用
// 因为节点入队后,需要把前驱节点的状态设为 Node.SIGNAL(-1)
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 如果前驱节点取消或者 CAS 失败,会进到这里唤醒线程(但是本场景下不会执行到这里)
MyLockSupport.unpark(node.thread);
// 返回true
return true;
}

将上面这段代码总结一下就是: 将本节点的waitState设置为了0. 然后让本节点插入到到锁的`等待队列`, 然后将前驱节点的waitState设置为了1. 然后返回了true.  

这一行的tansferForSignal返回了true, 取反了就是false了, 所以退出了 while循环. 至此signal方法就执行完毕了.

signal干的主要事情就是: 把`条件队列`里的第一个元素转移(尾插)到了锁的`等待队列`里.

`条件队列`就是firstWaiter为头结点的一个链表.

`等待队列`就是咱们上面将lock() unlock()的时候提到的锁的等待队列.

signal方法执行完了后, 接下来就该执行unlock()方法了. 如下图:

unlock()所做的事情就是, 释放当前的锁, 然后激活`等待队列`里的第一个线程.

而在本场景下, 现在等待队列里有且仅有一个元素, 就是signal方法转移的那个元素.

unlock()之前分析过, unlock会调用release方法:

release方法所做的就是释放锁(第一个红色代码), 然后唤醒`等待队列`里的第一个线程(第二个红色代码).

unlock()方法执行完了后, 刚才await挂起的那个线程就又被激活了.

所以接下来执行的是acquireQueued方法, 这个方法在将锁的时候讲过, 所以这里简单讲解一下:

final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (; ; ) {
// 本场景下: node是队列里的第一个元素, 也就是await的线程对应的node.
// 本场景下: p是node的前一个节点, 也就是head节点了
final Node p = node.predecessor();
// 本场景下: p==head. 锁现在空闲, tryAcquire也会成功.
if (p == head && tryAcquire(arg)) {
// 将node设置为新的head. head节点隐含的意思就是: head节点对应的线程是当前锁的持有者
setHead(node);
p.next = null; // help GC
failed = false; // 返回false. 因为本场景下该线程没有被中断过.
return interrupted;
} //--- 本场景下, 不会执行到下面的代码 if (shouldParkAfterFailedAcquire(p, node)
&& parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed) cancelAcquire(node);
}
}

最终这个方法返回了true. 接下来, await()方法继续执行剩下的几行代码就可以退出了:

这两行就是做了相应的维护操作, 和线程中断判断, 这里就不讲解了.

随后,await方法执行完了, 退出方法栈.

然后就继续往下执行.  执行System.out.println, 然后是unlock.

至此本段程序就执行完了.

[源码分析]ReentrantLock & AbstractQueuedSynchronizer & Condition的更多相关文章

  1. 一行一行源码分析清楚AbstractQueuedSynchronizer

    ​“365篇原创计划”第二十四篇. 今天呢!灯塔君跟大家讲: 一行一行源码分析清楚AbstractQueuedSynchronizer 在分析 Java 并发包 java.util.concurren ...

  2. 【JUC】JDK1.8源码分析之AbstractQueuedSynchronizer(二)

    一.前言 在锁框架中,AbstractQueuedSynchronizer抽象类可以毫不夸张的说,占据着核心地位,它提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.所以很有必 ...

  3. 【JDK】JDK源码分析-ReentrantLock

    概述 在 JDK 1.5 以前,锁的实现只能用 synchronized 关键字:1.5 开始提供了 ReentrantLock,它是 API 层面的锁.先看下 ReentrantLock 的类签名以 ...

  4. 【JUC】JDK1.8源码分析之AbstractQueuedSynchronizer

    一.前言 在锁框架中,AbstractQueuedSynchronizer抽象类可以毫不夸张的说,占据着核心地位,它提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.所以很有必 ...

  5. 源码分析清楚 AbstractQueuedSynchronizer

    本文关注以下几点内容: 深入理解 ReentrantLock 公平锁和非公平锁的区别 深入分析 AbstractQueuedSynchronizer 中的 ConditionObject 深入理解 j ...

  6. 【1】【JUC】JDK1.8源码分析之ReentrantLock

    概要: ReentrantLock类内部总共存在Sync.NonfairSync.FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQ ...

  7. AbstractQueuedSynchronizer源码分析

    AbstractQueuedSynchronizer源码分析 前提 AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer)是并发编程大师D ...

  8. Java并发编程之ReentrantLock源码分析

    ReentrantLock介绍 从JDK1.5之前,我们都是使用synchronized关键字来对代码块加锁,在JDK1.5引入了ReentrantLock锁.synchronized关键字性能比Re ...

  9. 并发编程(六)——AbstractQueuedSynchronizer 之 Condition 源码分析

    我们接着上一篇文章继续,本文讲讲解ReentrantLock 公平锁和非公平锁的区别,深入分析 AbstractQueuedSynchronizer 中的 ConditionObject 公平锁和非公 ...

随机推荐

  1. 【English】八、食物相关

    一.beer.wine.coffee.soup.oil.juice beer 啤酒 They drink beer. wine 葡萄酒 Wine and coffee. coffee 咖啡 Wine ...

  2. yum 安装 python-pip 失败解决方法

    这个包在EPEL源里,要添加EPEL源才可以.然后按博客里说的方法添加,执行以下命令: sudo rpm -ivh epel-release* 第一种方式:由于epel在禁用列表里需要另外加参数yum ...

  3. hadoop wordcout测试

    hadoop wordcout测试 安装好hadoop 环境后,启动HDFS等服务:输密码 1004 start-all.sh 查看启动情况 1006 jps 1007 cd ~ 切换到用户默认目录 ...

  4. 【原创】互联网项目中mysql应该选什么事务隔离级别

    摘要 企业千万家,靠谱没几家. 社招选错家,亲人两行泪. 祝大家金三银四跳槽顺利! 引言 开始我们的内容,相信大家一定遇到过下面的一个面试场景 面试官:"讲讲mysql有几个事务隔离级别?& ...

  5. Eclipse中快捷键Ctrl + Alt + 向上箭头 或者 Ctrl + Alt + 向下箭头与Windows冲突

    原文地址:https://blog.csdn.net/buaaroid/article/details/50804608 clipse中按ctrl + alt + 向上箭头没有任何反应,按ctrl + ...

  6. 创建pandas和sqlalchemy的j交互对象,方便于日常的数据库的增删改查(原创)

    #导入第三方库sqlalchemy的数据库引擎 from sqlalchemy import create_engine #导入科学计算库 import pandas as pd #导入绘图库 imp ...

  7. iOS App上架流程(2016详细版)来源DeveloperLY

    一.前言: 作为一名iOSer,把开发出来的App上传到App Store是必要的.下面就来详细讲解一下具体流程步骤. 二.准备: 一个已付费的开发者账号(账号类型分为个人(Individual).公 ...

  8. python定时执行任务的三种方式

    #!/user/bin/env python # @Time :2018/6/7 16:31 # @Author :PGIDYSQ #@File :PerformTaskTimer.py #定时执行任 ...

  9. win10下安装ubuntu18.04

    在win10下安装Ubuntu18.04,双系统共存.Ubuntu 18.04 使用的是Gnome桌面. 查看系统的启动模式: Win+R打开运行,输入msinfo32,回车查看系统信息.在BIOS模 ...

  10. this直接加在函数或者是 “原型”对象的区别

    如果加在函数上,可以用函数直接调用,如果是加在原型对象时,那就的创建新对象,才能使用,最重要的是影响继承 直接加在函数上的,不能被新对象继承