参考
https://redspider.gitbook.io/concurrent/
《Java并发编程的艺术》
进程与线程
进程:正在内存进行的程序
线程:为程序的不同功能运行分配线程,一个线程执行一个子任务,这样一个进程就包含了多个线程,每个线程负责一个单独的子任务。
多线程和多进程的区别:
1 操作开销
进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息。
2 资源分配
进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;
线程不占有内存空间,它的堆栈其实在进程中,所以线程通信比较容易实现,也容易发生错误越界处理
线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。一个线程崩溃可能影响整个程序的稳定性,可靠性较低。
进程分类
1
用户进程:执行内核之外程序的进程
系统进程:执行内核程序的进程,任何用户不可干预
2
批处理进程:按顺序启动其他进程的进程
交互进程:完成和用户交互的进程
守护进程:在暗处为其它进程执行提供基础条件的进程,随系统开启而开启,关闭而关闭,它们独立并周期性完成自己任务,如垃圾回收线程
并发一定有必要吗
并发反而降低效率
并发实际上是cpu通过切换线程,造成“同时进行”的假象,但是切换就必须保留原来的环境,这样才可以切换回去,这种环境被称为上下文。
上下文切换和线程创建都是有开销的,故线程也不是越多越好。
上下文切换减少的方法:
- 无锁并发编程
- CAS算法:常用的原子性算法
- 合理创建线程
- 协程:单线程完成多任务切换
并发容易造成死锁
死锁是线程互相持有并不释放资源,而持续等待对方释放资源的情况。这种情况会使得程序运行不下去。
死锁最大的存在原因是因为线程的异步性,它总是按照无法预测的速度完成每一个步骤,最终执行完任务。
而死锁的避免思路有三种:
1 线程同步,通过上锁机制在临界资源所在的临界区执行串行。
2 线程计时,当有线程达到规定最大等待时时,选择其中权重大的留下,其它放弃资源。
3 线程统计,通过数据结构获取一张线程图,当发现线程图形成回路时,就代表锁出现了死锁情况,选择权重大的留下。
而2 3也是InnoDB用来解决死锁的思路
另外,减少并发也是避免死锁的一种思路,比如采用CAS,协程等
并发底层三大原理
volatile
术语概要
内存可见性
内存可见性,指的是线程之间的可见性,当一个线程修改了共享变量时,另一个线程可以读取到这个修改后的值。
重排序
为优化程序性能,对原有的指令执行顺序进行优化重新排序。重排序可能发生在多个阶段,比如编译重排序、CPU重排序等。
happens-before
是一个给程序员使用的规则,只要程序员在写代码的时候遵循happens-before规则,JVM就能保证指令在多线程之间的顺序性符合程序员的预期。
内存屏障
硬件层面,内存屏障分两种:读屏障(Load Barrier)和写屏障(Store Barrier)。内存屏障有两个作用:
阻止屏障内外的指令重排序;
强制把写缓冲区/高速缓存中的脏数据等写回主内存,或者让缓存中相应的数据失效。
作用
保证变量的内存可见性
禁止volatile变量与普通变量重排序
作用原理
1 内存可见性实现:所谓内存可见性,指的是当一个线程对volatile修饰的变量进行写操作时,JMM会立即把该线程对应的CPU缓存中的共享变量的值刷新到主内存;当一个线程对volatile修饰的变量进行读操作时,JMM会把立即该线程对应的CPU缓存置为无效,从主内存中读取共享变量的值。
2 重排序实现volatile严格限制编译器和处理器对volatile变量与普通变量的重排序。它是通过内存屏障来实现的。
应用场景
在保证内存可见性这一点上,volatile有着与锁相同的内存语义,所以可以作为一个“轻量级”的锁来使用。但由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁可以保证整个临界区代码的执行具有原子性。所以在功能上,锁比volatile更强大;在性能上,volatile更有优势。
CAS(Compare And Swap)
概念
V:要更新的变量(var)
E:预期值(expected),如果这个值发生变化,则说明其他线程更改了值
N:新值(new)
先判断V是否等于E,如果是,则替代E为N,否则不做出操作
这是一种系统级的原子指令,不存在同步问题
如何保证操作的原子性
在处理器上实现了CAS
总线锁使用LOCK #信号,当一个处理器在总线上(主机部件公共线路)输出此信号时,其他处理器的请求将被阻塞住了,无论是缓存还是内存都锁定住
改进-通过缓存锁定保证原子性
一次性锁住整个内存空间显然过于暴力,如果缓存存在且上锁了,可以直接更改缓存对应的内存地址,并利用缓存一致性(???)使得其余CPU的对应缓存失效。这样大大增加了线程并发并行执行的效率
但是有两种情况是处理器不会使用缓存锁定
1 操作字段无法缓存,或者跨多个缓存行
2 处理器本身不支持
如何在程序中使用
在Java中,如果一个方法是native的,那Java就不负责具体实现它,而是交给底层的JVM使用c或者c++去实现。
在Java中,有一个Unsafe类,它在sun.misc包中。它里面是一些native方法,其中就有几个关于CAS的。
那Java具体是如何使用这几个方法来实现原子操作的呢?
JDK提供了一些用于原子操作的类,在java.util.concurrent.atomic包下面。在JDK 11中,有17个类。
通过无限循环语句,仅当交换成功才break,便可实现自旋交换。
创建线程
Thread类
继承Thread类,并重写run方法;start()会帮忙创建线程,而在分配到时间片后才会真正执行run()
public class thread {
public static class MyThread extends Thread{
@Override
public void run(){
System.out.println("MyThread");
}
}
public static void main(String[] args){
new MyThread().start();
}
}
Runnable接口
实际上Thread类也是实现Runnable接口的,所以可以直接实现Runnable接口来创建进程
public class Demo {
public static class MyThread implements Runnable {
@Override
public void run() {
System.out.println("MyThread");
}
}
public static void main(String[] args) {
new MyThread().start();
}
}
同时,Runnable是一个函数式接口
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
所以可以直接通过lambda表达式创建匿名内部Thread类完成进程创建
public static void main(String[] args) {
new Thread(() -> {
System.out.println("Java 8 匿名内部类");
return;
}).start();
}
所以,我们通常优先使用“实现Runnable接口”这种方式来自定义线程类。
Callable接口的使用
Callable较于Runnable,多了返回值,抛出异常,更推荐使用
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
Callable又如何被使用呢
public class FutureTask<V> implements RunnableFuture<V>
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
public interface RunnableFuture<V> extends Runnable, Future<V>
如图FutureTask注入了Callable,而本身实现于RunnableFuture,因为RunnableFuture继承于Runnable,所以FutureTask可以和普通的Runnable一样注入Thread,这样Thread就可以使用Callable接口了。
new Thread(FutureTask).start()
当然由于Thread本质也是实现Runnable接口来执行run,也可以直接跳过Thread,使用FutureTask.run()
Future接口
为了可取消而不返回值
FutureTask类
实现了RunnableFuture接口,这个接口
帮忙实现了
线程优先度
线程必定属于线程组,如果某个线程优先级大于线程所在线程组的最大优先级,那么该线程的优先级将会失效,取而代之的是线程组的最大优先级。
我们使用方法Thread类的setPriority()实例方法来设定线程的优先级,高优先度的
线程有更高概率得到执行,但真正的执行优先度还是有操作系统决定
守护线程/Daemon线程
会随着主线程结束而结束的子线程,通过Thread类的setDaemon(boolean on)来设置
由于虚拟机没有非守护线程会自动退出,所以daemon线程中finally不可以做为关闭和清理资源的最终手段
线程6状态及其转换
NEW:未调用Thread的start()
RUNNABLE:就绪或运行
BLOCKED:缺少共享资源
TERMINATED:结束
new->runnable:start()
runnable<->blocked:获取锁成功/等待锁
runnable->waiting:(Object)wait/(Thread)join/park
waiting->runnable:(Object)notify/(Object)notifyAll/unpark
runnable->timed_waiting->runnable:wait(long)/join(long)/sleep(long)/parkNanos 可以唤醒也可以等待自然醒
sleep并不释放锁(???)
线程中断机制
线程中断机制是一种协作机制。需要注意,通过中断操作并不能直接终止一个线程,而是通知需要被中断的线程自行处理。
中断和中断清除只能由处于Runnable的线程执行,执行后的线程如果处于Runnable状态也不会有反应,除非阻塞了,阻塞的被中断线程会抛出中断异常,除了park(),park()之后被中断唤醒不会抛出异常而是直接使用
Thread.interrupt():中断线程,将线程中断状态设为true
Thread.interrupted():清除中断,如果没有中断,返回false,表示未执行操作
Thread.isInterrupted():判断当前线程是否被中断
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
return isInterrupted(false);
}
private native boolean isInterrupted(boolean ClearInterrupted);
Java控制线程顺序间的几种通信方式
锁:可以用于同步,或资源共享,synchronized(lock){}来表示锁的区域
notify和wait:当资源被锁住时,如果一直处于阻塞态是会不断申请资源的,这会造成系统资源的浪费,wait()可以让线程进入沉睡,直到notify()唤醒,但唤醒后仍需等待对方释放锁
sleep():不释放锁
join():子线程完成后再执行父线程
局限性:
多线程时过于复杂
解决途径:
信号量
内存中的并发模型
并发模型有什么用
解决进程同步和通信的问题?即用什么机制来完成信息交流和如何保证线程相对顺序的问题
并发模型主流有哪些
消息传递并发模型
通过通信完成,把信息发给对方,同时实现了一种隐式同步
共享内存并发模型
通过共享区域读取数据完成,存在同步问题,必须通过互斥段实现线程同步,Java的选择
Synchronized
synchronized标记的地方就是临界区,每次只允许一个线程进入
Java多线程的锁是基于对象的,类锁也是基于Class对象的锁。
Synchronized的三种用法
// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
// code
}
// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
// code
}
// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
Object o = new Object();
synchronized (o) {
// code
}
}
两种等价实例锁
// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
// code
}
// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
synchronized (this) {
// code
}
}
两种等价类锁
// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
// code
}
// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
synchronized (this.getClass()) {
// code
}
}
关于双重校验锁实现对象单例的分析
public class Singleton {
private volatile static Singleton instance;
// 双重锁检验
public static Singleton getInstance() {
if (instance == null) { // 第7行
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 第10行
}
}
}
return instance;
}
}
}
为什么需要两层判定上锁后,前一层可以阻拦绝大部分线程的尝试,而后一层是为了防止实例化前进入了阻塞队列的线程获取资源后又重新实例化对象
volitile的用处 new其实并不是一步完成的,它是通过分配内存,初始化对象,分配指针完成的,volatile可以防止jvm重排序
Synchronized原理(来源: JavaGuide)
Synchronized语句块
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
Synchronized作用在实例/静态方法上
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
锁的级别
早期是直接用Lock mutex操作系统级锁实现,后期使用了分级锁提高效率
为了减少获得锁和释放锁带来的性能消耗,在Java 6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是:
无锁状态
偏向锁状态:同一个线程常常进入该片段,这种情形下会跳过轻量级锁后面的关卡
轻量级锁状态:不同进程进入时,往往不在同一时刻抢占锁,所以加入该时态
重量级锁状态:不断抢占会浪费cpu资源,所以加入阻塞状态
几种锁会随着竞争情况逐渐升级,锁的升级很容易发生,但是锁降级发生的条件会比较苛刻,锁降级发生在Stop The World期间,当JVM进入安全点的时候,会检查是否有闲置的锁,然后进行降级。
锁的存放
如果是非数组类型,则用2个字宽来存储对象头,如果是数组,则会用3个字宽来存储对象头。在32位虚拟机中,一个字宽是32位;在64位虚拟机中,一个字宽是64位。对象头的内容如下表:
长度 | 内容 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashCode或锁信息等 |
32/64bit | Class MetaData Address | 存储到对象类型数据的指针 |
32/64bit | Array Length | 数组长度(如果是数组) |
我们主要来看看Mark Word的格式:
锁状态 | 29 bit 或 61 bit | 1 bit 偏向锁判断 | 2 bit 锁标志位 |
---|---|---|---|
无锁 | 0 | 0 | 1 |
偏向锁 | 线程ID | 1 | 01 |
轻量级锁 | 指向栈中Lock Record 的指针 | 此时这一位不用于标识偏向锁 | 00 |
重量级锁 | 指向互斥量(重量级锁)的指针 | 此时这一位不用于标识偏向锁 | 10 |
GC标记 | 此时这一位不用于标识偏向锁 | 11 |
偏向锁
往往发生的情况就是同一线程获得同一把锁,这时候连同步和CAS操作都会浪费效率,所以偏向锁应运而生
偏向锁简单流程
线程进入这个同步块时,会去检查锁的Mark Word里面是不是放的自己的线程ID。
如果是,直接执行。
如果不是,就代表有另一个线程来竞争这个偏向锁。这个时候会尝试使用CAS来替换Mark Word里面的线程ID为新线程的ID,这个时候要分两种情况:
成功,表示之前的线程不存在了, Mark Word里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;
失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。
当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁。
轻量级锁
轻量级锁有什么用及什么时候用轻量级锁
不同时段多个线程使用同一把锁,不会发生锁竞争,不会发生线程堵塞,这时候用轻量级锁可以避免线程的沉睡和苏醒
轻量级锁流程
会把锁的Mark Word复制到当前线程的栈帧的Displaced Mark Word里面
自旋地尝试用CAS将锁的Mark Word替换为指向锁记录的指针(自旋有次数限制,一般用适应式自旋)
如果自旋到一定程度(和JVM、操作系统相关),依然没有获取到锁,称为自旋失败,那么这个线程会阻塞。同时这个锁就会升级成重量级锁。
锁的升级
每一个线程在准备获取共享资源时: 第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。
第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。
第三步,两个线程都把锁对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作, 把锁对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord。
第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋 。
第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败 。
第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。
Lock接口
Lock与synchronized不得不说的故事
在Lock接口出现之前,Java只能用synchronized来实现锁功能,然而synchronized的隐式获取释放锁虽然提供了一定便利,但也剥夺了自由,Lock接口可以显示地获取和释放锁,所以它可以做到以下几件synchronized无法做到的事:
A 可以一直尝试非阻塞地获取锁,synchronized自旋达到一定次数时会变为重量级锁
B 显示获取释放锁可以做到有序执行
C 获取所得线程的过程被中断时,可以抛出中断异常并释放锁。synchronized只能在线程运行时中断
如以下例子,wait可以让线程放弃锁,让给其它线程,但该线程依然保存了上下文,当轮到它时,它会从原先位置即使执行,所以应用while做条件判断而不是if:
public class ConsumeAndProduct {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
class Data {
private int num = 0;
// +1
public synchronized void increment() throws InterruptedException {
// 判断等待
if (num != 0) {
this.wait();
}
num++;
System.out.println(Thread.currentThread().getName() + "=>" + num);
// 通知其他线程 +1 执行完毕
this.notifyAll();
}
// -1
public synchronized void decrement() throws InterruptedException {
// 判断等待
if (num == 0) {
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName() + "=>" + num);
// 通知其他线程 -1 执行完毕
this.notifyAll();
}
}
ReadWirteLock
读锁可以多个线程共享,但写锁只能独占
并发下的集合
List
三种并发下使用ArrayList的方法
1 使用Vector,但由于该类在方法上都加了synchronized,效率很低
2 使用Collections.synchronizedList(),该方法调用了SynchronizedList类的构造方法,SynchronizedList类是一个重写了List方法的类,将其中方法都用Synchronized标记
3 使用CopyOnWriteArrayList,该类操作时采用读写分离,多个线程同时对此同一对象赋值时,会复制出多份给线程使用,虽然在无锁下提高了并发度,但需要极高的空间资源,适合多读少写的情景
ConcurrentHashMap(必备前置知识HashMap)
出现的意义
当扩增时,HashMap根据散列算法按重新计算的索引值插入Node会造成Entry[]死循环
并且HashTable的put()和get()都会阻止其它线程访问,从而大大降低并发效率。
ConcurrentHashMap通过把整个Entry数组切分开,并用Segment实现可重入锁来承载,这样可以让不同Segment可以被同时访问
初始化
concurrencyLevel,ssize,sshift,segmentshift,segmentMask,capacity的作用和关系
concurrencyLevel作为一个预估大概会有多少线程的标准,但它不能强制限制线程量,最大值为65535
ssize,初始为1,segment数组的长度,真正规定了线程最大承载数,ssize只能是2的倍数,同时也必须大于预估线程量,所以是大于等于concurrencyLevel的最小的2的倍数,所以最大值为65536
sshift,记录ssize左移位数,当ssize为2时,值为1,4时值为2,以此类推
以下两个笔者未懂
segmentshift用于定位参与哈希运算的位数,等于32-sshift
segmentMask哈希运算掩码,等于ssize-1
capacity是Entry数组总长度,initialcapacity是一开始为ConcurrentHashMap赋予的容量,如果没有赋予就会是DEFAULT_CAPACITY的16,如果put达到threshold就会扩容,最大容量为MAXIMUM_CAPACITY的1<<30,前两位留下用于控制目的(源码注释,笔者未懂)
初始化每个segment
initialCapacity和loadfactor两个形参分别有什么用
HashEntry数组的长度为大于等于initialCapacity除以ssize向上求界的值的最小的2倍值,初始值为1
loadfactor和HashMap中没有区别,同样是计算threshold的工具
初始化concurrentHashMap
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
private static final int tableSizeFor(int c) {
int n = -1 >>> Integer.numberOfLeadingZeros(c - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
numberOfLeadingZeros(c - 1)用于计算数字前面有几个零,而-1的补码为32个全1,假设c为16,那么会算出28个0,最后n为15
锁的几种分类
悲观锁和乐观锁
悲观锁认为每次线程访问共享资源都会引起线程冲突,适合写多读少,减少失败和重置次数来提升性能
乐观锁认为线程对共享资源的访问都不会引起冲突,适合读多写少,减少锁的数量来提升性能
公平锁和非公平锁
公平锁采用先进先出,公平而低效
非公平锁采用争抢的方式,容易发生饥饿
可重入锁和非可重入锁
重入锁,同个进程可以重复进入的锁。也就是说这个锁支持一个线程对资源重复加锁,但只支持同线程,所以也是一种排他锁
如果我们自己在继承AQS实现同步器的时候,没有考虑到占有锁的线程再次获取锁的场景,可能就会导致线程阻塞,那这个就是一个“非可重入锁”。
ReentrantLock中文翻译即为可重入锁
读写锁和排他锁
排他锁是只允许一个线程进入的锁,无论是可重入
AQS-同步组件的基本框架
具体有什么用
1 AQS有访问线程队列情况,共享性获取释放资源,非共享性获取释放资源三种API,而后两种可以分别实现不同类型的同步组件
2 锁和同步器区分得更加清晰,锁可以仅提供接口给线程访问,而同步器可以简化线程排队等细节
用法
推荐以静态类的方式嵌入Lock的实现类,在重写资源获取和释放方法时要注意异常捕获和使用CAS来上锁以防止同时获取/释放到同一份资源
抽象类AQS/AQLS/AOS的区别
AQS里面的“资源”是用一个int类型的数据来表示的,有时候我们的业务需求资源的数量超出了int的范围。
而AQLS代码跟AQS几乎一样,只是把资源的类型变成了long类型,来满足数量
AQS和AQLS都继承了AOS类,AOS是一个指定和获取锁的持有者的类
并发工具类
区别 | CountDownLatch | CyclicBarrier |
---|---|---|
原理 | await阻塞当前线程直到规定的数值为0 | 到达屏障的线程使用await自我阻塞,等到了设定数时即可重新争夺CPU |
是否可以重置 | 不可以 | 可以 |
Semaphore
用acquire获取信号量
用release释放信号量
线程池
笔者有一篇线程池文章
线程池的基本使用