【JUC并发工具类与相关的应用场景详解】

一、 并发操作工具类—ReentrantLock

1.1、什么是ReentrantLock

ReentrantLock是一种可重入的独占锁,允许同一个线程多次获取同一个锁而不被阻塞,同synchronized一样,是一种互斥锁,但是相对synchronized来说,ReentrantLock有以下特点:

  1. 可中断
  2. 可以设置超时时间
  3. 公平锁与非公平锁的切换
  4. 支持多个条件变量
  5. 与synchronized一样,支持可重入

如下图:
在这里插入图片描述

在多个线程同时操作同一个资源时,ReentrantLock相当于是给这些线程加了一道门锁,每个线程想要获取到资源,都需要先获取到这把钥匙,才能访问门锁后面持有的资源

1.2、ReentrantLock常用的API

ReentrantLock实现了Lock接口规范,常见的API如下:

  • void lock():当前线程调用该方法获取锁
lock.lock();//加锁
try{
	//业务代码
	....
} finally {
	lock.unlock();//解锁
}
  • void lockInterruptibly() throws InterruptedException:可中断的获取锁,和lock()方法不同点在于,该方法会响应中断,也就是在锁的获取过程中可以中断当前获取锁的线程
  • boolean tryLock():尝试非阻塞的获取锁资源,如果能获取到,返回true,否则返回false
  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException:设置超时时间尝试的获取锁资源,如果当前线程在超时时间之前能获取锁,就会返回true,反之返回false
if(lock.trylock(1, TimeUnit.SECONDS)){//尝试加锁
	try{
		//业务代码
		....
	} finally {
		lock.unlock();//解锁
	}
}

  • void unlock()释放当前线程锁资源
  • Condition newCondition()获取到等待/通知组件,该组件和当前的线程持有的锁进行绑定,当前的线程只有获取到锁,才能调用该组件的await()方法,而且调用后,当前线程将会把持有的锁资源释放掉
package com.practice.juc;

import java.util.Random;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockConditionTest {

    public static void main(String[] args) {
        //创建队列
        Queue queue = new Queue(10);
        //启动生产者线程
        new Thread(new Producer(queue)).start();
        //启动消费者线程
        new Thread(new Customer(queue)).start();

    }

    /**
     * 队列的定义
     */
    static class Queue{
        //元素数组
        private Object[] items;
        //数组长度大小
        int size = 0;

        //出队指针
        int takeIndex;

        //入队指针
        int putIndex;

        private ReentrantLock lock; //锁资源

        public Condition emptyCondition;//生产者阻塞时,如果当前队列为空,唤醒生产者线程

        public Condition fullCondition;//消费者者阻塞时,如果当前队列满了,唤醒消费者线程

        public Queue(int capacity){
            this.items = new Object[capacity];
            lock = new ReentrantLock();
            emptyCondition = lock.newCondition();
            fullCondition = lock.newCondition();
        }


        /**
         * 向队列中添加元素=>入队
         * @param value 添加的元素
         * @throws Exception
         */
        public void put(Object value) throws Exception{
            //加锁
            lock.lock();
            try{
                //如果当前使用的长度达到队列的长度,就需阻塞生产者线程
                while(size == items.length){
                    fullCondition.await();
                }
                //往队列中添加元素
                items[putIndex] = value;
                //如果是当前队列的下标达到了队列的长度,那就将当前下标置为0
                if(++putIndex == items.length){
                    putIndex = 0;
                }
                //当前长度加1并且唤醒消费者线程
                size ++;
                emptyCondition.signal();
            }finally {
                System.out.println("producer生产:"+value);
                lock.unlock();
            }

        }
        /**
         * 队列中出队
         * @throws Exception
         */
        public Object take() throws Exception{
            //加锁
            lock.lock();
            try{
                //如果当前的队列为空的话,就需要阻塞当前消费者线程
                while(size == 0){
                    emptyCondition.await();
                }
                Object value  = items[takeIndex];

                items[takeIndex] = null;
                if(++takeIndex == items.length){
                    takeIndex = 0;
                }

                size --;
                fullCondition.signal();//消费完之后,唤醒生产者生产消息
                return value;
            }finally {
                lock.unlock();
            }

        }
        }
    }

/**
 * 生产者
 */
class Producer implements Runnable{

        /**
         * 生产队列
         */
        private ReentrantLockConditionTest.Queue queue;

        public Producer(ReentrantLockConditionTest.Queue queue){
            this.queue = queue;
        }
        @Override
        public void run() {
            try {
                //每个1秒轮询的生产一次
                while(true){
                    Thread.sleep(1000);
                    queue.put(new Random().nextInt());
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    class Customer implements Runnable{

        /**
         * 消费队列
         */
        private ReentrantLockConditionTest.Queue queue;

        public Customer(ReentrantLockConditionTest.Queue queue){
            this.queue = queue;
        }
        @Override
        public void run() {
            try {
                while(true){
                    //每个2秒轮询的消费一次
                    Thread.sleep(2000);
                    System.out.println("customer消费消费消息:"+ queue.take());
                }

            }catch (Exception e){
                e.printStackTrace();
            }

        }
    }

运行结果截图:
在这里插入图片描述

1.3、ReentrantLock的使用

在使用前我们需要注意4个问题
1、默认情况下,ReentrantLock是非公平锁,在声明ReentrantLock()时,可以设置其参数为true=>就为公平锁;
2、因为ReentrantLock是可重入锁,所以当前线程的加锁次数和释放锁的次数要一致,否则就会导致线程阻塞或是程序异常;
3、加锁操作一定要放到try语句块代码之前,可以避免未加锁成功又释放锁的异常;
4、锁释放一定要放到finally中,不然会导致线程阻塞(程序出现异常情况也需要释放锁)

例如:抢票程序如下示例

package com.practice.juc;

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {

    private final ReentrantLock lock = new ReentrantLock();//默认是非公平锁,ReentrantLock(true)为公平锁
    private static int tickets = 8;//能卖的票总数

    /**
     * 买票操作
     */
    public void buyTicket() {
        lock.lock();//对资源加锁
        try {
            if(tickets > 0){ //买票前判断是否还有余票
                try {
                    System.out.println(Thread.currentThread().getName()+"=>买票成功~~~"+",购买了第"+tickets+"张票");
                    Thread.sleep(10);
                }catch (Exception e){
                    e.printStackTrace();
                }

            }else {
                System.out.println("票卖完了~~~"+Thread.currentThread().getName()+"抢票失败~");
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        finally {
            lock.unlock();//释放锁
        }
    }

    /**
     * 测试并发买票
     * @param args
     */
    public static void main(String[] args) {
        ReentrantLockTest reentrantLockTest = new ReentrantLockTest();

        //模拟10个线程进行买8张票
        for (int i = 0; i < 11; i++) {
            Thread thread = new Thread(()->{
                reentrantLockTest.buyTicket();//进行抢票
            },"线程"+i);
            thread.start(); //启动线程
        }

        try{
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("剩余票数为:"+tickets);
    }
}

程序运行结果为:
在这里插入图片描述

1.4、公平锁与非公平锁

ReentrantLock可支持公平锁与非公平锁两种方式:
公平锁:线程在获取锁时,按照等待的先后顺序进行排队获取锁;
非公平锁:线程在获取锁时,不按照等待的先后顺序获取锁,而是随机获取锁,ReentrantLock默认是非公平锁;
如下:

  private final ReentrantLock lock = new ReentrantLock();//默认是非公平锁
  private final ReentrantLock lock = new ReentrantLock(true);//更该参数为true该为:公平锁

如下图示:
在这里插入图片描述

在这里插入图片描述

1.5、可重入锁

可重入锁又称为递归锁,是指在同一个线程在外层方法获取锁的时候,再用该线程的内层方法自动获取锁,不会因为之前已经获取过还没释放锁而阻塞,在一定程度上可避免死锁问题,如下图示:

package com.practice.juc;

import java.util.concurrent.locks.ReentrantLock;

public class RecursionTest {

    private final ReentrantLock lock = new ReentrantLock();

    /**
     * 递归调用方法
     * @param callNum
     */
    public void recursionCall(int callNum){
        lock.lock();
        try{
            if(callNum == 0){
                return;
            }
            System.out.println("递归执行,callNum="+callNum);
            //递归调用方法
            recursionCall(callNum - 1);
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        RecursionTest recursionTest = new RecursionTest();
        recursionTest.recursionCall(10);
    }
}

运行结果如下图:
在这里插入图片描述

1.6、ReentrantLock的应用场景

  1. 解决多线程竞争资源的问题,例如多线程同时队同一个数据进行写操作;
  2. 实现多线程任务顺序执行
  3. 实现等待/通知机制

二、 并发操作工具类—Semaphore

2.1、什么是Semaphore

Semaphore主要是通过信号量的方式来控制访问资源的线程,具体的过程如下图:
在这里插入图片描述

2.2、Semaphore常用的API

在Semaphore类中的构造器为:
在这里插入图片描述

在声明这个Semaphore时,其中的permits表示许可数,fair表示线程竞争的公平性

其中在这个类中,包含了如下的常用方法:

  • acquire(),表示阻塞并获取许可
  • tryAcquire(),表示在没有获取到许可情况下没返回false
  • release(),释放许可

2.3、Semaphore的使用

如下示例:

package com.practice.juc;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;


public class SemaphoreTest {

    /**
     * 同一时刻最多只允许有两个并发
     */
    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();
            System.out.println("请求服务");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            semaphore.release();
        }
        return "返回商品详情信息";
    }

    public static String getProductInfo2() {

        if(!semaphore.tryAcquire()){
            System.out.println("请求被流控了");
            return "请求被流控了";
        }
        try {
            System.out.println("请求服务");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            semaphore.release();
        }
        return "返回商品详情信息";
    }
}

2.4、Semaphore的应用场景

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

三、 并发操作工具类—CountDownLatch

3.1、什么是CountDownLatch

CountDownLatch是一个同步协助类,允许一个或多个线程等待,直到全部线程完成操作后才能结束:
在这里插入图片描述
CountDownLatch使用给定的计数值(count)初始化。await方法会阻塞直到当前的计数值(count),由于countDown方法的调用达到0,count为0之后所有等待的线程都会被释放,并且随后对await方法的调用都会立即返回。这是一个一次性现象 —— count不会被重置

3.2、CountDownLatch常用的API

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

3.3、CountDownLatch的使用

如下示例:

package com.practice.juc;

import java.util.concurrent.CountDownLatch;

public class CountDownLatchTest {

    // 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() {
                @Override
                public void run() {
                    // 预备状态
                    System.out.println("参赛者"+Thread.currentThread().getName()+ "已经准备好了");
                    // 等待裁判吹哨
                    try {
                        begin.await();
                    // 开始跑步
                    System.out.println("参赛者"+Thread.currentThread().getName() + "开始跑步");
                    Thread.sleep(1000);
                    // 跑步结束, 跑完了
                    System.out.println("参赛者"+Thread.currentThread().getName()+ "到达终点");
                    // 跑到终点, 计数器就减一
                    end.countDown();

                    }catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }).start();
        }
        // 等待 5s 就开始吹哨
        Thread.sleep(5000);
        System.out.println("开始比赛");
        // 裁判吹哨, 计数器减一
        begin.countDown();
        // 等待所有玩家到达终点
        end.await();
        System.out.println("比赛结束");

    }
}

3.4、CountDownLatch的应用场景

  1. 并行任务同步:CountDownLatch可以用于协调多个并行任务的完成情况,确保所有任务都完成后再继续执行下一步操作
  2. 多任务汇总:CountDownLatch可以用于统计多个线程的完成情况,以确定所有线程都已完成工作
  3. 资源初始化:CountDownLatch可以用于等待资源的初始化完成,以便在资源初始化完成后开始使用
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值