JUC源码阅读(一)-AQS

一、AQS简介

AbstractQueuedSynchronizer,简称AQS,JUC并发包中常用的ReentrantLock, CountDownLatch等都依赖AQS。子类通过继承AQS并实现它的抽象方法来管理同步状态,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作,但是通过AQS实现的功能却是不同的。

二、AQS数据结构

下图就是AQS的数据模型:
在这里插入图片描述
接下来再来看看AbstractQueuedSynchronizer的成员变量

// CLH队列头结点
private transient volatile Node head;

// CLH队列尾结点
private transient volatile Node tail;

// 标识锁的状态:0:锁空闲;>1:锁被占用,大于1标识被重入的次数
private volatile int state;

// 继承AbstractOwnableSynchronizer的属性,表示当前持有锁的线程
private transient Thread exclusiveOwnerThread;

AQS中state状态的变更是基于CAS实现的,state状态通过volatile保证共享变量的可见性,再由CAS 对该同步状态进行原子操作,从而保证原子性和可见性。

protected final boolean compareAndSetState(int expect, int update) {
	// unsafe的CAS操作-CPU的原子指令
	return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

内部类: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;
	// 线程的等待状态 表示线程在Condtion上
	static final int CONDITION = -2;
	
	// 表示下一个acquireShared需要无条件的传播
	static final int PROPAGATE = -3;

	/**
	 *   SIGNAL:     当前节点的后继节点处于等待状态时,如果当前节点的同步状态被释放或者取消,
	 *               必须唤起它的后继节点
	 *         
	 *   CANCELLED:  一个节点由于超时或者中断需要在CLH队列中取消等待状态,被取消的节点不会再次等待
	 *               
	 *   CONDITION:  当前节点在等待队列中,只有当节点的状态设为0的时候该节点才会被转移到同步队列
	 *               
	 *   PROPAGATE:  下一次的共享模式同步状态的获取将会无条件的传播

	 * waitStatus的初始值时0,使用CAS来修改节点的状态
	 */
	volatile int waitStatus;

	/**
	 * 当前节点的前驱节点,当前线程依赖它来检查waitStatus,在入队的时候才被分配,
	 * 并且只在出队的时候才被取消(为了GC),头节点永远不会被取消,一个节点成为头节点
	 * 仅仅是成功获取到锁的结果,一个被取消的线程永远也不会获取到锁,线程只取消自身,
	 * 而不涉及其他节点
	 */
	volatile Node prev;

	/**
	 * 当前节点的后继节点,当前线程释放的才被唤起,在入队时分配,在绕过被取消的前驱节点
	 * 时调整,在出队列的时候取消(为了GC)
	 * 如果一个节点的next为空,我们可以从尾部扫描它的prev,双重检查
	 * 被取消节点的next设置为指向节点本身而不是null,为了isOnSyncQueue更容易操作
	 */
	volatile Node next;

	/**
	 * 当前节点的线程,初始化后使用,在使用后失效 
	 */
	volatile Thread thread;

	/**
	 * 链接到下一个节点的等待条件,或特殊的值SHARED,因为条件队列只有在独占模式时才能被访问,
	 * 所以我们只需要一个简单的连接队列在等待的时候保存节点,然后把它们转移到队列中重新获取
	 * 因为条件只能是独占性的,我们通过使用特殊的值来表示共享模式
	 */
	Node nextWaiter;

	/**
	 * 如果节点处于共享模式下等待直接返回true
	 */
	final boolean isShared() {
		return nextWaiter == SHARED;
	}

	/**
	 * 返回当前节点的前驱节点,如果为空,直接抛出空指针异常
	 */
	final Node predecessor() throws NullPointerException {
		Node p = prev;
		if (p == null)
			throw new NullPointerException();
		else
			return p;
	}

	Node() {    // 用来建立初始化的head 或 SHARED的标记
	}

	Node(Thread thread, Node mode) {     // 指定线程和模式的构造方法
		this.nextWaiter = mode;
		this.thread = thread;
	}

	Node(Thread thread, int waitStatus) { // 指定线程和节点状态的构造方法
		this.waitStatus = waitStatus;
		this.thread = thread;
	}
}

三、AQS核心方法

1、AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

acquire()是独占式获取同步状态,这个方法分别调用了下面4个方法

  1. tryAcquire:尝试获取同步状态
  2. addWaiter:获取同步状态失败,调用addWaiter方法把线程封装成一个Node添加到同步队列的尾部
  3. acquireQueued:使节点以自旋的方式获取同步状态,如果获取同步状态失败,要挂起线程
  4. selfInterrupt:线程如果在获取同步状态中和同步队列中被中断过,要进行自我中断。
a、AbstractQueuedSynchronizer#tryAcquire
protected boolean tryAcquire(int arg) {
	throw new UnsupportedOperationException();
}

AQS并没有实现这个方法,具体的实现由它的继承类进行重写,如ReentrantLock的Sync类等,很明显,这是个模板方法模式!

b、AbstractQueuedSynchronizer#addWaiter
/**
 * 把Node节点添加到同步队列的尾部
 */
private Node addWaiter(Node mode) {
	Node node = new Node(Thread.currentThread(), mode);  // 以独占模式把当前线程封装成一个Node节点
	// 尝试快速插入尾部
	Node pred = tail;  // 当前队列的尾节点赋给pred
	if (pred != null) {  // 先觉条件 尾节点不为空
		node.prev = pred;  // 把pred作为node的前继节点
		if (compareAndSetTail(pred, node)) { //利用CAS把node作为尾节点
			pred.next = node;    // 把node作为pred的后继节点
			return node;       // 直接返回node
		}
	}
	// 上一步快速插入尾部失败则通过enq自旋的方式把node插入到队列中。
	enq(node);
	return node;
}
/**
 * 采用自旋的方式把node插入到队列中
 */
private Node enq(final Node node) {
	for (;;) {
		Node t = tail;
		if (t == null) { // 如果尾结点为空,说明队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
			if (compareAndSetHead(new Node())) // 新建一个节点利用CAS设为头节点,就是这样的形式 head=tail=null
				tail = head;
		} else {    // 正常流程,放入队尾
			node.prev = t;  // 把t设为node的前驱节点
			if (compareAndSetTail(t, node)) {  // 利用CAS把node节点设为尾节点
				t.next = node;   // 更改指针  把node作为t的后继节点
				return t;   // 直接返回t
			}
		}
	}
}

尝试快速插入尾部的代码和enq(final Node node)方法中的代码有重复,之所以有这部分“重复代码”,是因为对某些特殊情况进行提前处理,牺牲一定的代码可读性换取性能提升。

c、AbstractQueuedSynchronizer#acquireQueued
/* 
 * 此主要是通过自旋方式获取同步状态
 */
final boolean acquireQueued(final Node node, int arg) {
	boolean failed = true;
	try {
		boolean interrupted = false;  // 默认线程没有被中断过
		for (;;) {
			final Node p = node.predecessor();  // 获取该节点的前驱节点p
			if (p == head && tryAcquire(arg)) {  // 如果p是头节点并且能获取到同步状态
				setHead(node);                   // 把当前节点设为头节点,这样就能把当前节点给移除
				p.next = null;                  // 把p的next设为null,便于GC
				failed = false;                 // 标志--表示成功获取同步状态,默认是true,表示失败
				return interrupted;             // 返回该线程在获取到同步状态的过程中有没有被中断过
			}
			
			// 上一步尝试获取同步状态失败,则在队列中挂起当前线程
			if (shouldParkAfterFailedAcquire(p, node) &&   
				parkAndCheckInterrupt())
				interrupted = true;      
		}
	} finally {
		if (failed)   // 如果fail为true,直接移除当前节点
			cancelAcquire(node);
	}
}
/* 
 * 用于判断是否挂起当前线程
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;//拿到前驱的状态
    if (ws == Node.SIGNAL)
        //如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
        return true;
    if (ws > 0) {
        /*
         * 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
         * 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
         //如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
private final boolean parkAndCheckInterrupt() {
     LockSupport.park(this);//调用park()使线程进入waiting状态(线程此时阻塞在这里)
     return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。(注意此方法被唤醒后才会执行)
}
d、AbstractQueuedSynchronizer#selfInterrupt

因为线程在进入CLH队列是通过调用LockSupport.park进入阻塞状态的,外部中断了该线程是不会立即中断的,只会修改Thread内部的中断状态值,不会抛出中断异常。直到被唤醒后,可以调用Thread.interrupted()方法查看阻塞过程中是否被中断过,然后再自我中断。

/**
 * 当前线程的自我中断
 */
private static void selfInterrupt() {
	Thread.currentThread().interrupt();
}

selfInterrupt 执行的前提是 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法返回 true。这个方法返回的是线程在获取锁的过程中是否发生过中断,返回 true 则证明发生过中断。所以 acquire 中的 selfInterrupt 其实是对获取锁的过程中发生过的中断的补充。

为什么不直接用 isInterrupt()判断?是因为在获取锁的过程中,是通过 park+ 死循环实现的。每次 park 被唤醒之后都会重置中断状态,所以拿到锁的时候中断状态都是被重置后的。

最后总结下独占式同步状态获取流程,也就是acquire(int arg)方法调用流程
在这里插入图片描述

2、AbstractQueuedSynchronizer#release
public final boolean release(int arg) {
	// 先尝试释放同步状态
	if (tryRelease(arg)) {
		Node h = head;
		// 如果头结点不为空,且状态是-1SIGNAL,则唤醒下一个阻塞的节点
		if (h != null && h.waitStatus != 0)
			unparkSuccessor(h);
		return true;
	}
	return false;
}

release()是独占式释放同步状态,这个方法分别调用了下面2个方法

  1. tryRelease:尝试释放同步状态
  2. unparkSuccessor:释放同步状态成功后,唤醒下一个阻塞的节点
a、AbstractQueuedSynchronizer#tryRelease
protected boolean tryRelease(int arg) {
	throw new UnsupportedOperationException();
}

同上面的tryAcquire()方法一样,AQS并没有实现这个方法,具体的实现由它的继承类进行重写,如ReentrantLock的Sync类等,很明显,这也是个模板方法模式!

b、AbstractQueuedSynchronizer#unparkSuccessor
private void unparkSuccessor(Node node) {
	// 注意:这里的node是head节点
	int ws = node.waitStatus;
	// 先将head节点的waitStatus设置为0
	if (ws < 0)
		compareAndSetWaitStatus(node, ws, 0);

	Node s = node.next;
	// 如果head节点的后继节点为null或者被取消(waitStatus=1),
	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);
}

上述代码中需要注意的是,在找最靠前的一个需要被唤醒的节点时,如果head节点的后继节点为null,则从尾结点开始往前找最靠前的一个有效节点,至于为什么是从后往前找,是因为前面把Node节点添加到同步队列的尾部时候,enq(final Node node)是通过compareAndSetTail(t, node)这个CAS操作的,但是当CAS操作成功(tail指向当前node),执行if代码块并不是原子操作,这个时候,node与前一个节点t之间,node的prev指针在CAS操作之前已经建立,而t的next指针还未建立,此时若其他线程调用了release()操作,寻找需要唤醒的下一个节点,从头开始找就无法遍历完整的队列,而从后往前找就可以。

注意:CLH队列是双向链表,也就是说2个节点要想建立链接,需要设置2个指针,比如t, node两个节点,只有当node.prev = t; t.next = node; 这两个指针都设置成功才能建立起链接,否则就会断掉。

private Node enq(final Node node) {
	for (;;) {
		Node t = tail;
		if (t == null) { // 如果t为空,说明队列为空,必须初始化
			if (compareAndSetHead(new Node())) // 新建一个节点利用CAS设为头节点,就是这样的形式 head=tail=null
				tail = head;
		} else {    // 尾节点不为空的情况
			// 把t设为node的前驱节点在CAS操作之前建立
			node.prev = t;  
			if (compareAndSetTail(t, node)) {  // 利用CAS把node节点设为尾节点
				// 把node作为t的后继节点在CAS操作之后才建立,
				// 在执行此方法之前,若其他线程调用了release()操作,寻找需要唤醒的下一个节点,从头开始找就无法遍历完整的队列,而从后往前找就可以。
				// 所以在释放锁之后寻找需要唤醒的下一个节点,需要从后往前找
				t.next = node;   
				return t;   // 直接返回t
			}
		}
	}
}

在这里插入图片描述

最后总结下独占式同步状态释放流程,也就是release(int arg)方法调用流程
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值