AQS源码导读

前言

AQS全称:AbstractQueuedSynchronizer,抽象的队列同步器,和synchronized不一样的是,它是使用Java编写实现的一个同步器,开发者能够基于它进行功能的加强和扩展。java

AQS堪称J.U.C包的半壁江山,不少并发工具类都是使用AQS来实现的,例如:ReentrantLock、Semaphore、CountDownLatch等。node

使用synchronized实现同步的原理是:每一个Java锁对象都有一个对应的Monitor对象(对象监视器),Monitor对象是由C++实现的。对象监视器维护了一个变量Owner,指向的是当前持有锁的线程,还维护了一个Entry List集合,存放的是竞争锁失败的线程。线程在这个集合里会被挂起休眠,直到Owner线程释放锁,JVM才去Entry List集合中唤醒线程来继续竞争锁,循环往复。面试

AQS的任务就是使用Java代码的方式,去完成synchronized中由C代码实现的功能。Java开发者不必定熟悉C语言,要读懂synchronized的源码实现并不是易事。不过好在JDK提供了AQS,经过阅读AQS的源码也能让你对并发有更深的理解。算法

AQS的核心思想是:经过一个volatile修饰的int属性state表明同步状态,例如0是无锁状态,1是上锁状态。多线程竞争资源时,经过CAS的方式来修改state,例如从0修改成1,修改为功的线程即为资源竞争成功的线程,将其设为exclusiveOwnerThread,也称【工做线程】,资源竞争失败的线程会被放入一个FIFO的队列中并挂起休眠,当exclusiveOwnerThread线程释放资源后,会从队列中唤醒线程继续工做,循环往复。 逻辑是否是和synchronized底层差很少?对吧。markdown

理论说的差很少了,本篇文章就经过ReentrantLock结合AbstractQueuedSynchronizer,经过阅读源码的方式来看一下AQS究竟是如何工做的,顺便膜拜一下Doug Lea大佬。多线程


AQS基础架构

阅读源码前,先来简单了解一下AQS的架构: 在这里插入图片描述 架构仍是比较简单的,除了实现Serializable接口外,就只继承了AbstractOwnableSynchronizer父类。 AbstractOwnableSynchronizer父类中维护了一个exclusiveOwnerThread属性,是用来记录当前同步器资源的独占线程的,没有其余东西。架构

AQS有一个内部类Node,AQS会将竞争锁失败的线程封装成一个Node节点,Node类有prevnext属性,分别指向前驱节点和后继节点,造成一个双向链表的结构。除此以外,每一个Node节点还有一个被volatile修饰的int变量waitStatus,它表明的是节点的等待状态,有以下几种值:并发

  1. 0:新建节点的默认值。
  2. SIGNAL(-1):表示后继结点在等待当前结点唤醒。
  3. CONDITION(-2):表示结点等待在Condition上,当其余线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  4. PROPAGATE(-3):共享模式下,前驱节点会唤醒全部的后继节点。
  5. CANCELLED(1):取消竞争资源,锁超时或发生中断才会触发。

能够看到,waitStatus是以0为临界值的,大于0表明节点无效,例如AQS在唤醒队列中的节点时,waitStatus大于0的节点会被跳过。app

AQS内部还维护了int类型的state变量,表明同步器的状态。例如,在ReentrantLock中,state就表明锁的重入次数,每lock一次,state就+1,每unlock一次,state就-1,当state等于0时,表明没有上锁。ide

AQS内部还维护了headtail属性,用来指向FIFO队列中的头尾节点,被head指向的节点,老是工做线程。线程在获取到锁后,是不会出队的。只有当head释放锁,并将其后继节点唤醒并设为head后,才会出队。


ReentrantLock.lock()作了什么?

示例程序:开启三个线程:A、B、C,按顺序依次调用lock()方法,这期间到底发生了什么???

一、刚开始没有任何线程竞争锁,AQS内部结构是这样的: 在这里插入图片描述 二、线程A调用lock()方法: 在这里插入图片描述 实际上是交给sync对象去上锁了,Sync类就是一个继承了AQS的类。

ReentrantLock默认采用的是非公平锁,无论队列中是否有等待线程,上来直接就尝试利用CAS抢锁,若是抢成功了,就将当前线程设为exclusiveOwnerThread并返回。 若是没有成功,则调用acquire(1)去获取锁。

// 非公平锁的lock,上来直接就抢锁,无论队列中有没有线程在等待。
final void lock() {
	// CAS的方式将修改state,若是修改为功,表示没有其余线程持有锁,将当前线程设为独占锁的持有者
	if (compareAndSetState(0, 1))
		setExclusiveOwnerThread(Thread.currentThread());
	else
		// CAS失败,表明其余线程已经持有锁了,此时去竞争锁
		acquire(1);
}
复制代码

公平锁就显得很是有礼貌,上来先询问队列中是否有线程在等待,若是有,则让它们先获取,本身入队等待。

final void lock() {
	acquire(1);
}

// 公平锁-获取锁
protected final boolean tryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
		/* 即便当前是无锁状态,也要判断队列中是否有线程已经在等待了。 若是有其余线程在等待,要让其余线程先获取锁,本身入队挂起。 若是队列中无线程,则尝试CAS竞争。 */
		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;
	}
	return false;
}
复制代码

无论是公平锁仍是非公平锁,线程A此时就能够获取到锁并返回了,此时AQS的内部结构以下: 在这里插入图片描述

假设线程A还未释放锁,线程B调用lock(),竞争锁失败,则调用acquire(1)去获取锁,这是AQS的模板方法。

/* 竞争锁的流程: 1.tryAcquire():再次尝试去获取锁。 2.addWaiter():若是还获取不到,在队列的尾巴添加一个Node。 3.acquireQueued():去排队。 */
public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
			acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		// 是否须要进行一次自我中断,来补上线程等待期间发生的中断。
		selfInterrupt();
}
复制代码

在acquire()方法中,首先会调用tryAcquire()去尝试获取锁,若是获取不到,则经过addWaiter()将当前线程封装为一个Node节点入队,再调用acquireQueued()去排队。 这里有一点须要注意,AQS在排队的过程当中,是不响应中断的,若是排队期间发生了中断,只能等排队结束后,AQS自动补上一个自我中断:selfInterrupt()。

非公平锁尝试获取锁的流程以下:

// 非公平锁尝试获取锁
final boolean nonfairTryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
		// state==0,表明其余线程已经释放锁了,再次CAS的方式修改state,成功则表明抢到锁,返回。
		if (compareAndSetState(0, acquires)) {
			setExclusiveOwnerThread(current);
			return true;
		}
	}
	// 其余线程还没释放锁,判断持有锁的线程是不是当前线程,若是是,则重入,state++。
	// 可重入锁,state就表明锁重入的次数,0说明锁释放了。
	else if (current == getExclusiveOwnerThread()) {
		int nextc = c + acquires;
		if (nextc < 0) // 锁不可能无限重入,重入的次数超过了int最大值后,就会抛异常。
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
	// 其余线程没释放锁,当前线程又不是持有锁的线程,则抢锁失败。
	return false;
}
复制代码

因为线程A没有释放锁,且线程B不是锁的持有线程,所以tryAcquire()会返回false。

尝试获取锁失败,则开始建立Node节点,并入队。addWaiter代码以下:

// 若是尝试获取锁失败,则入队。
private Node addWaiter(Node mode) {
	// 建立一个和当前线程绑定的Node节点
	Node node = new Node(Thread.currentThread(), mode);
	Node pred = tail;
	if (pred != null) {
		/* 若是tail不为null,则经过CAS的方式将tail指向当前Node。若是失败,会调用enq()重试。 1.当前节点的prev指向前任tail 2.CAS将tail指向当前节点 3.前任tail的next指向当前节点 这三个步骤不是原子的,若是执行到第2步时间片到期,持有锁的线程释放锁唤醒节点时, 若是从head向tail找,此时前任tail节点的next仍是null,会存在漏唤醒问题。 而prev的赋值先于CAS执行,因此在唤醒队列时,从tail向head找就没问题了。 */
		node.prev = pred;
		if (compareAndSetTail(pred, node)) {
			pred.next = node;
			return node;
		}
	}
	// 若是CAS入队失败,则自旋重试
	enq(node);
	return node;
}
复制代码

若是tail不为null,则将当前节点的prev指向现任tail,再经过CAS的方式将tail指向当前节点,最后前任tail的next指向当前节点便可。 这里有一点须要注意:

  1. 当前节点的prev指向前任tail
  2. CAS将tail指向当前节点
  3. 前任tail的next指向当前节点

这三个步骤不是原子的,若是执行到第2步时间片到期,持有锁的线程释放锁唤醒节点时,若是从head向tail找,此时前任tail节点的next仍是null,会存在漏唤醒问题。而prev的赋值先于CAS执行,因此在唤醒队列时,从tail向head找就没问题了。

若是CAS入队失败也不要紧,下面会调用enq()进行自旋重试,直到成功为止:

// CAS入队失败,自旋重试
private Node enq(final Node node) {
	for (;;) { // 这比while(true)好在哪里???
		Node t = tail;
		if (t == null) {
			// tail==null,说明队列是空的,作初始化。
			// 新建一个节点,head和tail都指向它。再进循环时,tail就不为null了。
			if (compareAndSetHead(new Node()))
				tail = head;
		} else {
			/* 队列不为空 1.当前节点的prev指向前任tail 2.CAS将tail指向当前节点 3.前任tail的next指向当前节点 不断重试,直到成功为止 */
			node.prev = t;
			if (compareAndSetTail(t, node)) {
				t.next = node;
				return t;
			}
		}
	}
}
复制代码

若是tail为null,说明队列还没初始化,这时会建立一个Node节点,head和tail都指向这个空节点。再次循环时,因为已经初始化了,进入else逻辑,仍是执行那三个步骤。

线程B入队后,此时AQS的内部结构以下: 在这里插入图片描述

节点B成功入队后,就是排队的操做了。线程B是继续竞争仍是Park挂起呢?

// Node入队后,开始排队
final boolean acquireQueued(final Node node, int arg) {
	boolean failed = true;// 是否获取锁失败
	try {
		/* 线程等待的过程当中是不响应中断的,若是期间发生中断, 则必须等到线程抢到锁后进行自我中断:selfInterrupt()。 */
		boolean interrupted = false;//获取锁的过程当中是否发生中断。
		for (;;) {
			final Node p = node.predecessor();// 获取当前节点的前驱节点
			/* 若是本身的前驱是head,本身就有资格去抢锁,有两种状况: 一、做为第一个节点入队。 二、head释放锁了,唤醒了当前节点。 */
			if (p == head && tryAcquire(arg)) {
				/* 若是抢锁成功,说明是head释放锁并唤醒了当前节点。 将head指向当前节点,failed = false表示成功获取到锁。 */
				setHead(node);
				p.next = null; // help GC
				failed = false;
				return interrupted;
			}
			// 竞争锁失败,判断是否须要挂起当前线程
			if (shouldParkAfterFailedAcquire(p, node) &&
					parkAndCheckInterrupt())
				interrupted = true;
		}
	} finally {
		// 获取不到锁就挂起线程,不断死循环,所以只要不出意外,最终必定能获取到锁。
		// 若是没有获取到锁就跳出循环了,说明线程不想竞争了,例如:锁超时。
		// 此时须要修改
		if (failed)
			cancelAcquire(node);
	}
}
复制代码

若是当前节点的前驱节点是head,则表明当前节点拥有竞争锁的资格。分两种状况:

  1. 做为第一个节点入队。
  2. head释放锁了,唤醒了当前节点。

此时线程B并非被线程A唤醒了,而是第一种状况,线程B会再次尝试获取锁,可是因为线程A还没释放,所以会失败。 线程B获取锁失败后,会执行shouldParkAfterFailedAcquire(),判断是否应该被Park挂起。

// 线程竞争锁失败是否要挂起
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	int ws = pred.waitStatus;
	if (ws == Node.SIGNAL)
		// 若是前驱节点的waitStatus=-1,当前节点就能够安心挂起了。
		return true;
	if (ws > 0) {
		// waitStatus以0为分界点。0是默认值,小于0表明节点为有效状态,大于0表明节点无效,例如:被取消了。
		// 若是前驱节点无效,就继续向前找,直到找到有效节点,并将其next指向本身。中间无效的节点会被GC回收。
		do {
			node.prev = pred = pred.prev;
		} while (pred.waitStatus > 0);
		pred.next = node;
	} else {
		// CAS的方式将前驱节点的waitStatus改成-1,表明当前节点在等待被前驱节点唤醒。
		compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
	}
	return false;
}
复制代码

线程被Park挂起的前提条件是:必须将前驱节点的waitStatus设为SIGNAL(-1),这样当前驱节点释放锁时,才会唤醒后继节点。

因为此时head节点的waitStatus等于0,不知足条件,因此线程B会尝试使用CAS的方式将其改成SIGNAL,且这一次不会线程B不会被Park。 此时,AQS的内部结构是: 在这里插入图片描述

再次循环,因为线程A还没释放锁,线程B在此获取锁失败,再次执行shouldParkAfterFailedAcquire(),此时前驱节点的waitStatus已是SIGNAL(-1)了,因此线程B能够安心Park了,返回true。

shouldParkAfterFailedAcquire()返回true表明须要将线程B挂起,所以会执行parkAndCheckInterrupt()

// 挂起当前线程,等待被前驱节点唤醒
private final boolean parkAndCheckInterrupt() {
	LockSupport.park(this);
	// 阻塞过程当中不响应中断,期间若是发生了中断,则补上自我中断:selfInterrupt()。
	return Thread.interrupted();
}
复制代码

挂起的过程就很简单了,调用了LockSupport.park()方法。前面已经说过,AQS排队的过程是不响应中断的,若是期间发生了中断,只能等待线程被唤醒后,补上自我中断,因此这里会返回线程的一个中断标志。

线程B如今已经被Park挂起了,只能等待线程A的唤醒才能继续运行。

acquireQueued()方法中,有一个finally语句块,它的做用是,若是线程没有获取到锁就退出了循环,说明线程获取锁超时或者发生中断了,那么节点就无效了,须要将它出队,调用cancelAcquire()

// 节点取消竞争锁
private void cancelAcquire(Node node) {
	// 忽略不存在的节点
	if (node == null)
		return;

	node.thread = null;//取消绑定的线程

	// 往前找,跳过CANCELLED的节点
	Node pred = node.prev;
	while (pred.waitStatus > 0)
		node.prev = pred = pred.prev;

	Node predNext = pred.next;

	// 将当前节点设为CANCELLED,这样即便出队失败也不要紧,唤醒节点时会跳过CANCELLED的节点
	node.waitStatus = Node.CANCELLED;

	if (node == tail && compareAndSetTail(node, pred)) {
		/* 若是node是尾巴,就使用CAS将tail指向前驱节点,当前节点直接出队。 出队成功,将tail的next置空。 */
		compareAndSetNext(pred, predNext, null);
	} else {
		/* 若是前面的操做失败了,有两种状况: 1.当前node原来是尾巴,取消过程当中有新节点插入,现已不是尾巴。 2.当前node原来就是中间节点。 当前node是中间节点的话,就须要作两件事: 1.修改前驱节点的waitStatus为SIGNAL,让其释放锁后记得唤醒后继节点。 2.将前驱节点的next指向后继节点,当前node出列。 出列的过程是容许失败的,即便没有出列,只要node的waitStatus设为CANCELLED, head在唤醒后继节点时也会跳过CANCELLED的节点。 修改前驱节点为SIGNAL的过程也是容许失败的,只要失败了就会唤醒node的后继节点, 让后继节点本身去修改前驱节点为SIGNAL。 */
		int ws;
		if (pred != head &&
				((ws = pred.waitStatus) == Node.SIGNAL ||
						(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
				pred.thread != null) {
			/* 修改前驱节点为SIGNAL成功,将前驱节点的next指向当前节点的后继节点,当前节点出列。 */
			Node next = node.next;
			if (next != null && next.waitStatus <= 0)
				compareAndSetNext(pred, predNext, next);
		} else {
			/* 失败有两种状况: 1.当前节点的prev为head,那老二就有资格去竞争了,唤醒当前节点的后继节点。 2.修改前驱节点为SIGNAL失败,唤醒后继节点,让它本身去修改前驱节点的状态。 */
			unparkSuccessor(node);
		}

		node.next = node; // help GC
	}
}
复制代码

节点取消获取锁的状况有两种须要考虑:

  1. 当前节点是tail。
  2. 当前节点是中间节点。

第一种状况处理就简单了,直接将tail指向当前节点的prev,而后prev的next置空,当前节点就出队了。 第二种状况就比较复杂,若是当前节点的前驱节点不是head的话,那么就必须将前驱节点的waitStatus设为SIGNAL(-1),而后将前驱节点的next指向当前节点的next,当前节点的next的prev,指向前驱节点。 在这里插入图片描述 若是说当前节点的前驱节点是head,那么就直接唤醒当前节点的后继节点,由于老三变老二了,它有资格去竞争锁了。

若是CAS修改节点的指向失败了,也不要紧,唤醒当前节点的后继节点,让它本身去修改前驱节点的waitStatus,当前节点能够安心出队。

很显然,线程B是不会触发cancelAcquire()方法的。

假设,此时线程A依然没有unlock,此时线程C也要来获取锁。显然线程C会竞争失败,AQS会将其封装为Node节点入队,并将线程B的Node节点的waitStatus改成SIGNAL(-1),而后Park休眠。 此时,AQS的内部结构是: 在这里插入图片描述


ReentrantLock.unlock()作了什么?

线程B、C成功入队并Park后,假设此时线程A执行unlock释放锁: 在这里插入图片描述 释放锁的过程,实际上是调用了sync的release(),这也是AQS的模板方法:

// 释放锁
public final boolean release(int arg) {
	/* 调用子类的tryRelease(),返回true表明成功释放锁。 对于ReentrantLock来讲,state减小至0表明须要释放锁。 */
	if (tryRelease(arg)) {
		/* head就表明持有锁的节点。 若是head的waitStatus!=0,说明有后继节点在等待被其唤醒。 还记得线程入队时,若是要挂起,必须将其前驱节点的waitStatus改成-1吗??? 若是节点入队不改前驱节点的waitStatus,它将没法被唤醒。 */
		Node h = head;
		if (h != null && h.waitStatus != 0)
			// 释放锁后要去唤醒后继节点
			unparkSuccessor(h);
		return true;
	}
	return false;
}
复制代码

AQS会调用子类实现的tryRelease(),当它返回true就表明成功释放了资源,AQS就会去唤醒队列中的节点。

/* 尝试释放锁,返回true表明锁成功释放。 只有持有锁的线程才能释放锁,所以不存在并发问题。 */
protected final boolean tryRelease(int releases) {
	int c = getState() - releases;
	// 不是持有锁的线程执行释放锁,抛异常。
	if (Thread.currentThread() != getExclusiveOwnerThread())
		throw new IllegalMonitorStateException();
	boolean free = false;
	if (c == 0) {
		// 当state==0才须要真正的释放锁
		free = true;
		setExclusiveOwnerThread(null);
	}
	setState(c);
	return free;
}
复制代码

ReentrantLock是可重入锁,state表明锁的重入次数,tryRelease()就是state自减的过程。当state减至0就表明锁成功释放了,同时会将exclusiveOwnerThread置空。

必须是持有锁的线程才能调用tryRelease(),不然会抛异常。

线程A执行完tryRelease()后,此时AQS的内部结构是: 在这里插入图片描述

tryRelease()返回true,AQS就要去唤醒队列中的节点了,执行unparkSuccessor()

// 唤醒后继节点
private void unparkSuccessor(Node node) {
	// 将waitStatus置为0
	int ws = node.waitStatus;
	if (ws < 0)
		compareAndSetWaitStatus(node, ws, 0);

	/* 若是后继节点的waitStatus>0,表明节点是CANCELLED的无效节点,会跳过。 而后从尾巴开始向头找,直到找到waitStatus <= 0的有效节点,并将其唤醒。 为何要从尾巴找? 是由于节点入队时,须要执行三个操做: 1.当前节点的prev指向前任tail 2.CAS将tail指向当前节点 3.前任tail的next指向当前节点 若是执行到步骤2时,时间片到期,此时前驱节点的next仍是null,会存在漏唤醒的问题。 而prev的赋值操做先于CAS执行,所以经过prev向前找总能找到。 */
	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);
}
复制代码

这里有一个颇有意思的操做,当next节点无效时,AQS会跳过它,从新寻找有效的节点。AQS会从tail开始向head找,而不是从head向tail找,这是为何呢???

从尾部向头部找,是由于节点入队时,须要执行三个操做:1.当前节点的prev指向前任tail、2.CAS将tail指向当前节点、3.前任tail的next指向当前节点。若是执行到步骤2时,时间片到期,此时前驱节点的next仍是null,会存在漏唤醒的问题。而prev的赋值操做先于CAS执行,所以经过prev向前找总能找到。

当节点被唤醒时,会从AQS的parkAndCheckInterrupt()方法里继续执行,从新获取锁。

线程A释放锁,并将线程B唤醒,线程B会继续去竞争。 在这里插入图片描述 此时线程B会竞争成功,同时会将head指向当前节点。 此时AQS内部结构是: 在这里插入图片描述 线程B运行一段时间后,也释放锁了,接着会唤醒线程C。 在这里插入图片描述 线程C会成功获取到锁: 在这里插入图片描述 线程C释放锁后,最后一个Node节点并不会出列,而是会保留。当下一次有线程来竞争锁时,成功后会自动将前任head覆盖。


问题

最后再总结一下关于AQS几个比较重要的问题。

1.工做线程何时出队?

FIFO队列中的一个节点竞争到资源时,它并非就立刻出队了,而是将head指向本身。节点释放锁后依然不会主动出队,而是等待下一个节点竞争锁成功后修改head的指向,将前任head踢出去。

2.AQS唤醒队列的规则是什么?

head指向的节点成功释放资源后,首先会判断当前节点的waitStatus是否等于0,若是等于0就不会去唤醒后继节点了,这也就是为何新的节点入队休眠的前提是必须将前驱节点的waitStatus改成SIGNAL(-1)的缘由,若是不改,后继节点将不会被唤醒,就会致使死锁。

AQS首先会唤醒当前节点的直接后继节点next,若是next为null,有两种状况:

  1. 确实没有后继节点了,以前next指向的节点可能因为超时等缘由退出竞争了。
  2. 存在后继节点,只是因为多线程的缘由,后继节点还没来得及将当前节点的next指向它。

第一种状况好办,后继节点为null,不唤醒就是了。 第二种状况就须要从tail向head寻找了,找到了有效节点再唤醒。

若是存在直接后继节点,可是节点的waitStatus大于0,AQS也是会选择跳过它的。前面已经说过,waitStatus大于0的节点表明无效节点,如CANCELLED(1)是已经取消竞争的节点。若是直接后继节点是无效节点的话,AQS会从tail开始向head遍历,直到找到有效节点,再将其唤醒。

总结:存在直接后继节点且节点有效,则优先唤醒后继节点。不然,从tail向head遍历,直到找到有效节点再唤醒。

3.唤醒节点为何要从尾巴开始?

这是由于,新节点入队时,须要执行三个步骤:

  1. 当前节点的prev指向前任tail
  2. CAS将tail指向当前节点
  3. 前任tail的next指向当前节点

这三个操做AQS并无作同步处理,若是在执行步骤2后CPU时间片到期了,此时的节点指向是这样的: 在这里插入图片描述 前驱节点的next尚未赋值,若是从头向尾找,就可能会存在漏唤醒的问题。 而prev的赋值先于tail的CAS操做以前执行,所以从尾向头找,就能够避免这个问题。

4.其余问题

占个坑,问题能够持续更新,想到了再更。有疑惑的同窗能够评论告诉我,我会把我知道的记录下来,你们一块儿再探讨一下。


尾巴

到如今还很印象深入,年初的时候有一次面试,就被问到AQS,当时没有静下心来研究,有些概念仍是很模糊,答得不是很好,面试官也不太满意。

刚好上周加班,今天调休,能够写篇文章放松一下。因而我决定挑战一下个人软肋AQS,静下心来阅读源码才发现,过去看网上的博客,一直比较模糊的概念,其实代码里已经写的很是清楚了。

过去读不懂的源码,日后慢慢都会读懂!!!


你可能感兴趣的文章:

相关文章
相关标签/搜索