Java并发包当中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、独占获取、共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronize简称AQS。AQS是一个抽象同步框架,可以用来实现一个依赖状态的同步器。
AQS核心有三点:
AQS有2个重要组成部分:
1、state同步状态,int类型
当state的值等于0的时候,表明没有线程占用它。当state>0时,表示有线程在占用它,新来的线程需要被加入到同步队列中。
state的访问方式有三种:
getState()
setState(int newState)
compareAndSetState(int expect, int update)
private volatile int state;
//返回当前的同步状态
protected final int getState() {
return state;
}
//设置同步状态
protected final void setState(int newState) {
state = newState;
}
//以CAS的方式设置当前状态
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
2、一个同步队列
AQS维持着一个同步队列,它是一个双向链表,每一个节点都是一个Node,代表着一个线程。
static final class Node {
//标记,表示节点正在共享模式下等待
static final Node SHARED = new Node();
//标记,表示节点正在独占模式下等待
static final Node EXCLUSIVE = null;
//waitStatus值,表明线程已取消
static final int CANCELLED = 1;
//waitStatus值,表明此节点释放或取消,当前节点需要唤醒下一个节点。
static final int SIGNAL = -1;
//waitStatus值,表明当前节点位于条件队列中,它不会作为同步队列的节点。
static final int CONDITION = -2;
//waitStatus值,表明下一个共享式同步状态将被无条件传播
static final int PROPAGATE = -3;
volatile int waitStatus;
//前驱节点
volatile Node prev;
//后继节点
volatile Node next;
//此节点持有的线程
volatile Thread thread;
//等待队列中的后继节点,如果当前线程是共享的,那么该字段是SHARED
Node nextWaiter;
//如果节点在共享模式下等待,则返回true
final boolean isShared() {
return nextWaiter == SHARED;
}
//返回上一个节点,如果为空,则抛出异常
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
//用于建立初始头部或共享标记
Node() {
}
//被addWaiter使用
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
//被Condition使用
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
同步队列由一个个节点构成,同步器拥有首节点和尾节点,假设有一个线程获取了锁,在这个线程没有释放锁之前,其他线程都将作为一个Node被加入到同步队列的尾部,这时候需要通过CAS保证其原子性,线程需要传递当前线程认为的尾节点和当前节点,只有设置成功后,当前节点才会被加入到队列的尾部。
节点加入到同步队列:
队列的首节点有且只有一个,因此不需要通过CAS去设置首节点,当队头释放锁后,会唤醒后面的节点,并出队,后继节点将会在获取同步状态成功后将自己设置为首节点。
首节点的变化:
AQS定义了两种同步状态,一种是独占式同步状态,一种是共享式同步状态。
独占式:只有单个线程能够成功获得同步状态并执行
共享式:多个线程可以成功获得同步状态并执行
独占式获取同步状态的方法是acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//尝试以独占模式获取
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//创建节点并通过enq方法加入到同步队列尾部
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;
}
}
//以自旋的方式通过CAS将当前节点加入到队尾
enq(node);
return node;
}
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);
}
}
首先该方法会通过tryAcquire方法尝试获取同步状态,如果获取失败,就会调用addWaiter方法构造一个独占式(Node.EXCLUSIVE)的节点,并利用CAP方式不断自旋,直到将该节点加入到同步队列尾部,通过acquireQueued方法进行自旋,直到获取同步状态。
同步器通过release方法进行同步状态的释放,该方法会唤醒头结点的后继节点线程。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
共享式同步状态的获取的方法是acquireShare。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
//参数获取锁
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
通过tryAcquireShared方法尝试获取锁,如果返回值大不小于0,表示能够获得锁,如果返回值小于0,就需要通过doAcquireShared不断自旋获得锁。
共享式同步状态的释放同步状态的方法是releaseShared。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
作用:LockSupport可以阻塞一个线程、唤醒一个线程、它是构建同步组件的基础工具。
它提供了一系列的静态方法包括park开头的方法和unpark(Thread thread)方法分别用来阻塞和唤醒一个线程。
AQS的一个内部类ConditionObject是Condition的一个实现类,每一个Condition包含一个等待队列,这个等待队列是一个单链表,在每一个Condition中都会有两个指针,一个指向头节点,一个指向尾节点。
一个锁可以包含有多个Condition,每一个Condition上面可能都会有多个线程进入等待状态。
当一个线程调用await方法时,节点将从同步队列移动到等待队列中。
当一个线程调用signal方法时,当前Condition中的同步节点的线程就会被唤醒,该线程就会尝试去竞争锁,如果这个锁被其他线程所持有,那么当前线程就会再次被加入到同步队列的尾部。
我们创建一个自定义锁的类MyLock,实现Lock接口,在类中定义内部类Sync继承抽象类AbstractQueuedSynchronizer,我们需要重写其tryAcquire方法和tryRelease方法,因为这两个方法中是直接抛出了异常,同时,为了实现newCondition方法,我们直接使用抽象类中已经实现了ConditionObject类
public class MyLock implements Lock {
//实现独占锁功能
private class Sync extends AbstractQueuedSynchronizer {
//获取锁
@Override
protected boolean tryAcquire(int arg) {
int state = getState();
if (state == 0) { //证明能够获得锁
//利用CAS原理修改state
if(compareAndSetState(0, arg)) {
//设置当前线程占有资源
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}
return false;
}
//释放锁
@Override
protected boolean tryRelease(int arg) {
int state = getState() - arg;
if (state == 0) { //判断释放后state是否为0
setExclusiveOwnerThread(null);
setState(state);//设置state为0
return true;
}
//存在线程安全吗?no,因为释放锁表明你已经独占了这个锁
setState(state);//设置当前state的值
return false;
}
public Condition newConditionObject() {
return new ConditionObject();
}
}
private Sync sync = new Sync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
//以中断的方式获得锁
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newConditionObject();
}
}
测试我们所实现的锁
public class Example01 {
private MyLock lock = new MyLock();
public static void main(String[] args) {
final Example01 ex = new Example01();
//创建两个线程,执行out方法
new Thread(()->{
ex.out();
}).start();
new Thread(()->{
ex.out();
}).start();
}
public void out() {
lock.lock();
System.out.println("进入");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();//需要使用finally
System.out.println("出去");
}
}
好像可以完成我们加锁的工作,不过,我们所实现的锁并不是可重入锁,下面的代码就会出现问题。
我们创建两个方法
public void method1() {
lock.lock();
System.out.println("method1");
method2();
lock.unlock();//需要使用finally
}
public void method2() {
lock.lock();
System.out.println("method2");
lock.unlock();//需要使用finally
}
直接使用一个线程调用method1
new Thread(()->{
ex.method1();
}).start();
我们发现,线程进入了method1,但是却进入不了method2。
这是因为我们获取锁时重写的tryAcquire方法,将state从0变为了arg,当我们再次访问tryAcquire方法,state不等于0,表明有线程占有,其直接给我们返回了false,当前线程正在等待自己释放锁。
我们需要对tryAcquire方法进行修改,使其支持可重入
protected boolean tryAcquire(int arg) {
int state = getState();
if (state == 0) { //证明能够获得锁
//利用CAS原理修改state
if(compareAndSetState(0, arg)) {
//设置当前线程占有资源
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
} else if(getExclusiveOwnerThread() == Thread.currentThread()) {
//判断进入线程是不是还是当前线程
setState(getState() + arg);//不存在线程安全
return true;
}
return false;
}
当state不等于0时,我们还需要判断,当前所占用锁的线程是否与现在进行tryAcquire调用的线程是同一个线程,如果是的话,那么返回为true,同时其state也要在原来state的基础上增加arg。
对于释放锁的方法,我们并不需要去修改。