🎉🎉🎉点进来你就是我的人了
欢迎志同道合的朋友一起加油喔🤺🤺🤺
目录
1. ReentrantLock
2. Semaphore(信号量)
3. CountDownLatch(倒计数器)
4. CyclicBarrier(循环栅栏)
5. LockSupport (线程阻塞工具类)
"JUC"是"Java Util Concurrent"的缩写,代表Java提供的一套并发工具类。这些工具类大大简化了编程并发和多线程应用的复杂性,提供了更高级、更强大、更安全的并发操作功能。以下是一些常见的JUC类:
ExecutorService
、ThreadPoolExecutor
、Executors
:这些类用于线程池的创建和管理。它们可以控制线程的数量,自动管理线程的生命周期,并提供将任务提交到线程池执行的方法。Future
和Callable
:Future
表示异步计算的结果,Callable
则是具有返回值的Runnable,用于在ExecutorService中执行任务。CountDownLatch
:一个同步辅助类,在完成一组正在其它线程中执行的操作之前,它允许一个或多个线程一直等待。CyclicBarrier
:它允许一组线程互相等待,直到所有线程都达到一个公共的屏障点。Semaphore
:一个计数信号量,常用于限制可以访问某些资源(物理或逻辑的)的线程数目。ConcurrentHashMap
:一个线程安全的HashMap。AtomicInteger
、AtomicLong
、AtomicReference
等:原子变量类,用于实现无锁的线程安全操作。Lock
、ReentrantLock
、Condition
:显示锁和条件对象,用于实现比synchronized更复杂的线程同步和通信。
此处主要介绍一下之前没有提到的类 (其它类的使用可以在我多线程的专栏查看)
1. ReentrantLock
ReentrantLock是标准库为我们提供的另一种锁,顾名思义。该锁也是可重入锁。
我们的Synchronized也是我们标准库提供的锁,那么它们有什么区别呢?
Synchronized: 是直接基于代码块进行加锁解锁的。
ReentrantLock:是使用了lock()方法和unlock()方法进行加锁解锁的。
方法 | 作用 |
lock() | 加锁,获取不到就死等 |
trylock(time 超时时间) | 加锁,如果一定时间内没有获取到就放弃 |
unlock() | 解锁 |
我们的ReentrantLock在使用起来可能有一些需要注意的事项:
a. lock写在try之前;
b.一定要在finaly里面进行unlock();
lock()
方法应在try
代码块之前调用。这是因为,如果lock()
方法在try
代码块内并且发生异常,那么锁可能无法正确获取,而在finally
代码块中,unlock()
方法仍会被执行。这可能会尝试释放并未真正被线程持有的锁,从而引发IllegalMonitorStateException
异常。unlock()
方法应在finally
代码块中调用,以确保锁一定会被释放,无论try
代码块中的代码是否抛出异常。这是因为,如果try
代码块中的代码抛出异常,并且unlock()
不在finally
中被执行,那么可能导致锁没有被正确释放,从而阻止其他线程获取该锁,进一步可能导致死锁现象。
上述是我们ReentrantLock的劣势,当然我们ReentrantLock也是有一些优势的。
1. 我们的ReentrantKLock提供了公平锁版本的实现,我们的Synchronized只实现了非公平锁。
2. 我们Synchronized尝试加锁,如果锁已经被占有,就进行阻塞等待(死等),ReentrantLock提供了更灵活的获取锁的方式: trylock()
方法 | 作用 |
trylock() | 无参数,能加锁就加,加不上就放弃 |
trylock(time) | 有参数,超过指定时间,加不上锁就放弃 |
3. ReentrantLock提供了一个更方便的等待通知机制,Synchronized搭配的是wait,notify,当我们notify的时候是随即唤醒一个wait状态的线程。ReentrantLock搭配一个Condition类,进行唤醒的时候可以唤醒指定线程。
以下是 Condition
的基本使用方法:
- await():类似于
Object.wait()
,使当前线程进入等待状态,并且释放其持有的锁,直到其他线程调用Condition.signal()
或Condition.signalAll()
方法,线程才有可能返回。 - signal():类似于
Object.notify()
,唤醒一个等待在此Condition
上的线程。 - signalAll():类似于
Object.notifyAll()
,唤醒所有等待在此Condition
上的线程。
以下是一个使用 ReentrantLock
和 Condition
的示例:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionTest {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
// 等待方法
public void waiter() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + ": 开始等待");
condition.await();
System.out.println(Thread.currentThread().getName() + ": 结束等待");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
// 发送信号方法
public void signaler() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + ": 发送信号");
condition.signal();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ConditionTest conditionTest = new ConditionTest();
Thread waiterThread = new Thread(conditionTest::waiter, "等待线程");
Thread signalerThread = new Thread(conditionTest::signaler, "信号线程");
// 启动等待线程
waiterThread.start();
// 确保等待线程开始等待
Thread.sleep(1000);
// 启动信号线程
signalerThread.start();
// 等待线程结束
waiterThread.join();
signalerThread.join();
}
}
在此示例中,"等待线程"会先运行并调用 waiter
方法,此方法会使线程在 Condition
上等待。接着,"信号线程"调用 signaler
方法,该方法通过调用 Condition.signal()
来唤醒等待在 Condition
上的线程。
2. Semaphore(信号量/许可证)
Semaphore 用于在多线程环境下用于协调各个线程, 以保证它们能够正确、合理的使用公共资源。
Semaphore 维护了一个许可集,我们在初始化 Semaphore 时需要为这个许可集传入一个数量值,该数量值代表同一时间能访问共享资源的线程数量。
- 线程可以通过
acquire()
方法获取到一个许可,然后对共享资源进行操作。注意如果许可集已分配完了,那么线程将进入等待状态,直到其他线程释放许可才有机会再获取许可 - 线程通过
release()
方法释放一个许可,“许可” 将被归还给许可集
举个通俗易懂的例子:去餐厅恰饭,一个餐厅只有 20 张椅子,那么同时只有 20 个人可以进去恰饭,也即这 20 个人拥有了许可证,而多出来的人,需要等餐厅内的一些人走了,才能拥有许可证即进入餐厅恰饭。
一个餐厅有 20 张椅子、一个餐厅有 20 张许可证,用代码来表示:
Semaphore s = new Semaphore(20);
当一个人走进这家餐厅恰饭, 他首先需要调用 acuqire
方法申请获得许可证:
s.acquire()
当这个人恰完饭离开餐厅后,他手中的许可证就被释放了(调用 release
方法),也即可以给别人用了:
s.release()
适用场景
通常用于资源有明确访问数量限制的场景,常用于限流(流量控制)。
- 比如:数据库连接池,同时进行连接的线程数量有限制,连接不能超过一定的数量,当连接达到了限制的数量后,后面的线程只能排队等待前面的线程释放了数据库链接才能获取数据库链接。
- 再比如:假设我们现在需要同时读取几万个文件的数据并存储到数据库中,单线程跑显然效率非常低下,于是呢,我们启动了 30 个线程来同时去读取文件。
- 读取完文件后还要存储到数据库中,但是,数据库的连接数只有 10 个,也就是说,虽然我们有 30 个读取文件的数据,但是同时只能由 10 个线程来保存数据
我们在这里设计一个场景,现在有三个停车位, 有10辆车去抢停车位
public class Main {
public static void main(String[] args) {
//三个停车位
Semaphore semaphore = new Semaphore(3);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 占据了停车位");
semaphore.release();
System.out.println(Thread.currentThread().getName() + " 离开了停车位");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
//假设10个线程相当于10辆车
for (int i = 1; i <= 10; i++) {
Thread t = new Thread(runnable);
t.start();
}
}
}
3. CountDownLatch(倒计数器)
CountDownLatch 的适用场景通常是多线程环境下,某些任务需要等待其他任务完成后才能开始执行。例如,一个主线程需要等待所有子线程执行完毕后再进行下一步操作,或者一个线程池需要等待所有任务都执行完毕后再进行资源回收等操作。
方法 | 作用 |
CountDownLatch(人数) | 在构造的时候,传入一个计数的个数 |
await() | 等待计数器为0在继续下面操作 |
countDown() | 计数器-1 |
CountDownLatch 的构造函数接收一个 int 类型的参数(count
)作为计数器,每调用一次 countDown 方法,这个 count 就会减 1。当 count 不为 0 的时候,我们可以调用 CountDownLatch 的 await
方法阻塞当前线程,直到 count 变为 0,当前线程才可以继续往下执行。
- 下面是一个使用 CountDownLatch 的示例代码,假设我们有一个主线程和三个子线程,主线程需要等待所有子线程执行完毕后再输出最终结果:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
private static final int THREAD_COUNT = 3;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
// 模拟任务执行
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 执行完成");
countDownLatch.countDown(); //计数器-1
}).start();
}
// 等待所有子线程执行完毕
countDownLatch.await();
// 输出最终结果
System.out.println("所有子线程执行完毕,主线程继续执行");
}
}
在上面的代码中,我们先创建了一个 CountDownLatch 对象,然后启动了三个子线程。每个子线程执行完毕后都会调用
countDown()
方法,表示自己已经执行完毕。主线程调用await()
方法等待所有子线程执行完毕,当 CountDownLatch 内部计数器变为 0 时,主线程继续执行。
4. CyclicBarrier(循环栅栏)
CyclicBarrier 跟 CountDownLatch 的功能是一样的,即 “等待计数” 功能,具体来说,CyclicBarrier 做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
- CyclicBarrier 默认的构造方法是
CyclicBarrier(int parties)
,其参数 parties 就表示被屏障拦截的线程数量,每个线程执行完各自的逻辑后可以调用await
方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程就会被阻塞。直到抵达屏障的数量达到 parties,屏障打开,被阻塞的线程才可以继续往下执行。
示例代码
// parties = 2 的同步屏障:
static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
new Thread(new Runnable() {
@Override
public void run() {
cyclicBarrier.await(); // 子线程已达到屏障
System.out.println("child Thread");
}
}).start();
cyclicBarrier.await(); // 主线程已到达屏障
System.out.println("main Thread");
- 随着主线程和子线程陆续抵达屏障,parties 数量被满足,屏障打开,各个线程可以接着自己的 await 方法往下执行。如下图:
另外,CyclicBarrier 还提供一个更高级的构造函数
CyclicBarrier(int parties,Runnable barrier-Action)
,就是说,当抵达屏障的线程数量满足 parties 后,在所有被阻塞的线程继续执行之前(即屏障打开之前),率先 barrier-Action 方法。就好比一堆人要刑满释放了,在出狱之前得给所有人都套上电子锁链,这个意思。
适用场景
CyclicBarrier 和 CountDownLatch 一样适用于某些任务需要等待其他任务完成后才能开始执行的场景。如果非要细分的话:
- CountDownLatch 一般用于某个线程 A 等待若干个其他线程执行完任务之后,它才执行;
- CyclicBarrier 一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
事实上,更大的区别在于,CountDownLatch 是一次性的,而且只能做减法;而 CyclicBarrier 可以重复使用。
CyclicBarrier 可以重复使用是指,在一个 CyclicBarrier 对象的计数器达到零并且所有等待线程被释放之后,该对象可以被重置 reset()
并用于下一轮的同步。也就是说,一旦 CyclicBarrier 达到了计数器的值,它就可以被重复使用,而不是像 CountDownLatch 一样只能使用一次。
例如,如果有一个需要每隔一段时间执行一次的任务,可以创建一个 CyclicBarrier 对象来协调所有线程的执行。每次任务执行完毕后,重置 CyclicBarrier 对象的计数器,等待下一次任务的执行。
以下是一个简单的示例代码,演示了 CyclicBarrier 的重复使用:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
private static final int THREAD_COUNT = 5;
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(THREAD_COUNT, new Runnable() {
@Override
public void run() {
System.out.println("所有线程已经到达barrier,开始执行任务...");
}
});
public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(new Task()).start();
}
}
static class Task implements Runnable {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 到达 barrier.");
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName() + " 执行任务完成.");
// 重置 CyclicBarrier 的计数器
cyclicBarrier.reset();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
在这个例子中,有 5 个线程执行同一个任务,每个线程到达 CyclicBarrier 后,都会等待其他线程到达。当所有线程都到达后,会执行 CyclicBarrier 构造函数中的任务,并且重置 CyclicBarrier 的计数器,以便下一轮的同步。
5. LockSupport (线程阻塞工具类)
传统的线程等待唤醒机制有两种方式分别是 synchronized(wait和notify)和 JUC 包中的显示锁 Lock(condition 的 await() 方法和 signal()方法),但是这两个方式有两个缺点,分别是都不能脱离 synchronized,和lock、unlock,如果脱离就会报错,还有就是 wait 和 notify,await 和 signal 的执行顺序要固定,必须先wait然后在notify,否则会导致程序无法结束。
- 所以出现第三种方式,那就是线程阻塞工具类 LockSupport(park和unpark),LockSupport 类可以在任何地方阻塞当前线程以及唤醒指定被阻塞的线程。
-
LockSupport
所有的方法都是静态方法,主要有两类方法:park
和unpark
。
public static void park(Object blocker); // 暂停当前线程
public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
public static void park(); // 无期限暂停当前线程
public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t);
LockSupport 类使用了一种名为 Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(Permit)
Thread 对象的 native 实现里有一个成员代表线程是否可以阻塞的许可
permit
,我们可以认为它是一个int型的变量,但它的值只能为 0 或 1。当为1时,再累加也会维持 1。初始为 0
- park:占据该线程的许可,阻塞线程(线程进入等待态),且不会释放当前线程占有的锁资源,并且该线程在下列情况发生之前都会被阻塞:
- 调用 unpark 函数,释放该线程的许可
- 该线程被中断
- 设置的时间到了。当 time 为0 时,表示无限等待,直到 unpark 发生。
- unpark:释放线程的许可,即激活调用 park 后阻塞的线程。这个函数不是安全的,调用这个函数时要确保线程依旧存活 (多次调用也只会释放一个许可)
示例代码:
public static void main(String[] args) {
Thread a = new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "-----come in " + System.currentTimeMillis());
LockSupport.park(); // 被阻塞....等待通知,他需要通过许可证
System.out.println(Thread.currentThread().getName() + "\t" + "-----被 唤 醒 " + System.currentTimeMillis());
}, "a");
a.start();
Thread b = new Thread(() -> {
LockSupport.unpark(a);
System.out.println(Thread.currentThread().getName() + "\t" + "-----通知了");
}, "b");
b.start();
}
所有类型的park和unpark方法最终都指向 Unsafe 类中的 native 方法:
/**
* 位于Unsafe中的方法
* 释放被park阻塞的线程,也可以被使用来终止一个先前调用park导致的阻塞,即这两个方法的调用顺序可以是先unpark再park。
*
* @param thread 线程
*/
public native void unpark(Object thread);
/**
* 位于Unsafe中的方法
* 阻塞当前线程直到一个unpark方法出现(被调用)、一个用于unpark方法已经出现过(在此park方法调用之前已经调用过)、线程被中断或者time时间到期(也就是阻塞超时)、或者虚假唤醒。
* 在time非零的情况下,如果isAbsolute为true,time是相对于新纪元(1970年)之后的毫秒,否则time表示当对当前的纳秒时间段。
*
* @param isAbsolute 是否是绝对时间,true 是 false 否
* @param time 如果是绝对时间,那么表示毫秒值,否则表示相对当前时间的纳秒时间段
*/
public native void park(boolean isAbsolute, long time);
本期内容到此结束,我们下期见!