J.U.C之AQS

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35508033/article/details/89513327

AbStractQueuedSynchronizer类,简称AQS,一个用来构建锁和同步器的框架 。从JDK1.5开始,引入了并发包,也就是J.U.C,大大提高了JAVA程序的并发性能,而AQS则是J.U.C的核心,是并发类中核心部分,它提供一个基于FIFO队列,这个队列可以构建锁或其他相关的同步装置的基础框架。

AQS底层数据结构:
底层采用双向链表,是队列的一种实现,因此可以当做是一个队列。其中Sync queue即同步队列,它是双向链表,包括hean结点(主要用作后续的调度)与tail结点。Condition queue不是必须的,单向链表,只有在需要使用到condition的时候才会存在这个单向链表,并且可能存在多个Condition queue

Design

  • 使用Node实现FIFO队列,可以用于构建锁或者其他同步装置的基础框架
  • 利用了一个int类型表示状态。在AQS中,存在一个state成员变量,基于AQS有一个同步组件ReentrantLock,在这个组件中,state表示获取锁的线程数,假如state == 0表示无线程获取锁,state == 1表示已有线程获取锁,state > 1表示锁的数量
  • 使用方法是继承。AQS的设计是基于模板方法,使用需要继承AQS,并覆写其中的方法。
  • 子类通过继承并通过实现它的方法管理其状态{acquire() 和 release()}的方法操纵状态
  • 可以同时实现排它锁和共享锁模式(独占、共享)。它的所有子类中,要么实现并使用它的独占功能API,要么实现共享锁的功能,而不会同时使用两套API。即便是它比较有名的子类ReentrantReadWirteLock也是通过两个内部类读锁和写锁分别使用两套API实现的。AQS在功能上,有独占控制和共享控制两种功能。
  • 在LOCK包中的相关锁(常用的有ReentrantLock、 ReadWriteLock)都是基于AQS来构建.然而这些锁都没有直接来继承AQS,而是定义了一个Sync类去继承AQS,因为锁面向的是使用用户,而同步器面向的则是线程控制,那么在锁的实现中聚合同步器而不是直接继承AQS就可以很好的隔离二者所关注的事情.

基于以上设计,AQS具体实现的大致思路:

AQS内部维护了一个CLH队列来管理锁,线程首先会尝试获取锁,如果失败,会将当前线程以及等待状态等信息包装成Node结点加入同步队列(Sync queue)中。接着不断循环尝试获取锁,条件是当前结点为head直接后继才会尝试,如果失败则会阻塞自己,直到自己被唤醒;而当持有锁的线程,释放锁的时候,会唤醒队列中后继线程。基于这些基础的设计和思路,JDK提供了许多基于AQS的子类。

同步组件概览

  • CountDownLatch:是闭锁,通过一个计数来保证线程是否需要一直阻塞
  • Semaphore:控制同一时间,并发线程的数目
  • CyclicBarrier:和CountDwonLatch相似,能阻塞线程
  • ReentrantLock
  • Condition:使用时需要ReentrantLock
  • FutureTask

CountDownLatch

构造器中的计数值(count)实际上就是闭锁需要等待的线程数量。这个值只能被设置一次,而且CountDownLatch没有提供任何机制去重新设置这个计数值。

典型的应用:并行计算,当某个任务需要处理运算量非常大,可以将该运算任务拆分为多个子任务,等待所有的子任务完成之后,父任务再拿到所有子任务的运算结果进行汇总。利用CountDownLatch可以保证任务都被处理完才去执行最终的结果运算,过程中每一个线程都可以看做是一个子任务。

@Slf4j
public class CountDownLatchExample {

    private final static int threadCount = 200;

    public static void main(String[] args) throws Exception {

        ExecutorService exec = Executors.newCachedThreadPool();

        final CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    test(threadNum);
                } catch (Exception e) {
                    log.error("exception", e);
                } finally {
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();
        log.info("finish");
        exec.shutdown();
    }

    private static void test(int threadNum) throws Exception {
        Thread.sleep(100);
        log.info("{}", threadNum);
        Thread.sleep(100);
    }
}

CountDownLatch还提供在指定时间内完成的条件(超出时间没有完成,完成多少算多少),如果等待时间没有完成,则继续执行。通过countDownLatch.await(int timeout,TimeUnit timeUnit);设置,第一个参数没超时时间,第二个参数为时间单位

Semaphore

Semaphore经常用于限制获取某种资源的线程数量,其内部是基于AQS的共享模式,AQS的状态表示许可证的数量,在许可证数量不够时,线程将会被挂起;而一旦有一个线程释放一个资源,那么就有可能重新唤醒等待队列中的线程继续执行。

应用场景

Semaphore可以用于做流量控制,特别公用资源有限的应用场景,比如数据库连接。假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发的读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有十个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,我们就可以使用Semaphore来做流控

@Slf4j
public class SemaphoreExample {
    //总共有20个线程数
    private final static int threadCount = 20;

    public static void main(String[] args) throws Exception {

        ExecutorService exec = Executors.newCachedThreadPool();
        //定义信号量,并且制定每次可用的许可数量
        final Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    //semaphore.acquire(3); // 获取多个许可
                    semaphore.acquire(); // 获取一个许可
                    test(threadNum);
                    //semaphore.release(3); // 释放多个许可
                    semaphore.release(); // 释放一个许可
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        exec.shutdown();
    }

    private static void test(int threadNum) throws Exception {
        log.info("{}", threadNum);
        Thread.sleep(1000);
    }
}

CyclicBarrier

CyclicBarrier也是一个同步辅助类,它允许一组线程相互等待, 直到到达某个公共的屏障点(common barrier point ),也称之为栅栏点。通过它可以完成多个线程之间相互等待,只有当每个线程都准备就绪后,才能各自继续进行后面的操作。它和CountDownLatch有相似的地方,都是通过计数器实现。当某个线程调用await()方法之后,该线程就进入等待状态,而且计数器是执行加一操作,当计数器值达到初始值(设定的值),因为调用await()方法进入等待的线程,会被唤醒,继续执行他们后续的操作。由于CyclicBarrier在等待线程释放之后,可以进行重用,所以称之为循环屏障。它非常适用于一组线程之间必需经常互相等待的情况。

与CountDownLatch比较

相同点:

  1. 都是同步辅助类。
  2. 使用计数器实现

不同点:

  1. CountDownLatch允许一个或多个线程,等待其他一组线程完成操作,再继续执行。
  2. CyclicBarrier允许一组线程相互之间等待,达到一个共同点,再继续执行。
  3. CountDownLatch不能被复用
  4. CyclicBarrier适用于更复杂的业务场景,如计算发生错误,通过重置计数器,并让线程重新执行
  5. CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。

场景比较:

  1. CyclicBarrier : 好比一扇门,默认情况下关闭状态,堵住了线程执行的道路,直到所有线程都就位,门才打开,让所有线程一起通过。
  2. CyclicBarrier可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个Excel保存了用户所有银行流水,每个Sheet保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个sheet里的银行流水,都执行完之后,得到每个sheet的日均银行流水,最后,再用barrierAction用这些线程的计算结果,计算出整个Excel的日均银行流水。
  3. CountDownLatch : 监考老师发下去试卷,然后坐在讲台旁边玩着手机等待着学生答题,有的学生提前交了试卷,并约起打球了,等到最后一个学生交卷了,老师开始整理试卷,贴封条
@Slf4j
public class CyclicBarrierExample {
    //定义屏障,指定数量为5个
    private static CyclicBarrier barrier = new CyclicBarrier(5);

    public static void main(String[] args) throws Exception {

        ExecutorService executor = Executors.newCachedThreadPool();
        //往线程池中放入线程
        for (int i = 0; i < 10; i++) {
            final int threadNum = i;
            Thread.sleep(1000);
            executor.execute(() -> {
                try {
                    race(threadNum);
                } catch (Exception e) {
                    log.error("exception", e);
                }
            });
        }
        executor.shutdown();
    }

    private static void race(int threadNum) throws Exception {
        Thread.sleep(1000);
        log.info("{} is ready", threadNum);
        //如果当前线程就绪,则告诉CyclicBarrier 需要等待
        barrier.await();
        // 当达到指定数量时,继续执行下面代码
        log.info("{} continue", threadNum);
    }
}

猜你喜欢

转载自blog.csdn.net/qq_35508033/article/details/89513327