1.ReentrantLock、Semaphore、CountDownLatch 等常用的并发工具类的用法?
常用的工具类贴图:
常用的并发工具类有很多如上图所示,我们挑最常用的三种工具类来说
ReentrantLock:
ReentrantLock是一种可重入的独占锁,它允许同一个线程多次获取同一个锁而不会被阻塞;它的功能类似于Synchronized 是一种互斥锁,可以保证线程安全
基本语法:
//加锁 阻塞
lock.lock();
try {
...
} finally {
// 解锁
lock.unlock();
}
//尝试加锁 非阻塞
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
...
} finally {
lock.unlock();
}
}
在使用 ReentrantLock
时要注意以下 4 个问题:
- 默认情况下,
ReentrantLock
为非公平锁,而非公平锁; - 加锁次数和释放锁次数一定要保持一致,否则会导致线程阻塞或程序异常;
- 加锁操作一定要放在
try
代码块之前,这样可以避免未加锁成功又释放锁的异常; - 释放锁一定要放在
finally
代码块中,否则会导致线程阻塞。
代码示例:
import java.util.concurrent.locks.ReentrantLock;
/**
* 模拟抢票场景
*/
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-- + "张票"); //写
} 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的基本用法,对比synchronized 我们可以手动控制加锁和解锁十分可控,这是我们推荐使用ReentrantLock的一个重要原因!
公平锁和非公平锁:
公平锁:线程在获取锁时,按照等待的先后顺序获取锁。
我们针对上图所画的抢票场景就可以很好的理解公平锁和非公平锁,D在过来排队时,如果是允许它先插一脚那么就是非公平如果是直接排在C后面那就是公平,其实源码中就是先做一次获取锁的CAS的操作,详情可见后续篇章。
我们利用ReentrantLock 创建也比较简单:
ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁
ReentrantLock lock = new ReentrantLock(true); //公平锁
可重入锁:
可重入锁是指同一个线程可以多次获得同一个锁,而不会发生死锁。在线程持有锁的情况下,可以再次请求获取同一个锁,而这个请求是允许的,不会被阻塞。这种机制能够避免线程由于自身持有锁而陷入无限等待的情况。
例如,一个线程在执行某个方法时获得了锁,但在这个方法内部又调用了另一个同步方法,而该方法也需要获得同一个锁。这种情况下,由于锁是可重入的,线程不会被阻塞,而是继续执行,因为线程已经拥有了该锁的所有权。
Java中的synchronized
关键字和ReentrantLock
都是可重入锁。这种特性可以简化代码逻辑,但需要注意在设计时避免造成不必要的锁竞争和死锁。
import java.util.concurrent.locks.ReentrantLock;
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 对象
public void recursiveCall(int num) {
lock.lock(); // 获取锁
try {
if (num == 0) {
return;
}
System.out.println("执行递归,num = " + num);
recursiveCall(num - 1);
} finally {
lock.unlock(); // 释放锁
}
}
}
上述代码就是利用可重入锁进行的一个递归调用,发现不会出现死锁问题且递减成功
结合condition实现生产者消费者模式:
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumerExample {
private static final int CAPACITY = 5;
private final Queue<Integer> queue = new LinkedList<>();
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
class Producer implements Runnable {
@Override
public void run() {
while (true) {
lock.lock();
try {
while (queue.size() == CAPACITY) {
notFull.await(); // 等待队列不满 队列满了就不生产了
}
int item = (int) (Math.random() * 100);
queue.add(item);
System.out.println("Produced: " + item);
Thread.sleep(2000);
notEmpty.signal(); // 通知消费者队列不为空
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
class Consumer implements Runnable {
@Override
public void run() {
while (true) {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 等待队列不空
}
int item = queue.poll();
System.out.println("Consumed: " + item);
Thread.sleep(1000);
notFull.signal(); // 通知生产者队列不满
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
public static void main(String[] args) {
ProducerConsumerExample example = new ProducerConsumerExample();
Thread producerThread = new Thread(example.new Producer());
Thread consumerThread = new Thread(example.new Consumer());
producerThread.start();
consumerThread.start();
}
}
上面的代码就是生产者消费者的简单实现,运行时可能出现在生产者全部生产之后才会消费,即便已经唤醒了消费者,这是因为线程的唤醒只是让线程处于一个就绪态,并不一定会立刻执行,需要等到真正获取到时间片才会执行,还有一点就是对于lock ,这里的lock是为了防止多线程生产和消费影起的并发问题,上述代码虽然只有一个线程执行生产消费,但是使用锁也可以提供一种一致的编程模型,使得代码更加清晰可读,而且可以为以后的扩展做好准备。
ReentrantLock具体应用场景如下:
- 解决多线程竞争资源的问题,例如多个线程同时对同一个数据库进行写操作,可以使用
ReentrantLock
保证每次只有一个线程能够写入。 - 实现多线程任务的顺序执行,例如在一个线程执行完某个任务后,再让另一个线程执行任务。
- 实现多线程等待/通知机制,例如在某个线程执行完某个任务后,通知其他线程继续执行任务。
Semaphore:
Semaphore(信号量)是一种并发控制机制,用于控制对共享资源的访问。它可以用来限制同时访问某个资源的线程数量,从而避免资源的过度竞争或滥用。Semaphore维护一个许可(permit)的数量,线程可以通过获取许可来访问资源,当许可数量达到上限时,其他线程需要等待,直到有许可被释放。
Semaphore可以被用于多种场景,例如控制线程的并发数量、实现资源池等。它提供了两个主要的操作:acquire()
和 release()
。acquire()
用于获取一个许可,如果没有可用的许可,则线程会被阻塞,直到有许可可用。release()
用于释放一个许可,使其他等待的线程可以获取许可并继续执行。
Semaphore的构造方法可以指定初始化的许可数量,也可以选择是否使用公平策略(公平策略会按照线程的等待时间来获取许可)。通过调整Semaphore的许可数量,可以动态地控制并发访问资源的线程数量。
构造函数:
常用的方法:
代码示例:
package com.bingfa001;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantLock;
/**
* 模拟限流场景
*/
public class SemaphoreDemo {
/**
* 同一时刻最多只允许有两个并发
*/
private static Semaphore semaphore = new Semaphore(2);
private static Executor executor = Executors.newFixedThreadPool(10);
private static ReentrantLock lock =new ReentrantLock();
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() {
//lock.lock();
if(!semaphore.tryAcquire()){
System.out.println("请求被流控了");
return "请求被流控了";
}
try {
System.out.println("请求服务");
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
semaphore.release();
// lock.unlock();
}
return "返回商品详情信息";
}
}
上述代码就是一个限流场景,调用了sleep()方法,让同一时刻获取资源的线程在资源释放前获取失败。如果对方法加锁那么就会等待之前的线程释放资源后再去获取资源,就都可以请求成功了!
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* 实现连接池
*/
public class SemaphoreDemo{
final static ExecutorService executorService = Executors.newCachedThreadPool();
public static void main(String[] args) {
final ConnectPool pool = new ConnectPool(2);
//5个线程并发来争抢连接资源
for (int i = 0; i < 5; i++) {
final int id = i + 1;
executorService.execute(new Runnable() {
@Override
public void run() {
Connect connect = null;
try {
System.out.println("线程" + id + "等待获取数据库连接");
connect = pool.openConnect();
System.out.println("线程" + id + "已拿到数据库连接:" + connect);
//进行数据库操作2秒...然后释放连接
Thread.sleep(2000);
System.out.println("线程" + id + "释放数据库连接:" + connect);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
pool.releaseConnect(connect);
}
}
});
}
}
}
//数据库连接池
class ConnectPool {
private int size;
private Connect[] connects;
//记录对应下标的Connect是否已被使用
private boolean[] connectFlag;
//信号量对象
private Semaphore semaphore;
/**
* size:初始化连接池大小
*/
public ConnectPool(int size) {
this.size = size;
semaphore = new Semaphore(size, true);
connects = new Connect[size];
connectFlag = new boolean[size];
initConnects();//初始化连接池
}
private void initConnects() {
for (int i = 0; i < this.size; i++) {
connects[i] = new Connect();
}
}
/**
* 获取数据库连接
*
* @return
* @throws InterruptedException
*/
public Connect openConnect() throws InterruptedException {
//得先获得使用许可证,如果信号量为0,则拿不到许可证,一直阻塞直到能获得
semaphore.acquire();
return getConnect();
}
private synchronized Connect getConnect() {
for (int i = 0; i < connectFlag.length; i++) {
if (!connectFlag[i]) {
//标记该连接已被使用
connectFlag[i] = true;
return connects[i];
}
}
return null;
}
/**
* 释放某个数据库连接
*/
public synchronized void releaseConnect(Connect connect) {
for (int i = 0; i < this.size; i++) {
if (connect == connects[i]) {
connectFlag[i] = false;
semaphore.release();
}
}
}
}
/**
* 数据库连接
*/
class Connect {
private static int count = 1;
private int id = count++;
public Connect() {
//假设打开一个连接很耗费资源,需要等待1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("连接#" + id + "#已与数据库建立通道!");
}
@Override
public String toString() {
return "#" + id + "#";
}
}
应用场景总结:
CountDownLatch:
CountDownLatch
是一种同步工具,用于控制一个或多个线程等待一组操作完成后再继续
构造函数:
常用方法:
CountDownLatch
提供了两个主要方法:
countDown()
: 该方法会减少内部计数器的值,表示一个操作已经完成。每次调用countDown()
方法都会使内部计数器减1。await()
: 该方法会使当前线程阻塞,直到内部计数器的值减到零。当计数器为零时,等待的线程会被唤醒,继续执行。
代码示例:
1.实现一个百米赛跑的场景
package com.bingfa001;
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() {
@Override
public void run() {
// 预备状态
System.out.println("参赛者" + Thread.currentThread().getName() + "已经准备好了");
// 等待裁判吹哨
try {
begin.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 开始跑步
System.out.println("参赛者" + Thread.currentThread().getName() + "开始跑步");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 跑步结束, 跑完了
System.out.println("参赛者" + Thread.currentThread().getName() + "到达终点");
// 跑到终点, 计数器就减一
end.countDown();
}
}).start();
}
// 等待 5s 就开始吹哨
Thread.sleep(5000);
System.out.println("开始比赛");
// 裁判吹哨, 计数器减一
begin.countDown();
// 等待所有玩家到达终点
end.await();
System.out.println("比赛结束");
}
}
2.多任务共同执行后进行汇总
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
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("主线程:在所有任务运行完成后,进行结果汇总");
}
}
应用场景总结:
-
并行任务同步:
CountDownLatch
可以用于协调多个并行任务的完成情况,确保所有任务都完成后再继续执行下一步操作。 -
多任务汇总:
CountDownLatch
可以用于统计多个线程的完成情况,以确定所有线程都已完成工作。 -
资源初始化:
CountDownLatch
可以用于等待资源的初始化完成,以便在资源初始化完成后开始使用。