Java架构师学习之路之并发编程四:Java同步器之synchronized&Lock&AQS(下)
synchronized
1. synchronized基础回顾
经过上一章的学习,我们知道了synchronized锁有以下特性:
- 实现了可见性和原子性
- 通过锁对象的对象头记录锁和当前占有锁线程的相关信息
- 翻译为字节码文件后会看到monitorenter和monitorexit两个指令
- JDK1.8以后,synchronized锁会进行:无锁(01)->偏向锁(01)->轻量级锁(00)->重量级锁(10) 这样的升级
- JIT会对锁进行粗化和消除优化
- 锁的膨胀升级不可逆
在本章,将会继续学习synchronized锁膨胀升级的具体过程,以及JUC并发包下的锁的实现。
2. synchronized膨胀升级流程
阶段一:
无锁 >> 偏向锁:
当一段时间内只有一个线程竞争锁时,为了避免重复消耗资源竞争锁,因此会在锁对象头的mark word中直接记录当前线程ID,并且在线程获取锁时判断当前线程ID是否和锁头中记录的ID相同。
阶段二:
偏向锁 >> 轻量级锁:
当锁被一个线程(t1)获取后,该锁升级为偏向锁。但是一旦有其他线程(t2)尝试获取锁,即锁头中记录的threadId(t1)不等于当前线程Id(t2)时,该线程获取锁失败。此时t2将会自旋几次,尝试CAS替换threadId。如果替换成功,则继续以偏向锁执行。若自旋几次后仍然获取失败,则等到t1执行到安全点后,暂停t1,将检查t1是否已经执行完同步块,如果同步块执行完毕,则将锁头中的线程信息置空,并恢复t1的执行,由t2继续获取锁。如果同步块未执行完毕,则先将锁头markword中保存的线程信息和锁状态copy到当前持有锁的线程(t1)栈中,同时markword中只保存指向该copy副本的地址。并将线程t1恢复执行。同时t2线程也会把markword信息copy到自己的线程栈,并尝试CAS修改markword的指针。
阶段三:
轻量级锁 >> 重量级锁
在阶段二中,t2线程在多次尝试修改markword指针失败后,轻量级锁升级为重量级锁,同时t2线程被挂起。假如t1在升级为重量级锁后才执行完毕,会尝试CAS修改markword指针,但是此时由于是重量级锁,修改会失败,失败后将锁释放,唤醒阻塞的线程,并开始新一轮竞争。重量级锁的竞争是依赖于申请操作系统的mutex互斥量实现的。
小知识:
CAS操作的基准次数可以通过设置JVM参数进行修改。默认10次。
同时JVM对CAS操作次数进行了优化,会基于上次操作消耗的次数会动态修改CAS次数。
假如线程t1在第一次获取锁使用了5次机会,则认为该线程获取锁的几率高,即使第二次获取锁使用了10次机会都没有成功,但是JVM会允许t1多自旋可能3-5次。相反则减少次数。
JUC下的Lock对象和AQS
1. 什么是JUC
JUC全称:java.util.concurrent,即并发编程包。该包下的大部分实现都是由Doug Lee编写的(yyds!)。
2. 什么是Lock对象,如何使用
Lock对象是JUC包下的一个接口,是一组规范。常用的实现类有:
- ReentrantLock
- ReentrantReadWriteLock.ReadLock
- ReentrantReadWriteLock.WriteLock
…
这些锁都是基于AQS实现。
使用方式如下:
Lock lock = new ReentrantLock();
lock.lock();
// 业务代码...
lock.unlock();
3. AQS是什么
AQS全称:AbstractQueuedSynchronizer,是一种队列同步框架,内部使用**同步队列、条件队列、共享和独占状态**。
4. Lock实现原理(NonFairLock为例)
咱们已经知道了lock的使用方法,加锁只需要执行一个lock()
方法,解锁只需要执行unlock()
方法。那么其实现原理可以通过这两个方法去查看。
首先咱们进入到ReentrantLock
类中:
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
public ReentrantLock() {
sync = new NonfairSync();
}
public void lock() {
sync.lock();
}
public void unlock() {
sync.release(1);
}
}
咱们可以看到lock方法和unlock方法都使用了sync对象。并且new ReentrantLock()时创建的是NonFairSync对象。
那么咱们看一看NonFairSync类:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final boolean initialTryLock() {
Thread current = Thread.currentThread();
if (compareAndSetState(0, 1)) { // first attempt is unguarded
setExclusiveOwnerThread(current);
return true;
} else if (getExclusiveOwnerThread() == current) {
int c = getState() + 1;
if (c < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(c);
return true;
} else
return false;
}
/**
* Acquire for non-reentrant cases after initialTryLock prescreen
*/
protected final boolean tryAcquire(int acquires) {
if (getState() == 0 && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
}
再看看Sync类:
abstract static class Sync extends AbstractQueuedSynchronizer {
// 指向队列头节点---volatile修饰
private transient volatile Node head;
// 指向队列尾节点---volatile修饰
private transient volatile Node tail;
// 标识锁被占有/重入的次数---volatile修饰
private volatile int state;
final void lock() {
if (!initialTryLock())
acquire(1);
}
}
由此我们可以发现,当我们创建ReentrantLock对象时,空参构造会创建一个非公平锁sync对象。而如果我们使用有参构造并传入true,则会创建公平锁sync对象。而他们的父类sync类则继承于AbstractQueuedSynchronizer。
我们接着查看一下AbstractQueuedSynchronizer类:(注意该类还继承于AbstractOwnableSynchronizer类)
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
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;
// 线程的状态,也可以理解为信号量,上述4个常量值将被记录在该字段中---volatile修饰
volatile int waitStatus;
// 上一个线程
volatile Node prev;---volatile修饰
// 下一个线程
volatile Node next;---volatile修饰
// 下一个等待的线程(条件队列)
Node nextWaiter;
}
}
在AbstractQueuedSynchronizer类中,我们可以看到还有个叫Node的内部类,这个内部类就是组成队列的节点。其中prev和next代表的是同步队列的上一个节点和下一个节点,其类似于双向链表。nextWaiter代表的是条件队列的下一个线程,类似于单向链表。并且每一个节点中都包含了一个waitStatus,这个waitStatus意味着当前节点中存储的线程的信号量,包含了1、-1、-2、-3四种状态。
说完这些基本属性之后,我们再回到方法调用上进行分析:
以NonFairSync为例,调用的是下列lock方法:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
该方法中,首先进行了compareAndSetState操作,这就是CAS操作,内部调用了Unsafe魔术类,最终通过硬件实现。该操作将尝试将Sync对象中的state加1。
如果替换成功,则代表竞争到锁了,接下来会执行一个方法:
setExclusiveOwnerThread(Thread.currentThread());
该方法从名字上可以看到,是为了给当前sync对象设置独占线程。该独占线程字段在AbstractOwnableSynchronizer类中。
如果没有竞争到锁,将会执行一个重点方法:
else
acquire(1);
一起来看下acquire(1)
的源码:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先进行了!tryAcquire(1)
的判断,由于我们实际的对象是NonFairSync对象,所以去该类中找到tryAcquire的实现:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
然后找到nonfairTryAcquire方法:
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;
}
该方法中,首先获取state值,当state == 0
时,意味着该锁没有被占用,因此,线程将尝试CAS将0更新为1,成功后设置当前线程为独占线程。
当state != 0
时,将首先判断当前线程和独占线程是不是同一个线程,如果是,则进行重入,直接set方法将原来的state替换为nextc,而nextc = c + 1
。最终返回一个true表示获取锁成功,返回false标识获取锁失败。
我们再重新回到这段代码:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我们仅仅执行完第一个判断对不对,这下咱们知道了,!tryAcquire
是为了尝试获取锁的,因此这里尝试获取锁失败后(返回false),会执行第二个判断:
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
在这里我们首先需要看一下队列的初始状态:
即使是空队列,也是有head和tail指针的,但是都指向一个null节点。
在addWaiter(Node.EXCLUSIVE)
方法中,是如下操作:
Node node = new Node(Thread.currentThread(), mode);
创建一个node,并且该node的nextWaiter为null。Node pred = tail;
获取tail指针的指向赋值给pred,此时pred为null。if (pred != null)
判断,如果为pred == null则执行enq(node)
。并返回node节点。其实就是尝试将节点添加到头部或者尾部。
enq(node)
的实现为:
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;
}
}
}
}
- 如果
if (pred != null)
为ture,则执行下面的逻辑:
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
其实就是前面有节点了,先将当前节点的prev指向前一个节点,再将当前节点原子替换到尾部,最后将前一个节点的next指向当前节点,完成双向指向。
我们看看看enq中的死循环debug:
首先是第一次循环
此时发现tail、t、head、node.prev、node.next都是null,且当前节点唯一标志为:Node@1372
此时将走第一个if,来看看第一个循环结束后是什么样的:
此时发现,tail和head都已经有值了,且都是:Node@1376
接下来进入第二次循环:
此时tail和t都有值了,都指向Node@1376。我们再走进else代码块中并且断点到return语句:
此时我们可以看到,传进来的Node@1372的pre指向了新创建的Node@1376,同时1376的next指向了1372,我们的双向链表就完成了。同时还需要注意的是tail和head两个指针,head指向了新创建的空节点Node@1376,tail指向了当前节点Node@1372。
也就是说,即使只有一个节点加入队列,在最终完成后,至少拥有两个节点。
该方法返回的是head指向的头节点。
用图来表示一下:
第一次for循环之前(啥也没有):
第一次for循环后:
第二次for循环,当前节点准备插入
第二次for循环结束:
此时返回head指向的节点Node@1376,但是在我们调用enq方法时并没有使用Node@1376,所以最终返回的是当前节点。
看完了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;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这里我们最容易看到的就是一个死循环。接下来一步步分析:
- 在
final Node p = node.predecessor();
中,返回的是prev,也就是前一个节点。 - 第一个if中,如果prev != head则直接抛出异常。所以一般情况下,这里是相等的(我也没理解哪里会导致不相等…)。
- 紧接着进行
tryAcquire(arg)
的操作,这里其实又在尝试获取锁了。 - 第3步获取失败后将进入
shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()
的判断,当两个判断都ture的情况下,会对线程进行interrupt的操作,暂停线程。第一个判断里,会先判断prev的waitStatus,如果是signal(也就是-1)的情况下直接reture true,因为在当前线程值钱还有线程需要唤醒。接下来判断prev的waitStatus是不是等于取消状态(>0),如果是取消状态,则表明上一个线程执行完了,此时将prev从队列中剔除,并返回false。如果是-2和-3的状态,则执行compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
将上一个线程设定为可以被唤醒的状态并返回false。然后当第一个判断返回ture后,进行parkAndCheckInterrupt()
的判断。在该方法里,进行interrupt操作并检查是否interrupt成功,成功则返回trure。
这就是非公平锁lock()的全部过程。
unlock的过程等有空分析了咱们再补上。
这一章相对比较难,我分析了挺久时间,也在head和tail指针那里迷茫了很久,如果大家有什么不同的意见,或者文章中哪里写的不正确、有遗漏或者不清楚,请各位大佬指正~~
下一章: Java架构师学习之路之并发编程五:Tools与CountDownLatch与Semaphore原理