thinkFirst
先回顾一下,在不显式依赖任何Lock包相关实现的情况下,如何做到线程安全?
-
Synchronized
- 用一个底层实现的monitor对象,作为竞争对象的锁实例。
- 将monitor对象的地址(指针)信息记录在synchronized的对象头中
- 一旦需要获取这把锁,就做竞争。轻量级锁就是CAS变更+自旋,重量级锁就涉及到内核态到用户态的切换
-
volatile实现可见性与顺序性
-
Atmoic包实现无锁编程
synchronized实现了线程安全性上的”全能“,通过实现原理,做出一个猜想:
通过外加状态值,对状态值进行竞争,是否可以做到在Java代码层面实现在JVM中Synchronized的效果?
这里回想一下,sync的功能特性:
- 锁住代码,并且在锁结束前,不允许其他线程调用
- 当然了,在sync中可以通过Thread.wait让出锁
- 支持重入特性
第二步可以通过解锁后再加锁来解决。
我们知道:AQS提供了和sync类似的功能,那么这些特性应该都是具备的,带着这些准备,来看看AQS最常用的实现类:ReentrantLock的使用方法。
ReentrantLock的使用方法及其简单:
ReentrantLock lock = new ReentrantLock();
lock.lock();
//逻辑
lock.unlock();
那么就从这三个地方入手,来看看是具体是如何实现的。
构造方法
ReentrantLock有2个构造方法:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
也就是说:会新建一个sync,默认为非公平,也可以显示指定为公平锁。
这俩都对应着内部的一个抽象类:Sync,而这个抽象类继承了AQS。
这些内部类并没有重写构造方法,因此使用的构造方法是AQS的,而AQS的就是个空方法,因此类里的属性,赋值都是默认值,state = 0.
private volatile int state;
lock
不管AQS我们先往下看:
lock.lock();
这里对应到的是
public void lock() {
sync.lock();
}
这里公平、非公平就体现出来了,两个上锁的方式是不同的:
-
fair:
final void lock() { acquire(1); } -
nonFair:
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
由于最后都是调用到了acquire方法,因此这里就是公平、非公平的不同之处了。
从外部来看,非公平锁,会先尝试CAS将state的值变成1,而公平锁就是直接acquire了,从语义上来说这里的acquire就是获取锁了。
acquire
那么接下来看看acquire方法:
//其实这里的arg,在实现里调用传入的都是1
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
从字面意义上来说,这里的流程的意思应该是:
- 尝试获取锁,如果成功了就退出;
- 获取锁失败了,那么就往获取队列中加入一个waiter,并让当前线程interrupt。
那么就按顺序来看看流程:
tryAcquire
这个是AQS留给子方法的hook,此处以ReentrantLock为实现类,看看实现方法:
- fair:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
这里一看:其实也很简单,并没有循环的操作,那么说明获取锁的过程,都是只尝试一次的。
有几点发现:
-
第一步获取c,如果c=0那么代表没有锁占用;后面如果当前线程和独占的线程相同那么state+1
- 这里可以合理地进行推测:是否这里的state,代表的是当前线程的锁重入次数?
-
这里会判断是否队列前有前驱,该部分代码如下:
public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }发现:
-
这里没有加锁,那么是否会发生:t和h,在进入到这里之后发生了变更?
- 其实这个并没有问题,因为下一步会尝试CAS修改;如果修改失败,那么就跳到下面了
-
这里还有一个node的结构问题,这里能看到这里的node,应该是个链表结构,那么等到后面再来看看是如何进行维护的。
-
-
如果当前线程是重入了,那么这里会有一个setState(nextc); 的操作,这里并没有任何并发保护;但state值是volatile的,那么这样子修改并没有太大的问题;如果希望在sync内部再次多线程sync,那么应该使用更多的锁来确保线程安全,这里也是相同的。
-
nonFair:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
//注意,这里是ReentarntLock中sync的方法,如果是tryLock,那么无论是否公平与否,都会调用这个
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 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;
}
细看代码,其实省去的是:
if (c == 0) {
//省去了hasQueuePredecessor的这一步
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
那么非公平锁就是当当前锁没有线程使用的时候就直接CAS变state值了,这里的含义就代表了会先去这么做,省去了判断排队的动作。
addwaiter,acquireQueued
上一步中,如果我们获取到了锁,那么就直接返回了;如果很不幸地,获取锁失败了,接下来就会尝试进入队列了。
在上一步tryAcquire中,我们可以猜测:AQS是通过队列的方式来维护获取的,那么根据名称也可以合理推测这里代表的是尝试入队。
addWaiter
那么第一步应该是先把节点搞出来,对应的就是addWaiter方法,如下:
//上一步传入的是node.exculsive
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) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
不管语义上的问题(这里明明已经加到链表上了,那么应该是自己动起来了,为啥还要再acquireQueued?),先看看代码。
这里会先尝试将tail设置为当前节点并CAS设置这个tail,如果失败了或者tail为空就执行下面的enq方法。
- 这里的注解上写的也很明确,先尝试快速入队。
enq方法干的事情和上面也很相似,不同的是如果CAS失败了就会死等了,直到设置成功了:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued
解决了上面node的问题,接下来就看看这个方法了。
这里的代码是一个final方法,因此只要看AQS中的就可以了:
//这里的node是上面塞到队列最后的(至少塞完的时候是这样),arg就是acquire的传值,都是1
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);
}
}
其中这块就不需要再说了,简单来说就是获得锁并CAS操作成功的情况下返回:
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
那么,如果拿不到锁就执行这块:
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
这块的意思是,如果当前线程需要等待,那么就挂起(park)。这里就有一点sync为了避免轻量级锁过度使用CPU而让锁升级的味道在了。
shouldParkAfterFailedAcquire
看看这个方法:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.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;
}
这里就必须聊一下,AQS中node的几个状态了:
在前面的学习中我们知道了AQS会把线程封装成队列进行维护,队列中的抽象对象就是node。
这些node的状态值为waitStatus,有如下几种状态:
signal(-1)
- 当前节点的前节点正在(或马上要)阻塞(通过park),因此当前节点必须unpark前节点,当前节点释放或取消。
- 为避免竞争,获取的方法必须先判断是否需要信号,然后重试获取,失败则阻塞。
cancelled(1)
- 当前节点因为超时或被中断而取消。
- 节点并不会保持在这个状态。
- 该状态的线程永远不会再阻塞了。
condition(-2)
当前节点此时正在条件队列中(Condition queue)。
不会被用作同步队列节点,直到当状态再转化后变成0为止。
- 0并不代表任何此属性的其他用途,只是单纯简化机制。
propagate(-3)
- 一个
releaseShared应当被传播到其他节点。- 这个值只会被设定在头节点上。
- 在
doReleaseShared中被设置,用于确保传播行为持续进行,即使因为中断,其他线程已经做完这个工作了。0
- 非以上的所有情况。
那么这里就这么判断了:
- 如果当前节点状态为signal了,那么当前线程需要挂起;
- 如果不是,那么当前节点是否是取消状态?是的画就先把队列里取消的都清理掉;不是的话就把当前节点设置为signal。
这样子就可以知道:每次只有一个线程在CAS自旋请求锁其他的都park了,可以最大程度地降低CPU无意义地损耗。
unlock
那么这里入队获取,就大概清楚了;现在的问题变成了另外几个:
- 既然只有一个线程自旋请求锁其他的都park了,那么其他的线程啥时候被唤醒?
- 出队操作是怎么样的?
既然等待的线程,都是因为有其他线程目前已经占有锁了才等待,那么线程不等了的情况应该是在 持有锁的线程释放锁的时机被唤醒,来尝试获取被释放的锁。
因此我们就来看看这个释放锁对应的代码:
public void unlock() {
sync.release(1);
}
这里的releaseAQS搞成final了,那么就直接看看这里代码即可:
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的模板方法了,不过ReentrantLock里这个方法也是final,这里说明公平锁和非公平锁,在释放锁操作上是相同的。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
这里的操作就相对简单了:
- 这里的入参releases,代表的含义是在这里释放了几次锁(可重入性质相关的)。如果当前的state会变成0,那么就复位独占线程;否则,依然返回释放失败。
那么,如果这里的tryRelease成功了,且头节点不为空状态值不为0,那么就进入到下一步:
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) {
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);
}
这里我们注意到,这里会把当前释放成功时的头节点,状态值置为0;
同时,如果后续还有线程,那么会尝试从末尾往前找到第一个处于等待状态的节点;找到之后,将这个节点唤醒。
总结
那么AQS主要的功能,就都大致了解完了,具体的流程可见:
processon.com/chart_image…
\