Java并发编程——AQS

本文转至 链接太长 ,如果页面失效,请直接关注微信公众号 “小孩子4919 我们都是小青蛙 查找文章—java并发性能(五)之牛逼的AQS(上)。特此申明!

 

我们之前都是直接在线程中使用各种同步机制,我们可以把相关的同步问题抽象出来单独定义一些工具,这些工具可以在合适的并发场景下复用,下边我们看如何自定义我们自己的同步工具

设计java的大叔们为了我们方便的自定义各种同步工具,为我们提供了大杀器AbstractQueuedSynchronizer类,这是一个抽象类,以下我们会简称AQS,翻译成中文就是抽象队列同步器。这家伙老有用了,封装了各种底层的同步细节,我们程序员想自定义自己的同步工具的时候,只需要定义这个类的子类并覆盖它提供的一些方法就好了。我们前边用到的显式锁ReentrantLock就是借助了AQS的神力实现的,现在马上来看看这个类的实现原理以及如何使用它自定义同步工具。

 

1.同步状态

AQS中维护了一个名叫state的字段,是由volatile修饰的,它就是所谓的同步状态

private volatile int state;

并且提供了几个访问这个字段的方法:

方法名描述
protected   final   int   getState( )获取state的值
protected   final   void   setState( int newState )设置state的值
protected   final   boolean   compareAndSetState( int  expect, int  update )使用CAS方式更新state的值

可以看到这几个方法都是final修饰的,说明子类中无法重写它们。另外它们都是protected修饰的,说明只能在子类中使用这些方法。

在一些线程协调的场景中,一个线程在进行某些操作的时候其他的线程都不能执行该操作,比如持有锁时的操作,在同一时刻只能有一个线程持有锁,我们把这种情景称为独占模式;在另一些线程协调的场景中,可以同时允许多个线程同时进行某种操作,我们把这种情景称为共享模式

我们可以通过修改state字段代表的同步状态来实现多线程的独占模式或者共享模式

比如在独占模式,我们可以把state的初始值设置成0,每当某个线程要进行某项独占操作前,都需要判断state的值是不是0,如果不是0的话意味着别的线程已经进入该操作,则本线程需要阻塞等待;如果是0的话就把state的值设置成1,自己进入该操作。这个先判断再设置的过程我们可以通过CAS操作保证原子性,我们把这个过程称为尝试获取同步状态。如果一个线程获取同步状态成功了,那么在另一个线程尝试获取同步状态的时候发现state的值已经是1了就一直阻塞等待,直到获取同步状态成功的线程执行完了需要同步的操作后释放同步状态,也就是把state的值设置为0,并通知后续等待的线程。

共享模式的道理也差不多,比如说某项操作我们允许10个线程同时进行,超过这个数量的线程就需要阻塞等待。那么我们就可以把state的初始值设置为10,一个线程尝试获取同步状态的意思就是先判断state的值是否大于0,如果不大于0的话意味着当前已经有10个线程在同时执行该操作,本线程需要阻塞等待;如果state的值大于0,那么可以把state的值减1后进入该操作,每当一个线程完成操作的时候需要释放同步状态,也就是把state的值加1,并通知后续等待的线程。

所以对于我们自定义的同步工具来说,需要自定义获取同步状态与释放同步状态的方式,而AQS中的几个方法正是用来做这个事儿的:

方法名描述
protected  boolean  tryAcquire( int  arg )独占式的获取同步状态,获取成功返回true,否则false
protected  boolean  tryRelease( int  arg )独占式的释放同步状态,释放成功返回true,否则false
protected  boolean  tryAcquireShared( int  arg )共享式的获取同步状态,获取成功返回true,否则false
protected  boolean  tryAcquireShared( int  arg )共享式的释放同步状态,释放成功返回true,否则false
protected  boolean  isHeldExclusively(  )

在独占模式下,如果当前线程已经获取到同步状态,则返回true;

其他情况则返回fasle

我们说AQS是一个抽象类,我们以tryAcquire为例看看它在AQS中的实现:

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

我的天,竟然只是抛出个异常,这不科学。是的,在AQS中的确没有实现这个方法,不同的同步工具针对的具体并发场景不同,所以如何获取同步状态和如何释放同步状态是需要我们在自定义的AQS子类中实现的,如果我们自定义的同步工具需要在独占模式下工作,那么我们就重写tryAcquiretryReleaseisHeldExclusively方法,如果是在共享模式下工作,那么我们就重写tryAcquireSharedtryReleaseShared方法。比如在独占模式下我们可以这样定义一个AQS子类:

public class Sync extends AbstractQueuedSynchronizer {

    @Override
    protected boolean tryAcquire(int arg) {
        return compareAndSetState(0, 1);
    }

    @Override
    protected boolean tryRelease(int arg) {
        setState(0);
        return true;
    }

    @Override
    protected boolean isHeldExclusively() {
        return getState() == 1;
    }
}

tryAcquire表示尝试获取同步状态,我们这里定义了一种极其简单的获取方式,就是使用CAS的方式把state的值设置成1,如果成功则返回true,失败则返回falsetryRelease表示尝试释放同步状态,这里同样采用了一种极其简单的释放算法,直接把state的值设置成0就好了。isHeldExclusively就表示当前是否有线程已经获取到了同步状态。如果你有更复杂的场景,可以使用更复杂的获取和释放算法来重写这些方法。

通过上边的唠叨,我们只是了解了啥是个同步状态,学会了如何通过继承AQS来自定义独占模式和共享模式下获取和释放同步状态的各种方法,但是你会惊讶的发现会了这些仍然没有什么卵用。我们期望的效果是一个线程获取同步状态成功会立即返回true,并继续执行某些需要同步的操作,在操作完成后释放同步状态,如果获取同步状态失败的话会立即返回false,并且进入阻塞等待状态,那线程是怎么进入等待状态的呢?不要走开,下节更精彩。

 

2. 同步队列

AQS中还维护了一个所谓的同步队列,这个队列的节点类被定义成了一个静态内部类,它的主要字段如下

static final class Node {
    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter;

    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
}

AQS中定义一个头节点引用,一个尾节点引用

private transient volatile Node head;
private transient volatile Node tail;

通过这两个节点就可以控制到这个队列,也就是说可以在队列上进行诸如插入和移除操作。可以看到Node类中有一个Thread类型的字段,这表明每一个节点都代表一个线程我们期望的效果是当一个线程获取同步状态失败之后,就把这个线程阻塞并包装成Node节点插入到这个同步队列中,当获取同步状态成功的线程释放同步状态的时候,同时通知在队列中下一个未获取到同步状态的节点,让该节点的线程再次去获取同步状态。

这个节点类的其他字段的意思我们之后遇到会详细唠叨,我们先看一下独占模式共享模式下在什么情况下会往这个同步队列里添加节点,什么情况下会从它里边移除节点,以及线程阻塞和恢复的实现细节。

 

3. 独占式同步状态获取与释放

独占模式下,同一个时刻只能有一个线程获取到同步状态,其他同时去获取同步状态的线程会被包装成一个Node节点放到同步队列中,直到获取到同步状态的线程释放掉同步状态才能继续执行。初始状态的同步队列是一个空队列,里边一个节点也没有,就长这样:

 

接下来我们就要详细看一下获取同步状态失败的线程是如何被包装成Node节点插入到队列中同时阻塞等待的。

前边说过,获取和释放同步状态的方式是由我们自定义的,在独占模式需要我们定义AQS的子类并且重写下边这些方法:

protected boolean tryAcquire(int arg)
protected boolean tryRelease(int arg)
protected boolean isHeldExclusively()

在定义了这些方法后,谁去调用它们呢?AQS里定义了一些调用它们的方法,这些方法都是由public final修饰的:

忽然摆了这么多方法可能有点突兀哈,我们先看一下acquire方法的源代码:

public final void acquire(int arg) {

    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

代码显示acquire方法实际上是通过tryAcquire方法来获取同步状态的,如果tryAcquire方法返回true则结束,如果返回false则继续执行。这个tryAcquire方法就是我们自己规定的获取同步状态的方式。假设现在有一个线程已经获取到了同步状态,而线程t1同时调用tryAcquire方法尝试获取同步状态,结果就是获取失败,会先执行addWaiter方法,我们一起来看一下这个方法:

private Node addWaiter(Node mode) {

    Node node = new Node(Thread.currentThread(), mode);  //构造一个新节点
    Node pred = tail;
    if (pred != null) { //尾节点不为空,插入到队列最后
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {       //更新tail,并且把新节点插入到列表最后
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) {    //tail节点为空,初始化队列
            if (compareAndSetHead(new Node()))  //设置head节点
                tail = head;
        } else {    //tail节点不为空,开始真正插入节点
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

可以看到,这个addWaiter方法就是向队列中插入节点的方法。首先会构造一个Node节点,假设这个节点为节点1,它的thread字段就是当前线程t2,这个节点被刚刚创建出来的样子就是这样:

然后我们再分析一下具体的插入过程。如果tail节点不为空,直接把新节点插入到队列后边就返回了,如果tail节点为空,调用enq方法先初始化一下headtail节点之后再把新节点插入到队列后边。enq方法的这几行初始化队列的代码需要特别注意:

if (t == null) {    //tail节点为空,初始化队列
    if (compareAndSetHead(new Node()))  //设置head节点
        tail = head;
} else {
    //真正插入节点的过程
}

也就是说在队列为空的时候会先让headtail引用指向同一个节点后再进行插入操作,而这个节点竟然就是简简单单的new Node(),真是没有任何添加剂呀~ 我们先把这个节点称为0号节点吧,这个节点的任何一个字段都没有被赋值,所以在第一次节点插入后,队列其实长这样:

其中的节点1才是我们真正插入的节点,代表获取同步状态失败的线程,0号节点是在初始化过程中创建的,我们之后再看它有什么用。

addWaiter方法调用完会返回新插入的那个节点,也就是节点1acquire方法会接着调用acquireQueued方法:

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尝试获取同步状态,这个主要是怕获取同步状态的线程很快就把同步状态给释放了,所以在当前线程阻塞之前抱着侥幸的心理再试试能不能成功获取到同步状态,如果侥幸可以获取,那就调用setHead方法把头节点换成自己:

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

同时把本Node节点的thread字段设置为null,意味着自己成为了0号节点

如果当前Node节点不是头节点或者已经获取到同步状态的线程并没有释放同步状态,那就乖乖的往下执行shouldParkAfterFailedAcquire方法:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;   //前一个节点的状态
    if (ws == Node.SIGNAL)  //Node.SIGNAL的值是-1
        return true;
    if (ws > 0) {   //当前线程已被取消操作,把处于取消状态的节点都移除掉
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {    //设置前一个节点的状态为-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

这个方法是对Node节点中的waitStatus的各种操作。如果当前节点的前一个节点的waitStatusNode.SIGNAL,也就是-1,那么意味着当前节点可以被阻塞,如果前一个节点的waitStatus大于0,意味着该节点代表的线程已经被取消操作了,需要把所有waitStatus大于0的节点都移除掉,如果前一个节点的waitStatus既不是-1,也不大于0,就把如果前一个节点的waitStatus设置成Node.SIGNAL。我们知道Node类里定义了一些代表waitStatus的静态变量,我们来看看waitStatus的各个值都是什么意思吧:

 

未完待续!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值