JUC包名全称java.util.concurrent,是JDK提供的并发相关的工具类,相比Synchronized使用起来更灵活,可以实现更加丰富的多线程操作,常用的类包括ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等
1. ReentrantLock
ReentrantLock是一种可重入的独占锁,它允许同一个线程多次获取同一个锁而不会被阻塞。
它的功能类似于synchronized是一种互斥锁,可以保证线程安全。相对于 synchronized, ReentrantLock具备如下特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
- 与 synchronized 一样,都支持可重入
它的主要应用场景是在多线程环境下对共享资源进行独占式访问,以保证数据的一致性和安全性。
常用API
Lock接口
ReentrantLock是Lock接口的实现类,常见API如下
}
在使用时要注意 4 个问题:
- 默认情况下 ReentrantLock 为非公平锁而非公平锁;
- 加锁次数和释放锁次数一定要保持一致,否则会导致线程阻塞或程序异常;
- 加锁操作一定要放在 try 代码之前,这样可以避免未加锁成功又释放锁的异常;
- 释放锁一定要放在 finally 中,否则会导致线程阻塞。
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:线程在获取锁时,不按照等待的先后顺序获取锁,而是随机获取锁,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
- 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
- 缺点:这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致资源浪费。
ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁 ReentrantLock lock = new ReentrantLock(true); //公平锁
可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。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保护之内。
案例:基于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();
}
}
/**
* 队列封装类
*/
class Queue {
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();
}
}
}
/**
* 生产者
*/
class Producer implements Runnable {
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();
}
}
}
/**
* 消费者
*/
class Customer implements Runnable {
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();
}
}
}
ReentrantLock具体应用场景如下:
1.解决多线程竞争资源的问题,例如多个线程同时对同一个数据库进行写操作,可以使用ReentrantLock保证每次只有一个线程能够写入。
2.实现多线程任务的顺序执行,例如在一个线程执行完某个任务后,再让另一个线程执行任务。
3.实现多线程等待/通知机制,例如在某个线程执行完某个任务后,通知其他线程继续执行任务。
2. Semaphore(seməfɔː(r))
Semaphore(信号量)是一种用于多线程编程的同步工具,用于控制同时访问某个资源的线程数量,即一个资源最多只能根据所设置的访问线程数量的线程数访问吗,多余的线程会被阻塞。
Semaphore维护了一个计数器,线程可以通过调用acquire()方法来获取Semaphore中的许可证,当计数器为0时,调用acquire()的线程将被阻塞,直到有其他线程释放许可证;线程可以通过调用release()方法来释放Semaphore中的许可证,这会使Semaphore中的计数器增加,从而允许更多的线程访问共享资源。
常用API:
构造器
// 调用 await() 方法的线程会被挂起,它会等待直到 count 值为 0 才继续执行
public void await() throws InterruptedException { };
// 和 await() 类似,若等待 timeout 时长后,count 值还是没有变为 0,不再等待,继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
// 会将 count 减 1,直至为 0
public void countDown() { };
// 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(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
//循环 通过reset()方法可以进行重置
public void reset()