【Java并发编程 第五章】并发核心AQS与Lock接口

79 阅读11分钟

同步器

Lock接口

Lock简介

锁是用来控制多个线程访问共享资源的方式,一般来说,一个所能防止多个线程同时访问共享资源(但是有些时候锁可以运行多个线程并发的访问共享资源,比如读写锁)。在Lock接口出现之前,Java程序主要依靠synchronized关键字来实现锁功能,它提供了与synchronized关键字类似的同步功能。虽然它缺少了隐式获取和释放锁的便捷性,但是却拥有更加灵活的获取锁特性。

Lock接口使用示例:

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();
}

与synchronized相比Lock特性

特性描述
尝试非阻塞获取锁当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取到锁
能被中断地获取锁与synchronized不同,获取到锁的线程能被中断,当获取到锁的线程被中断时,中断异常将被抛出,同时锁被释放
超时获取锁在指定的截止时间之前获得锁,如果截止时间到了仍然没有获取到锁,则立即返回

Lock 主要API

public interface Lock {

    // 获取锁。
    // 如果锁不可用,那么当前线程将被禁用以进行线程调度,并处于休眠状态,直到获得锁为止。
    void lock();

    // 可中断地获取锁,和 lock() 方法不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程。
    void lockInterruptibly() throws InterruptedException;

    // 尝试非阻塞的获取锁,该方法调用后立即返回。如果能获取到锁,则返回 true,反之 false。
    boolean tryLock();

    // 超时获取锁,当前线程在调用后以下三种情况会返回:
    // 1.当前在超时时间内获取到了锁。
    // 2.当前线程在超时时间内被中断。
    // 3.超时时间结束,返回 false。
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    // 释放锁。
    void unlock();

    // 在等待条件之前,锁必须由当前线程持有。对Condition.await()的调用将在等待之前自动释放锁,并在等待返回之前重新获取锁。
    Condition newCondition();
}

AQS

AQS简介

抽象队列同步器 Abstarct Queued Synchronizer(以下简称同步器),是用来构建锁或其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者 (Doug Lea)期望它能够成为实现大部分同步需求的基础。

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。

同步器是实现锁的(或其他同步组件)的关键,在锁的实现中聚合了同步器,利用同步器实现锁或其他同步组件的语义。可这样理解二者之间的关系:

  • 锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏具体细节。
  • 同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待与唤醒等底层操作。

锁和同步器很好的隔离了使用者和实现者所需要关注的领域。

AQS API

同步器是基于模板模式进行设计的。

重写同步器指定的方法时,需要调用同步器提供的三个方法来访问或修改同步状态。

  • getState():获取同步状态。
  • setState(int newState):设置当前状态。
  • compareAndSetState(int expect, int update):使用CAS设置当前状态,能够保证原子性。

抽象方法

方法名称描述
protected boolean tryAcquire(int arg)独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态。
protected boolean tryRelese(int arg)独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态。
protected int tryAcquireShared(int arg)共享式获取同步状态,返回大于0的值,表示获取成功,反之失败。
protected int tryReleaseShared(int arg)共享式释放同步状态。
protected boolean isHeldExclusively()当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占。

模板方法

同步器的设计是基于模板方法模式,该模式是基于继承的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码

  • 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法
  • 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,这些模板方法会调用使用者重写的方法

AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:

  • 默认情况下,每个方法都抛出 UnsupportedOperationException
  • 这些方法的实现必须是内部线程安全的
  • AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用
方法名称描述
void acquire(int arg)独占式获取同步状态,如果当前线程获取同步在成功,则由此方法返回;否则,将会进入同步队列等待,该方法会调用重写的tryAcquire(int arg)方法。
void acquireInterruptibly(int arg)acquire(int arg)相同,但是此方法会响应中断,当前线程未获取到同步状态而进入同步队列,如果当前线程被中断,则该方法会抛出InterruptedException并返回。
boolean tryAcquireNanos(int arg, long nanos)acquireInterruptibly(int arg)的基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,那么将会返回false,反之true。
void acquireShared(int arg)共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式获取的主要区别是同一时刻可以由多个线程获取到同步状态。
boolean acquireSharedInterruptibly(int arg)acquireShared(int arg)相同,但会响应中断。
boolean tryAcquireSharedNanos(int arg, long nanos)boolean acquireSharedInterruptibly(int arg)的基础上增加了超时限制。
boolean release(int arg)独占式的释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒。
boolean releaseShared(int arg)共享式的释放同步状态。
Collection<Thread> getQueuedThreads()获取等待在同步队列上的线程集合。

以上方法可分为三类:

  • 独占式获取与释放同步状态。
  • 共享式获取与释放同步状态。
  • 查询同步队列中的等待线程情况。

利用AQS实现一个简易的独占互斥锁

独占锁:同一时刻只能有一个线程获取到锁,其他线程只能处于同步队列中等待。当获取锁的线程释放了锁,其他线程才能获取锁。

public class Mutex implements Lock {

    // 当前互斥锁的同步器
    private static class Sync extends AbstractQueuedSynchronizer {

        // 是否处于独占状态
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // 1:获取锁
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // 释放锁
        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }

            if (getExclusiveOwnerThread() == Thread.currentThread()) {
                setExclusiveOwnerThread(null);
                setState(0);
                return true;
            }

            return false;
        }

        // 条件变量
        Condition newCondition() {
            return new ConditionObject();
        }
    }

    private final Sync sync = new Sync();

    // 仅需将操作代理到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.newCondition();
    }
}

核心思想

AQS:AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架,许多同步类实现都依赖于该同步器

AQS 用状态属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁

  • 独占模式是只有一个线程能够访问资源,如 ReentrantLock
  • 共享模式允许多个线程访问资源,如 Semaphore,ReentrantReadWriteLock 是组合式

AQS 核心思想:

  • 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置锁定状态

  • 请求的共享资源被占用,AQS 用队列实现线程阻塞等待以及被唤醒时锁分配的机制,将暂时获取不到锁的线程加入到队列中

    CLH 是一种基于单向链表的高性能、公平的自旋锁,AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配

JUC-AQS原理图.png

设计原理

设计原理:

  • 获取锁:

    while(state 状态不允许获取) {	// tryAcquire(arg)
        if(队列中还没有此线程) {
            入队并阻塞 park
        }
    }
    当前线程出队
    
  • 释放锁:

    if(state 状态允许了) {	// tryRelease(arg)
    	恢复阻塞的线程(s) unpark
    }
    

AbstractQueuedSynchronizer 中 state 设计:

  • state 使用了 32bit int 来维护同步状态,独占模式 0 表示未加锁状态,大于 0 表示已经加锁状态

    private volatile int state;
    
  • state 使用 volatile 修饰配合 cas 保证其修改时的原子性

  • state 表示线程重入的次数(独占模式)或者剩余许可数(共享模式)

  • state API:

    • protected final int getState():获取 state 状态
    • protected final void setState(int newState):设置 state 状态
    • protected final boolean compareAndSetState(int expect,int update)CAS 安全设置 state

封装线程的 Node 节点中 waitstate 设计:

  • 使用 volatile 修饰配合 CAS 保证其修改时的原子性

  • 表示 Node 节点的状态,有以下几种状态:

    // 默认为 0
    volatile int waitStatus;
    // 由于超时或中断,此节点被取消,不会再改变状态
    static final int CANCELLED =  1;
    // 此节点后面的节点已(或即将)被阻止(通过park),【当前节点在释放或取消时必须唤醒后面的节点】
    static final int SIGNAL    = -1;
    // 此节点当前在条件队列中
    static final int CONDITION = -2;
    // 将releaseShared传播到其他节点
    static final int PROPAGATE = -3;
    

阻塞恢复设计:

  • 使用 park & unpark 来实现线程的暂停和恢复,因为命令的先后顺序不影响结果
  • park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
  • park 线程可以通过 interrupt 打断

队列设计:

  • 使用了 FIFO 先入先出队列,并不支持优先级队列,同步队列是双向链表,便于出队入队

    // 头结点,指向哑元节点
    private transient volatile Node head;
    // 阻塞队列的尾节点,阻塞队列不包含头结点,从 head.next → tail 认为是阻塞队列
    private transient volatile Node tail;
    
    static final class Node {
        // 枚举:共享模式
        static final Node SHARED = new Node();
        // 枚举:独占模式
        static final Node EXCLUSIVE = null;
        // node 需要构建成 FIFO 队列,prev 指向前继节点
        volatile Node prev;
        // next 指向后继节点
        volatile Node next;
        // 当前 node 封装的线程
        volatile Thread thread;
        // 条件队列是单向链表,只有后继指针,条件队列使用该属性
        Node nextWaiter;
    }
    

JUC-AQS队列设计.png

  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet,条件队列是单向链表

     public class ConditionObject implements Condition, java.io.Serializable {
         // 指向条件队列的第一个 node 节点
         private transient Node firstWaiter;
         // 指向条件队列的最后一个 node 节点
         private transient Node lastWaiter;
     }
    

源码分析

主要获取与释放方法将结合AQS的一个具体实现类 ReentrantLock 来分析。

详情参考下文。

Node节点源码

static final class Node {
    /**
     * Marker to indicate a node is waiting in shared mode
     */
    static final AbstractQueuedSynchronizer.Node SHARED = new AbstractQueuedSynchronizer.Node();
    /**
     * Marker to indicate a node is waiting in exclusive mode
     */
    static final AbstractQueuedSynchronizer.Node EXCLUSIVE = null;
    /**
     * waitStatus value to indicate thread has cancelled
     */
    static final int CANCELLED = 1;
    /**
     * waitStatus value to indicate successor's thread needs unparking
     */
    static final int SIGNAL = -1;
    /**
     * waitStatus value to indicate thread is waiting on condition
     */
    static final int CONDITION = -2;
    /**
     * waitStatus value to indicate the next acquireShared should
     * unconditionally propagate
     */
    static final int PROPAGATE = -3;

    // 等待状态:
    // 1.CANCELLED:1,由于在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消等待,节点进入该状态后不会变化。
    // 2.SIGNAL:-1,后继节点处于等待状态,当前节点的线程如果释放了同步状态或被取消,将会通知后继节点,使得后继节点得以运行。
    // 3.CONDITION:-2,在 condition 中等待的线程,当其他线程调用 Condition.signal方法后,节点将会从等待队列转移到同步队列,加入同步状态获取中。
    // 4.PROPAGATE:-3,表示下一次共享式同步状态将会无条件传播下去。
    // 5.INITAL:0,初始状态。
    volatile int waitStatus;


    // 前驱节点
    volatile Node prev;

    // 后继节点
    volatile Node next;

    // 获取同步状态的线程
    volatile Thread thread;

    // 等待队列中的后继节点。如果当前节点是共享的,那么这个字段将会是一个SHARED常量,也就是说节点类型(独占与共享)和等待队列中的后继节点共用一个字段。
    Node nextWaiter;
}

参考