前言
并发工具类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();
}
}
}
使用时要点:
- 默认情况下ReentrantLock为非公平锁,要想成为公平锁,需要传入true参数
- 加锁次数和释放锁次数一定要保持一致,否则会导致线程阻塞或程序异常。
- 加锁操作一定要放在try代码之前,这样可以避免 未加锁成功又释放锁的异常。
- 释放锁一定要放在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总结:
- 解决多线程竞争资源的问题,例如多个线程同时对同一个数据库进行写操作,可以使用ReentrantLock保证每次只有一个线程能够写入。
- 通过等待/通知机制(await、signal),实现生产者与消费者
- 实现多线程任务的顺序执行,例如排队买票,有可能会遭遇插队【NonfairSync】,但也是顺序执行。
面试题:
ReentrantLock分公平锁和非公平锁,底层是如何实现的
不管是公平锁还是非公平锁,底层都是使用AQS来进行排队,它们的区别在于线程在使用lock()方法加锁时:
1.如果是公平锁,会先检测AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队。
2.如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。
ReentrantLock中tryLock()和lock()方法的区别
tryLock是尝试加锁,可能获取到锁,也可能获取不到,不会阻塞线程。
lock是阻塞加锁。
二、Semaphore(信号量)
Semaphore 是一种用于多线程编程的同步工具,用于控制同时访问某个资源的线程数量。 跟RateLimiter的区别在于时间,RateLimiter限制一秒内只能有N个线程执行,超过了就只能等待下一秒
使用操作
使用时要点:
- acquire() 获取许可证、release()释放许可证,许可证的数量由计数器维护,当计数器为0,调用acquire()的线程将被阻塞。
- 支持公平锁和非公平锁两种模式
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);
}
- 应用场景:服务接口限流、限制数据库连接资源数
/**
* 服务接口限流
*/
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(闭锁)是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作集。
使用操作
使用时要点:
- state【计数器】+ await()+countDown()
当state 不为0的时候,会入队阻塞(同acquireQueued(addWaiter(Node.EXCLUSIVE), arg)))
public CountDownLatch(int count)
- 应用场景:并行任务同步、多任务汇总、资源初始化。
并行任务同步
协调多个并行任务的完成情况,确保所有任务都完成后再继续,比如:百米赛跑。
@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中从第一个开始唤醒,直到没有许可。