并发编程之JUC工具类


前言

并发工具类JUC


一、ReentrantLock

ReentrantLock是一种可重入的独占锁,它允许同一个线程多次获取同一个锁而不会被阻塞,主要应用场景是在多线程环境下对共享资源进行独占式访问,以保证数据的一致性和安全
特点:

  • 可中断【lockInterruptibly(),该方法优先响应中断,没有中断标识才去获取锁】
  • 可以设置超时时间【tryLock(long timeout, TimeUnit unit)】
  • 可以设置为公平锁【构造器 ReentrantLock(boolean fair) , true为公平锁】
  • 支持多个条件变量【newCondition()】
  • 与Synchronized一样,都支持可重入

ReentrantReadWriteLock 通过高低位算法(高位读,低位写),实现读写锁。

使用操作

 public void lockMethod(){
	 lock.lock();
	 try{
	 //业务逻辑
	 }finally{
	 lock.unlock();
	 }
}

public void tryLockMethod() throws InterruptedException {
    if (lock.tryLock(1000, TimeUnit.MILLISECONDS)){
        try {
            //业务代码
        }finally {
            lock.unlock();
        }
    }
}

使用时要点:

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

ReentrantLock是如何实现公平以及可重入:
通过state和独占线程实现了可重入。
公平与非公平的区别是非公平先执行cas操作,加锁失败后,再执行入队、阻塞。

ReentrantLock是如何设计入队和出队:

在这里插入图片描述

在这里插入图片描述

ReentrantLock是如何支持多个条件:
结合Condition,调用Condition的await 和signal方法。

/**
 * 结合Condition 实现生产者和消费者
 */
public class R3 {
    public static void main(String[] args) throws InterruptedException {
        Queue q1 = new Queue(5);
        new Thread(new Product(q1)).start();
        new Thread(new Consume(q1)).start();
        Thread.sleep(2000);

    }
}

class Product implements Runnable {
    private Queue queue;

    public Product(Queue queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        for (int i = 0; i < queue.tables.length * 2; i++) {
            try {
                queue.put(i);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

class Consume implements Runnable {
    private Queue queue;

    public Consume(Queue queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        for (int i = 0; i < queue.tables.length * 2; i++) {
            try {
                Object take = queue.take();
                System.out.println("消费了:" + take);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}

class Queue {
    Object[] tables;
    private int putIndex;
    private int takeIndex;

    private int size;

    private Condition full;//满了要等待,
    private Condition empty;

    private ReentrantLock lock;

    public Queue(int tablesSize) {
        this.tables = new Object[tablesSize];
        lock = new ReentrantLock();
        empty = lock.newCondition();
        full = lock.newCondition();
    }

    public void put(int value) throws InterruptedException {
        lock.lock();
        try {

            while (size == tables.length) {
                full.await();
            }
            tables[putIndex] = value;

            //
            if (++putIndex == tables.length) {
                putIndex = 0;
            }
            size++;
            empty.signal();

        } finally {
            System.out.println("producer生产:" + value);
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (size == 0) {
                empty.await();
            }
            Object value = tables[takeIndex];
            tables[takeIndex] = null;

            if (++takeIndex == tables.length) {
                takeIndex = 0;
            }
            size--;

            full.signal();
            return value;
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock总结:

  1. 解决多线程竞争资源的问题,例如多个线程同时对同一个数据库进行写操作,可以使用ReentrantLock保证每次只有一个线程能够写入。
  2. 通过等待/通知机制(await、signal),实现生产者与消费者
  3. 实现多线程任务的顺序执行,例如排队买票,有可能会遭遇插队【NonfairSync】,但也是顺序执行。

面试题:
ReentrantLock分公平锁和非公平锁,底层是如何实现的
 不管是公平锁还是非公平锁,底层都是使用AQS来进行排队,它们的区别在于线程在使用lock()方法加锁时:
  1.如果是公平锁,会先检测AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队。
  2.如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。
ReentrantLock中tryLock()和lock()方法的区别
tryLock是尝试加锁,可能获取到锁,也可能获取不到,不会阻塞线程。
lock是阻塞加锁。


二、Semaphore(信号量)

Semaphore 是一种用于多线程编程的同步工具,用于控制同时访问某个资源的线程数量。 跟RateLimiter的区别在于时间,RateLimiter限制一秒内只能有N个线程执行,超过了就只能等待下一秒

使用操作

使用时要点:

  1. acquire() 获取许可证、release()释放许可证,许可证的数量由计数器维护,当计数器为0,调用acquire()的线程将被阻塞。
  2. 支持公平锁和非公平锁两种模式
    public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }
    //permits 表示许可证的数量  fair=true 表示公平,反之 表示非公平
    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
    }
  1. 应用场景:服务接口限流、限制数据库连接资源数
/**
 * 服务接口限流
 */
public class SemaphoreD1 {

    //限制同一时间最大只能流入2个
    private static Semaphore semaphore = new Semaphore(2);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        semaphore.acquire(1);//acquire获取锁方法会检查中断标识,tryAcquire() 不会检查中断标识
                        log.info(Thread.currentThread().getName() + "跑起来");
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    } finally {
                        log.info(Thread.currentThread().getName() + "释放了");
                        semaphore.release(1);
                    }
                }
            }, "线程" + i).start();
        }
    }
}

三、CountDownLatch

CountDownLatch(闭锁)是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作集。

使用操作

使用时要点:

  1. state【计数器】+ await()+countDown()
    当state 不为0的时候,会入队阻塞(同acquireQueued(addWaiter(Node.EXCLUSIVE), arg)))
public CountDownLatch(int count)
  1. 应用场景:并行任务同步、多任务汇总、资源初始化。

并行任务同步
协调多个并行任务的完成情况,确保所有任务都完成后再继续,比如:百米赛跑。

@Slf4j
public class Cd1 {
    //裁判
    public static CountDownLatch  begin = new CountDownLatch(1);
    //运动员
    public static CountDownLatch end = new CountDownLatch(6);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 6; i++) {
            new Thread(new Match()).start();
        }
        Thread.sleep(5000);
        log.info("开始比赛");
        begin .countDown();
        end.await();
        log.info("比赛结束");
    }

}

@Slf4j
class Match implements Runnable {
    @SneakyThrows
    @Override
    public void run() {
        log.info("参赛者" + Thread.currentThread() + "准备好了");
        //等待裁判吹哨
        Cd1.begin .await();
        log.info("参赛者" + Thread.currentThread() + "开始跑了");
        Thread.sleep(1000);
        log.info("参赛者" + Thread.currentThread() + "跑完了");
        Cd1.end.countDown();

    }
}

在这里插入图片描述

多任务汇总
这个多任务汇总是等待所有任务完成后,再做业务处理,任务与任务之间没有顺序关系。例子:计算各科得分,汇总总分。

public static void main(String[] args) throws InterruptedException {
        CountDownLatch c2=new CountDownLatch(6);
        for (int i = 0; i < 6; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"批阅得分");
                    c2.countDown();
                }
            }).start();
        }
        c2.await();
        System.out.println("得分汇总");

    }

面试题:
CountDownLatch和Semaphore的区别和底层原理
区别:
 CountDownLatch适合的场景是赛跑,需要等其他线程都准备好,一起执行,等所有线程都结束才结束。
 Semaphore是控制某个资源最多同时能执行的个数。
底层原理:
 CountDownLatch的底层原理是调用await()方法的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒。
 Semaphore的底层原理是调用acquire()来获取许可,如果没有许可,会利用AQS排队,等release()释放许可后,会从AQS中从第一个开始唤醒,直到没有许可。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值