一、前言
上一篇《Java锁之ReentrantLock(一)》已经介绍了ReentrantLock的基本源码,分析了ReentrantLock的公平锁和非公平锁机制,最终分析ReentrantLock还是依托于
AbstractQueuedSynchronizer
同步队列器(以下简称同步器)实现,所以本篇开始分析同步器内部的代码实现,考虑到代码结构比较长,所以分析源码时会精简部分不重要的代码,但是最终还是会以不影响代码逻辑的情况下进行精简。
二、同步器分析
-
同步器主要属性
根据上图源码我们可以知道,
AbstractQueuedSynchronizer
内部构建了一个Node节点对象,同时构造了一个具有volatile属性头节点与尾部节点,保证了多线程之间的可见性,同时最重要的是定义了一个int类型变量state,通过上一篇文章分析,我们知道了ReenTrantLock是否获取到锁的判断就是state是否大于0,等于0表示锁空闲,大于0,表示锁已经被获取。接下来我们重点分析下Node节点内部构造以及同步器的实现原理,Node源码如下:
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;
//等待队列中的后继节点,如果当前节点是共享的,那么这个nextWaiter=SHARED
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;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
这里需要重点说明的属性是
waitStatus
,该状态就是包括节点内部声明的几个常量,如下:
常量名 | 功能 |
---|---|
CANCELLED | 值为1,当前节点进入取消状态,原因是由于被中断或者是等待超时而进入取消状态,需要说明的是,节点线程进入取消状态后,状态不会再改变,也就不会再阻塞获取锁 |
SIGNAL | 值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使得后继节点的线程得以运行 |
CONDITION | 值为-2,节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到同步状态的竞争中 |
PROPAGATE | 值为-3,表示下一次共享式同步状态获取会无条件的传播下去,比如如果头节点获取到共享式同步状态,判断状态是PROPAGATE,会继续调用doReleaseShared,使得后继节点继续获取锁 |
INITIAL | 值为 0,表示初始状态(这个应该是老版本中的代码中存在,目前查看jdk1.8已经没有显示声明INITIAL状态,因为初始化时候,int变量默认就是0) |
分析同步器的属性,我们可以大概画出构造器的队列示意图,如下:
首先同步器声明了头节点和尾部节点,head节点指向一个node节点表示该节点是队列的头部节点,tail节点指向一个node节点表示该节点是尾部节点,同时,每个节点都有pre和next属性,指向node节点,然后如图所示构建成一个FIFO双向链表式队列。下面我们查看下同步器常用主要方法
-
同步器主要方法列表
方法名称 | 功能 |
---|---|
compareAndSetState(int expect, int update) | CAS进行设置同步状态 |
enq(final Node node) | 循环入等待队列,直到入队成功为止 |
addWaiter(Node mode) | 以当前线程创建一个尾部节点,并加入到尾部 |
unparkSuccessor(Node node) | 唤醒节点的后继节点 |
doReleaseShared() | 释放共享模式下的同步状态 |
setHeadAndPropagate(Node node, int propagate) | 设置头节点,并继续传播同步许可 |
release(int arg) | 独占式释放同步状态 |
acquireShared(int arg) | 共享式释放同步状态 |
hasQueuedPredecessors() | 判断是否有比当前线程等待更久的线程(用于公平锁) |
-
同步器加锁
以上是同步器主要的方法,我们接下来会对上述部分方法进行重点分析,要了解同步器如何完成加锁,等待获取锁,释放锁的功能,我们先回顾上一篇文章分析ReentranLock的Lock()方法,实现源码如下:
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
我们发现重点是acquire(1)方法,该方法是父类也就是同步器提供的,源码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
源码其实可以拆分为三部分:
-
tryAcquire(arg)
尝试获取锁 -
addWaiter(Node.EXCLUSIVE), arg)
以当前线程构建成节点添加到队列尾部 -
acquireQueued(final Node node, int arg)
让节点以死循环去获取同步状态,获取成功就退出循环
其实解析为三部分就很清楚这个方法的作用了,首先尝试获取锁,获取不到就把自己添加到尾部,然后在队列中死循环去获取锁,最重要的部分就是
acquireQueued(final Node node, int 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)) { //如果前任节点是头节点,
//并且当前节点获取到了锁,
//也就是说队列头节点释放了锁,同时把当前节点设置为头节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//判断当前节点是否应该被阻塞,那么就把当前线程阻塞挂起,防止无谓的死循环
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//该方法主要靠前驱节点判断当前线程是否应该被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 如果前任节点的状态等于SIGNAL,
* 说明前任节点获取到了同步状态,当前节点应该被阻塞,返回true
*/
return true;
if (ws > 0) {
/*
* 前任节点被取消
*/
do {//循环查找取消节点的前任节点,
//直到找到不是取消状态的节点,然后剔除是取消状态的节点,
//关联前任节点的下一个节点为当前节点
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* CAS设置前任节点等待状态为SIGNAL,
* 设置成功表示当前节点应该被阻塞,下一次循环调用就会
* return true
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//把当前线程挂起,从而阻塞住线程的调用栈,
//同时返回当前线程的中断状态。
//其内部则是调用LockSupport工具类的park()方法来阻塞该方法。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//阻塞线程
return Thread.interrupted();
}
以上就是lock.lock()的加锁过程,我们总结分析下:
- 首先,直接尝试获取锁,获取成功直接结束。
- 如果获取锁失败,就把当前线程构造一个尾部节点,CAS方式加入到队列的尾部
- 在队列中,死循环式的判断前任节点是否是头节点,如果是头节点就尝试获取锁,如果不是就把自己挂起,等待前任节点唤醒自己,这样可以避免多个线程死循环带来的性能消耗。
-
同步器解锁
lock.unlock()释放锁的过程,分析源码,老规矩,上源码:
//释放锁
public void unlock() {
sync.release(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;
}
//该方法和之前分析的代码类似,主要是设置Sate状态
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);//设置同步状态占有线程为null
}
setState(c);
return free;
}
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);//唤醒阻塞的线程
}
以上就是对lock.unlock()分析,同样我们总结分析下
- 首先既然加锁成功与否判断是根据State不为0来判断,所以,释放锁就会把State设置为0,同时设置锁的所有者线程为null
- 锁释放成功了,接着就会唤醒在队列的后继节点,通过调用
LockSupport.unpark(s.thread)
来唤醒线程的,LockSupport
主要依托于sun.misc.Unsafe
类来实现的,该类提供了操作系统硬件级别的方法,不在本文讨论中。
三、尾言
- 本次主要分析了AQS同步器的加锁和解锁的实现,其实jdk很多同步工具类都是依赖于AQS同步器实现的,了解了AQS同步器的原理后,对理解其他并发工具的原理也很有帮助,比如
CountDownLatch
,Semaphore
,CyclicBarrier(依赖ReentrantLock)
等。下一篇《Java锁之ReentrantReadWriteLock》继续细化分析,分析读锁和写锁,分析ReentrantLock是如何实现读锁的重复获取,锁降级等功能的。