【Java并发学习】深入浅出AbstractQueuedSynchronizer

1 从ReentrantLock说起

        Java从语言级别支持多线程,用户可以很容易的编写多线程的程序。需要注意的是,当多个线程访问共享资源时,为了保证数据的正确性,需要进行同步。Java提供了synchronized关键字(内置锁),synchronized块和synchronized方法可以用来进行同步。与此同时,java.util.concurrent包中还提供了ReentrantLock,它实现了Lock接口,提供了与synchronized相同的互斥性和内存可见性。

        那么ReentrantLock是如何实现的?通过源码来看一下:

public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;
  
    abstract static class Sync extends AbstractQueuedSynchronizer {
        ...
    }
  
    public void lock() {
        sync.lock();
    }
     
    public void unlock() {
        sync.release(1);
    }
}

        可以看到,ReentrantLock有Sync类型的成员变量sync,而lock()方法和release()方法的实现也是通过委托给sync的同名方法来实现的。Sync继承了一个名为AbstracQueueSynchronizer的类(简称AQS),AQS就是ReentrantLock实现的基石。

2 AQS简介

        AQS为实现依赖于先进先出(FIFO)等待队列的同步器提供了一个框架,它的设计目标是成为那些依靠单个int值来表示状态的大多数同步器的基石。

        同步器有两种方法:至少一个acquire操作--它会使调用线程阻塞除非/直到同步状态允许该线程继续执行。至少一个release操作--它对同步状态的改变可能会让一个或多个被阻塞的线程解除阻塞。

java.util.concurrent包没有为同步器定义一个单一的统一接口。有些同步器通过公共接口定义(例如Lock),但是其他的只包括专门的版本。所以,不同的类的acquire和release操作的方法名不尽相同。例如,Lock.lock,Semaphore.acquire,CountDownLatch.await以及FutureTask.get对应的都是框架中的acquire操作。

        对于是否只维护独享状态这一点来说,不同的同步器之间可能存在区别。对于只维护独享状态的同步器来说,同一时刻只能有一个线程继续通过阻塞点。而对于支持共享状态的同步器来说,可以有多个线程同时执行。一般的锁类当然只维护独享状态,但是诸如信号量这样的同步器,可以被指定数量的线程同时获取。基于这一点,AQS同时同时支持两种模式。

3 AQS设计要点和实现原理

  • 原子地管理同步状态

    使用一个变量来表示同步器的同步状态。例如,可重入锁处于锁定状态还是释放状态(0表示未锁定,n表示重入的次数)。此外,由于state是会被多线程并发访问的共享数据,因此对state的操作需要是原子的。

  • 线程的阻塞和唤醒

    多线程并发访问时,如果产生争用,对于获取操作失败的线程,要进行阻塞。而当持有锁的线程释放锁后,需要把阻塞的线程唤醒,继续执行。

  • 队列的维护
    对于进入阻塞状态的线程,需要使用一个队列进行保存,以便后续的唤醒操作。因为该队列也会被并发访问,因此它需要是线程安全的。

3.1 同步状态

// 同步状态
private volatile int state;
 
// 返回同步状态的当前值,此操作有volatile读的内存语义 
protected final int getState() {
    return state;
}
 
// 设置同步状态的值,此操作有volatile写的内存语义
protected final void setState(int newState) {
    state = newState;
}
 
// 如果同步状态的当前值和预期值相同,就原子地将同步状态的值设为指定的值
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
    AbstractQueuedSynchronizer 使用一个32位int来维护同步状态,并暴露了getState,setState,和compareAndSetState操作来访问和更新这个状态。getState和setState方法分别有volatile读和volatile写的内存语义,而compareAndSetState是一个compare-and-swap(CAS)操作。

3.2 阻塞与唤醒

       AQS使用LockSupport的park和unpark方法实现线程的阻塞和唤醒。

       LockSupport.park方法阻塞当前线程,除非/直到LockSupport.unpark被执行。对unpark的调用不会被计数,所以park之前的多个unpark只会解除阻塞一个park。此外,操作是针对每个线程的,而非每个同步器。一个线程在一个新的同步器上调用park可能会立即返回–因为之前可能曾经对这个线程调用过unpark。如果没有调用过unpark,则下一次的park调用会阻塞。

        park操作支持可选的相对或绝对的过期时间,并且与Thread.interrupt相集成---中断一个线程时,会unpark该线程。

3.3 队列

     队列的维护是AQS的核心。AQS使用的是先进先出(FIFO)的队列,所以它不支持基于优先级的同步。该队列是CLH队列的变体,它是一个非阻塞的数据结构,队列自身不需要使用更底层的锁来构建。


       Node类的成员:

  • prev:指向前驱节点
  • next:指向后继节点
  • waitStatus:节点状态
  • thread:将节点入队的线程
  • nextWaiter:指向条件队列上的下一个节点

       

       阻塞线程队列和条件等待队列中的节点类型都是Node(prev和next被阻塞线程队列使用,而nextWaiter被条件等待队列使用),当执行signal操作时,条件队列中的节点会被移动到阻塞线程队列中。

入队操作:

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) {
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
       入队时使用CAS操作, 即便有争用,也总有一个线程会成功插入,所以一定会有进展。

出队操作:

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

      出队时,只需将head指向当前线程所对应的节点,并把thread和prev置null即可。 

waitStatus的取值:

  • CANCELLED:1 已取消,等待超时或被中断

  • SIGNAL:-1 后继节点被阻塞,需要唤醒

  • CONDITION:-2 节点在条件队列中

  • PROPAGATE:-3 保证共享模式下release操作能够向后传播下去

  • 默认值:0

       节点状态默认值是0,当新节点入队时,在调用park之前,要保证前一个节点的waitStatus被设置成了SIGNAL。当节点取消对锁的等待,或者已经等待超时了,则waitStatus的值会被设为CANCELLED。条件队列中的节点的waitStatus是CONDITION。共享模式下,release操作时可能会把waitStatus设置为PROPAGATE。

独占模式下的acquire&release

acquire流程图

release流程图

4 与内置锁的比较

相比于Java内置的同步机制,AQS有一些优势:

  • 可中断的等待
  • 提供了除互斥锁之外的其他同步器
  • 便于扩展
  • 提供了公平性的选择

5 其他

模板方法模式

AQS定义了tryAcquire,tryRelease等protect方法,子类根据需要实现对应的方法,而AQS的acquire,release方法会分别调用tryAcquire,tryRelease,根据他们的返回值进行相应的处理。

方法空实现

AQS支持独占模式和共享模式,一般子类只需实现一种即可,AQS为这些需要子类实现的方法给出了空实现:

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
  
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}
  
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}
  
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}
        这样一来,需要实现tryAcquire和tryRelease的子类就不需要自己实现tryAcquireShared和tryReleaseShared了。

功能委托

        ReentrantLock并没有直接继承AQS,而是通过内部类Sync继承AQS,再把lock和unlock的功能委托给Sync,这样减少了ReentrantLock和AQS的耦合。

        其他同步器,例如,CountDownLatch,Semaphore,FutureTask也都是类似的。

6 结语

       AQS作为java.util.concurrent包中的核心类,对同步器的共性进行了抽象,为各种同步器的实现提供了基础。它提供了一些内置同步机制所不具有的特性,当内置同步机制不能满足需求时,可以使用基于AQS的同步器作为必要的补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值