引言
本文是第一篇源码类分析的文章,先说说我为什么看源码?
- 毕竟源码里面无秘密,你想要的原理源码它都有;
- 写源码的人都是功力深厚的大佬,你可以从中看到他们对代码设计的艺术。读万卷书,行万里路,同样的道理,源码看多了,总会从量变到质变,最终写出优秀的代码。
再说说自己看源码的方法:
- 一定要记住自己看源码的目的啊!!!同志们,这个真的太重要了,我以前刚开始看源码的时候,本来最开始只想看某一个方法(假设是A方法)的源码,然后发现A方法调用了B方法,又去看B方法,B方法又调用C方法,就像套娃一样没完没了,最后花了老长时间都还是没弄懂A方法,一度陷入自我怀疑;
- 看源码要懂得先整体再局部,一条主路走通,然后再慢慢细化,“不识庐山真面目,只缘生在此山中”,古人诚不我欺;
- 要学会放弃,是的你没看错,要学会放弃,只要是不重要的东西,搞不懂没关系,先放放,别死钻牛角尖,毕竟时间宝贵,没必要死磕,做这种不划算的买卖;
- 要做笔记,好记性不如烂笔头,不管是以什么样的形式,要做笔记,我是真的明显感觉记忆力不如以前,但是也可能是现在每天要面对的事情更多,时间更加碎片化,所以做笔记可以让你快速回忆起当时你是怎么理解某个地方的知识点;
- 做完笔记还得温故而知新,不然就会翻书马冬梅,面试孙红雷;
- 看源码要学会问自己问题,然后再看源码里是怎么解决的,别被源码牵着走,这一点会在文章里有所体现;
- 心态别急躁,源码都是大佬写的,咱这一遍两遍看不懂也是很正常的,说实话今天要讲的这部分源码我自己也看了很多遍,才勉强能看明白,所以遇到看不懂的时候告诉自己:稳住,我们能赢。
上面就是自己的一些心得吧,才疏学浅,多多指教,暂时没得留言功能,有兴趣的可以关注公众号加我微信相互探讨,废话不多说了,直接开干。
在上一篇介绍了synchronized
关键字的最后提到了Java并发编程中的另外一把锁ReentrantLock
,那么本文就带着大家去分析下Doug Lea
大佬的杰作,在Java并发包java.util.concurrent
里占据重要地位的AbstractQueuedSynchronizer
(队列同步器),然后分别介绍响应中断与不响应中断的ReentrantLock
分别是怎么以AQS为基础实现的,最后对比synchronized
与ReentrantLock
的异同点。
模板方法设计模式
在介绍AQS前,我们先简单介绍一种设计模式——模板方法设计模式,这种设计模式中,抽象父类会先定义一个或多个模板结构和一些基本方法,但是并不会具体去实现结构里的行为,那么子类通过继承的方式在不改变模板结构的前提下,去实现具体的行为,这就给你了很大的想象空间了。
总的来说:
- 父类是抽象类;
- 模板方法一般是通过final声明,防止子类覆盖改变模板结构;
- 子类通过继承的方式来具体实现。
上面文字可能有点抽象,直接上代码就很好理解了。
如下,先定义抽象模板类:有一个模板方法一天的生活,有些基本方法实现:
public abstract class NormalTemplate {
//定义模板方法,声明为final
public final void day() {//定义一天的生活
//起床
wakeup();
//早餐
breakfast();
//交通工具去上班
transportation();
//工作
work();
}
//基本方法
public void wakeup() {
System.out.println("6点挣扎起床");
}
public void breakfast() {
System.out.println("牛奶面包...");
}
public void transportation() {
System.out.println("挤到怀疑人生的地铁...");
}
//工作内容自由发挥
protected abstract void work();
}
再定义幸福的一天是怎么样的,通过子类继承模板方法如下:
public class HappyTemplate extends NormalTemplate {
@Override
public void wakeup() {
System.out.println("睡到自然醒...");
}
@Override
public void transportation() {
System.out.println("骑着我心爱的小摩托,永远也不会堵车");
}
@Override
protected void work() {
System.out.println("工作?什么是工作?");
}
public static void main(String[] args) {
HappyTemplate happy = new HappyTemplate();
happy.day();
}
}
结果如下所示:
除了早餐以外,其他都被子类覆盖,模板模式其实挺简单的,到这里大家应该对这种设计模式有了解了吧。
AQS与ReentrantLock原理分析
那么模板模式和AQS是什么关系呢,没错,AQS就是采用的模板模式,里面定义了一些模板方法和一些基本方法,然后基于AQS开发各种同步组件,比如ReentrantLock
,这个意识很重要,这就是上面阅读源码方法中,先看整体。当初我刚开始看ReentrantLock
时,不懂这个道理,一会在ReentrantLock
中一会又跳到AQS中,来来回回搞不清。
ReentrantLock
和前文讲的synchronized
不同,前者是JDK层面的具有阻塞/唤醒线程的锁,后者是JVM层面具有阻塞/唤醒线程的锁。那么就有两个问题:
- 既然
ReentrantLock
是基于AQS模板而来,那么这个模板为ReentrantLock
定义了些什么呢? - 既然
ReentrantLock
是具有阻塞/唤醒线程的锁,那么这样的锁应该具备哪些特质呢?
这就是上面看到的看源码第六点,要带着问题去看源码。
一个具有阻塞/唤醒线程的锁,起码应该具备以下几个要素:
- 需要有个变量来表示当前锁的状态,起码至少有被占用和空闲两个状态,同时既然是控制多线程并发,那么是不是至少也得保证这个变量多线程并发安全,不能有的线程看上去是被占用,有的线程看上去是空闲;
- 如果锁被占用,至少得知道是谁占用,也就是要记录当前持有锁的线程;
- 如果加锁失败需要阻塞的线程,总要有个地方存储他们,且这个数据结构也必须是线程安全的;
- 既然涉及到阻塞/唤醒,那么是谁去完成这个工作。
带着这些问题,一起去扒开AQS源码看看它是怎么实现的,特别说明,本文的源码分析是基于JDK1.8。
首先回答上面提到的问题,在AQS中是如何解决的:
- 对于锁的状态,AQS中用了一个整型的volatile变量来表示,对于volatile变量我相信应该不用再多解释了吧,如果你还不会
文章中已经甚至从硬件层面进行了分析,如果没看过的同学赶紧看一下,一定会有更多的收获,这里就不展开说了;
- 对于记录锁的持有线程,在AQS的父类
AbstractOwnableSynchronizer
中有有一个变量exclusiveOwnerThread
来记录,是不是有种套中套,模中模的感觉,但是主要抓住了模板方法的设计模式,还是比较清晰的; - 对于存储阻塞线程的队列,AQS中的定义了种名叫
CLH
的队列,这个名字其实就是三位大佬(Craig, Landin, and Hagersten)名字的首字母,至于保证线程安全,采用的CAS来保证,其底层结构是双向链表,将阻塞的线程封装成链表的节点,head节点指向链表头部,tail节点指向链表尾部;入队就是将节点加在tail节点后面,然后对tail节点进行CAS;出队就是对head进行CAS,把CAS往后移;初始时,head = tail = null
,然后添加阻塞线程时,会新建一个空节点,然后将head和tail都指向这个节点,再往里添加新的节点,所以当head = tail
表示队列为空。过程如下所示:
- 阻塞/唤醒工作是怎么完成的呢?在这里是用的park()/unpark()操作原语,但是这都是native方法,在Java中通过LockSupport工具类对原语进行了封装;分析sycnhronized时同样也提到了阻塞/唤醒原语wait()与notify(),区别它们,同样park()/unpark()操作原语可以实现精准唤醒,而这是wait()/notify()所不具备的。
部分代码体现如下所示:
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
/**
* The current owner of exclusive mode synchronization.
*/
private transient Thread exclusiveOwnerThread;
}
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//省略部分代码
/**
* The synchronization state.
* 同步状态
*/
private volatile int state;
//CLH节点
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
//等待状态
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
//前驱节点
volatile Node prev;
//后继节
volatile Node next;
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
//获取锁 !!!这就是一个模板方法!!!
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//释放锁 !!!这就是一个模板方法!!!
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
}
接下来具体分析ReentrantLock
是如何基于AQS来具体实现的:
首先要使用ReentrantLock
肯定要构造一个具体对象实例,所以先看看构造函数:
/**
无参构造函数
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
带有公平策略的构造函数
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
从上面可以看出,构造函数比较简单,分为无参默认构造函数和带有公平策略参数的有参构造函数,所以根据公平策略的不同ReentrantLock
可以分为公平锁与非公平锁,默认是非公平锁;至于什么是公平与非公平会在加锁时进行具体说明。
从构造函数可以看出,构造的结果都是一个sync
对象,接下里的加锁与释放锁都是基于这个对象。
//加锁
public void lock() {
sync.lock();
}
//释放锁
public void unlock() {
sync.release(1);
}
那么如此重要的一个对象究竟是什么呢?没错,它就是AQS的一个子类。
abstract static class Sync extends AbstractQueuedSynchronizer {
//部分代码
abstract void lock();//抽象方法
final boolean nonfairTryAcquire(int acquires) {
...//篇幅原因,避免重复,下文会有详细分析
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}
由于篇幅限制,本文只贴出了部分源码,从上文的分析可知,FairSync
代表公平锁类,NonfairSync
代表非公平锁类。接下来我们再看这两个类:
//非公平锁
static final class NonfairSync extends Sync {
//部分代码
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); //调用AQS模板方法
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//公平锁
static final class FairSync extends Sync {
//部分代码
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
...//篇幅限制,避免重复,该方法会在下文具体分析
}
}
看到这里我们应该就有一个ReentrantLock
锁的整体脉络了,先整体,在局部。
整体:
ReentrantLock
通过内部抽象类Sync
继承AQS,然后通过内部类NonfairSync
与FairSync
继承Sync
实现了非公平锁与公平锁。在NonfairSync
与FairSync
中具体化了内部抽象类Sync
的加锁行为,并调用了AQS里的模板方法来实现加锁的逻辑。
局部(解锁细节):
非公平锁加锁:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); //调用AQS模板方法
}
线程调用lock方法,一上来线程就尝试修改state的值,也就是抢锁,如果成功,则将自己设置为独占锁的线程,如果失败则调用AQS中的模板方法acquire(1)
。其实这里是第一个地方体现锁的非公平性,也就是先来的线程不一定会先拿到锁。
那么在公平锁中是如何体现公平性的呢?
公平锁加锁:
final void lock() {
acquire(1);//调用AQS模板方法
}
从源码可以看出,不会一上来就去抢锁,而是直接调用调用AQS中的模板方法acquire(1)
。
AQS中acquire(1)
模板方法如下:
//AQS中模板方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
整体分析逻辑:
模板加锁逻辑主要如下两部分:
- 先通过
tryAcquire(arg)
方法尝试加锁; - 若果加锁失败
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
方法将线程放入CLH队列,如果成功由于&&
会短路,导致后面的acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
方法不执行,也就不会加入到阻塞队列中。
具体实现:
在NonfairSync
(非公平锁)中,先说第一部分tryAcquire(arg)
:
//NonfairSync中的具体实现方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
NonfairSync
中的tryAcquire(arg)
方法是一个空壳方法,实际调用是父类Sync
中的nonfairTryAcquire(acquires)
方法,如下:
//Sync中的方法
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
int c = getState(); //获取当前锁状态
if (c == 0) { //无人持有锁,则开始CAS尝试拿锁
if (compareAndSetState(0, acquires)) {
//拿锁成功,把exclusiveOwnerThread设为当前线程
setExclusiveOwnerThread(current);
return true;
}
} //如果锁已经被占有,判断是否被当前线程持有
else if (current == getExclusiveOwnerThread()) {
//如果被当前线程持有,累加state变量
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);//修改state变量
return true;
}
//如果锁被其他线程占有,则加锁失败,返回false
return false;
}
这段逻辑比较简单,也比较容易理解,我已经在代码中详细注释,大家一定能看的懂。但是这里有一个关键点:当无人持有锁时,非公平锁会直接开始通过CAS抢锁,这一点也是非公平性体现的第二个地方!!!。
在公平锁中是如何保证公平性的呢,公平锁的加锁代码如下:
//公平锁
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
int c = getState();//获取当前锁状态
if (c == 0) { //当无人持有锁时,判断自己是否是头节点
//如果是则尝试通过CAS加锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//拿锁成功,把exclusiveOwnerThread设为当前线程
setExclusiveOwnerThread(current);
return true;
}
}//如果锁已经被占有,判断是否被当前线程持有
else if (current == getExclusiveOwnerThread()) {
//如果被当前线程持有,累加state变量
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);//修改state变量
return true;
}
//如果锁被其他线程占有,则加锁失败,返回false
return false;
}
从源码中可以看出,整体框架和非公平锁差不多,唯一的区别就是,当无人持有锁时,会通过hasQueuedPredecessors()
来判断当前队列中有没有其他线程,没有才会抢锁,什么叫公平,这就是公平。
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
上面讲完了模板方法中的第一部分,接下来分析第二部分:当加锁失败时,将线程放入CLH队列并阻塞,通过acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
实现,这部分代码均在AQS中实现,先分析addWaiter(Node.EXCLUSIVE)
,
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的构造函数将线程以节点的方式保存起来, 接着将底层双向链表中的尾节点tail赋值给临时变量pred。如果尾节点不为空,通过CAS将节点插入队尾;如果尾节点为空,则执行enq(node)
方法:该方法会进行队列初始化,新建一个空Node,然后不断自旋,直到最后成功把节点加入队列尾部。
private Node enq(final Node node) {
for (;;) { //相当于while(true)
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node())) //新建空Node
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
对照前面讲的CLH队列初始化,应该就能明白上面的过程。
要注意一点:addWaiter(Node.EXCLUSIVE)
该方法仅仅是将线程对象放入CLH队列尾部,并未进行阻塞线程,所以线程阻塞的工作是 acquireQueued(...)
方法完成的。
接下来,分析acquireQueued(...)
方法是如何进行线程阻塞的。先上源码,如下:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//拿到当前节点的前驱节点
final Node p = node.predecessor();
//被唤醒,如果自己在队列头部
//(即自己的前一个节点是head指向的空Node)则尝试拿锁
if (p == head && tryAcquire(arg)) {
//拿锁成功,线程出队列,会把head指针往后挪一个位置,
//同时将node置为空,所以head还是指向一个空Node
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())//调用park阻塞自己
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
上面代码中我做了部分注释,接下来我们再分析下这个函数本身:
首先,代码中有一个for(;;)
循环,所以该函数返回的那一刻就是拿到锁那一刻;
其次,这个函数有一个返回值,是布尔类型变量,那表示什么意思呢? 返回变量是interrupted
,表示中断状态,虽然lock不会响应中断,但是它会记录在被阻塞期间没有其他线程向他发送过中断信号。如果有就返回true
,没有返回false
;
当返回true
时,基于这个返回值就会调用selfInterrupt()
,自己给自己发送中断信号,也就是把自己的中断标志位设为true
,之所以要这么做,是因为自己在被阻塞期间收到其他线程的中断信号没有及时做出响应,现在进行补偿响应。
//AQS中模板方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
线程调用park()函数把自己阻塞起来,停在这里,直到被其他线程唤醒,由此加锁完成。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//阻塞线程
return Thread.interrupted();
}
park()函数返回有两种情况:
- 其他线程调用
unpark(Thread t)
,精准唤醒; - 其他线程调用
t.interrupt()
,因为LockSupport.park(t)
是响应中断的。
被唤醒之后,通过Thread.interrupted()
来判断是否被中断唤醒。如果是情况1则返回false;若是情况2则返回true。
由于LockSupport.park(t)
会响应中断,所以acquireQueued(...)
才写了死循环:当被唤醒时,如果发现自己排在队列头部,则尝试拿锁,如果失败则再次阻塞自己,反复这个过程直到,拿到锁退出这个函数。
由此,公平锁与非公平锁的加锁过程已经分析完毕。
下面分析解锁:
//首先
//Sync
public void unlock() {
sync.release(1);
}
//然后调用AQS模板方法
//AQS
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//最终
//Sync具体实现
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
//只有锁的拥有者才有资格释放锁,否者抛出异常
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {//每调用1次tryRelease(1),state - 1,
//直到减到0才代表完全释放锁
free = true;
setExclusiveOwnerThread(null);
}
setState(c);//由于锁的互斥性,所以没有使用CAS
//直接调用set更改锁状态
return free;
}
整体:
先整体看AQS模板方法中规定的解锁逻辑,同样也分两部分:
- 调用
tryRelease(...)
方法解锁; - 调用
unparkSuccessor(h)
方法唤醒队列中的后继者。
由于锁的排他性,所以公平锁与非公平释放锁都一样且比较简单。
响应中断的锁——lockInterruptibly 原理分析
上面分析的lock,当发生线程中断时不会立即响应,而是会先记录下这个中断信号,当阻塞完成以后再进行中断补偿。那么lock里也有直接响应中断的锁,直接先上源码:
//ReentrantLock中
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
同样也是通过sync
调用acquireInterruptibly(1)
方法。该方法在AQS中实现如下:
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
其中tryAcquire(...)
方法被FairSync
和NonfairSync
实现,上面已经分析过,这里主要分析doAcquireInterruptibly
方法,源码如下:
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//抛出中断异常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
朋友们,是不是和不响应中断的代码很像,看到区别了吗?就是当收到中断信号,也就是parkAndCheckInterrupt(...)
方法返回ture时,说明有中断信号,不再阻塞,直接抛出异常,跳出for循环,整个函数返回。从而达到中断的目的。
synchronized与ReentrantLock的对比
相同点:
- 都可用于线程同步;
- 都是可重入锁
- 都具有非公平锁
不同点:
- ReentrantLock可响应中断,synchronized则不行;
- ReentrantLock可超时等待,synchronized则不行;
- ReentrantLock可有公平锁,synchronized则没有;
- ReentrantLock加锁与释放锁都需要显示调用,synchronized则不需要显示释放锁;
- ReentrantLock提供了更丰富的api获取锁状态;
- ReentrantLock内部基于AQS中的队列同步器实现加锁,synchronized以锁对象或类进行加锁。
使用:
synchronized
的用法以前的文章中已经详细分析过,这里只列举lock的使用,代码如下:
private static Lock lock = new ReentrantLock();
public static int lockValue = 0;
public static void increaseLock() {
try {
lock.lock();
lockValue++;
} catch (Exception ex) {
} finally {
lock.unlock();
}
}
到这里,本文内容也差不多结束了,以我自己的经验,大家理解AQS及ReentrantLock
时一定要牢记模板方法设计模式,这样会清晰很多,才不会有很乱的感觉。
对于公平锁与公平锁的流程图,由于篇幅限制,就不在这里展示了,想获取高清原图,请大家关注公众号:肖说一下 并回复AQS即可获取。
参考
Java并发原理实现 余春龙