多线程/并发编程——同步工具类(CountDownLatch、Semaphore、ReadWriteLock、CyclicBarrier )

2022-09-27 13:26:16

多线程/并发编程——同步工具类

上一篇文章中,我们详细剖析了并发工具包 J.U.C 的底层实现 AQS 的细节,不过只解释了基于 AQS 的最常用的同步工具锁 ReentrantLock。诚然 ReentrantLock 是很常用、很重要的一个同步工具类。不过基于 AQS 其实还有很多同步工具类,下面我们就来学习一下这些同步工具类的简单实用

本篇文章中只介绍同步工具类的基本使用,其底层原理实现都是基于 AQS,如果想要详细理解 AQS 的实现,请移步:https://blog.csdn.net/qq_42583242/article/details/108746299

一、CountDownLatch

在这里插入图片描述

CountDown 顾名思义叫做倒数,Latch 即门闩,CountDownLatch 又称闭锁。

可以理解为一班固定座位数的客车,滚动发车,当坐满了,没有空座位了就发车。

在程序中根据具体的业务场景,一旦满足某个条件,就调用latch.countDown() 让 count 减一,一直减到 count 变成0,才允许当前线程向下运行,执行latch.await() 之后的逻辑。

代码示例:

publicclassTestCountDownLatch{publicstaticvoidmain(String[] args){usingCountDownLatch();
        System.out.println("业务逻辑");}privatestaticvoidusingCountDownLatch(){
        Thread[] threads=newThread[10];
        CountDownLatch latch=newCountDownLatch(threads.length);for(int i=0; i<threads.length; i++){
            threads[i]=newThread(()->{int result=0;for(int j=0; j<100; j++) result+= j;//每一个线程结束的时候,让门闩限制减一
                latch.countDown();});}for(int i=0; i< threads.length; i++){
            threads[i].start();}try{//指定一个条件,判断什么时候,代码可以继续向下运行,比如100个线程结束之后的定时任务。
            latch.await();}catch(InterruptedException e){
            e.printStackTrace();}//下面可以添加具体的业务逻辑
        System.out.println("业务逻辑");
        System.out.println("end latch");}}

使用步骤,主要有以下几个方法配合使用:

  1. CountDownLatch latch = new CountDownLatch(count);创建一个闭锁,指定count。即指定座位数量
  2. latch.await();阻塞,让逻辑不再向下运行。即人不满车不走
  3. latch.countDown();满足条件就让 count 减一,直到 count 为 0,就继续向下运行。即无空座位就发车

使用 CountDownLatch 闭锁,就相当于一个门闩栓在哪,使用程序控制什么时候允许向下走,完成定时任务。

二、Semaphore

在这里插入图片描述

构造函数:

//permits:允许的数量publicSemaphore(int permits){
        sync=newNonfairSync(permits);}//其底层实现是 AQS,和 ReentrantLock 一样,有公平和非公平的概念,根据构造函数传参来决定创建公平锁还是//非公平锁publicSemaphore(int permits,boolean fair){
        sync= fair?newFairSync(permits):newNonfairSync(permits);}

Semaphore 即信号量,允许多少个线程同时执行

应用场景:限流。比如售票,只有 5 个窗口,那 Semaphore 传参 permits 为 5,同时在买票的人只有 5 个人。

在这里插入图片描述

代码示例:

publicclassTestSemaphore{publicstaticvoidmain(String[] args){//Semaphore s = new Semaphore(2);//允许两个线程执行,并可以指定是否是公平锁
        Semaphore s=newSemaphore(2,false);//允许一个线程同时执行//Semaphore s = new Semaphore(1);newThread(()->{try{//从 Semaphore 获的许可,才可以向下允许,如果 Semaphore 已经满了,就不能获得锁
                s.acquire();

                System.out.println("T1 running...");
                Thread.sleep(200);
                System.out.println("T1 running...");}catch(InterruptedException e){
                e.printStackTrace();}finally{
                s.release();}}).start();newThread(()->{try{//从 Semaphore 获的许可,才可以向下允许,如果 Semaphore 已经满了,就不能获得锁
                s.acquire();

                System.out.println("T2 running...");
                Thread.sleep(200);
                System.out.println("T2 running...");

                s.release();}catch(InterruptedException e){
                e.printStackTrace();}}).start();}}

三、ReadWriteLock

ReadWriteLock 是与 Reentrantlock 同级别的,同样继承自 J.U.C.lock 接口,其底层实现也是 AQS

在这里插入图片描述

读写锁其实就是共享锁和排他锁的概念:

  • 共享锁:即 ReadLock 读锁
  • 排他锁:即 WriteLock 写锁

比如数据库中的某条数据读的时候特别多,写的时候特别少。当某一时刻有大量并发请求此数据,有读有写,此时的处理可以分为以下情况:

  • 不加锁:可能出现写的线程写一半,另外的读线程就把数据读出来了;还有可能出现两个写线程对同一条记录修改,造成数据不一致
  • 加互斥锁:如果对数据加 Synchronized ,肯定能保证数据一致性,但是如果大量并发只是查询数据,并没有修改数据,结果却使用 Synchronized,性能就会很低
  • 加读写锁:读线程来的时候,对数据加 ReadLock,这样其他读线程能读数据,但是不能修改数据。写线程来的时候会尝试对数据加 WriteLock,其他线程既不能读,也不能写,保证数据一致性。这样既能保证数据一致性,也保证了比较优秀的效率

数据库的锁是数据库的锁,程序的锁是程序的锁,两者不要混为一谈,它们不是一个应用层面的东西

数据库的锁是访问数据库的时候,加在数据库上面的,至于读数据的时候是给行加锁,还是给整张表加锁;是加读锁还是写锁,这些数据数据库层面的东西,操作磁盘。

数据从数据库读出来是放在内存中的,程序的锁是作用于内存中的数据,多线程下操作内存中的数据一致性,操作内存。

ReadWriteLock 读写锁是与 Reentrantlock 的 Condition 相当相似的。

代码示例:

publicclassTestReadWriteLock{static Lock lock=newReentrantLock();privatestaticint value;static ReadWriteLock readWriteLock=newReentrantReadWriteLock();static Lock readLock= readWriteLock.readLock();static Lock writeLock= readWriteLock.writeLock();publicstaticvoidread(Lock lock){try{
            lock.lock();//故意延时,模拟读取操作
            Thread.sleep(1000);
            System.out.println("read over!");}catch(InterruptedException e){
            e.printStackTrace();}finally{
            lock.unlock();
            System.out.println("read unlock");}}publicstaticvoidwrite(Lock lock,int v){try{
            lock.lock();//故意延时,模拟写操作
            Thread.sleep(1000);
            value= v;
            System.out.println("write over!");}catch(InterruptedException e){
            e.printStackTrace();}finally{
            lock.unlock();}}publicstaticvoidmain(String[] args){//如果使用普通的reentryLock,就是排他锁,所有的读操作也都要排着队,一替一个轮流读,效率比较低//        Runnable readR = ()-> read(lock);//如果使用ReentrantReadWriteLock,中的读锁,是共享锁,允许所有的读操作并发执行,效率比较高。
        Runnable readR=()->read(readLock);//        Runnable writeR = ()->write(lock, new Random().nextInt());
        Runnable writeR=()->write(writeLock,newRandom().nextInt());for(int i=0; i<18; i++)newThread(readR).start();for(int i=0; i<2; i++)newThread(writeR).start();}}

使用步骤:同 ReentrantLock 相似,调用方法的时候,根据业务选择共享锁还是独占锁

四、CyclicBarrier

CyclicBarrier 的底层实现并非 AQS,但是属于同步工具类,所以在此处一并阐述了

Barrier 即栅栏,CyclicBarrier又称为 Java 中关于线程的计数器

CyclicBarrier的概念:相当于是个栅栏,在阻塞运行,当线程数量达到一定限制,会推翻栅栏,向下运行。线程都向下运行之后,栅栏重新起来,阻挡下一批线程,如此循环往复。

也可以理解成类似发电站水坝,达到一定刻度就会开闸放水一次。水位降低以后,重新关闭闸门,直到再次达到一定刻度,如此循环往复。

应用场景:

  • 定时任务:比如服务器当每满100个客户端连接,会做出什么反应,
  • 抽奖活动:参与人数满100人自动开奖

代码示例:

publicclassTestCyclicBarrier{publicstaticvoidmain(String[] args)throws InterruptedException{//只传一个参数,表示只是限制20个线程之后,推翻栅栏向下运行,运行后面的代码逻辑//CyclicBarrier barrier = new CyclicBarrier(20);//可以传两个参数,第二个参数可以重开一个线程,执行相应的业务逻辑
        CyclicBarrier barrier=newCyclicBarrier(3,newRunnable(){@Overridepublicvoidrun(){
                System.out.println("执行CyclicBarrier的任务——有3个人了,抽一次奖");}});for(int i=0; i<99; i++){newThread(()->{try{//这里可以书写具体的业务逻辑
                        System.out.println(Thread.currentThread().getName()+"到达");
                        barrier.await();//这里可以写具体的执行逻辑
                        Thread.sleep(0);}catch(InterruptedException e){
                        e.printStackTrace();}catch(BrokenBarrierException e){
                        e.printStackTrace();}}).start();}}}

执行结果:

Thread-0到达
Thread-1到达
Thread-2到达
执行CyclicBarrier的任务——有3个人了,抽一次奖
Thread-3到达
Thread-4到达
Thread-5到达
执行CyclicBarrier的任务——有3个人了,抽一次奖
......

使用步骤,主要有以下几个方法配合使用:

  1. CyclicBarrier barrier = new CyclicBarrier(count);只传一个参数,表示只是限制指定线程之后,推翻栅栏向下运行,运行后面的代码逻辑
  2. CyclicBarrier barrier = new CyclicBarrier(3, new Runnable());可以传两个参数,第二个参数可以重开一个线程,执行相应的业务逻辑
  3. barrier.await();阻塞,直到满足一定数量才能推翻栅栏向下运行

应用场景:CyclicBarrier 可以完成只有等其他线程都完事了,某些线程才可以继续向下运行,可以完成多个线程相互间协同工作。

在这里插入图片描述

一个复杂的操作,需要从数据库、网络、硬盘都读取到指定数据,才可以执行业务:

  • 串行执行:一个线程先从数据库取数据,得到数据之后,再去网络读数据,再去硬盘读数据。一旦发生数据库阻塞、网络延迟等意外,会造成整个流程效率很低(先洗脸,再洗衣服,再吃饭)
  • 并发执行:使用三个线程,不同线程执行不同操作,当三个线程都获得数据,此操作就可以执行(先洗衣服,同时洗脸、吃饭)

PS:本篇文章只是简单介绍了有可能会用到的同步工具类(好吧,其实用到的情况很少,更多还是为了应付面试。面试造火箭,入职拧螺丝嘛!都懂),有关同步工具类的选择,需要根据具体的业务场景

关联文章:

https://blog.csdn.net/javazejian/article/details/75043422

多线程—Java内存模型与线程

多线程——Volatile 关键字详解

多线程——线程安全及实现机制

多线程——深入剖析 Synchronized

多线程\并发编程——ReentrantLock 详解

多线程/并发编程——CAS、Unsafe及Atomic

  • 作者:执拗如少年
  • 原文链接:https://blog.csdn.net/qq_42583242/article/details/108922557
    更新时间:2022-09-27 13:26:16