JUC 常用并发工具类教程(包括可重入锁ReentrantLock、CountDownLatch、读写锁ReadWriteLock、信号量Semaphore、常见队列等)

一.简介

JUC(java.util .concurrent):用于处理线程的Java并发工具包,里边提供了各种各样的控制同步和线程通信的工具类,JDK 1.5以上支持。
包结构如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二.常用工具类

1.ReentrantLock

1.1 介绍
ReentrantLock是一个互斥锁,也是一个可重入锁。ReentrantLock锁在同一个时间点只能被一个线程锁持有,但是它可以被同一个线程多次获取,每获取一次AQS的state就加1,每释放一次state就减1。主要解决的问题是避免线程死锁。如果一个锁不可重入,一个已经获得同步锁X的线程,在释放锁X之前再去竞争锁X的时候,相当于会出现自己要等待自己释放锁。

1.2 ReentrantLock 和 Synchronized的对比
ReentrantLock通过AQS实现,Synchronized通过监视器模式;
ReentrantLock通过使用lock、unlock加锁解锁,更灵活,而且支持tryLock、lockInterruptibly等方法;
ReentrantLock有公平模式和非公平模式,Synchronized是非公平锁;
ReentrantLock可以关联多个条件队列,Synchronized只能关联一个条件队列。

1.3 简单使用

package com.example.springb_web.juc;

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {
    
    


    //默认是非公平锁,这里可以指定为公平锁
    //公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,
    // 永远都是队列的第一位才能得到锁
    Lock lock_fair = new ReentrantLock(true);

    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(()->{
    
    
            Lock lock = new ReentrantLock();
            //加锁,相当于synchronized(this),这里必须要紧跟在try代码
            lock.lock();
            try {
    
    
                System.out.println("t1执行开始");
                //每隔0.5秒执行一个任务
                for(int i=0;i<5;i++){
    
    
                    Thread.sleep(500);
                    System.out.println("t1执行任务"+i);
                }
                System.out.println("t1执行结束");
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally{
    
    
                //lock必须手动释放锁,且必须放在finally第一行
                lock.unlock();
            }
        });
        t1.start();

        Thread t2 = new Thread(()->{
    
    
            Lock lock = new ReentrantLock();
            boolean locked = false;
            try {
    
    
                Instant start = Instant.now();
                //tryLock:在指定时间内获取锁,如果获取不到,线程可以继续执行
                locked = lock.tryLock(5, TimeUnit.SECONDS);
                Instant end = Instant.now();
                long timeElapsed = Duration.between(start, end).toMillis();
                if(locked){
    
    
                    System.out.println("t2获取锁,耗时"+timeElapsed);
                }else{
    
    
                    System.out.println("t2没获取锁,耗时"+timeElapsed);
                }

            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } finally{
    
    
                //lock必须手动释放锁
                if(locked){
    
    
                    lock.unlock();
                }
            }
        });
        t2.start();

        Thread t3 = new Thread(()->{
    
    
            Lock lock = new ReentrantLock();
            try {
    
    
                //在一个线程等待锁的过程中,可以被打断
                lock.lockInterruptibly();
                System.out.println("t3执行开始");
                //每隔0.5秒执行一个任务
                for(int i=0;i<5;i++){
    
    
                    Thread.sleep(500);
                    System.out.println("t3执行任务"+i);
                }
                System.out.println("t3执行结束");
            } catch (Exception e) {
    
    
               System.out.println("t3被打断了");
            } finally{
    
    
                //lock必须手动释放锁
                lock.unlock();
            }
        });
        t3.start();
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        //这里让线程3秒后被打断,只有t3能成功打断,t1打断会报错
        //t1.interrupt();
        t3.interrupt();

    }




}

代码正确运行后结果如下:
在这里插入图片描述

2.CountDownLatch

2.1 介绍
CountDownLatch:一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。CountDownLatch最重要的方法是countDown()和await(),前者作用是是计数器数据减一,后者在当前计数到达零之前,await 方法会一直阻塞。

应用场景: 有一个任务想要往下执行, 但必须要等到其他的任务执行完毕后才可以继续往下执行。

2.2 实现原理
①CountDownLatch是一个同步计数器,他允许一个或者多个线程在另外一组线程执行完成之前一直等待,基于AQS共享模式实现的
②是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作来。

2.3 CountDownLatch与join
CountDownLatch可以手动控制在n个线程里调用n次countDown()方法使计数器进行减一操作,也可以在一个线程里调用n次执行减一操作。
而 join() 的实现原理是不停检查join线程是否存活,如果 join 线程存活则让当前线程永远等待。所以两者之间相对来说还是CountDownLatch使用起来较为灵活。

2.4 CountDownLatch和CyclicBarrier
(1)CountdownLatch 不能重复使用,CyclicBarrier 可以;
(2)CountdownLatch 是主线程等待多个工作线程结束,CyclicBarrier是多个线程之间互相等待,直到所有线程达到一个障碍点(Barrier point);
(3)CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的CyclicBarrier其中一个线程由于中断,错误,或超时导致永久离开屏障点,其他线程也将抛出异常(前者await方法写在线程外面,后者写在线程里面)

2.5 简单使用

package com.example.springb_web.juc;

import java.util.concurrent.CountDownLatch;

public class CountDownLatchTest {
    
    

    public static volatile int job_count;

    public static void main(String[] args) {
    
    
        new CountDownLatchTest().test();
    }

    //假设有100个任务,有五个线程同时运行,每个线程最多可执行30次任务,并且在任务执行完成后需要第一时间
    //得到通知,以便执行后续的任务,此时可以使用countDownLatch
    public void test(){
    
    
        Thread[] threads = new Thread[5];
        job_count = 100;
        CountDownLatch latch = new CountDownLatch(job_count);
        for(int i = 0;i<threads.length;i++){
    
    
            int order = i;
            threads[i] = new Thread(()->{
    
    
                for(int j=0;j<30;j++){
    
    
                    synchronized (this) {
    
    
                        if (job_count > 0) {
    
    
                            System.out.println("线程" + order + "执行了第" + j + "个任务");
                        }
                        job_count --;
                        latch.countDown();
                    }
                }

            });
        }
        for(int i = 0;i<threads.length;i++){
    
    
            threads[i].start();
        }
        try {
    
    
            //计数器归0前会一直阻塞,后面的程序不会走
            latch.await();
            System.out.println("任务执行完毕");
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }

    }
}

3.CyclicBarrier

3.1 介绍
CyclicBarrier:循环屏障,可以给离散的任务添加逻辑层次。
各个线程会互相等待,直到所有线程都完成了,再一起突破屏障。如上课时,只有所有学生都到了,老师才会上课。

3.2 简单使用

package com.example.springb_web.juc;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierTest {
    
    

    public static void main(String[] args) {
    
    
        new CyclicBarrierTest().test();
    }

    //假设有2个班级,每个班级学生数量为5个,每个班级为一个单位,一个班级交卷了才会执行给出提示。
    // 如果使用countDownLatch的话,需要写两遍代码,CyclicBarrier就可以执行两次
    public void test(){
    
    
        Thread[] threads = new Thread[5];
        //第一个参数代表执行的任务数,第二个参数
        CyclicBarrier cyclicBarrier = new CyclicBarrier(threads.length,()->System.out.println("这个班级已经全部交卷"));
        //将屏障重置为初始状态。
        cyclicBarrier.reset();
        for(int i = 0;i<threads.length*2;i++){
    
    
            int order = i;
            new Thread(()->{
    
    
                //这里必须加锁
                synchronized (this) {
    
    
                    System.out.println("学生" + order + "已经交卷");
                    //查询这个障碍是否处于破碎状态。
                    boolean broken = cyclicBarrier.isBroken();
                    if (!broken) {
    
    
                        //返回目前正在等待障碍的各方的数量。
                        int done = cyclicBarrier.getNumberWaiting();
                        //返回突破障碍所需的数量
                        int wait = cyclicBarrier.getParties();
                        System.out.println( "共有"+ wait + "个学生,已交卷" + done + "份");
                    }
                }
                try {
    
    
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
    
    
                    e.printStackTrace();
                }
            }).start();

        }



    }
}


4.Semaphore

(1)介绍:Semaphore 通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。

(2)使用场景:主要用于那些资源有明确访问数量限制的场景,常用于限流。
如:数据库连接池,同时连接的线程有数量限制,当连接达到了限制数量后,后来的线程只能排队等前面的线程释放了数据库连接才能获得连接。

(3)实现原理:
①semaphore类的核心是Sync内部类,它继承了AQS类,适当重写了一些方法,其他的方法都调用的这个Sync中的方法,包括Sync类的两个子类:FairSync(公平锁)和NonFairSync(非公平锁)。默认使用的是非公平锁。
②semaphore的信号量机制使用的是AQS类的state属性,利用CAS自旋来保证对state属性的操作是原子性的,默认每次获取或释放信号量都是1,除非你指定要使用的信号量或释放的信号量数。

(4)使用示例
①常用API:
public void acquire():表示一个线程获取1个许可,那么线程许可数量相应减少一个
public void release():表示释放1个许可,那么线程许可数量相应会增加
void acquire(int permits):表示一个线程获取n个许可,这个数量由参数permits决定
void release(int permits):表示一个线程释放n个许可,这个数量由参数permits决定
int availablePermits():返回当前信号量线程许可数量
int getQueueLength(): 返回等待获取许可的线程数的预估值

②示例:

package com.example.user.utils;

import java.util.concurrent.Semaphore;

public class SemaphoreTest {
    
    

    //模拟线程池连接数量控制
    public void dataBaseConnectionControl() {
    
    

        int maxConnectionNum = 8;
        int threadNum = 30;
        Semaphore semaphore = new Semaphore(maxConnectionNum);

        for(int i=0;i<threadNum;i++) {
    
    
            new Thread(() -> {
    
    
                try {
    
    
                    semaphore.acquire();
                    System.out.println("成功获取到数据库连接,当前可用的连接数共"+semaphore.availablePermits()
                            +"个,当前等待的线程共"+semaphore.getQueueLength()+"个。");
                    Thread.sleep(1000);
                    semaphore.release();
                    System.out.println("已有一个连接使用完毕!");
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }).start();
        }
    }

    public static void main(String[] args) {
    
    
        SemaphoreTest demo = new SemaphoreTest();
        demo.dataBaseConnectionControl();
    }

}

5.Exchanger

(1)介绍:Exchanger类的功能是使两个线程之间进行数据交换。Exchanger类中的exchange()方法具有阻塞的功能,也就是此方法在被调用后等待其他线程来获取数据,如果没有其他线程取得数据,在一直阻塞等待。
它比生产者、消费者模式使用的wait/notify要更加方便。

(2)使用场景:基因算法、数据校对、挂牌交易等场景。
如:基因算法(高中生物学过),很多性状都有基因序列,根据不同的基因序列可以计算出孩子的各种性状可能性(如AA/Aa与aa结合是Aa或者aa))。

(3)实现原理:
两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

(4)使用示例
①常用API:
String exchange(V x):用于交换,启动交换并等待另一个线程调用exchange;
String exchange(V x,long timeout,TimeUnit unit):用于交换,启动交换并等待另一个线程调用exchange,并且设置最大等待时间,当等待时间超过timeout便停止等待

②示例:

package com.example.user.utils;

import java.util.concurrent.Exchanger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExchangerTest {
    
    

    private static void lplTrade(String data1, Exchanger exchanger) {
    
    
        try {
    
    
            System.out.println(Thread.currentThread().getName() + "在交易截止之前把 " + data1 + " 交易出去");
            Thread.sleep((long) (Math.random() * 1000));

            String data2 = (String) exchanger.exchange(data1);
            System.out.println(Thread.currentThread().getName() + "交易得到" + data2);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
    
    

        ExecutorService executor = Executors.newCachedThreadPool();

        final Exchanger exchanger = new Exchanger();

        //注意这里的交易成员的数量是双数,如果是单数,最后会有一个线程得不到交换,一直在阻塞。
        executor.execute(new Runnable() {
    
    
            String data1 = "the shy";

            @Override
            public void run() {
    
    
                lplTrade(data1, exchanger);
            }
        });


        executor.execute(new Runnable() {
    
    
            String data2 = "369";

            @Override
            public void run() {
    
    
                lplTrade(data2, exchanger);
            }
        });

        executor.execute(new Runnable() {
    
    
            String data3 = "bin";

            @Override
            public void run() {
    
    
                lplTrade(data3, exchanger);
            }
        });

        executor.execute(new Runnable() {
    
    
            String data4 = "ale";

            @Override
            public void run() {
    
    
                lplTrade(data4, exchanger);
            }
        });

        executor.execute(new Runnable() {
    
    
            String data5 = "yskm";

            @Override
            public void run() {
    
    
                lplTrade(data5, exchanger);
            }
        });

        executor.execute(new Runnable() {
    
    
            String data5 = "zeus";

            @Override
            public void run() {
    
    
                lplTrade(data5, exchanger);
            }
        });

        executor.shutdown();
    }

}

6.phaser

(1)介绍:Phaser是JDK 7新增的一个同步辅助类“阶段器”,用来解决控制多个线程分阶段共同完成任务的情景问题。其作用相比CountDownLatch和CyclicBarrier更加灵活。

(2)使用场景:同CoutDownLatch和CyclicBarrier,而且它支持对任务的动态调整,并支持分层结构来达到更高的吞吐量。
如:5个学生一起参加考试,一共有三道题,要求所有学生到齐才能开始考试,全部同学都做完第一题,学生才能继续做第二题,全部学生做完了第二题,才能做第三题,所有学生都做完的第三题,考试才结束。
分析这个题目:这是一个多线程(5个学生)分阶段问题(考试考试、第一题做完、第二题做完、第三题做完),所以很适合用Phaser解决这个问题。。

(3)实现原理:
参考链接

(4)使用示例
①API:
int register():增加一个数,返回当前阶段号
int arriveAndAwaitAdvance():到达后等待其他任务到达,返回到达阶段号
protected boolean onAdvance(int Phase , int registeredParties):类似于CyclicBarrier的触发命令,通过重写该方法来增加阶段到达动作
boolean isTerMinated():判断是否结束

②示例:

package com.example.user.utils;

import java.util.concurrent.Phaser;
import java.util.concurrent.TimeUnit;

public class PhaserTest extends Phaser {
    
    

    /**
     * 题目:20个学生参加考试,一共有两道题,要求所有学生到齐才能开始考试,全部做完第一题,才能继续做第二题。
     *
     * Phaser有phase和party两个重要状态,
     * phase表示阶段,party表示每个阶段的线程个数,
     * 只有每个线程都执行了phaser.arriveAndAwaitAdvance();
     * 才会进入下一个阶段,否则阻塞等待。
     * 例如题目中20个学生(线程)都调用phaser.arriveAndAwaitAdvance();就进入下一步
     */
    public static void main(String[] args) {
    
    
        MyPhaser phaser = new MyPhaser();
        StudentTask[] studentTask = new StudentTask[20];
        //注册
        for (int i = 0; i < studentTask.length; i++) {
    
    
            studentTask[i] = new StudentTask(phaser);
            phaser.register();    //注册一次表示phaser维护的线程个数
        }

        Thread[] threads = new Thread[studentTask.length];
        for (int i = 0; i < studentTask.length; i++) {
    
    
            threads[i] = new Thread(studentTask[i], "Student "+i);
            threads[i].start();
        }

        //等待所有线程执行结束
        for (int i = 0; i < studentTask.length; i++) {
    
    
            try {
    
    
                threads[i].join();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }

        System.out.println("Phaser has finished:"+phaser.isTerminated());

    }

}
class MyPhaser extends Phaser {
    
    

    @Override
    protected boolean onAdvance(int phase, int registeredParties) {
    
        //在每个阶段执行完成后回调的方法

        switch (phase) {
    
    
            case 0:
                return studentArrived();
            case 1:
                return finishFirstExercise();
            case 2:
                return finishExam();
            default:
                return true;
        }

    }

    private boolean studentArrived(){
    
    
        System.out.println("学生准备好了,学生人数:"+getRegisteredParties());
        return false;
    }

    private boolean finishFirstExercise(){
    
    
        System.out.println("第一题所有学生做完");
        return false;
    }


    private boolean finishExam(){
    
    
        System.out.println("第二题所有学生做完,结束考试");
        return true;
    }

}


class StudentTask implements Runnable {
    
    

    private Phaser phaser;

    public StudentTask(Phaser phaser) {
    
    
        this.phaser = phaser;
    }

    @Override
    public void run() {
    
    
        System.out.println(Thread.currentThread().getName()+"到达考试");
        phaser.arriveAndAwaitAdvance();

        System.out.println(Thread.currentThread().getName()+"做第1题时间...");
        doExercise1();
        System.out.println(Thread.currentThread().getName()+"做第1题完成...");
        phaser.arriveAndAwaitAdvance();

        System.out.println(Thread.currentThread().getName()+"做第2题时间...");
        doExercise2();
        System.out.println(Thread.currentThread().getName()+"做第2题完成...");
        phaser.arriveAndAwaitAdvance();
    }

    private void doExercise1() {
    
    
        long duration = (long)(Math.random()*10);
        try {
    
    
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

    private void doExercise2() {
    
    
        long duration = (long)(Math.random()*10);
        try {
    
    
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

}

7.ReentrantReadWriteLock

(1)介绍:读写锁,包含两种锁–读锁(持有时可进行读操作)和写锁(持有时可进行写操作)。其中读锁是共享锁,写锁是排他锁。允许并发读,只能独占写。

(2)使用场景:读读并发、读写互斥、写写互斥。如果一个对象并发读的场景大于并发写的场景,那就可以使用 ReentrantReadWriteLock来达到保证线程安全的前提下提高并发效率。如:缓存的更新操作。
PS:高并发的情况下,读写锁性能比排它锁要好一些。如果同时都在读的时候,是不需要锁资源的,只有读和写在同时工作的时候才需要锁资源,如果直接用互斥锁,会导致资源的浪费。 ​

(3)实现原理:
自定义队列同步器实现的,读写状态就是其同步器的同步状态,有公平锁和非公平锁两种实现方式;
同步状态(state,一个整型变量)分成了两部分,高16位表示读,低16位表示写,通过位运算确定读和写各自的状态;
写锁是一个支持重进入的排他锁,读锁是一个支持重进入的共享锁,CAS操作修改state的值;
使用ThreadLocal封装HoldCounter对象,保证每个线程记录自己的重入锁数量;
使用锁降级提高效率。

锁降级(不支持锁升级):是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

(4)使用示例
①API:
构造方法:
public ReentrantReadWriteLock():默认构造方法,非公平锁
public ReentrantReadWriteLock(boolean fair):true 为公平锁
常用API:
public ReentrantReadWriteLock.ReadLock readLock():返回读锁
public ReentrantReadWriteLock.WriteLock writeLock():返回写锁
public void lock():加锁
public void unlock():解锁
public boolean tryLock():尝试获取锁

②使用示例:
redis使用教程可以参考:链接
JedisServiceImpl.java中我定义了方法getDataByReadWriteLock()

public String getDataByReadWriteLock(String namespace,String key) {
    
    
        //获取读锁
        lock.readLock().lock();
        try{
    
    
            //如果缓存有效, 直接使用data
            String data = (String) cacheManager.get(namespace,key);
            if(!StringUtils.isEmpty(data)){
    
    
                return data;
            }
        }finally {
    
    
            //释放读锁
            lock.readLock().unlock();
        }

        //获取写锁
        lock.writeLock().lock();
        try{
    
    
            //如果缓存无效,更新cache;有效时间120秒
            cacheManager.save(namespace,key,"从数据库查的数据",120);
            return "从数据库查的数据";
        }finally {
    
    
            //释放写锁
            lock.writeLock().unlock();
        }
    }

8.LockSupport

(1)介绍:用来挂起和唤醒线程的工具类。park()和unpark()方法,分别可以阻塞和唤醒线程(作用类似于wait/notify方法)

(2)与wait/notify,await/signal对比
①wait/notify必须配合Synchronized关键字进行使用,否则会报异常
②ReentrantLock的await()和signal()方法也是一样,必须配置lock()方法进行使用,否则会报监视器异常,同时必须先阻塞才能被唤醒
③LockSupport可以直接进行使用,不需要配合其他关键词或方法;即使先执行"唤醒"方法,再执行阻塞方法,线程依旧可以顺利执行

(3)实现原理
LockSupport引入了许可证的思想,对象最多只能拥有1个许可证,当执行unpark()方法的时候会赋给指定对象一个许可证,执行park()方法的时候判断当前对象是否拥有许可证,没有则进行阻塞,
直到其拥有一个许可证才放行,如果拥有则直接放行。放行后将该对象拥有的许可证置空。

PS:使用LockSupport进行多线程操作时,通常需要配合阻塞队列同步使用,以便存储我们需要唤醒的对象。

9.常用队列的使用

参考:https://blog.csdn.net/tttalk/article/details/121947982?spm=1001.2014.3001.5501

猜你喜欢

转载自blog.csdn.net/tttalk/article/details/127066308