目前java有两种方式实现线程同步一种是synchronized方式 ,一种是基于AQS框架的方式;
AQS 同步和 synchronized 关键字同步是采用的两种不同的机制。
网上很多都在分析AQS的源码,基本都是在做源码的翻译注释工作,为什么要这样设计,这样设计的优缺点,却并没有讲到;
首先看下两者的优缺点:
synchronized 同步,synchronized 关键字经过编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令,这两个字节码需要关联到一个监视对象,当线程执行 monitorenter 指令时,需要首先获得获得监视对象的锁,这里监视对象锁就是进入同步块的凭证,只有获得了凭证才可以进入同步块,当线程离开同步块时,会执行 monitorexit 指令,释放对象锁,这个过程都是jvm自动为我们完成,并且为了优化synchronized,它还使用了偏向锁,轻量级锁,自旋锁,避免获取不到锁直接阻塞线程,以上方式获取锁失败,最后才升级为重量级锁阻塞线程,缺点是没有读写分离,无法控制释放锁的时机;
AQS同步则大量使用了“CAS更新volatile变量,不成功则重试”的方式来实现状态同步,避免了直接阻塞线程,减少了线程切换的开销,内部使用了变种的CLH队列锁,它是一种基于链表的高性能、公平队列锁,进入这个队列中的每个线程只会监视其前一个节点的状态,来判断自己是可以继续争抢锁还是需要阻塞,相比传统只使用一个变量来对所有线程做同步相比,可以减少多cpu的缓存同步开销(如果不明白为什么会减少缓存同步开销,请先了解一下jvm内存模型及其可见性在jvm中是如何实现的就明白我这句话说的什么了,简单来说就是所有线程监视一个变量时,会导致运行在不同cpu上的线程同时读写这一个变量,就需要额外的同步操作同步cpu缓存中的这个变量;而每个线程监视自己的变量,同一时刻只有一个cpu读写这个变量,就不存在cpu缓存同步开销了),具有更好的效率;AQS阻塞线程使用的是LockSupport实现的,持有锁的线程在释放锁的时候,进入CLH队列前,会先尝试使用CAS操作,去获取锁,无法马上获取到锁的线程会从CLH队列的队尾进入这个队列;如果是公平锁,则会直接进入队列;
AQS的优点:
1. 如果锁能够快速释放,那么AQS同步器能最大限度的避免线程阻塞带来的上下问切换的开销,因为AQL同步器会在线程阻塞前使用cas操作尝试加锁,如果尝试失败,才会进入到阻塞状态,当同步块的执行时间小于线程切换开销,并且锁竞争不激烈的情况下,能大大提升性能;
2. AQS使用了变种的CLH队列,因为队列里的线程只监视其前面节点线程的状态,根据前面节点来判断自己是继续争用锁,还是需要被阻塞; 因为每个线程只会读写前一个线程的状态值,这个值只会被当前线程使用到,相比传统的所有线程都监视读写一个同步变量,CLH可以减少变量的变更带来的多处理器缓存同步的开销;
在 AQS 同步中,使用一个 int 类型的变量 state 来表示当前锁的状态,state在不同的同步方式下,用法不同;以独占式同步(一次只能有一个线程进入同步块)为例,state 的值大于等于0,其中 0 表示当前同步块中没有线程,大于0表示同步块中已经有线程在执行,state等于几代表这个线程加了几次锁。当线程要进入同步块时,需要首先判断 state 的值是否为 0,假设为 0,会尝试将 state 修改为 1,只有修改成功了之后,线程才可以进入同步块。
获取到锁的线程,在执行完同步代码后,释放锁,释放锁会将state最终设置为0,将当前持有所锁的线程设置为null,然后从CLH队列队头获取第一个park的线程,unpark它,被unpark的线程再去争用锁(为什么还要去争用,而不是直接获取锁呢?因为非公平模式下,新到来的线程会尝与被唤醒的线程争用锁);
以下对两种方式涉及到的类做对比:
synchronized 方式 | AQS框架 | |
同步方式 | synchronized 同步块 | ReentrantLock或ReentrantReadAndWriteLock的lock()、tryLock()、lockInterruptibly()和unlock()方法 |
释放锁等待 | Object.wait() | Condition.await() |
唤醒等待线程 | Object.nobify(),Object.notifyAll() | Condition.signal(),Condition.signalAll() |
除此之外AQS框架还有一个CountDownLatch类,用于线程的同步;
AbstractQueuedSynchronizer 有一个同步队列Sync queue,CLH队列的变种,是双向链表,包括head结点和tail结点,head结点主要用作后续的调度,它的作用是用来存放等待获取锁的jied。
除了同步队列,AbstractQueuedSynchronizer 还有Condition queue,不是必须的,它是一个单向链表,只有当使用Condition时,才会存在此单向链表。并且可能会有多个Condition queue,用来存放执行了Condition.await()的节点;
AQS是构建锁或者其他同步组件的基础框架,JUC并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。它是JUC并发包中的核心基础组件。AQS解决了子类实现同步器时涉及当的大量细节问题,例如获取同步状态、FIFO同步队列。基于AQS来构建同步器可以带来很多好处。它不仅能够极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。AQS的使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。
AQS四个重要的成员变量
一、private transient Thread exclusiveOwnerThread;
其父类父类AbstractOwnableSynchronizer的成员变量,代表当前持有独占锁的线程对象,有且只能有一个,其它线程只能进入到等待队列;
二、private volatile int state;
state代表加锁状态,不同的实现类state的作用不一样:
1.对于ReentrantLock是代表独占锁数量,无锁时state=0,有锁时state>0, 第一次加锁时,将state设置为1,持有锁的线程,可以多次加锁,只有持有锁的线程才可以多次加锁,经过判断加锁线程就是当前持有锁的线程时(即exclusiveOwnerThread==Thread.currentThread()),即可加锁,每次加锁都会将state的值+1,state等于几,就代表当前持有锁的线程加了几次锁,解锁时每解一次锁就会将state减1,state减到0后,锁就被释放掉了,其它线程又可以加锁了(所以加了几次锁就要解锁几次);当持有锁的线程释放锁以后,如果是公平锁,则等待队列会获取到加锁权限,在等待队列头部取出第一个线程去获取锁,获取锁的线程会被移出队列,如果是非公平锁,获取到加锁权限的有可能是等待队列中的第一个线程,也有可能是一个新加入的竞争锁的线程;
2.对于ReentrantReadAndWriteLock,state为4字节的整形,它的高位两个字节用来存储读锁数量(共享锁),低位两个字节用来存储写锁数量(独占锁),获取共享锁前要先判断有没有独占锁,如果存在读占锁,并且持有独占锁的线程不是当前线程,则获取锁失败;
3.对于CountDownLatch,state为初始化时传入的CountDown的次数,当state为0时,解除阻塞;
4.对于ThreadPoolExecuter的内部类Worker,state用来标识当前线程是否允许中断,Worker类就是线程池内运行的线程,它实现了Runable接口,继承了AbstractOwnableSynchronizer,state有3个值,-1代表Worker线程刚被实例化,还没运行,0代表线程已经运行,处于无锁状态,1代表线程已经加锁;线程的锁用于线程池在RUNNING和SHUTDOWN状态,Worker线程如果在执行任务,不允许被中断;
三、private transient volatile Node head;
等待获取锁的线程队列头节点
四、private transient volatile Node tail;
等待获取锁的线程的尾节点
如果state>0,其它需要加锁的线程需要等待持有锁的线程释放锁,等待获取锁的线程会放入到一个先进先出的链表队列,head和tail就是等待获取锁的队列的头节点和尾节点,除了头节点外,每个节点都与一个线程绑定(即在创建节点时,设置Node.thread = Thread.currentThread),等待获取锁的线程会通过调用Unsafe.park()方法阻塞等待;
持有锁的线程释放锁时会将state值为0,然后调用Unsafe.unpark()方法取消等待队列中头部的第一个线程的阻塞状态,去获取锁。
等待队列中的线程如果被park阻塞,这时其它线程中断了它,park就会解除阻塞,马上返回(这不是我们需要的,我们需要的是park方法一直阻塞,直到持有锁的线程释放了锁调用了unpark方法取消阻塞的返回),处于中断状态的线程 ,再调用park方法是无法阻塞的,所以这时必须要将线程状态重置为非中断状态,然后再次调用park方法阻塞等待,在线程获取到锁后再调用线程interrupt方法,恢复线程的中断状态;
四个重要的抽象方法
//尝试获取独占锁,锁竞争时不一定能获取成功,成功则返回true,否则返回false
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//尝试释放独占锁,锁竞争时不一定能释放成功,成功则返回true,否则返回false
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
//尝试获取共享锁,锁竞争时不一定能获取成功,成功则返回true,否则返回false
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
//尝试释放共享锁,锁竞争时不一定能释放成功,成功则返回true,否则返回false
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
上面这4个方法我们会发现都会抛出不支持的操作的异常,一般会在它的子类中重新实现这几个方法,原理都是通过调用UNSAFE类的CAS操作,来对state做加1操作(加锁)或减1操作(释放锁),因为是CAS操作,所以有可能失败;
我们一般实现这几个方法里只需要根据情况更改state和exclusiveOwnerThread的值,然后返回操作成功还是失败,同步过程则会由AQS帮我们实现;
加锁操作
public final void acquire(int arg) {
//如果尝试加锁失败,则调用AddWaiter方法将当前线程封装为Node对象,放入到等待加锁的队列的队尾,Node.EXCLUSIVE代表独占锁
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
如果加锁失败,则将线程封装为一个Node对象放入到等待队列;
等待队列是由Node对象组成的一个双向链表,除了链表的头节点,每个Node都持有一个线程,以下是node的数据结构(Node是AQS的内部类);
static final class Node {
/**下面两个常量代表节点的类型**/
//常量:代表Node节点是一个要获取共享锁的节点
static final Node SHARED = new Node();
//常量:代表Node节点是一个要获取独占锁节点
static final Node EXCLUSIVE = null;
/**下面4个常量代表Node节点的等待状态**/
//代表该线程已经出现了异常后正等待获取锁超时,取消等待,这样的节点会被直接移出等待队列,放弃锁的争用;
static final int CANCELLED = 1;
/**代表该节点的后继节点包含的线程需要被执行,执行UNPARK操作,取消阻塞,去争用锁**/
static final int SIGNAL = -1;
// 代表该节点处于Condition.await()状态 在condition队列中
static final int CONDITION = -2;
表示当前场景下后续的acquireShared能够得以执行在condition队列中
static final int PROPAGATE = -3;
//节点的等待状态,值为上面4个常量的值,初始值为0,如果是头节点则也为0,值为0,表示当前节点在sync队列中,等待着获取锁
volatile int waitStatus;
//该节点的前一个节点
volatile Node prev;
//该节点的后一个节点
volatile Node next;
//当前节点封装的等待获取锁的线程,如果该节点是链表的头节点,则Thread为null
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() { // Used to establish initial head or SHARED marker
}
//此节点进入等待获取锁的阻塞队列
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
//此节点会进入Condition.await()队列
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
以下是入队过程:
//将当前线程封装为Node对象,如果队列已经初始化,使用cas操作尝试将节点放入等待队列队尾;如果队列为空或cas入队失败,则执行enq方法,enq方法中如果队列没有初始化,则会初始化队列,然后使用cas操作将对象入队;
private Node addWaiter(Node mode) {
//将当前线程封装为一个node节点
Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failure
//获取尾节点
Node pred = tail; //如果队列已经初始化,则尝试将node放入到队列尾部,
if (pred != null) {
//将当前节点的前继节点指向尾节点
node.prev = pred;
//使用cas操作将尾节点指向当前节点
if (compareAndSetTail(pred, node)) {
//将尾节点的后继节点指向自己
pred.next = node;return node;//如果入队成功,返回Node
}
}
//如果队列没有初始化,或入队失败,则采用自旋锁的方式,将node加进队列;
//CLH队列是懒初始化的
enq(node);return node;
}
//for循环,如果队列为空,则初始化队列(初始化头结点head,然后将tail指向head),否则使用cas将节点入队
//自旋锁,将node加入到队列尾部
private Node enq(final Node node) {
for (;;) {
//获取尾节点
//如果尾节点为null,说明队列为空,且还没有初始化,需要要初始化队列
if (t == null) {
//使用cas方式尝试设置头结点为一个新的Node节点;
if (compareAndSetHead(new Node()))
tail = head; //如果设置头结点成功,则将尾节点指向头节点;
//如果设置失败,说明有其它需要入队等待的线程已经初始化了队列;则继续循环
} else {//如果尾节点不是null 说明队列不为空
//将当前节点的前继节点指向尾节点
node.prev = t;if (compareAndSetTail(t, node)) {//使用cas将尾节点指向当前节点
//将尾节点后继节点指向自己
t.next = node;return t;
}
}
}
}
//进入队列后的操作,如果其前一个节点为头结点,则尝试获取锁,如果获取成功,则将当前节点设置为头结点后,直接返回线程中断状态,否则查看其前一个节点的状态,根据前一个节点的状态决定是否要执行park状态阻塞自己;
// sync队列中的结点在独占且忽略中断的模式下获取(资源)
final boolean acquireQueued(final Node node, int arg) {
// 标志
boolean failed = true;
try {
// 中断标志
boolean interrupted = false;
for (;;) { // 无限循环
// 获取node节点的前驱结点
final Node p = node.predecessor();
// 当前节点的前继节点为头结点,则说明它的前面没有等待节点,于是就尝试去获取锁,如果获取锁成功,则将当前节点设置为头结点;
if (p == head && tryAcquire(arg)) {
// 设置头结点为当前节点
setHead(node);
//设置旧的头结点的后继节点为null,断开链表,帮助垃圾回收器回收该对象,否则垃圾回收器还会扫描其next指向的对象是否也是垃圾对象
p.next = null; // help GCfailed = false; // 设置标志
return interrupted;
}
// 如果当前节点前继节点不是头节点,或者是头结点,但是尝试获取锁失败
if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- CANCELLED,值为1,表示当前的线程取消竞争锁的操作,可能是等待超时的原因,也可能是发生了异常;
- SIGNAL,值为-1,表示当前节点的后继节点包含的线程等待着被unpark,去竞争锁;
- CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;
- PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;
- 值为0,表示当前节点在sync队列中,等待着获取锁。
// 当获取锁失败后,检查并且更新结点状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前驱结点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) // 状态为SIGNAL,为-1
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
// 可以进行park操作
return true;
if (ws > 0) { // 表示前继节点状态为CANCELLED,为1 ,如果前继节点都取消了,那么当前节点就不能被park,需要返回false后,继续执行循环;
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
//将当前节点的前继节点,指向前继节点的前继节点;
} while (pred.waitStatus > 0); // 找到pred结点前面最近的一个状态不为CANCELLED的结点
// 赋值pred结点的next域
pred.next = node;
} else { // 为PROPAGATE -3 或者是0 表示无状态,(为CONDITION -2时,表示此节点在condition queue中),前继节点调用await释放了锁,那么当前节点就不可以被park
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
// 比较并设置前驱结点的状态为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 不能进行park操作
return false;
}
// 这里用了LockSupport.park(this)来挂起线程,然后就停在这里了,等待被唤醒
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
如果当前线程被中断,park方法则会取消阻塞状态,当前线程则继续往下执行,通过 Thread.interrupted()方法清理中断状态,并返回清理前的中断状态,返回后就又到了acquireQueued方法的for循环中;
解锁操作
持有锁的线程执行完同步块后,会执行以下代码释放锁;
/**
* Releases in exclusive mode. Implemented by unblocking one or* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(int arg) {
//执行tryRelease操作释放锁,这个方法需要我们自己去实现
if (tryRelease(arg)) {
//如果释放锁成功,如果head不为null,说明队列已经被初始化,并且head的waitStatus不为0,说明后继节点有被park的等待的,则执行
Node h = head;if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
/**
****/
/** 有两个地方会调用这个方法,一个是释放锁,唤醒头部第一个不为取消节点的线程,第二个是节点取消等待
* 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;
// 如果head节点waitStatus<0, 将其修改为0
if (ws < 0)compareAndSetWaitStatus(node, ws, 0);
/*
* 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.
*/
//如果当前节点没有后继节点,或者当前节点后继节点状态为,状态为CANCELLED 状态,则从队尾往前找,找到队列中最靠近队头的第一个非CANCELLED节点
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;
}
//唤醒线程
LockSupport.unpark(s.thread);
}
取消等待
/**
* Cancels an ongoing attempt to acquire.
*
* @param node the node
*/
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
//将当前节点线程值为null;
node.thread = null;
// Skip cancelled predecessors
//缓存当前节点的前置节点
Node pred = node.prev;
//如果前置节点状态是CANCELLED,那么则将当前节点的前置节点设置为前面第一个不是CANCELLED状态的节点,并将pred指向这个不为CANCELLED状态的节点;
while (pred.waitStatus > 0)node.prev = pred = pred.prev;
//这时候pred指向的是当前节点前面第一个状态不为CANCELLED的节点
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
//缓存前置节点的下一个节点,如果当前节点前置节点不是CANCELLED状态的,那么这个节点就是当前节点自己
Node predNext = pred.next;//将当前节点设置为CANCELLED状态
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
//如果当前节点是尾节点,并设置其前置节点(前面第一个不为CANCELLED状态的节点)设置为尾节点成功
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
// 设置pred结点的next节点为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;
//如果同时满足以下三个条件
//1.如果前置节点不是头节点并且
//2.前置节点状态为SIGNAL ,或者 1.前置节点状态不为CANCELLED 2. 设置前置节点状态为SIGNAL成功 (也就是说必须让前置节点状态为SIGNAL)
//前置节点的线程不为null
if (pred != head &&((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
//缓存当前节点下一个节点
Node next = node.next;
//如果当前节点的后继节点不为null,并且状态不为CANCELLED ,则将前继节点的next指向后继节点,断开前面的链表;
if (next != null && next.waitStatus <= 0)compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
//将当前节点的下一个节点设置为自己
node.next = node; // help GC
}
}
未完待续;