测试代码:
https://github.com/kevindai007/springboot_houseSearch/tree/master/src/test/java/com/kevindai/juc
juc中的类太多,大分部又都需要些一个demo才能更好的理解,因此再开一篇
咱们首先开始研究LockSupport这个类,这个类是用来创建锁和其他同步工具类的基本线程阻塞原语.Java锁和同步器框架的核心AQS:AbstractQueuedSynchronizer,就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的.LockSupport通过底层unsafe提供park,unpark操作.简单点说:底层维护一个二义性的变量来保存一个许可,需要注意的是这个许可是一次性的,unpark操作设置该值为1,park操作检查该值是否为1,为1直接返回,不为1,,则阻塞。
这个类的代码都是很简单的调用unsafe类的方法,没什么好分析的,咱们主要看下他的使用
public class LockSupportTest {
public static void main(String[] args) throws InterruptedException {
//主线程一直处于阻塞状态。因为许可默认是被占用的,调用park()时获取不到许可,所以进入阻塞状态
// LockSupport.park();
// System.out.println("block.");
//多次unpark,只有一次park也不会出现什么问题,结果是许可处于可用状态
// Thread thread = Thread.currentThread();
// LockSupport.unpark(thread);//释放许可
// LockSupport.unpark(thread);//释放许可
// LockSupport.park();// 获取许可
// System.out.println("b");
// LockSupport是不可重入的,如果一个线程连续2次调用LockSupport.park(),那么该线程一定会一直阻塞下去
// LockSupport.unpark(thread);
//
// System.out.println("a");
// LockSupport.park();
// System.out.println("b");
// LockSupport.park();
// System.out.println("c");
//线程如果因为调用park而阻塞的话,能够响应中断请求(中断状态被设置成true),但是不会抛出InterruptedException
Thread t = new Thread(new Runnable()
{
private int count = 0;
@Override
public void run()
{
long start = System.currentTimeMillis();
long end = 0;
while ((end - start) <= 1000)
{
count++;
end = System.currentTimeMillis();
}
System.out.println("after 1 second.count=" + count);
//等待或许许可
LockSupport.park();
System.out.println("thread over." + Thread.currentThread().isInterrupted());
}
});
t.start();
Thread.sleep(2000);
// 中断线程
t.interrupt();
System.out.println("main over");
}
}
非常重要
下面来研究一下abstractQueuedSynchronizer(简称aqs),aqs是一个线程同步的框架,也是整个juc包的基础(Semaphore、CountDownLatch、ReentrantLock、ReentrantReadWriteLock等类均在其基础上完成的),下面咱们来看看其实现(为方便理解,部分逻辑需要引用ReentrantLock的代码来解释)
首先很容易发现aqs有三个很重要的属性:
//头结点
private transient volatile Node head;
//尾节点
private transient volatile Node tail;
/**
* The synchronization state.
* 同步状态
* state可以理解有多少线程获取了资源,即有多少线程获取了锁,初始时state=0表示没有线程获取锁.
*独占锁时,这个值通常为1或者0
*如果独占锁可重入时,即一个线程可以多次获取这个锁时,每获取一次,state就加1
*一旦有线程想要获得锁,就可以通过对state进行CAS增量操作,即原子性的增加state的值
*其他线程发现state不为0,这时线程已经不能获得锁(独占锁),就会进入AQS的队列中等待.
*释放锁是仍然是通过CAS来减小state的值,如果减小到0就表示锁完全释放(独占锁)
*/
private volatile int state;
下面来说一下aqs的大致逻辑
- AQS维护了一个队列,并记录队列的头节点和尾节点
- 队列中的节点是获取不到资源而阻塞的线程
- AQS同样维护了一个状态,这个状态应该是判断线程能否获取到锁的依据,如果不能,就加入到队列
- 当某个节点获取到资源后就移除队列,然后让其后面的节点尝试获取资源
下面咱们来看看Node节点是如何实现的
volatile Node prev;//此节点的前一个节点。
volatile Node next;//此节点的后一个节点
volatile Thread thread;//节点绑定的线程。
volatile int waitStatus;//节点的等待状态
//节点状态:取消状态,该状态表示节点超时或被中断就会被踢出队列
static final int CANCELLED = 1;
//节点状态:等待触发状态,只有前一个节点的状态为SIGNAL时,当前节点的线程才能被挂起
static final int SIGNAL = -1;
//节点状态:等待条件状态,表明节点对应的线程因为不满足一个条件(Condition)而被阻塞。
static final int CONDITION = -2;
//节点状态:状态需要向后传播,使用在共享模式头结点有可能处于这种状态,表示锁的下一次获取可以无条件传播
static final int PROPAGATE = -3;
//需要补充的而是0时新节点才会有的状态
可以看出Node维护了一个双向队列,,并且每个节点都有自己的状态
再看看AQS中定义的几个重要的方法:
public final void acquire(int arg);//请求获取独占式资源(锁)
public final boolean release(int arg);//请求释放独占式资源(锁)
public final void acquireShared(int arg);//请求获取共享式资源
public final boolean releaseShared(int arg);//请求释放共享式资源
//独占方式。尝试获取资源,成功则返回true,失败则返回false
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
protected int tryReleaseShared(int arg) {
throw new UnsupportOperationException();
}
可以看到aqs用acquire()和release()方法提供对资源的获取和释放
但是try**()结构的方法都是只抛出了异常,很显然这类方法是需要子类去实现的.
这也因为AQS定义了两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可以同时执行,如Semaphone、CountDownLatch), AQS负责获取资源(修改state的状态),而自定义同步器负责就要实现上述方法告诉AQS获取资源的规则.
下面来分析分析这几个方法:
1、acquire(int)
此方法是aqs实现独占式资源获取的顶层方法,这个方法和ReentrantLock.lock()等有着相同的语义.下面我们开始看源码
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个函数共调用了4个方法, 其中tryAcquire(arg)是在子类Sync中实现, 其余在均是AQS中提供.
而这个方法的流程比较简单:
- tryAcquire()尝试获取资源,如果成功, 则方法结束
- addWaiter()方法以独占方式将线程加入队列的尾部
- acquireQueued()方法是线程在等待队列中等待获取资源
- selfInterrupt(), 如果线程在等待过程中被中断过,在这里相应中断.(线程在等待过程中是不响应中断的,只有获取资源后才能自我中断)
下面来一一解读这些方法:
(1)、tryAcquire()
此方法尝试去获取独占资源.如果获取成功,则返回true,否则返回false。tryAcquire()方法前面已经说过,这个方法是在子类中是实现的. 而在ReentrantLock中,这个方法也正是tryLock()的语义.如下是ReentrantLock对tryAcquire()实现的源码(ReentranLock中tryAcquire()与nonfairTryAcquire()一致):
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//等于0表示当前锁未被其他线程获取到
if (!hasQueuedPredecessors() //检查队列中是否有线程在当前线程的前面
&& compareAndSetState(0, acquires)) {//CAS操作state,锁获取成功
setExclusiveOwnerThread(current); //设置当前线程为占有锁的线程
return true;
}
} else if (current == getExclusiveOwnerThread()) {//非0,锁已经被获取,并且是当前线程获取.支持可重入锁
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc); //更改状态位,
return true;
}
return false;//未能获取锁
}
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
/**
*如果h=t,则队列未被初始化,返回false
*如果队列中没有线程正在等待, 返回true
*如果当前线程是队列中的第一个元素, 返回true,否则返回false
**/
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
(2)、sqc中addWaiter(int)
再看acquire()的第二个流程,获取锁失败, 则将线程加入队列尾部, 返回新加入的节点
private Node addWaiter(Node mode) {
//以独占模式构建节点,节点有共享和独占两种模式
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//如果pred不为空,说明有线程在等待
//尝试使用CAS入列,如果入列失败,则调用enq采用自旋的方式入列
//该逻辑在无竞争的情况下才会成功,快速入列
if (pred != null) {
node.prev = pred; //双向队列
if (compareAndSetTail(pred, node)) {//CAS更新尾部节点
//将原tail节点的后节点设置为新tail节点
//由于CAS和设置next不是原子操作,因此可能出现更新tail节点成功,但是未执行pred.next = node,导致无法从head遍历节点;
//但是由于前面已经设置了prev属性,因此可以从尾部遍历;
//像getSharedQueuedThreads、getExclusiveQueuedThreads都是从尾部开始遍历
pred.next = node; //双向队列
return node;
}
}
enq(node); //如果队列没有初始化活更新尾部节点失败,程序就会到这一步,通过自旋入列
return node;
}
private Node enq(final Node node) {
for (;;) {//自旋+CAS配合使用方式,一直循环知道CAS更新成功.
Node t = tail;
if (t == null) {//队列为空, 没有初始化,必须初始化
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) { //设置尾节点,此时的head是头节点,不存放数据
t.next = node;
return t;
}
}
}
}
(3)、sqc中acquireQueued()
addWaiter()完成后返回新加入队列的节点, 紧接着进入下一个流程acquireQueued(), 在这个方法中, 会实现线程节点的阻塞和唤醒. 所有节点在这个方法的处理下,等待资源
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; //是否拿到资源
try {
boolean interrupted = false; //等待过程中是否被中断过
for (;;) { //又是一个自旋配合CAS设置变量
final Node p = node.predecessor(); //当前节点的前驱节点
if (p == head && tryAcquire(arg)) {//如果前驱节点是头节点, 则当前节点已经具有资格尝试获取资源
setHead(node); //获取资源后,设置当前节点为头节点
p.next = null; // help GC
failed = false;
return interrupted;
}
//如果不能获取资源,就进入waiting状态
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)
/*
*此时前驱节点完成任务后能够唤醒当前节
*记住,唤醒当前节点的任务是前驱节点完成
*/
return true;
if (ws > 0) { //ws大于0表示节点已经被取消,应该移出队列.
do {
//节点的前驱引用指向更前面的没有被取消的节点.所以被取消的节点没有引用之后会被GC
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//找到了合适的前驱节点后,将其状态设置为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
接下来是 parkAndCheckInterrupt() 方法, 真正让节点进入waiting状态的方法,是在这个方法中调用的.
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //使线程进入waiting状态,查看上面的LockSupport类介绍
return Thread.interrupted(); //检查是否被中断
}
(4)、selfInterrupt()
acquire()方法不是立即响应中断的. 由于线程获取同步状态失败加入到同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除,而是在获取资源后进行自我中断处理
private static void selfInterrupt() {
Thread.currentThread().interrupt();
}
至此独占锁获取资源的过程已经分析完了,理解流程只有也并不十分复杂,简单来说就是尝试获取资源, 如果获取不到就进入等待队列变成等待状态
2、release(int)
讲了如何获取到资源,接下来就应该如何释放资源.这个方法会在独占的模式下释放指定的资源(减小state),此方法与ReentrantLock.unlock()有相同的语意
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)、tryRelease()这个方法是在子类中实现的.我们以ReentrantLock.unlock()为例解读资源释放的过程
protected final boolean tryRelease(int releases) {
int c = getState() - releases; //state减去指定的量,
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { //独占锁模式下,state为0时表示没有线程获取锁,这时才算是当前线程完全释放锁
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
(2)、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) { //waitStatus表示节点已经被取消,应该踢出队列
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);
}
至此独占锁的获取、释放资源流程都已经完了,我也是查的不少资料才把这个流程捋清楚,快给我点赞
上面分析了独占锁的流程,下面咱们接着类分析共享锁的过程
1、acquireShared()
此方法是aqs实现共享式资源获取的顶层方法.下面我们开始看源码
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
这个函数共调用了2个方法, 其中tryAcquireShared(arg)是在子类Sync中实现, doAcquireShared则是AQS中提供.
方法流程很简单,首先尝试获取资源,如果状态小于0(未获取成功),则调用doAcquireShared()方法加入阻塞队列.下面咱们分别来看看这两个方法
(1)、tryAcquireShared()
tryAcquireShared()在aqs中仅是一个抽象方法,具体实现在子类中,这里我以CountdownLatch为例进行分析
//等于0表示当前锁未被其他线程获取到,即当前线程获取到锁时返回1,否则返回-1
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
2、doAcquireShared()
共享模式获取的核心公共方法,咱们看看源码
private void doAcquireShared(int arg) {
//添加当前线程为一个共享模式的节点,addWaiter()方法在独占模式是分析过,在此不做重复分析
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {//如果前驱节点是头节点, 则当前节点已经具有资格尝试获取资源
int r = tryAcquireShared(arg);
//此时当state值大于0则认为获取成功
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//如果前驱节点不是头节点则不能能获取资源,就进入waiting状态.判断当前节点是否应该被阻塞,是则阻塞等待其他线程release
//此处的方法前面也分析过,在此不做研究
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//如果出异常,没有完成当前节点的出队,则取消当前节点
if (failed)
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);//把当前节点设为头结点
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())//如果后继节点为共享模式且参数propagate是否大于0或者PROPAGATE是否已被设置,则唤醒后继节点
doReleaseShared();
}
}
这样共享锁的基本流程就结束了,简单来说就是尝试获取资源,如果获取不到就加入队列中等待.与独占锁不同的是,独占锁尝试获取资源时会检查队列中是否有其他线程,如果没有就设置当前线程为占有锁的线程,即只有一个线程持有资源;而共享模式当调用doAcquireShared时,会看后续的节点是否是共享模式,如果是,会通过doReleaseShared()唤醒后续节点,让所有等待的共享节点获取资源
下面来分析一下释放资源的过程
1、releaseShared()
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//尝试释放资源
doReleaseShared();
return true;
}
return false;
}
咱们直接去CountdownLatch中看看tryReleaseShared()方法
protected boolean tryReleaseShared(int releases) {
for (;;) {//自旋+cas改变状态
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
再来看看doReleaseShared()方法,这是共享模式释放资源的核心方法
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {/如果等待队列中有等待线程
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//此方法前面分析过,用于唤醒后续节点
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
此方法逻辑也比较简单,就是唤醒第一个等待节点.但需要注意的是,根据前面acquireShared的逻辑,被唤醒的线程会通过setHeadAndPropagate继续唤醒后续等待的线程
到这里AQS就分析完了,到这里应该对独占锁、共享锁有一个认识,不清楚没关系,后续咱们会在其实现类中结合实际情况,进行更加深入的分析,如果有什么想讨论的欢迎留言一起讨论