Java并发编程:AQS篇

未完待续。。。

AQS介绍

AQS的全称是:AbstractQueuedSynchronizer。主要用来构造锁和同步器。源码如下:

java 复制代码
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable

AQSJUC同步框架的基石。AQS通过一个FIFO队列维护线程同步状态,实现类只要继承该类,并重写指定方法,就可以实现一套线程同步机制。

AQS根据资源互斥级别提供了独占和共享 两种资源访问模式;同时定义Condition结构,提供了wait/signal等待唤醒机制。在JUC中, ReentrantLockSemaphoreReentrantReadWriteLockSynchronousQueueCountDownLatch等等皆是基于 AQS 的。

原理概述

AQS中维护了一个volatile int state变量和一个CLH双向队列。队列中的结点持有线程引用,每个结点都可以通过getState()setState()compareAndSetState()对state进行修改和访问。

当线程获取锁时,也就是试图对state变量做修改,如果修改成功则获取到锁;如果修改失败,则将线程包装为结点挂在到队列中,等待持有锁的线程释放锁并唤醒队列中的结点。

源码

AQS中的重要属性

java 复制代码
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     * 头结点,就是当前持有锁的线程
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     * 阻塞的尾节点,每个新的等待线程进来,都插入到最后,也就形成了一个链表
     */
    private transient volatile Node tail;

    /**
     * The synchronization state.
     * 0代表没有被占用,大于0代表有线程持有当前锁。
     */
    private volatile int state;

    // 代表当前持有独占锁的线程。比如:因为锁可以重入,reentrantlock.lock可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁。
    private transient Thread exclusiveOwnerThread;
 }

AQS模板方法

AQS内部封装了队列维护逻辑,给实现类提供了以下模板方法:

java 复制代码
tryAcquire(int); 尝试获取独占锁,可获取返回true,否则false
tryRelease(int); 尝试释放独占锁,可释放返回true,否则false
tryAcqureShared(int); 尝试以共享方式获取锁,失败返回负数,只能获取一次返回0,否则返回个数
tryReleaseShared(int);尝试释放共享锁,可获取返回true,否则false
isHeldExclusively();判断线程是否独占资源

如果实现类只需实现独占锁/共享锁,可以只实现tryAcquire/tryRelease或tryAcqureShared/tryReleaseShared

AQS 状态值

AQS 的核心是状态值 state,它代表了锁或同步器的状态。在 AQS 中,通常将 state 划分为两部分:

  • 高 16 位:用于表示状态信息,如获取锁的次数或资源数量。
  • 低 16 位:用于表示线程的等待状态,如是否已经获取锁或在等待队列中等待。

FIFO队列

AQS使用一个FIFO队列来管理线程。这个队列由Node对象组成,每个Node包含了一个等待线程及一个等待状态。 AQS中的等待队列是一个双向链表,如下图所示,但是等待队列中不包含头节点,头节点是已经拿到了锁的节点。

Node的源码如下

java 复制代码
static final class Node {
    /** Marker to indicate a node is waiting in shared mode */
    // 表示节点当前在共享模式下
    static final Node SHARED = new Node();
    /** Marker to indicate a node is waiting in exclusive mode */
    // 标识节点当前在独占模式下
    static final Node EXCLUSIVE = null;
    
    // =====下面的几个int常量是给waitStatus用的=====
    
    /** waitStatus value to indicate thread has cancelled */
    // 代码此线程取消了争抢这个锁
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    // 表示当前node的后继结点对应的线程需要被唤醒,也就是说:当前Node被唤醒是需要被当前Node的前继节点来唤醒的
    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、-1、-2、-3、0.如果这个值大于0,代表此线程取消了等待
    volatile int waitStatus;
    // 前驱节点的引用
    volatile Node prev;
   
    // 后继节点的引用
    volatile Node next;
    
    // 线程
    volatile Thread thread;
    // 条件队列
    Node nextWaiter;
  • SIGNAL属性表示当前node的后继结点对应的线程需要被唤醒,也就是说:当前Node是需要被其前继节点来唤醒的。
  • Node的数据结构,目前只需要记住有:waitStatus+thread+pre+next四个属性

acquire方法

java 复制代码
public final void acquire(int arg) {
    // 尝试获取锁,如果获取成功直接返回
    // 如果获取锁失败,则调用acquireQueued方法
    if (!tryAcquire(arg) &&
        //addWaiter方法,将当前线程封装成node节点,并加入到阻塞队列尾部
        // acquireQueued,这个方法非常重要,线程挂起、唤醒获取锁,都在这个方法中
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

addWaiter 方法

上述acquire方法中,如果获取锁失败,调用的addWaiter方法如下:

java 复制代码
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;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果代码走到这里,有两种情况:1)阻塞队列为空head = tail = null;2)cas操作失败,存在竞争入队
    // enq方法:采用自旋的方式进行入队 
    // 自旋就是 上面一次CAS失败了 那我就一直重复的去进行CAS操作 总有一次是成功的
    enq(node);
    return node;
}

常见的同步工具类

ReentrantLock

使用ReentrantLock

java 复制代码
class LockReentrant implements Runnable{
    private final Lock lock = new ReentrantLock();

    public void method1() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "method1()");
            method2();
        } finally {
            lock.unlock();
        }
    }
}

上述的lock.lock调用的是ReentrantLock的lock方法

java 复制代码
public void lock() {
    sync.lock();
}

Sync类

sync.lock调用的方法如下

ReentrantLock使用Sync管理锁的加锁与释放,Sync继承AbstractQueuedSynchronizer,Sync有两个实现类,分别是非公平锁和公平锁。

java 复制代码
abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;

    /**
     * Performs {@link Lock#lock}. The main reason for subclassing
     * is to allow fast path for nonfair version.
     */
    abstract void lock();

    /**
     * Performs non-fair tryLock.  tryAcquire is implemented in
     * subclasses, but both need nonfair try for trylock method.
     */
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

加锁(公平锁)

static final class FairSync extends Sync中的方法

java 复制代码
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    //该方法用了关键字修饰,如果有子类继承该类,则无法重写这个方法
    // 加锁
    final void lock() {
        // acquire调用的是AQS中的方法,在上面已经详细写了
        acquire(1);
    }

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    // 这个方法好像不是显示调用的,业务代码里都是直接调用lock方法,没见到调用这个方法的
    protected final boolean tryAcquire(int acquires) {
        // 获取当前线程?怎么直观的知道当前是哪个线程呢?是指调用了lock方法的线程吗
        final Thread current = Thread.currentThread();
        // 获取当前锁的状态,直接调用父类AQS中的方法
        int c = getState();
        // c=0,表示目前没有线程获取到锁
        if (c == 0) {
            // hasQueuedPredecessors 查看当前节点的前面有没有节点在排队(因为是公平锁,所以这么判断一下)并使用compareAndSetState获取锁成功,则将当前线程设置到锁中(独占锁),表示目前是当前线程获取到锁。
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 进入到这个分支,表明当前锁的持有者已经是当前线程了,也就是可重入
        else if (current == getExclusiveOwnerThread()) {
            // 计算锁的状态值,有可能是加锁,也有可能是解锁
            int nextc = c + acquires;
            // 如果锁的状态值<0,报错
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            // 设置锁的状态值
            setState(nextc);
            return true;
        }
        // 走到这个路径,说明获取不到锁,则返回false
        return false;
    }
}

// 这个方法好像不是显示调用的,业务代码里都是直接调用lock方法,没见到 protected final boolean tryAcquire

解锁

java 复制代码
// 解锁的方法如下
public void unlock() {
    // 调用AQS中的release方法,见下面一段代码
    sync.release(1);
}

public final boolean release(int arg) {
    // 根据多态,应该是调用的ReentrantLock中的tryRelease方法,见下面一段代码
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

// ReentrantLock中tryRelease方法
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 是否完全释放锁
    boolean free = false;
    // 下面就是判断是否是可重入锁的情况了 如果c==0 也就是说没有嵌套锁了 可以释放了 否则还不能释放掉
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

// 唤醒后继节点,AQS中的方法,这个没有显示调用?
// 从上面调用处知道,参数node是head头结点
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.
     */
    // 下面的代码就是唤醒后继节点 但是有可能后继节点取消了等待
    // 从队尾往前找 找到waitStatus<=0的所有节点中排在最前面的
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从后往前找 不必担心中间有节点取消(waitStatus==1)的情况
        // 注意这里为什么不用担心 就是因为前面阻塞队列入队的操作了
        // 不理解的可以回去看看阻塞队列入队操作
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        // 唤醒线程
        LockSupport.unpark(s.thread);
}

在并发环境下,加锁和解锁需要三个部件的协调:

  1. 锁状态,也就是锁state值,这个值就是表示是否有线程持有锁的标志位。加锁就是CAS操作这个state值加1,解锁就是CAS操作这个state值减1。
  2. 线程的挂起和唤醒,AQS中使用了LockSupport的park()方法来挂起线程,unpark方法来唤醒线程。
  3. 阻塞队列,争抢锁的线程可能有很多个所以会存在阻塞队列去管理这些没有抢到锁的线程,AQS使用的是一个FIFO的队列,双向链表的形式去管理。AQS采用了CLH锁的变体来实现。

Semaphore 信号量

synchronized和ReentrantLock都是一次只允许一个线程访问某个资源,而Semaphore可以让多个线程同时访问共享资源。通过acquire()获取一个许可,如果没有就等到。通过release()释放一个许可。比如:

scss 复制代码
// 初始共享资源数量5,表示同一时刻N个线程中只有5个线程能获取到共享资源,其他的都会阻塞
final Smaphore semaphore = new Semaphore(5);
semaphore.acquire();
semephore.release()

构造方法

java 复制代码
public Semaphore(int permits) { 
    //参数permits表示许可数目,即同时可以允许多少线程进行访问 
    sync = new NonfairSync(permits); 
} 

public Semaphore(int permits, boolean fair) { 
    //这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可 
    sync = (fair)? new FairSync(permits) : new NonfairSync(permits); 
}

其他重要方法

java 复制代码
public void acquire() throws InterruptedException { } //获取一个许可 
public void acquire(int permits) throws InterruptedException { } //获取permits个许可 
public void release() {} //释放一个许可 
public void release(int permits) {} //释放permits个许可

上述4个方法都会被阻塞,需要等待获取到结果之后才返回。如果想要立即得到执行结果,需要以下几个方法

java 复制代码
public boolean tryAcquire() { }; //尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false 
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { }; //尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false public boolean 
tryAcquire(int permits) { }; //尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false 
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; //尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false

实战

java 复制代码
package multithread.aqs.semaphore;

import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class Worker implements Runnable {
    private static int count = 0;
    private final int id = count++;
    private int finished = 0;
    private Random random = new Random(47);
    private Semaphore semaphore;
    public Worker(Semaphore semaphore) {
        this.semaphore = semaphore;
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                semaphore.acquire();
                System.out.println(this +"占用一个机器在生产...  ");
                TimeUnit.MILLISECONDS.sleep(random.nextInt(2000));
                synchronized (this) {
                    System.out.println("已经生产了" + (++finished) + "个产品," + "释放出机器");
                }
                semaphore.release();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "_" + id;
    }
}


public class WorkerAppMain {
    public static void main(String[] args) {
        //工人数
        int N = 8;
        //机器数
        Semaphore semaphore = new Semaphore(5);
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < N; i++) {
            exec.execute(new Worker(semaphore));
        }
        exec.shutdown();
    }
}

CountDownLatch 倒计时器

可以实现类似计数器的功能。比如有一个任务A,要等到其他4个任务都执行完毕之后才能执行,可以利用CountDownLatch来实现。CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对该值进行设置,当CountDownLatch使用完毕后,它不能再次被使用。

重要方法

java 复制代码
public void await() throws InterruptedException { }; //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行 
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行 
public void countDown() { //将count值减1
    sync.releaseShared(1)
}; 

原理

CountDownLatch是共享锁的一种实现,默认构造AQS的state值为count。

java 复制代码
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

private static final class Sync extends AbstractQueuedSynchronizer {
    Sync(int count) {
        setState(count);
    }
  //...
}

当线程调用countDown()方法时,其实使用了tryReleaseShare方法以CAS的操作来减少state,直至state为0。当state为0时,表示所有的线程都调用了countDown方法,那么在CountDownLatch上等待的线程就会被唤醒并继续执行。

实战

两种典型用法

  • 某一线程在开始运行前等待n个线程执行完毕。比如:启动一个服务时,主线程需要等待多个组件加载完成之后再继续执行
    • CountDownLatch的计数器初始化为1(new CountDownLatch(n)),每当一个任务线程执行完毕,就将计数器减1(countdownlatch.countDown())。
    • 当计数器的值变为0时,在CountDownLatchawait的线程就会被唤醒。
  • 实现多个线程开始执行任务的最大并行性。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开炮。
    • 初始化一个共享的CountDownLatch对象,将其计数器初始化为1,多个线程在开始执行任务前首先countdownlatch.await()。当主线程调用countDown()时,计数器变为0,多个线程同时被唤醒。

代码示例

java 复制代码
public class CountDownLatchExample {
  // 请求的数量
  private static final int THREAD_COUNT = 550;

  public static void main(String[] args) throws InterruptedException {
    // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
    // 只是测试使用,实际场景请手动赋值线程池参数
    ExecutorService threadPool = Executors.newFixedThreadPool(300);
    final CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
    for (int i = 0; i < THREAD_COUNT; i++) {
      final int threadNum = i;
      threadPool.execute(() -> {
        try {
          test(threadNum);
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          // 表示一个请求已经被完成
          countDownLatch.countDown();
        }

      });
    }
    countDownLatch.await();
    threadPool.shutdown();
    System.out.println("finish");
  }

  public static void test(int threadnum) throws InterruptedException {
    Thread.sleep(1000);
    System.out.println("threadNum:" + threadnum);
    Thread.sleep(1000);
  }
}

CyclicBarrier 循环栅栏

CyclicBarrierCountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。

CountDownLatch 的实现是基于 AQS 的,而 CycliBarrier 是基于 ReentrantLock(ReentrantLock 也属于 AQS 同步器)和 Condition 的。

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。

CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

  • CountDownLatch 一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
  • CyclicBarrier 一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
  • CountDownLatch 是不能够重用的,而 CyclicBarrier 是可以重用的。

参考文章

  1. 掘金:AQS源码分析------一步一步带你走进AQS的世界
  2. 掘金:【深入AQS原理】我画了35张图就是为了让你深入 AQS
  3. 知乎:AQS源码分析看这一篇就够了
  4. 掘金:Java并发之AQS详解
  5. 腾讯云:CountDownLatch、CyclicBarrier、Semaphore的区别,你知道吗?
  6. JavaGuide面试题
相关推荐
2401_857439691 小时前
Spring Boot新闻推荐系统:用户体验优化
spring boot·后端·ux
进击的女IT2 小时前
SpringBoot上传图片实现本地存储以及实现直接上传阿里云OSS
java·spring boot·后端
一 乐3 小时前
学籍管理平台|在线学籍管理平台系统|基于Springboot+VUE的在线学籍管理平台系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·学习
艾伦~耶格尔6 小时前
Spring Boot 三层架构开发模式入门
java·spring boot·后端·架构·三层架构
man20176 小时前
基于spring boot的篮球论坛系统
java·spring boot·后端
攸攸太上7 小时前
Spring Gateway学习
java·后端·学习·spring·微服务·gateway
罗曼蒂克在消亡7 小时前
graphql--快速了解graphql特点
后端·graphql
潘多编程7 小时前
Spring Boot与GraphQL:现代化API设计
spring boot·后端·graphql
大神薯条老师8 小时前
Python从入门到高手4.3节-掌握跳转控制语句
后端·爬虫·python·深度学习·机器学习·数据分析
2401_857622668 小时前
Spring Boot新闻推荐系统:性能优化策略
java·spring boot·后端