目录
1. ReentrantLock
1.1 ReentrantLock
ReentrantLock 是java.util.concurrent.locks下的一个类,是一种可重入的独占锁,它允许同一个线程多次获取同一个锁而不会被阻塞。它的功能类似于synchronized是一种互斥锁,可以保证线程安全。相对于 synchronized,ReentrantLock具备如下特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
- 与 synchronized 一样,都支持可重入
主要应用场景:
多线程环境下对共享资源进行独占式访问,以保证数据的一致性和安全性。
1.2 常用API及基本语法
使用注意:
- 默认情况下 ReentrantLock 为非公平锁而非公平锁;
- 加锁次数和释放锁次数一定要保持一致,否则会导致线程阻塞或程序异常;
- 加锁操作一定要放在 try 代码之前,这样可以避免未加锁成功又释放锁的异常;
- 释放锁一定要放在 finally 中,否则会导致线程阻塞。
常用API:
- void lock(): 获取锁,调用该方法当前线程会获取锁,当锁获得后,该方法返回
1 //加锁 阻塞 2 lock.lock(); 3 try { 4 ... 5 } finally { 6 // 解锁 7 lock.unlock(); 8 } 9
- void lockInterruptibly() throwsInterruptedException: 可中断的获取锁,和lock()方法不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程
- boolean tryLock(): 尝试非阻塞的获取锁,调用该方法后立即返回。如果能够获取到返回true,否则返回false
//尝试加锁 非阻塞 12 if (lock.tryLock(1, TimeUnit.SECONDS)) { 13 try { 14 ... 15 } finally { 16 lock.unlock(); 17 }
- boolean tryLock(long time, TimeUnit unit)throws InterruptedException: 超时获取锁,当前线程在以下三种情况下会被返回:
当前线程在超时时间内获取了锁
当前线程在超时时间内被中断
超时时间结束,返回false
- void unlock(): 释放锁
- Condition newCondition(): 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将释放锁
1.3 模拟案例
案例一: 模拟一个抢票场景
/**
* 模拟抢票场景
*/
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();//默认非公平
private static int tickets = 8; // 总票数
public void buyTicket() {
lock.lock(); // 获取锁
try {
if (tickets > 0) { // 还有票 读
try {
Thread.sleep(10); // 休眠10ms
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "购买了第" + tickets-- + "张票"); //写
buyTicket();
} else {
System.out.println("票已经卖完了," + Thread.currentThread().getName() + "抢票失败");
}
} finally {
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) {
ReentrantLockDemo ticketSystem = new ReentrantLockDemo();
for (int i = 1; i <= 10; i++) {
Thread thread = new Thread(() -> {
ticketSystem.buyTicket(); // 抢票
}, "线程" + i);
// 启动线程
thread.start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("剩余票数:" + tickets);
}
}
案例二:ReentrantLock和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(); } } }
1.4 公平锁与非公锁
ReentrantLock支持公平锁和非公平锁两种模式: 公平锁和非公平锁
公平锁:线程在获取锁时,按照等待的先后顺序获取锁。
非公平锁:线程在获取锁时,不按照等待的先后顺序获取锁,而是随机获取锁ReentrantLock默认是非公平锁
开启公平锁:
ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁 ReentrantLock lock = new ReentrantLock(true); //参数true,公平锁
1.5 可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁。
可重入锁的一个优点是可一定程度避免死锁。
在实际开发中,可重入锁常常应用于递归操作、调用同一个类中的其他方法、锁嵌套等场景中。
public class ReentrantLockDemo2 { public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); // 创建计数器对象 // 测试递归调用 counter.recursiveCall(10); } } class Counter { private final ReentrantLock lock = new ReentrantLock(); // 创建 ReentrantLock 对象 private volatile int count = 0; // 计数器 public void recursiveCall(int num) { lock.lock(); // 获取锁 try { if (num == 0) { return; } System.out.println("执行递归,num = " + num); recursiveCall(num - 1); } finally { lock.unlock(); // 释放锁 } } }
执行结果
1.6 应用场景
- 解决多线程竞争资源的问题,例如多个线程同时对同一个数据库进行写操作,可以使用ReentrantLock保证每次只有一个线程能够写入。
- 实现多线程任务的顺序执行,例如在一个线程执行完某个任务后,再让另一个线程执行任务。
- 实现多线程等待/通知机制,例如在某个线程执行完某个任务后,通知其他线程继续执行任务。
2. Semaphore
2.1 Semaphore
Semaphore(信号量)是一种用于多线程编程的同步工具,用于控制同时访问某个资源的线程数量。
Semaphore维护了一个计数器,线程可以通过调用acquire()方法来获取Semaphore中的许可证,当计数器为0时,调用acquire()的线程将被阻塞,直到有其他线程释放许可证;线程可以通过调用 release()方法来释放Semaphore中的许可证,这会使Semaphore中的计数器增加,从而允许更多的线程访问共享资源。
2.1 构造方法以及常用API
构造方法:
permits 表示许可证的数量(资源数)
fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程
常用API:
acquire() 表示阻塞并获取许可 。
tryAcquire() 方法在没有许可的情况下会立即返回 false,要获取许可的线程不会阻塞 。release() 表示释放许可 。
2.3 模拟案例
Semaphore实现服务接口限流
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(()->getProductInfo()); } } 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 "返回商品详情信息"; } }
2.4 应用场景
以下是一些使用Semaphore的常见场景:
限流:Semaphore可以用于限制对共享资源的并发访问数量,以控制系统的流量。
资源池:Semaphore可以用于实现资源池,以维护一组有限的共享资源。
3.CountDownLatch
3.1 CountDownLatch
CountDownLatch(闭锁)是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作集。
CountDownLatch使用给定的计数值(count)初始化。await方法会阻塞直到当前的计数值 (count),由于countDown方法的调用达到0,count为0之后所有等待的线程都会被释放,并且随 后对await方法的调用都会立即返回。这是一个一次性现象 —— count不会被重置。
3.2 构造器以及常用API
构造器:
常用API:
- public void await() throws InterruptedException { }; -->调用 await() 方法的线程会被挂起,它会等待直到 count 值为 0 才继续执行
- public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };--> 和 await() 类似,若等待 timeout 时长后,count 值还是没有变为 0,不再等待,继续执行
- public void countDown() { }; --> 会将 count 减 1,直至为 0
3.3 模拟案例
案例一:模拟百米赛跑
import java.util.concurrent.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(3000); // 跑步结束, 跑完了 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 (int i = 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.4 应用场景总结
- 并行任务同步:CountDownLatch可以用于协调多个并行任务的完成情况,确保所有任务都完成后再继续执行下
- 一步操作。
- 多任务汇总:CountDownLatch可以用于统计多个线程的完成情况,以确定所有线程都已完成工作。
- 资源初始化:CountDownLatch可以用于等待资源的初始化完成,以便在资源初始化完成后开始使用