【并发编程-基础】(六)AQS同步组件

一、AQS介绍

在这里插入图片描述

AQS的设计内容:

  • 使用Node实现FIFO队列,可以用于构建锁或者其它同步装置的基础挂架。
  • 利用了一个int类型表示状态。
  • 使用方法是继承。
  • 子类通过继承并通过实现它的方法管理其状态{ acquire和 release}的方法操纵状态。
  • 可以同时实现排它锁和共享锁模式(AQS的功能两类:独占、共享。这两类不会同时实现)
  • 内部维护了一个队列来管理锁,线程会首先尝试获取锁,如果失败就将当前线程以及等待状态等信息包装成一个node节点,加入到之前介绍的同步队列中,接下来继续不断地尝试获取锁,条件是当前节点为head的直接后继才会尝试,如果失败就会阻塞自己,直到自己被唤醒,而当持久锁的线程释放锁的时候,会唤醒队列中的后继线程。
  • 基于这些基础的设计,jdk提供了许多基于AQS的子类,就是我们常用的同步的组件。

二、AQS常用同步组件

2.1、CountDownLatch(倒计时器)

       2.1.1、CountDownLatch简介
  1. 使用一个计数器,来完成类似于阻塞的操作,如果计数器的值不为0,则组织进程进入,可以用作防止存在任务未完成就开始进入下一步。
  2. 是一个同步辅助类,可以完成类似于阻塞当前线程的功能。一个线程或多个线程一直等待,直到其他线程执行的操作完成。通过一个给定的计数器来实现,该计数器的操作是具备原子性的操作,就是同时只能有一个线程去操作该计数器。
  3. 调用线程await方法的线程会一直处于阻塞状态,直到其它线程调用countDown方法,使当前计数器的值变为0,每次调用的时候计数器的值会-1,计数器的值被减到0 的时候,所有因调用await的方法而处于等待状态的线程就会继续往下执行,这个操作只能被实现一次,因为计数器的值是不能被重置的。
  4. 使用场景:并行计算,通过countDownLatch保证所有请求都被处理完。
       2.1.2、CountDownLatch 使用
@Slf4j
public class CountDownLatchExample1 {

    private static int threadCount = 200;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();
        final CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
			//Thread.sleep(1);
            exec.execute(() -> {
                try {
                    test(threadNum);
                } catch (Exception e) {
                    log.error("exception ", e);
                } finally {
                    countDownLatch.countDown();
                }
            });
        }
        //第一个是值,第二个是时间的单位
        //countDownLatch的计数器减到0的时候才执行下面这句话
        countDownLatch.await(10, TimeUnit.MILLISECONDS);
        log.info("finish");
        exec.shutdown();
    }

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

2.2、Semaphore(信号量)(牛逼)

       2.2.1、Semaphore 简介
  • Semaphore称为信号量,可以控制并发访问的线程个数
  • 信号量是用来对某一共享资源所能访问的最大个数进行控制
  • 可以轻松完成操作系统中信号量功能。
  • 实用场景:常用于仅能提供优先访问的资源,比如数据库的最大连接数只有20,而上层系统的并发数可能远远大于20,如果同时对数据库进行操作,可能导致无法获取数据库连接数而导致异常,这个时候就可以通过信号量semaphore来做并发访问控制,当semaphore控制到1 的时候就跟单线程差不多了。
       2.2.2、Semaphore 使用
方法 介绍
void acquire() 从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
void release() 释放一个许可,将其返回给信号量。
int availablePermits() 返回此信号量中当前可用的许可数。
boolean hasQueuedThreads() 查询是否有线程正在等待获取。
(1)直接使用,不限定每次获取和每次释放的信号量个数

每次信号量区间(acquire和release之间)中只能塞入20个信号量,如果当信号量满了,则进程将在acquire处等待空闲信号量。由于下面的进程每次执行都会在获取到信号量之后睡眠一秒,所以每次输出都是20个,但是不保证执行顺序。

@Slf4j
public class SemaphoreExample1 {

    private static int threadCount = 200;

    public static void main(String[] args){
        ExecutorService exec = Executors.newCachedThreadPool();
        //创建Semaphore信号量,初始化许可大小为20
        final Semaphore semaphore = new Semaphore(20);

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

                }
            });
        }
        log.info("finish");
        exec.shutdown();
    }

    private static void test(int threadNum) throws InterruptedException {
        log.info("{}", threadNum);
        Thread.sleep(1000);
    }
}
(2)限定每次获取和每次释放的信号量个数

semaphore.acquire(4);//获取多个许可,表示之后每一个进程都会耗费4个许可。
semaphore.release(4);//释放多个许可,表示之后每一个进程都会释放4个许可。

感觉这两句话只有占用资源的用处,控制经过某部分的代码块的某进程的许可数量

@Slf4j
public class SemaphoreExample2 {

    private static int threadCount = 200;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();

        final Semaphore semaphore = new Semaphore(20);

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

                }
            });
        }
        log.info("finish");
        exec.shutdown();
    }

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

(3)使用tryAcquire()尝试获取资源
@Slf4j
public class SemaphoreExample3 {

    private static int threadCount = 200;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();

        final Semaphore semaphore = new Semaphore(20);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    //尝试获取一个许可,获取到了就允许执行
                    if (semaphore.tryAcquire()) {
                        test(threadNum);
                        semaphore.release();//释放一个许可
                    }
                } catch (Exception e) {
                    log.error("exception ", e);
                } finally {

                }
            });
        }
        log.info("finish");
        exec.shutdown();
    }

    private static void test(int threadNum) throws InterruptedException {
        log.info("{}", threadNum);
        Thread.sleep(1000);
    }
}
(4)使用tryAcquire()尝试获取资源的同时设置最长等待时间
@Slf4j
public class SemaphoreExample4 {

    private static int threadCount = 200;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();

        final Semaphore semaphore = new Semaphore(20);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    //尝试获取一个许可,获取到了就允许执行
                    //等一秒尝试获取许可
                    //只有5秒的执行,5秒之后再也不等待了
                    if (semaphore.tryAcquire(5, TimeUnit.SECONDS)) {
                        test(threadNum);
                        semaphore.release();//释放一个许可
                    }
                } catch (Exception e) {
                    log.error("exception ", e);
                } finally {

                }
            });
        }
        log.info("finish");
        exec.shutdown();
    }

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

2.3、CyclicBarrier

       2.3.1、CyclicBarrier简介

  • 和CountDownLatch很像,都能阻塞进程,CountDownLatch是-1操作,CyclicBarrier是+1操作,CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的。
  • CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的。
  • 是一个同步辅助类,它允许一组线程相互等待,直到到达某个公共的屏障点。
  • 通过它可以完成多个线程之间相互等待,只有当每个线程都准备就绪后,才能各自继续往下执行后面的操作。
  • 和CountDownLatch有一些相似的地方,都是通过计数器来实现的。当某个线程调用了await方法之后,该线程就进入了等待状态,计数器执行的是+1操作。当计数器的值达到初始值的时候,因为调用await方法进入等待的线程被唤醒,继续执行他们后续的操作,由于它在释放等待线程后可以重用,所以可以称它为循环屏障,可以重复使用。
  • 场景和CountDownLatch很相似。主要是实现了多个线程相互等待。
  • 可以用于多线程计算数据,最后合并计算结果的场景。
       2.3.2、CyclicBarrier的方法
(1)构造方法
public CyclicBarrier(int parties)
//parties 是参与线程的个数
    
public CyclicBarrier(int parties, Runnable barrierAction)
//第二个构造方法有一个 Runnable 参数,这个参数的意思是最后一个到达线程要做的任务
(2)重要方法
public int await()
//线程调用 await() 表示自己已经到达栅栏

public int await(long timeout, TimeUnit unit)
//BrokenBarrierException报错 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时
(3)其他方法
int getNumberWaiting()
//返回当前在屏障处等待的参与者数目。

int getParties()
//返回要求启动此 barrier 的参与者数目。

boolean isBroken()
//查询此屏障是否处于损坏状态。

void reset()
//将屏障重置为其初始状态。如果调用了该函数,则在等待的线程将会抛出BrokenBarrierException异常。
       2.3.3、CyclicBarrier使用
(1) CyclicBarrier的基本使用
@Slf4j
public class CyclicBarrierExample1 {
    //当barrier.await();上方堵住的进程有5个时,全部放行,计数器变为0,如果上方堵住进程没到5个时,就会继续堵住
    private static CyclicBarrier barrier = new CyclicBarrier(5);

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

        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < 100; i++) {
            final int threadNum = i;
            Thread.sleep(1000);
            executor.execute(() -> {
                try {
                    race(threadNum);
                } catch (Exception e) {
                    log.error("exception ",e);
                }
            });
        }

    }

    private static void race(int threadNum) throws Exception {
        Thread.sleep(1000);
        log.info("{} is ready", threadNum);
        barrier.await();
        log.info("{} continue", threadNum);
    }
}

(2)CyclicBarrier设置等待超时时间
@Slf4j
public class CyclicBarrierExample2 {
    private static CyclicBarrier barrier = new CyclicBarrier(5);

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

        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);
                }
            });
        }

    }

    private static void race(int threadNum) throws Exception {
        Thread.sleep(1000);
        log.info("{} is ready", threadNum);
        try {
            //如果在2秒内没有塞满,则会报错
            barrier.await(2000, TimeUnit.MILLISECONDS);
        }catch (Exception e){
            System.out.println(barrier.getParties());
            log.error("exception  {}",e);
        }
        log.info("{} continue", threadNum);
    }
}
(3)CyclicBarrier结合lambda表达式
@Slf4j
public class CyclicBarrierExample3 {
    //线程达到屏障的时候优先执行里面的内容
    private static CyclicBarrier barrier = new CyclicBarrier(5, () -> {
        log.info("callback is running");
    });

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

        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);
                }
            });
        }

    }

    private static void race(int threadNum) throws Exception {
        Thread.sleep(1000);
        log.info("{} is ready", threadNum);
        barrier.await();
        log.info("{} continue", threadNum);
    }
}

2.4、ReentrantLock与锁(特别牛逼,建议深入了解)

Java有两类锁,第一种是synchronized,一种就是ReentrantLock,它的核心是lock和unlock。

       2.4.1、ReentrantLock(可重入锁)和synchronized区别
  1. 可重入性:(在进入锁,两者都可以,两者在这里的差别不大)。
  2. 锁的实现:synchronized是通过JVM实现的,ReentrantLock是通过JDK实现的(一个是操作系统控制实现,一个用户自己敲代码实现)后者可以看源码来看。
  3. 性能的区别:偏向锁、轻量锁引入之后synchronized就比后者好一些了,就倾向于这个。
  4. 功能:便利性(synchronized好一些)。后者需要手动声明加锁、手动释放锁。细腻度和灵活度后者好一些。
       2.4.2、ReentrantLock独有功能
  1. 可指定是公平锁(先等待先获得)还是非公平锁。synchronized都是非公平锁。
  2. 提供了一个Condition类,可以分组唤醒需要唤醒的线程。不像synchronized必须全部唤醒。
  3. 提供能够中断等待锁的线程的机制,lock.lockInterruptibly()
  4. 如果要实现这三个独有功能,就最好使用这个ReentrantLock
  5. Sync能做的reen都能做,但是反过来则不一定
       2.4.3、ReectrantLock使用
(1)通过Lock父类接口实例化ReectrantLock
@Slf4j
@ThreadSafe
public class LockExample2 {
    private static int threadTotal = 200;//同时最多200个请求同时执行
    private static int clientTotal = 50000;//总共5000个请求

    private static long count = 0;//获取到的数量

    private final static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        //使用线程池和信号量来模仿客户端

        ExecutorService exec = Executors.newCachedThreadPool();//线程池
        final Semaphore semaphore = new Semaphore(threadTotal);//信号量
        //保证线程完全完整的执行
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int index = 0; index < clientTotal; index++) {
            exec.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        exec.shutdown();
        log.info("count:{}", count);
    }

    private static void add() {
        lock.lock();
        try {
            count++;
        }finally {
            lock.unlock();
        }
    }
}
(2)StampedLock的使用
@Slf4j
public class LockExample5 {
    private static int threadTotal = 200;//同时最多200个请求同时执行
    private static int clientTotal = 50000;//总共5000个请求

    private static long count = 0;//获取到的数量

    private final static StampedLock lock = new StampedLock();

    public static void main(String[] args) throws InterruptedException {
        //使用线程池和信号量来模仿客户端
        ExecutorService exec = Executors.newCachedThreadPool();//线程池
        final Semaphore semaphore = new Semaphore(threadTotal);//信号量
        //保证线程完全完整的执行
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int index = 0; index < clientTotal; index++) {
            exec.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        exec.shutdown();
        log.info("count:{}", count);
    }

    private static void add() {
        long stamp = lock.writeLock();
        try {
            count++;
        }finally {
            //有lock就一定要有unlock
            lock.unlock(stamp);
        }
    }
}
(3)ReentrantReadWriteLock使用
@Slf4j
public class LockExample3 {

    private final Map<String,Data> map = new TreeMap<>();

    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private final  Lock readLock = lock.readLock();

    private final Lock writeLock = lock.writeLock();

    public Data get(String key ){
        readLock.lock();
        try {
            return map.get(key);
        }finally {
            readLock.unlock();
        }
    }
    public Set<String> getAllKeys(){
        readLock.lock();
        try {
            return map.keySet();
        }finally {
            readLock.unlock();
        }
    }
    public Data put(String key,Data value){
        writeLock.lock();
        try {
            return map.put(key,value);
        }finally {
            writeLock.unlock();
        }
    }
    class Data{

    }

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

    }
}
(4)ReentrantLock 和 Condition配合使用
@Slf4j
public class LockExample6 {
    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        Condition condition = reentrantLock.newCondition();

        new Thread(()->{
           try {
               reentrantLock.lock();
               log.info("wait signal");
               condition.await();
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           log.info("get signal");
           reentrantLock.unlock();
        }).start();

        new Thread(()->{
            reentrantLock.lock();
            log.info("get lock");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            condition.signalAll();
            log.info("send signal ~");
            reentrantLock.unlock();
        }).start();
    }
}
发布了20 篇原创文章 · 获赞 1 · 访问量 561

猜你喜欢

转载自blog.csdn.net/weixin_42295814/article/details/103790074