最近想深入学习一下java并发的基础知识,总感觉在对java.util.concurrent一知半解,很多东西好像只有点印象,这次接着学习源码的机会来深入了解一下。那么为什么把AbstractQueuedSynchronizer这个类放在最前面呢?其实相信很多人跟我有同样的经历,首先学习的是ExecuteService线程池,然后学习里面的工厂类Executors,和其中的submit、execute方法,但是学到一半的时候,发现里面很多东西用的都是锁的概念,比如我们最常用的synchronized关键字和ReentrantLock类,这里对这个也不是很了解,然后想继续学习下去,又对ReentrantLock这个类源码学习一下,最后,最后发现其中一个很核心的类AbstractQueuedSynchronizer,学习完之后,又回过头来进行总结一下,发现这个类有很多的实现子类,都是我们常用的,那么索性先把这个类放在最前面,把其中相关的方法都学习一遍。
首先,什么是AQS呢?AQS(AbstractQueuedSynchronizer)框架就是提供了一个自动管理同步状态、阻塞和非阻塞线程,以及等待队列的通用机制。内部主要包含了两个内部类一个是Node和ConditionObject,其中基于node节点构建了一个FIFO的队列,采用的是链表的数据结构进行存储。首先我们看下该类的主要结构:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
static final class Node {}
public class ConditionObject implements Condition, java.io.Serializable {}
}
可以看到其中包含了两个内部类,Node主要用作数据结构,用来保存对应的节点信息。这里面我们接着看这个抽象方法中几个比较重要的方法,tryAcquire/tryRelease/tryAcquireShared/tryReleaseShared/isHeldExclusively这个5个方法,可以看到这个5个方法都是protected类型的,所以可以让子类直接去实现它,这几个方法都没有对应的方法体,因此对应的逻辑可以写在子类中。通过tryAcquire可以获取锁,而通过tryAcquire可以去直接释放锁。AOS是一个典型的模板方法设计模式的典型运用案例,AQS为一个抽象类,却没有抽象方法,所有的方法具体实现都要子类去实现。它提供了:如何让现场如队列,如何让现场出队列,现场如何等待和转移等模板方法,而子类需要做的事情,就是如何决定线程出入队列。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
三 AQS的原理
AQS使用的是一个int类型的成员变量state来表示当前的同步状态,当state>0的时候表示已经获取到了锁,当state=0的时候,表示释放了锁,它共提供了3个方法getState(),setState()和compareAndSetState()来对同步的状态state进行操作,整个过程采用的是原子操作,保证安全性。
AQS通过内置的同步队列来完成线程的排队工作,如果当前线程获取锁失败的时候,会将当前线程以及等待状态信息的构造成一个节点,加入同步队列中,同时会阻塞当前线程,当同步锁释放的时候,则会把节点中的线程唤醒,使得其获得锁。
四、ReentrantLock简介
ReentrantLock的功能是实现代码的并发访问控制,实际上是一种排他锁,这里我们要用到的其中两个不同的锁,就NofaiSync和FairSync,就是通常所说的公平锁和非公平锁,默认的是采用非公平的锁。这里我们可以这个类的结构,其中几个几个内部类,其中Sync是一个抽象类,并且继承了AbstractQueuedSynchronizer,而NonfairSync和FairSync则是Sync的子类。两个子类都实现了tryAcquire方法用来获取锁,下图就是其中包含的主要方法和内部类名。
五、AQS的源码解析
1. 基本数据结构和知识点介绍
这里借助ReentrantLock的入口,先来了解下AQS的原理,首先来看下比较重要的一个数据结构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; //表示当前节点的后续节点的获取acquireShared被无条件执行
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;
}
Node() {
}
Node(Thread thread, Node mode) { 等待队列使用的节点
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // 条件队列使用的节点
this.waitStatus = waitStatus;
this.thread = thread;
}
}
以上是使用的数据结构的定义,在AQS还有结构非常重要的内部变量,在初始化的AQS的队列中,head和tail默认是null。
private transient volatile Node head; //等待队列的head节点,只有当调用的setHead方法的时候才进行加载,属于懒加载
private transient volatile Node tail; //等待状态的尾节点
private volatile int state; //AQS队列的状态
其实上面构造的就是一个节点,这个我们可以用数据结构中的链表来表示,大致构造的如下图所示,在等待队列中有个头结点head和一个尾节点tail分别指向队列的头和尾,这是一个双向链表。
在学习源码的时候还遇到很多地方使用到了函数CompareAndSwap或者CompareAndSet方法,这些方法都是调用JDK的内部类unsafe,是基于硬件保证原子更新的,如果这个值是expect就更新为update,是无锁并发的基础。
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
2. 获取锁的源码分析
首先看下程序的入口,先构建一个锁,只要执行方法lock()即可获取锁,首先来看下ReetrantLock这个锁的构造函数
ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.unlock();
public ReentrantLock() {
sync = new NonfairSync(); //非公平锁
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync(); //公平锁
}
//这个方法获取锁,这里用的syn变量,这个公平锁和非公平锁的父类
public void lock() {
sync.lock();
}
在这里,有公平锁和非公平锁之分,其中公平锁中,每个线程抢占锁的顺序为先后调用lock()方法的顺序依次获取锁,而非公平锁中每个线程抢占锁的顺序不确定,先获取锁,获取不到,加入到等待队列中,和调用方法lock()的顺序无关。所以这个地方在公平锁中,等待最短的时间肯定是最先获取到锁,反之在非公平锁中,等待最长的时间的线程,可能更加的有机会获取到锁。这里,我们以公平锁的源码进行分析,其实调用的是Sync的子类,FairSync和NonfairSync
abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class FairSync extends Sync {}
static final class NonfairSync extends Sync {}
可以看到,这个方法里面调用了acquire()方法,并传递了一个参数1,表示需要获取锁
final void lock() {
acquire(1);
}
接着往下看这个方法的内容,这个方法用了调用了两个方法作为判断是否获取到锁,其中tryAcquire 方法是AbstractQueuedSynchronizer 这个类的protected方法,由其子类去实现它,从这个&&操作符可以判断,只有当获取锁失败的的时候才会去执行下面一个方法acquireQueued(),构造一个Node,并添加到等待队列中。其实这个方法是阻塞式的。
AQS获取独占锁
1. 尝试去获取锁,tryAcquire(),获取成功,直接返回,获取失败,进行后一步操作
2.没有获取成功,把线程加入到等待队列中,当前线程中断
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先tryAcquire(int arg)这个方法的具体实现如下
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); //获取当前队列的状态
if (c == 0) { //发现所没有被获取
if (!hasQueuedPredecessors() && //这个方法点进去很简单,只有一个判断,是否存在比当前线程等待更加的线程,其实就是head==tail是否成立
compareAndSetState(0, acquires)) { //没有的话调用CAS进行更新状态,自己获取锁
setExclusiveOwnerThread(current); //
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //这种状态需要判断一下,此时如果获取锁的线程等于exclusiveownerThread,则将state+1
int nextc = c + acquires; //这就是重入锁的实现方式
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
其中三个方法hasQueuedPredecessors/compareAndSetState分别用来判断和更新状态,如果此处获取失败,回到上个方法,需要进行阻塞,首先是把当前节点加入到等待队列中,首先来看下addWaiter方法
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) { //当前队列的尾节点,如果不为null,说明已经存在了等待节点,则把该节点加入到等待队列中
node.prev = pred; //这个就是链表尾部添加节点很常用的操作了,把tail节点设置成当前节点的前驱,并把tail的next属性指向自己
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node); //上一步是判断尾部节点是否null,如果为null的情况下该怎么加入呢
return node;
}
private Node enq(final Node node) {
for (;;) { //for循环,一定要保证等待队列构造成功
Node t = tail;
if (t == null) { // 发现尾部节点为null一定要构造节点设置成头节点
if (compareAndSetHead(new Node()))
tail = head;
} else { //第二次循环的时候,此时已经有了头节点,把当前节点的前驱节点设置成头节点,并把自己设置成tail节点,添加到队列的最后面
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这就要用到上面的enq()方法了,开始构造等待队列,通过上面的方法,当前节点会被存放到等待队列中,接下来看下AQS中的acquireQueued方法。从代码中可以看出,这里面又做一次获取锁的操作
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);
}
}
下面来说下shouldParkAfterFailedAcquire这个方法,这个方法有点自旋锁的味道,他会先判断你是不是头结点,如果不是头节点的时候,在判断你的前驱节点的状态,判断是否需要挂起,如果前驱节点的状态是signle,这个线程将被park。直到另外的线程release的时候,并且next节点等于当前节点的时候,才进行unpark操作。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) //判断前驱节点的状态是否是single,来觉得是否挂起当前线程,前置节点执行完,即可唤醒当前节点,此时只要安全的挂起即可
return true;
if (ws > 0) {
do {//ws>0,说明前置节点自己取消同步状态,不需要获取锁,所以通过while循环一直往前寻找,知道找到>0的节点
node.prev = pred = pred.prev; //前驱节点被取消,跳过所有取消的前驱节点
} while (pred.waitStatus > 0); //这样做的目的是去掉同步队列中已经取消了同步的节点
pred.next = node;
} else {
//如果是其他的状态,则需要把当前的节点设置成signle,所以需要确保挂起之前不能获得同步状态
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
通过上面的操作的操作,如果一个新的线程过来,是否获取到锁,如果没有,则进入等待队列中等,获取锁则返回,这里还有一个方法cancelAcquire没讲?当初看到这里有个疑问,每个线程调用一次lock()方法去尝试获取锁,然后获取失败之后,加入到等待队列中,然后中断自己本身,那么问题来了?后面等待的线程如何又唤醒自己呢?在代码中也没看到这方面的逻辑,这里先不说,先看锁的释放逻辑。
AQS释放所独占锁
这里我们先看下锁的释放逻辑,调用unlock()方法,起调用的是tryRelease方法来释放锁。这里根据先后调用的顺序,先看下tryRelease方法
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;
}
具体的处理过程如下:
1. 首先等待队列的状态减1,然后判断是否是自己
2. 更新状态,释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases; //释放一个锁,当前的状态减掉1
if (Thread.currentThread() != getExclusiveOwnerThread()) //只有自己才能释放自己
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { //说明已经无锁
free = true;
setExclusiveOwnerThread(null);
}
setState(c); //释放一个锁,减掉1,也就是说加锁多少次,就要释放多少次
return free;
}
接下来看下另外一个方法unparkSuccessor来唤醒后继线程
private void unparkSuccessor(Node node) {
int ws = node.waitStatus; 获取当前节点的状态
if (ws < 0) //说明当前节点的状态并不是已经取消,那么把节点状态设置成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);
}
总结下AQS释放锁的过程,释放的过程分为2步,第一是首先释放自己占有的锁,调用tryrelease方法,如果失败,则返回,如果释放锁成功,就拿到队列的头结点。第二是根据拿到的头结点,找到第一个后继的有效结点,然后将其从等待队列中移除,最后激活对应的线程。
下面看下节点的取消过程,主要是方法cancelAcquire,看下起主要的源码,其实主要就是状态设置和链表中取消一个节点的设置操作。
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
Node pred = node.prev; //这个过程和之前的类似,都是获取节点的有效想的前驱节点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next; //找到node节点的后继节点
node.waitStatus = Node.CANCELLED; //同时把状态设置成取消
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {//如果node是尾节点,就把node的前驱节点设置为尾节点,同时把对应的next设置成null
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) { //如果node的前驱节点不是头结点,则把状态设置成signle
Node next = node.next; //找到node的候机节点
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next); //同时进行连接,取消node的节点
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
以上就是AbstractQueuedSynchronizer这个类中关于获取锁和释放锁的主要的方法和流程,他是通过构造一个队列来获取锁和释放锁,当当前线程执行完毕之后,会释放当前锁,同时在激活下一个线程,还有一部分方法这里没有列出来,不如非公平锁,共享锁等,其中就是主体方法都是一样的,不过是加了一些逻辑判断,这里不再一一列出了...