最近想看JUC包下源码,本来是想看后继续写公众号的,发现了看这些源码前我首先要了解下什么是AQS,懂了他,JUC下的很多问题都不再是问题了,奈何平时工作忙,只能利用加班的时间看看源码,
所以自己潜水了一周多,在默默的研究AQS
先普及下基础:
自旋锁:在访问共享变量时,如果访问资源的线程没有拿到访问权限(未拿到锁),不是让线程释放资源,而是让线程处于无限循环的请求锁,并一直检测自己的状态,只有当请求到锁时才会继续下面的执行,否则就会一直处于阻塞状态
缺点
- 可能导致死锁
- 过多的占用CPU资源,让不能获得锁的时候,线程并不会释放资源,而是不断的请求获取同步状态,这样就会导致不必要的CPU占用
AQS实现版本:AQS在同步管理状态时,也是通过自旋实现的,但是它基于现有的自旋锁的改变,做了很多的优化
- 优化1:将杂乱无章的线程通过队列(CLH队列)来管理
- 优化2:将自旋过程中对共享变量的访问来获取同步状态的过程转化为了对前驱节点状态的访问
大概流程为
AQS, AbstractQuenedSynchronizer,即队列同步器,它是构建锁或者其他同步组建的基础框架如ReentrantLock、ReentrantReadWriteLock、countDownLatch、Semaphone等,AbstractQuenedSynchronizer
是JUC并发包中的核心基础组建
AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state=0时表示释放了锁,它提供了三个方法getState()、setState(int newState)、compareAndSetState(int expect,int update))
来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的
AQS通过内置FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(即获取锁失败)AQS则会将当前线程以及等待状态等信息构成一个节点(NODE)并将其加如同步队列,同时会阻塞当前
线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态
AQS主要提供了如下一些方法:
- getState():返回同步状态的当前值;
- setState(int newState):设置当前同步状态;
- compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性;
- tryAcquire(int arg):独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态;
- tryRelease(int arg):独占式释放同步状态;
- tryAcquireShared(int arg):共享式获取同步状态,返回值大于等于0则表示获取成功,否则获取失败;
- tryReleaseShared(int arg):共享式释放同步状态;
- isHeldExclusively():当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占;
- acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法;
- acquireInterruptibly(int arg):与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException异常并返回;
- tryAcquireNanos(int arg,long nanos):超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true;
- acquireShared(int arg):共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;
- acquireSharedInterruptibly(int arg):共享式获取同步状态,响应中断;
- tryAcquireSharedNanos(int arg, long nanosTimeout):共享式获取同步状态,增加超时限制;
- release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒;
- releaseShared(int arg):共享式释放同步状态;
那么CLH同步队列是什么呢
CLH队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构成一个节点(Node)并将其加入到CLH队列,同时会阻塞当前
线程,当同步状态释放时,会把首节点唤醒(公平锁),使其获取同步状态
源码解析
以ReentrantLock来解析AQS源码
public class ReentrantLock implements Lock, java.io.Serializable { private static final long serialVersionUID = 7373984872572414699L; /** Synchronizer providing all implementation mechanics */ private final Sync sync; /** * Base of synchronization control for this lock. Subclassed * into fair and nonfair versions below. Uses AQS state to * represent the number of holds on the lock. */ abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = -5179523762034025860L;
Sync下
/** * Sync object for non-fair locks */ static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1))//操作,state是0表示没有线程加锁,如果state是1表示已经有线程加锁成功 cas操作 setExclusiveOwnerThread(Thread.currentThread());//加锁成功 设为独占线程 else acquire(1);//没有获取到锁 } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } } /** * Sync object for fair locks */ static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540
再看函数
acquire
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
再此尝试加锁
看tryAcquire方法
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }
再看nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState();//获取state的值。如果是0表示未有线程持有锁 if (c == 0) {//0表示未有线程持有锁,尝试枷锁 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) {//重入锁操作 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
然后再看
addWaiter方法
addWaiter(Node.EXCLUSIVE)
独占式加锁
此方法就是如果线程获取同步状态失败,就会将当前线程以及等待状态等信息造成一个节点(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; /** 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal后,该节点将会从等待队列中转移到同步队列汇总 */ static final int CONDITION = -2; /** * waitStatus value to indicate the next acquireShared should * unconditionally propagate */ static final int PROPAGATE = -3; /*等待状态 */ volatile int waitStatus; /** 前驱节点 */ volatile Node prev; /** 后继节点 */ volatile Node next; /** * 获取同步状态的线程 */ volatile Thread thread; /** */ Node nextWaiter; /** * Returns true if node is waiting in shared mode. */ final boolean isShared() { return nextWaiter == SHARED; } /** * Returns previous node, or throws NullPointerException if null. * Use when predecessor cannot be null. The null check could * be elided, but is present to help the VM. * * @return the predecessor of this node */ 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; } }
然后看addWaiter方法
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode);//新建node // 尝试添加尾节点 Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) {//cas方法设置尾节点 pred.next = node;//把当前节点放在尾节点后面 return node; } } enq(node);// 当1 尾节点为空 2创建节点没有成功加到尾部,即有其他线程也在尾部添加节点时 不断尝试 return node; }
private Node enq(final Node node) { for (;;) {//多次尝试,直到成功为止 Node t = tail; if (t == null) { // 如果尾部节点不存在,设置为首节点 if (compareAndSetHead(new Node())) tail = head; } else {//如果 创建节点没有成功加到尾部则再次的把该节点添加到尾部
node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
//
两个方法都是通过一个CAS方法compareAndSetTail(Node expect, Node update)来设置尾节点,该方法可以确保节点是线程安全添加的。在enq(Node node)方法中,AQS通过“死循环”的方式来保证节点可以正确添加,只有成功添加后,当前线程才会从该方法返回,否则会一直执行下去。
addWaiter方法把当前请求锁失败的节点插入到队列中去后还的执行acquireQueued方法,因为我们执行插入队列之后还没有阻塞当前线程
然后看 acquireQueued(addWaiter(Node.EXCLUSIVE), 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;
}
//shouldParkAfterFailedAcquire方法判断如果获取锁失败是否需要阻塞,如果需要的话就执行parkAndCheckInterrupt方法,如果不需要就继续循环
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
下面看一下shouldParkAfterFailedAcquire方法:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * 前置节点状态是signal,那当前节点可以安全阻塞,因为前置节点承诺执行完之后会通知唤醒当前节点 */ return true; if (ws > 0) { /* 前置节点如果已经被取消则一直往前遍历直到前置节点不是取消状态,接着把往前遍历时第一个不是取消状态的后续节点设为当前节点的前序节点
*/ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
如果前面返回true,即前序节点的状态是signal时,则需要阻塞,然后调用parkAndCheckInterrupt()进行阻塞
private final boolean parkAndCheckInterrupt() { LockSupport.park(this);//LockSupport阻塞当前线程
return Thread.interrupted();//阻塞返回后返回当前线程是否中断
}
至此,一次完整的线程lock就完成了
1.调用tryAcquire方法尝试获取锁,获取成功的话就修该state的状态,获取失败的话就把当前线程放入等待队列中去
2、放入等待队列后会判断前序节点的节点状态是否是signal,如果是的话直接阻塞当前线程等待唤醒,如果不是的话判断是否是cancel状态,如果是的话就往前遍历把cannel状态的节点从队列中删除,
如果状态是0或者propate的话将其修改称signa
3、阻塞被唤醒之后如果是等待队列队首并且尝试获取锁成功就返回true,否则就继续执行前一步的代码进入阻塞
看一下线程释放锁的代码
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
先看tryRelease方法
protected final boolean tryRelease(int releases) { int c = getState() - releases;//释放后C的状态的值 if (Thread.currentThread() != getExclusiveOwnerThread())//如果持有锁的线程不是当前线程直接抛出异常 throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) {//如果state=0说明锁被释放了,其他线程可以请求获取锁 free = true; setExclusiveOwnerThread(null); } setState(c);//这里只会有一个线程执行到这里,不用cas操作 return free; }
接着看 unparkSuccessor(h)方法
此方法就是唤醒下一个继承锁的线程
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);//如果状态小于0,把状态改为0.因为node节点的线程释放了锁,后续不需要做任何的操作
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {..如果下一个节点为空或者下一个节点的状态大于0(取消状态),则从tail节点开始遍历找到离当前节点最近的且waitStatus<=0
//即非取消状态的节点并唤醒
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t; //这一步离头节点最近的一个非取消状态的线程置为了后续节点 在lock的时候 当前序节点是头节点的时候就会尝试加锁
}
if (s != null)
LockSupport.unpark(s.thread);//这里的unpark会唤醒 排队队列里的第一个节点 对应lock里的unpack
}
至此一次unlock调用完成了,总结来说就是:
1.修改状态位
2.唤醒排队的节点
3结合lock方法,被唤醒的节点会自动替换当前节点成为head
再来总结下AQS的流程
获取锁:
当一个新的线程想要获取同步状态时,首先判断state,如果state是0说明当前锁是空闲状态,可以直接给他执行锁,并将state状态设为1,并且将线程添加到同步队列里,
当然该线程是队列的头节点,此时又有一个线程来获取同步状态。首先改state为1,修改失败,说明已经有线程持有锁,那么此线程就要阻塞了,将该线程加入到同步队列里
同步队列行为:从获取到锁的过程,我们可以发现,实际能拿到锁的线程只有头节点,只有头节点释放了资源之后,后续的节点才有可能拿到锁,有了这个特性,我们直到当前节点想要拿到锁,就必须遵循以下两个条件
1、当前节点的前驱节点是头节点
2、头节点完成了任务释放了锁
当前线程即诶单加入到队列后,为了能够在有机会获取到锁的死后及时响应,那么就必须不断的去判断上述所说的节点是否满足,所以自旋的时候不再自旋state,而是子酸当前前驱节点是否满足唤醒的条件
释放锁:
1.将头节点的状态设置为Node.Singal(唤醒下一个节点)
2、如果下一个节点处于挂起状态,那么就唤醒他,让他运行在自旋状态
3、将当前的同步器状态设置为空闲状态
头节点完成了上述过程后,下次后续节点再进行自旋判断时,发现前驱节点已经释放了锁,那么就再次申请获取锁,获取到锁后,重置以下state,下一次轮回开始了
作者简介:
敏敏 猫眼电影java工程师,爱源码更爱生活,欢迎关注公众号 心情开花,会不时发布源码文章