一、 并发操作工具类—ReentrantLock
1.1、什么是ReentrantLock
ReentrantLock是一种可重入的独占锁,允许同一个线程多次获取同一个锁而不被阻塞,同synchronized一样,是一种互斥锁,但是相对synchronized来说,ReentrantLock有以下特点:
- 可中断
- 可以设置超时时间
- 公平锁与非公平锁的切换
- 支持多个条件变量
- 与synchronized一样,支持可重入
如下图:
在多个线程同时操作同一个资源时,ReentrantLock相当于是给这些线程加了一道门锁,每个线程想要获取到资源,都需要先获取到这把钥匙,才能访问门锁后面持有的资源
1.2、ReentrantLock常用的API
ReentrantLock实现了Lock接口规范,常见的API如下:
- void lock():当前线程调用该方法获取锁;
lock.lock();//加锁
try{
//业务代码
....
} finally {
lock.unlock();//解锁
}
- void lockInterruptibly() throws InterruptedException:可中断的获取锁,和lock()方法不同点在于,该方法会响应中断,也就是在锁的获取过程中可以中断当前获取锁的线程;
- boolean tryLock():尝试非阻塞的获取锁资源,如果能获取到,返回true,否则返回false;
- boolean tryLock(long time, TimeUnit unit) throws InterruptedException:设置超时时间尝试的获取锁资源,如果当前线程在超时时间之前能获取锁,就会返回true,反之返回false;
if(lock.trylock(1, TimeUnit.SECONDS)){//尝试加锁
try{
//业务代码
....
} finally {
lock.unlock();//解锁
}
}
- void unlock():释放当前线程锁资源;
- Condition newCondition():获取到等待/通知组件,该组件和当前的线程持有的锁进行绑定,当前的线程只有获取到锁,才能调用该组件的await()方法,而且调用后,当前线程将会把持有的锁资源释放掉;
package com.practice.juc;
import java.util.Random;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockConditionTest {
public static void main(String[] args) {
//创建队列
Queue queue = new Queue(10);
//启动生产者线程
new Thread(new Producer(queue)).start();
//启动消费者线程
new Thread(new Customer(queue)).start();
}
/**
* 队列的定义
*/
static class Queue{
//元素数组
private Object[] items;
//数组长度大小
int size = 0;
//出队指针
int takeIndex;
//入队指针
int putIndex;
private ReentrantLock lock; //锁资源
public Condition emptyCondition;//生产者阻塞时,如果当前队列为空,唤醒生产者线程
public Condition fullCondition;//消费者者阻塞时,如果当前队列满了,唤醒消费者线程
public Queue(int capacity){
this.items = new Object[capacity];
lock = new ReentrantLock();
emptyCondition = lock.newCondition();
fullCondition = lock.newCondition();
}
/**
* 向队列中添加元素=>入队
* @param value 添加的元素
* @throws Exception
*/
public void put(Object value) throws Exception{
//加锁
lock.lock();
try{
//如果当前使用的长度达到队列的长度,就需阻塞生产者线程
while(size == items.length){
fullCondition.await();
}
//往队列中添加元素
items[putIndex] = value;
//如果是当前队列的下标达到了队列的长度,那就将当前下标置为0
if(++putIndex == items.length){
putIndex = 0;
}
//当前长度加1并且唤醒消费者线程
size ++;
emptyCondition.signal();
}finally {
System.out.println("producer生产:"+value);
lock.unlock();
}
}
/**
* 队列中出队
* @throws Exception
*/
public Object take() throws Exception{
//加锁
lock.lock();
try{
//如果当前的队列为空的话,就需要阻塞当前消费者线程
while(size == 0){
emptyCondition.await();
}
Object value = items[takeIndex];
items[takeIndex] = null;
if(++takeIndex == items.length){
takeIndex = 0;
}
size --;
fullCondition.signal();//消费完之后,唤醒生产者生产消息
return value;
}finally {
lock.unlock();
}
}
}
}
/**
* 生产者
*/
class Producer implements Runnable{
/**
* 生产队列
*/
private ReentrantLockConditionTest.Queue queue;
public Producer(ReentrantLockConditionTest.Queue queue){
this.queue = queue;
}
@Override
public void run() {
try {
//每个1秒轮询的生产一次
while(true){
Thread.sleep(1000);
queue.put(new Random().nextInt());
}
}catch (Exception e){
e.printStackTrace();
}
}
}
class Customer implements Runnable{
/**
* 消费队列
*/
private ReentrantLockConditionTest.Queue queue;
public Customer(ReentrantLockConditionTest.Queue queue){
this.queue = queue;
}
@Override
public void run() {
try {
while(true){
//每个2秒轮询的消费一次
Thread.sleep(2000);
System.out.println("customer消费消费消息:"+ queue.take());
}
}catch (Exception e){
e.printStackTrace();
}
}
}
运行结果截图:
1.3、ReentrantLock的使用
在使用前我们需要注意4个问题:
1、默认情况下,ReentrantLock是非公平锁,在声明ReentrantLock()时,可以设置其参数为true=>就为公平锁;
2、因为ReentrantLock是可重入锁,所以当前线程的加锁次数和释放锁的次数要一致,否则就会导致线程阻塞或是程序异常;
3、加锁操作一定要放到try语句块代码之前,可以避免未加锁成功又释放锁的异常;
4、锁释放一定要放到finally中,不然会导致线程阻塞(程序出现异常情况也需要释放锁)
例如:抢票程序如下示例
package com.practice.juc;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest {
private final ReentrantLock lock = new ReentrantLock();//默认是非公平锁,ReentrantLock(true)为公平锁
private static int tickets = 8;//能卖的票总数
/**
* 买票操作
*/
public void buyTicket() {
lock.lock();//对资源加锁
try {
if(tickets > 0){ //买票前判断是否还有余票
try {
System.out.println(Thread.currentThread().getName()+"=>买票成功~~~"+",购买了第"+tickets+"张票");
Thread.sleep(10);
}catch (Exception e){
e.printStackTrace();
}
}else {
System.out.println("票卖完了~~~"+Thread.currentThread().getName()+"抢票失败~");
}
}catch (Exception e){
e.printStackTrace();
}
finally {
lock.unlock();//释放锁
}
}
/**
* 测试并发买票
* @param args
*/
public static void main(String[] args) {
ReentrantLockTest reentrantLockTest = new ReentrantLockTest();
//模拟10个线程进行买8张票
for (int i = 0; i < 11; i++) {
Thread thread = new Thread(()->{
reentrantLockTest.buyTicket();//进行抢票
},"线程"+i);
thread.start(); //启动线程
}
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("剩余票数为:"+tickets);
}
}
程序运行结果为:
1.4、公平锁与非公平锁
ReentrantLock可支持公平锁与非公平锁两种方式:
公平锁:线程在获取锁时,按照等待的先后顺序进行排队获取锁;
非公平锁:线程在获取锁时,不按照等待的先后顺序获取锁,而是随机获取锁,ReentrantLock默认是非公平锁;
如下:
private final ReentrantLock lock = new ReentrantLock();//默认是非公平锁
private final ReentrantLock lock = new ReentrantLock(true);//更该参数为true该为:公平锁
如下图示:
1.5、可重入锁
可重入锁又称为递归锁,是指在同一个线程在外层方法获取锁的时候,再用该线程的内层方法自动获取锁,不会因为之前已经获取过还没释放锁而阻塞,在一定程度上可避免死锁问题,如下图示:
package com.practice.juc;
import java.util.concurrent.locks.ReentrantLock;
public class RecursionTest {
private final ReentrantLock lock = new ReentrantLock();
/**
* 递归调用方法
* @param callNum
*/
public void recursionCall(int callNum){
lock.lock();
try{
if(callNum == 0){
return;
}
System.out.println("递归执行,callNum="+callNum);
//递归调用方法
recursionCall(callNum - 1);
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
RecursionTest recursionTest = new RecursionTest();
recursionTest.recursionCall(10);
}
}
运行结果如下图:
1.6、ReentrantLock的应用场景
- 解决多线程竞争资源的问题,例如多线程同时队同一个数据进行写操作;
- 实现多线程任务顺序执行
- 实现等待/通知机制
二、 并发操作工具类—Semaphore
2.1、什么是Semaphore
Semaphore主要是通过信号量的方式来控制访问资源的线程,具体的过程如下图:
2.2、Semaphore常用的API
在Semaphore类中的构造器为:
在声明这个Semaphore时,其中的permits表示许可数,fair表示线程竞争的公平性
其中在这个类中,包含了如下的常用方法:
- acquire(),表示阻塞并获取许可
- tryAcquire(),表示在没有获取到许可情况下没返回false
- release(),释放许可
2.3、Semaphore的使用
如下示例:
package com.practice.juc;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class SemaphoreTest {
/**
* 同一时刻最多只允许有两个并发
*/
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(()->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() {
if(!semaphore.tryAcquire()){
System.out.println("请求被流控了");
return "请求被流控了";
}
try {
System.out.println("请求服务");
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
semaphore.release();
}
return "返回商品详情信息";
}
}
2.4、Semaphore的应用场景
- 限流:Semaphore可以用于限制对共享资源的并发访问数量,以控制系统的流量
- 资源池:Semaphore可以用于实现资源池,以维护一组有限的共享资源
三、 并发操作工具类—CountDownLatch
3.1、什么是CountDownLatch
CountDownLatch是一个同步协助类,允许一个或多个线程等待,直到全部线程完成操作后才能结束:
CountDownLatch使用给定的计数值(count)初始化。await方法会阻塞直到当前的计数值(count),由于countDown方法的调用达到0,count为0之后所有等待的线程都会被释放,并且随后对await方法的调用都会立即返回。这是一个一次性现象 —— count不会被重置
3.2、CountDownLatch常用的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、CountDownLatch的使用
如下示例:
package com.practice.juc;
import java.util.concurrent.CountDownLatch;
public class CountDownLatchTest {
// 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();
// 开始跑步
System.out.println("参赛者"+Thread.currentThread().getName() + "开始跑步");
Thread.sleep(1000);
// 跑步结束, 跑完了
System.out.println("参赛者"+Thread.currentThread().getName()+ "到达终点");
// 跑到终点, 计数器就减一
end.countDown();
}catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}).start();
}
// 等待 5s 就开始吹哨
Thread.sleep(5000);
System.out.println("开始比赛");
// 裁判吹哨, 计数器减一
begin.countDown();
// 等待所有玩家到达终点
end.await();
System.out.println("比赛结束");
}
}
3.4、CountDownLatch的应用场景
- 并行任务同步:CountDownLatch可以用于协调多个并行任务的完成情况,确保所有任务都完成后再继续执行下一步操作
- 多任务汇总:CountDownLatch可以用于统计多个线程的完成情况,以确定所有线程都已完成工作
- 资源初始化:CountDownLatch可以用于等待资源的初始化完成,以便在资源初始化完成后开始使用