JUC并发工具类在大厂中的应用

180 阅读9分钟

1.ReentrantLock

ReentrantLock是一种可重入独占锁,允许同一个线程多次获得同一个锁而不被阻塞。 它的功能类似于sychronized是一种互斥锁,可以保证线程安全。相对于synchronized,ReentrantLock具备如下特点:

  • 可中断
  • 支持非公平锁和公平锁,默认是非公平锁
  • 可设置超时时间
  • 支持多个条件变量
  • 与sychronized一样,都支持可重入

它的主要应用场景是在多线程环境下对共享资源进行独占式访问,以保证数据的一致性和安全性

image.png

image.png

常用的API

Lock接口

ReentrantLock实现了Lock接口的规范

  • void lock()

获取锁,调用该方法,当前线程会获取锁,当锁获得后,该方法返回

  • void lockInterruptibly() throws InterruptedException

可中断的获取锁,该方法会相应中断,即在获取锁的过程中可以中断线程,这是和lock()方法不同的地方

  • boolean tryLock()

尝试非阻塞的获取锁,调用该方法后立即返回,如果能获取到返回true,否则返回false

  • boolean tryLock(long time,TimeUnit unit) throws InterruptException

超时获取锁,当前线程在以下三种情况下会被返回:

  • 当前线程在超时时间内获取了锁
  • 当前线程在超时时间内被中断
  • 超时时间结束,返回false
  • void unlock()

释放锁

  • Condition new Condition()

获取等待通知组件,该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将会释放锁

基本语法

//加锁、阻塞
lock.lock();
try{
...
}finally{

//解锁
lock.unlock();
}
==============================
//加锁、阻塞
if(lock.tryLock(1,TimeUtil.SECONDS))
try{
...
}finally{

//解锁
lock.unlock();
}

使用的过程中注意的事项:

  1. 默认情况下,ReentrantLock实现的是非公平锁而非公平锁
  2. 加锁次数和释放锁次数一定要保持一致,否则或导致线程阻塞和程序异常
  3. 加锁操作一定要放在try代码之前,这样可以避免未加锁成功又释放锁的异常
  4. 释放锁一定要放在finally中,否则会导致线程阻塞

ReentrantLock的使用

模拟抢票

public class ReentrantLockDemo{
private final ReentrantLock lock = new ReentrantLock();
private static int tickets = 8;
public void buyTickets(){
lock.lock();
try{
if(tickets > 0)//还有票
try{
Thread.sleep(10);//模拟并发的效果
}catch(InterruptException e){
e.printStackTrace();
}
System.out.printLn(Thread.getCurrentThread().getName() + "购买了第" + ticket + "张票");
}
else{
System.out.println("票已经卖完了" + Thread.currentThread.getName() + "抢票失败");
}
}
finally{
lock.unlock();
}
}
public static void main(String[] args){
ReentrantLockDemo ticketSystem = new ReentrantLockDemo();
for(i = 0;i <= 10;i++ )
Thread thread = new Thread(() ->{
ticketSystem.buyTicket();//抢票

},"线程" + i).start();
}
try{
Thread.sleep(3000);
}catch(InterruptException e){
throw new RuntimeException(e);
}
System.out.println("剩余票数:" + tickets);
}
}

公平锁和非公平锁 ReentrantLock支持公平锁和非公平锁两种模式: 默认是非公平锁

ReentrantLock lock = new ReentrantLock();//参数默认为false,非公平锁
ReentrantLock lock = new ReentrantLock(true);//公平锁

买票的时候允许插队的情况,插队就是一种不公平的现象,用到非公平锁

image.png 可重入锁: 可重入锁又名递归锁,可以一定程度上避免死锁,是指同一线程在外层方法获取锁的时候,再次进入该线程的内层方法会自动获取(前提是锁对象得是同一个),不会因为之前获取过还没释放而阻塞。Java中的ReentrantLock和Synchronized都是可重入锁,在实际开发过程中,可重入锁通常用于递归操作调用同一个类中的其他的方法、锁嵌套等场景。

class Counter{
private final ReentrantLock lock = new ReentrantLock(); // 创建 ReentrantLock 对象
public void recursiveCall(int num) { lock.lock(); // 获取锁
try {
            if (num == 0) {
                return;

}
        
System.out.println("执行递归,num = " + num);
    recursiveCall(num - 1);
} finally {
  

lock.unlock(); // 释放锁 }
    }
    public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter(); // 创建计数器对象
// 测试递归调用
        counter.recursiveCall(10);
    }
}
         

结合Condition实现生产者消费者模式

java.util.concurrent类库中提供Condition类来实现线程之间的协调。调用Condition.await() 方法使 线程等待,其他线程调用Condition.signal() 或 Condition.signalAll() 方法唤醒等待的线程。 注意:调用Condition的await()和signal()方法,都必须在lock保护之内。 (调用对象的wait()方法和notify()方法需要在synchronized修饰的同步代码块中) 案例:基于ReentrantLock和Condition实现一个简单队列

public class ReentrantLockDemo3{
public static void main(String[] args) { // 创建队列
Queue queue = new Queue(5); //启动生产者线程
new Thread(new Producer(queue)).start(); //启动消费者线程
new Thread(new Customer(queue)).start();
} }
/**
* 队列封装类 */
classQueue{
private Object[] items ; int size = 0;
                  
int takeIndex;
int putIndex;
private ReentrantLock lock;
public Condition notEmpty; //消费者线程阻塞唤醒条件,队列为空阻塞,生产者生产完唤醒 public Condition notFull; //生产者线程阻塞唤醒条件,队列满了阻塞,消费者消费完唤醒
public Queue(int capacity){
    this.items = new Object[capacity];
    lock = new ReentrantLock();
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}
public void put(Object value) throws Exception { //加锁
    lock.lock();
    try {
while (size == items.length) // 队列满了让生产者等待 notFull.await();
        items[putIndex] = value;
        if (++putIndex == items.length)
            putIndex = 0;
        size++;
notEmpty.signal(); // 生产完唤醒消费者
} finally {
System.out.println("producer生产:" + value); //解锁
lock.unlock();
} }
public Object take() throws Exception {
    lock.lock();
try {
// 队列空了就让消费者等待 
while (size == 0)
notEmpty.await();
            Object value = items[takeIndex];
            items[takeIndex] = null;
            if (++takeIndex == items.length)
                takeIndex = 0;
            size--;
notFull.signal(); //消费完唤醒生产者生产
            return value;
        } finally {
            lock.unlock();
        }
} }
/** *生产者 */
classProducerimplementsRunnable{
    private Queue queue;
    public Producer(Queue queue) {
        this.queue = queue;
}
@Override
    public void run() {
        try {
// 隔1秒轮询生产一次 while (true) {
                Thread.sleep(1000);
                queue.put(new Random().nextInt(1000));
            }
        } catch (Exception e) {
            e.printStackTrace();
} }
}
/**
* 消费者 */
classCustomerimplementsRunnable{
    private Queue queue;
    public Customer(Queue queue) {
        this.queue = queue;
}
@Override
    public void run() {
        try {
// 隔2秒轮询消费一次 while (true) {
                Thread.sleep(2000);
System.out.println("consumer消费:" + queue.take()); }
        } catch (Exception e) {
            e.printStackTrace();
} }
}
                      
                                  

                  

1.3 应用场景总结

ReentrantLock具体应用场景如下:

  1. 解决多线程竞争资源的问题,例如多个线程同时对同一个数据库进行写操作,可以使用ReentrantLock保证每次 只有一个线程能够写入。

  2. 实现多线程任务的顺序执行,例如在一个线程执行完某个任务后,再让另一个线程执行任务。

  3. 实现多线程等待/通知机制,例如在某个线程执行完某个任务后,通知其他线程继续执行任务。

2. Semaphore

Semaphore(信号量)是一种用于多线程编程的同步工具,用于控制同时访问某个资源的线程数量。 Semaphore维护了一个计数器,线程可以通过调用acquire()方法来获取Semaphore中的许可证,当计数器为0时,调用acquire()的线程将被阻塞,直到有其他线程释放许可证;线程可以通过调用 release()方法来释放Semaphore中的许可证,这会使Semaphore中的计数器增加,从而允许更多的 线程访问共享资源。

2.1 常用API 构造器

  • permits 表示许可证的数量(资源数)
  • fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程

常用方法

  • acquire() 表示阻塞并获取许可
  • tryAcquire() 方法在没有许可的情况下会立即返回 false,要获取许可的线程不会阻塞
  • release() 表示释放许可

2.2 Semaphore使用 Semaphore实现服务接口限流

 @Slf4j
public class SemaphoreDemo{ 
/**
* 同一时刻最多只允许有两个并发
*/
private static Semaphore semaphore = new Semaphore(2);
private static Executor executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
    for(int i=0;i<10;i++){
        executor.execute(()->getProductInfo2());
    }
}
public static String getProductInfo() {
    try {
semaphore.acquire(); log.info("请求服务"); Thread.sleep(2000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }finally {
        semaphore.release();
}
return "返回商品详情信息"; }
public static String getProductInfo2() {
if(!semaphore.tryAcquire()){ log.error("请求被流控了"); return "请求被流控了";
} try {
log.info("请求服务");
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }finally {
        semaphore.release();
    }
return "返回商品详情信息";
}
}
                                      

Semaphore实现数据库连接池

publicclassSemaphoreDemo2{
    public static void main(String[] args) {
        final ConnectPool pool = new ConnectPool(2);
        ExecutorService executorService = Executors.newCachedThreadPool();
//5个线程并发来争抢连接资源 for(inti=0;i<5;i++){
            final int id = i + 1;
            executorService.execute(new Runnable() {
@Override
                public void run() {
                    Connect connect = null;
                    try {
System.out.println("线程" + id + "等待获取数据库连接");
connect = pool.openConnect();
System.out.println("线程" + id + "已拿到数据库连接:" + connect); //进行数据库操作2秒...然后释放连接
Thread.sleep(2000);
System.out.println("线程" + id + "释放数据库连接:" + connect);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        pool.releaseConnect(connect);
}
} });
} }
}
                                
    //数据库连接池
classConnectPool{
private int size;
private Connect[] connects;

 //记录对应下标的Connect是否已被使用
private boolean[] connectFlag;
//信号量对象
private Semaphore semaphore;
/**
* size:初始化连接池大小
*/
public ConnectPool(int size) {
this.size = size;
semaphore = new Semaphore(size, true);
connects = new Connect[size];
connectFlag = new boolean[size];
initConnects();//初始化连接池
}
private void initConnects() {
for (int i = 0; i < this.size; i++) {
               connects[i] = new Connect();
                       
}
}

/**
* 获取数据库连接
*
* @return
* @throws InterruptedException
*/
public Connect openConnect() throws InterruptedException {
//得先获得使用许可证,如果信号量为0,则拿不到许可证,一直阻塞直到能获得semaphore.acquire();
return getConnect();
}

private synchronized Connect getConnect() {
                
      

        for (int i = 0; i < connectFlag.length; i++) {
            if (!connectFlag[i]) {
//标记该连接已被使用 connectFlag[i] = true; return connects[i];
} }
        return null;
    }
/**
* 释放某个数据库连接 */
    public synchronized void releaseConnect(Connect connect) {
        for (int i = 0; i < this.size; i++) {
            if (connect == connects[i]) {
                connectFlag[i] = false;
                semaphore.release();
} }
} }
/**
* 数据库连接 */
classConnect{
private static int count = 1;
    private int id = count++;
public Connect() { //假设打开一个连接很耗费资源,需要等待1秒 try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
System.out.println("连接#" + id + "#已与数据库建立通道!");
                                      
  
}
@Override
    public String toString() {
        return "#" + id + "#";
} }

  

2.3 应用场景总结

以下是一些使用Semaphore的常见场景:

  1. 限流:Semaphore可以用于限制对共享资源的并发访问数量,以控制系统的流量。
  2. 资源池:Semaphore可以用于实现资源池,以维护一组有限的共享资源。

3. CountDownLatch

CountDownLatch(闭锁)是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作 集。 CountDownLatch使用给定的计数值(count)初始化。await方法会阻塞直到当前的计数值 (count),由于countDown方法的调用达到0,count为0之后所有等待的线程都会被释放,并且随 后对await方法的调用都会立即返回。这是一个一次性现象 —— count不会被重置。

3.1 常用API

构造器 常用方法


// 调用 await() 方法的线程会被挂起,它会等待直到 count 值为 0 才继续执行
public void await() throws InterruptedException{};
// 和 await() 类似,若等待 timeout 时长后,count 值还是没有变为 0,不再等待,继续执行
public boolean await(longtimeout,TimeUnitunit) throws InterruptedException{};
// 会将 count 减 1,直至为 0
   
public void countDown(){};

3.2 CountDownLatch使用

模拟百米赛跑:

public class CountDownLatchDemo{
// begin 代表裁判 初始为 1
private static CountDownLatch begin = new CountDownLatch(1);
// end 代表玩家 初始为 8
private static CountDownLatch end = new CountDownLatch(8);
    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 8; i++) {
            new Thread(new Runnable() {
               准备好了");
跑步");
终点");
@SneakyThrows
@Override
public void run() {
// 预备状态 System.out.println("参赛者"+Thread.currentThread().getName()+ "已经
// 等待裁判吹哨
begin.await();
// 开始跑步 System.out.println("参赛者"+Thread.currentThread().getName() + "开始
Thread.sleep(1000);
// 跑步结束, 跑完了 System.out.println("参赛者"+Thread.currentThread().getName()+ "到达
// 跑到终点, 计数器就减一
    end.countDown();
}
               }).start();
}
// 等待 5s 就开始吹哨 Thread.sleep(5000);
   
     System.out.println("开始比赛");
// 裁判吹哨, 计数器减一
begin.countDown();
// 等待所有玩家到达终点
end.await();
System.out.println("比赛结束");

}

多任务完成后合并汇总

很多时候,我们的并发任务,存在前后依赖关系;比如数据详情页需要同时调用多个接口获取数据, 并发请求获取到数据后、需要进行结果合并;或者多个数据操作完成后,需要数据check。


public class CountDownLatchDemo2{
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(5); for(inti=0;i<5;i++){
            final int index = i;
            new Thread(() -> {
try {
Thread.sleep(1000 + ThreadLocalRandom.current().nextInt(2000)); System.out.println("任务" + index +"执行完成"); countDownLatch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
}
// 主线程在阻塞,当计数器为0,就唤醒主线程往下执行 countDownLatch.await(); System.out.println("主线程:在所有任务运行完成后,进行结果汇总");
} }
                    

3.3 应用场景总结

以下是使用CountDownLatch的常见场景:

  1. 并行任务同步:CountDownLatch可以用于协调多个并行任务的完成情况,确保所有任务都完成后再继续执行下一步操作。
  2. 多任务汇总:CountDownLatch可以用于统计多个线程的完成情况,以确定所有线程都已完成工作。
  3. 资源初始化:CountDownLatch可以用于等待资源的初始化完成,以便在资源初始化完成后开始使用。

4. CyclicBarrier

CyclicBarrier(回环栅栏或循环屏障),是 Java 并发库中的一个同步工具,通过它可以实现让一组线 程等待至某个状态(屏障点)之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后, CyclicBarrier可以被重用。

4.1 常用API 构造器

// parties表示屏障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 我已经到达了屏 障,然后当前线程被阻塞。
public CyclicBarrier(int parties)
// 用于在线程到达屏障时,优先执行 barrierAction,方便处理更复杂的业务场景(该线程的执行时机是
在到达屏障之后再执行)
public CyclicBarrier(int parties, Runnable barrierAction)

常用方法

//指定数量的线程全部调用await()方法时,这些线程不再阻塞
// BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断
或者超时
public int await() throws InterruptedException,BrokenBarrierException
public int await(longtimeout,TimeUnitunit)throwsInterruptedException,
  BrokenBarrierException, TimeoutException
  //循环 通过reset()方法可以进行重置 public void reset();

4.2 CyclicBarrier使用

模拟人满发车 利用CyclicBarrier的计数器能够重置,屏障可以重复使用的特性,可以支持类似“人满发车”的场景

public class CyclicBarrierDemo{
public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5,
() -> System.out.println("人齐了,准备发车"));
        for (int i = 0; i < 10; i++) {
            final int id = i+1;
            executorService.submit(new Runnable() {
@Override
                public void run() {
                    try {
System.out.println(id+"号马上就到");
int sleepMills = ThreadLocalRandom.current().nextInt(2000); Thread.sleep(sleepMills);
System.out.println(id + "号到了,上车"); cyclicBarrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }catch(BrokenBarrierException e){
                        e.printStackTrace();
} }
}); }
}}
                            
    

多线程批量处理数据

public classCyclicBarrierBatchProcessorDemo{
public static void main(String[] args) { //生成数据
             List<Integer> data = new ArrayList<>();
             for (int i = 1; i <= 15; i++) {
                 data.add(i);
//指定数据处理批次
             int batchSize = 5;
             CyclicBarrierBatchProcessor processor = new CyclicBarrierBatchProcessor(data,
     batchSize);
     }
//处理数据 processor.process(batchData -> {
for (Integer i : batchData) { System.out.println(Thread.currentThread().getName() + "处理数据" + i);
} });
} }
class CyclicBarrierBatchProcessor{ private List<Integer> data; private int batchSize;
private CyclicBarrier barrier; private List<Thread> threads;