今天学了点并发相关的知识
Posted suvue
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了今天学了点并发相关的知识相关的知识,希望对你有一定的参考价值。
Java并发相关知识集锦
1.class文件
class文件包含JAVA程序执行的字节码;数据严格按照格式(虚拟机要求的一种规范)紧凑排列在class文件中的二进制流,中间无任何分隔符;文件开头有一个0xcafebabe(16进制)特殊的一个标志。
这类文件专门给JVM读里面的内容,因此具有很复杂的格式,程序员阅读可以进行工具查看。
1.1查看class文件内容
Demo1.class的内容如下:
public class Demo1 {
public static void main(String[] args){
int x = 500;
int y = 100;
int a = x/y;
int b = 50;
System.out.println(a+b);//55
}
}
//在java文件的根目录打开cmd命令窗口,再输入下面的命令。
//查看Java版本
java -version
//在Demo1类文件的根目录进行编译
javac Demo1.class
//javap命令查看内容
javap -v Demo1.class>Demo1.txt
>
作用是将生成的内容,保存到Demo1.txt中。
下面我们分析下Demo1.txt中的内容吧!
1.2 class内容 - 版本号/访问控制
public class cn.suvue.discipline.practice.demo.Demo1
minor version: 0 //次版本号
major version: 52 //主版本号
flags: ACC_PUBLIC, ACC_SUPER //访问标志
- 版本号规则:JDK5,6,7,8分别对应49,50,51,52
1.3 class内容 - 常量池
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#4 = Class #19 // cn/suvue/discipline/practice/demo/Demo1
#5 = Class #20 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 Demo1.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Class #21 // java/lang/System
#16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#17 = Class #24 // java/io/PrintStream
#18 = NameAndType #25:#26 // println:(I)V
#19 = Utf8 cn/suvue/discipline/practice/demo/Demo1
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/System
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (I)V
- 与String常量池不同,它存放的是类信息中包含的静态常量,经过编译之后就能确认。
- 网上能找到对应的码表,这里就不例举了,需要时自行百度。
1.4 class内容 - 构造方法
public cn.suvue.discipline.practice.demo.Demo1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
- Demo1这个示例中,我们并没有写构造函数,由此可见,没有定义构造函数时,会有隐式的无参构造函数。
1.5 class内容 - 程序入口main方法
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC //描述方法的访问控制
Code:
//stack:方法对应栈帧中操作数栈的深度
//locals:本地变量数量
//size:参数数量
stack=3, locals=5, args_size=1
//下面是JVM执行引擎去执行这些源码编译过的指令码。
//javap命令翻译出来的是操作符,class文件内存储的是指令码。
//前面的数字,是偏移量(字节),jvm根据这个区分不同的指令。详情可百度JVM指令码表。
0: sipush 500
3: istore_1
4: bipush 100
6: istore_2
7: iload_1
8: iload_2
9: idiv
10: istore_3
11: bipush 50
13: istore 4
15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_3
19: iload 4
21: iadd
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: return
2.JVM运行时数据区
- 线程独占:每个线程都会有它独立的空间,随线程生命周期而创建和销毁。
- 线程共享:所有线程能访问这块内存数据,随虚拟机或GC而创建和销毁。
2.1方法区
- JVM在方法区中存储加载的类信息、常量、静态变量、编译后的代码等数据。
- 这是虚拟机规范中的一个逻辑区划,不同虚拟机有不同的实现。如:Oracle的Hotspot在Java7中方法区放在永久代,Java8放在元数据空间,并且通过GC机制对这个区域进行管理。
2.2堆内存
- 堆内存还可以细分为:老年代、新生代(Eden、From Suvivor、To Suvivor)
- 在JVM启动时创建,存放对象的实例。垃圾回收器主要就是管理内存。
- 如果堆内存满了,就会出现OutOfMemroyError,后续在内存模型中详细讲解。
2.3虚拟机栈
- 每个线程都在这个空间有一个私有的空间。
- 线程栈由多个栈帧(Stack Frame)组成。
- 一个线程会执行一个或多个方法,一个方法对应一个栈帧。
- 栈帧内容包含:局部变量表、操作数栈、动态链接、方法返回地址、附加信息等。
- 栈内存默认最大是1M,超出则抛出StackOverflowError。
2.4本地方法栈
- 和虚拟机栈功能类似,虚拟机栈是为虚拟机执行Java方法而准备的,本地方法栈是为虚拟机使用Native本地方法而准备的。
- 虚拟机规范没有规定具体的实现,由不同的虚拟机厂商去实现。
- HotSpot虚拟机中虚拟机栈和本地方法栈的实现是一样的,同样,超出最大内存以后也会抛出StackOverflowError。
2.5程序计数器
- 程序计数器记录当前线程执行字节码的位置,存储的是字节码指令地址,如果执行Native方法,则计数器值为空。
- 每个线程都在这个空间有一个私有的空间,占用内存空间很少。
- CPU同一时间,只会执行一条线程中的指令。
- 多线程环境下,JVM会轮流切换线程并分配CPU资源。
- 线程切换后,需要通过程序计数器,来恢复正确的执行位置。
2.6程序完整运行分析(加载)
- Java类编译后加载到方法区,如Demo1类,但是又不止一个Demo1类,可能还会又很多的类。
- 代码中写的字符串,会以字符串常量的形式存储到运行时常量池,这个区域在1.7之前称为永久代,1.8开始称为元数据空间。
2.7程序完整运行分析(运行)
- 这里执行的是我们Demo1的测试代码,所以不考虑本地方法栈。
2.8程序完整运行分析(详细)
- JVM运行中更底层的实现,针对不同的操作系统或者处理器,会有不同的实现。
3.线程状态
- NEW:尚未启动的线程的线程状态。
- Runable:可运行线程的线程状态,等待CPU调度。
- Blocked:线程阻塞等待监视器锁定的线程状态。处于synchronized同步代码块或方法中被阻塞。
- Waiting:等待线程的线程状态。
- Timed Waiting:具有指定等待时间的等待线程的线程状态。
- Terminated:终止线程的线程状态。线程正常完成执行或者出现异常。
4.线程终止
4.1不正确的线程终止 -- Stop
Stop:终止线程,并且清除监视器的信息,但是可能导致线程安全问题,JDK不建议用。
Destroy:JDK未实现该方法。
来看下代码演示吧
package cn.suvue.discipline.practice.demo;
/**
* 演示线程stop方法的错误用法
*
* 输出结果为i=1 j=0 没能够保证数据一致性
* @author suvue
* @date 2020/2/6
*/
class StopThread extends Thread{
private int i,j;
@Override
public void run() {
synchronized (this){
++i;
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
++j;
}
}
public void print(){
System.out.println("i="+i+" j="+j);
}
}
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
StopThread thread = new StopThread();
thread.start();
Thread.sleep(2000);
thread.stop();
while (thread.isAlive()){
}
thread.print();
}
}
输出结果为i=1 j=0 没能够保证数据一致性。
4.2正确的线程终止 - interrupt
如果目标线程在调用Object class 的wait()、wait(long) 或wait(long,int)方法、join()、join(long,int)或sleep(long,int)方法时被阻塞,那么Interrupt会生效,该线程的中断状态将被清除,抛出InterruptedException异常。
如果目标线程是被I/O或者NIO中的Channel所阻塞,同样,I/O操作会被中断或者返回特殊异常值,达到终止线程的目的。
如果以上条件都不满足,则会设置此线程的中断状态。
对于Demo3中的示例,stop改成interrupt后,最终输出为“i=1 j=1” , 数据一致。
总结一下:
- stop方式会强行终止线程
- Intercept方式会以抛出异常的方式终止线程,将处理交给我们程序员自行处理。
4.3正确的线程终止 -- 标志位
代码逻辑中,增加一个判断,用来控制线程执行的中止。
public class Demo4 extends Thread{
public volatile static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
while (flag) { // 判断是否运行
System.out.println("运行中");
Thread.sleep(1000L);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
// 3秒之后,将状态标志改为False,代表不继续运行
Thread.sleep(3000L);
flag = false;
System.out.println("程序运行结束");
}
}
5.CPU缓存与内存屏障
为了提高程序运行的性能,现代CPU在很多方面对程序进行了优化。例如:CPU高速缓存,尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存以提高性能。
5.1多级缓存
- L1Cache(一级缓存)是CPU第一层高速缓存,分为数据缓存和指令缓存。一般服务器CPU的L1缓存的容量通常在32~4096KB。
- L2Cache(二级缓存)由于L1高速缓存容量的限制,为了再次提高CPU的运算速度,在CPU外部放置一高速存储器,即二级缓存。
- L3Cache(三级缓存)现在都是内置的。而它的实际作用是,L3缓存的应用可以进一步降低内存延迟,同时提升大数据量计算时处理器的性能。具有较大L3缓存的处理器提供更有效的文件系统缓存行为及较短消息和处理器队列长度。一般是多核共享一个L3缓存!
- CPU在读取数据时,先在L1中寻找,再从L2寻找,再从L3寻找,然后是内存,再后是外部存储器。
5.2缓存同步协议
多CPU读取同样的数据进行缓存,进行不同运算之后,最终写入主内存以哪个CPU为准?在这种高速缓存回写的场景下,定义了缓存一致性协议供多数CPU厂商对它进行实现。
MESI协议,它规定每条缓存有个状态位,同时定义了下面四个状态:
- 修改态(Modified):此cache行已被修改过(脏行),内容已不同于主存,为此cache专有;
- 专有态(Exclusive):此cache行内容同于主存,但不出现于其它cache中。
- 共享态(Shared):此cache行内容同于主存,但也出现在其他cache中。
- 无效态(Invalid):此cache行内容无效(空行)。
总结一下
- 多处理器时,单个CPU对缓存中数据进行了改动,需要通知给其他CPU。
- 也就是意味着,CPU处理,不但要控制自己的读写操作,还要监听其他CPU发出的通知,从而保证最终一致。
5.3CPU性能优化手段 - 运行时指令重排
- 指令重排的场景:当CPU写缓存时发现缓存区块正在被其他CPU占用,为了提高CPU处理性能,可能将后面的读缓存命令优先执行。
- 并非随便重排,需要遵循as-if-serial语义。语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime和处理器都必须遵循as-if-serial语义。
- 也就是说,编译器和处理器不会对存在数据依赖关系的操作做重排序。
5.4两个问题
- CPU高速缓存下的一个问题:缓存中的数据与主内存中的数据并不是实时同步的,各CPU(或CPU核心)间缓存的数据也不是实时同步。在同一个时间点,各CPU所看到同一内存地址的数据的值可能是不一致的。
- CPU执行指令重排序优化下有一个问题:虽然遵守了as-if-serial语义,但仅在单CPU自己执行的情况下能保证结果正确。多核多线程中,指令逻辑无法分辨因果关联,可能出现乱序执行,导致程序运行结果错误。
5.5内存屏障
处理器提供了两个内存屏障指令(Memory Barrier)用于解决上述两个问题:
- 写内存屏障(Store Memory Barrier):在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。强制写入主内存,这种显示调用,CPU就不会因为性能考虑而去对指令重排。
- 读内存屏障(Load Memory Barrier):在指令中插入Load Barrier,可以让高速缓存中的数据失效,强制重新从主内存中加载数据。强制读取主内存内容,让CPU缓存与主内存保存一致,避免了缓存导致的一致性问题。
6.线程通信
6.1通信的方式
要想实现多个线程之间的协同,如:线程执行先后顺序、获取某个线程执行的结果等等。涉及到线程之间相互通信,分为下面四类:
1)文件共享
2)网络共享
3)共享变量
4)jdk提供的线程协调API细分为:suspend/resume、wait/notify、park/unpark
6.2文件共享
6.3变量共享
6.4线程协作-JDK API
JDK中对于需要多线程协作完成某一任务的场景,提供了对应的API支持。
多线程协作的典型场景:生产者 - 消费者模型。(线程阻塞、线程唤醒)
示例:线程1去买包子,没有包子,则不再执行。线程-2生产出包子,通知线程-1继续执行。
6.4.1被弃用的suspend和resume
作用:调用suspend挂起目标线程,通过resume可以恢复线程执行。
public static Object baozidian = null;
public void suspendResumeTest() throws InterruptedException {
Thread consumeThread = new Thread(() -> {
while (baozidian == null) {
System.out.println("进入等待");
Thread.currentThread().suspend();
}
System.out.println("买到包子,准备回家");
});
consumeThread.start();
//3秒钟之后 生产一个包子
Thread.sleep(3000);
baozidian=new Object();
consumeThread.resume();
System.out.println("通知消费者");
}
- 被弃用的原因,容易写出死锁的代码,所以用wait/notify和park/unpark机制进行替代。
死锁示例
- 同步代码块中使用
public static Object baozidian = null;
/**
* 死锁的suspend/resume。 suspend并不会像wait一样释放锁,故此容易写出死锁代码
*/
public void suspendResumeDeadLockTest() throws InterruptedException {
Thread consumeThread = new Thread(() -> {
while (baozidian == null) {
System.out.println("进入等待");
synchronized (this) {
Thread.currentThread().suspend();
}
}
System.out.println("买到包子,回家");
});
consumeThread.start();
//3秒之后生产包子
Thread.sleep(3000);
baozidian = new Object();
//争取到锁之后,再恢复consumeThread
synchronized (this) {
consumeThread.resume();
}
System.out.println("通知消费者");
}
- suspend在resume之后执行
public static Object baozidian = null;
/**
* 导致程序永久挂起的suspend/resume
*/
public void suspendResumeDeadLockTest2() throws InterruptedException {
Thread consumeThread = new Thread(() -> {
while (baozidian == null) {
System.out.println("进入等待");
try {//这里模拟一点延迟
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread.currentThread().suspend();
}
System.out.println("买到包子,回家");
});
consumeThread.start();
//等待3秒,生产包子
Thread.sleep(3000);
baozidian = new Object();
consumeThread.resume();
System.out.println("通知消费者");
}
6.4.2 wait/notify机制
- 这些方法只能由同一对象锁的持有者线程调用,也就是写在同步代码块里面,否则会抛出IllegalMonitorStateException异常。
- wait方法导致当前线程等待,加入该对象的等待集合中,并且放弃当前持有的对象锁。
- notify/notifyAll方法唤醒一个或所有正在等待这个对象锁的线程。
- 注意:虽然wait会自动解锁,但是对顺序有要求,如果在notify被调用之后,才开始wait方法的调用,线程会永远处于WAITING状态。
- 正确代码示例
public static Object baozidian = null;
/**
* 正常的waitNotify
*/
public void waitNotifyTest() throws InterruptedException {
Thread consumeThread = new Thread(() -> {
while (baozidian == null) {
synchronized (this) {
try {
System.out.println("进入等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("买到包子,回家");
});
consumeThread.start();
Thread.sleep(3000);
baozidian = new Object();
synchronized (this) {
this.notifyAll();
System.out.println("通知消费者");
}
}
- 会导致死锁的代码示例(先调用了notify,再调用了wait)
public static Object baozidian = null;
/**
* 会导致程序永久等待的wait/notify
*/
public void waitNotifyDeadLockTest() throws InterruptedException {
Thread consumeThread = new Thread(() -> {
while (baozidian == null) {
try {//模拟延迟
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
try {
System.out.println("进入等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("买到包子,回家");
}
});
consumeThread.start();
//3秒之后,生产包子
Thread.sleep(3000);
baozidian = new Object();
synchronized (this){
this.notifyAll();
}
System.out.println("通知消费者");
}
6.4.3 park/unpark机制
- 线程调用park则等待"许可",unpark方法为指定线程提供许可。
- 不要求park和unpark方法的调用顺序。
- 多次调用unpark之后,再调用park,线程会直接运行。但不会叠加,也就是说,连续多次调用park方法,第一次会拿到“许可”直接运行,后续调用会进入等待。
- 正确代码
public static Object baozidian = null;
/**
* 正常的park/unpark
*/
public void parkUnParkTest() throws InterruptedException {
Thread consumeThread = new Thread(() -> {
while (baozidian == null) {
try { //模拟延迟,来调换park/unpark的调用顺序
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("进入等待");
LockSupport.park();
}
System.out.println("买到包子,回家");
});
consumeThread.start();
//3秒之后生产包子
Thread.sleep(3000);
baozidian = new Object();
LockSupport.unpark(consumeThread);
System.out.println("通知消费者");
}
- 导致死锁的错误代码
public static Object baozidian = null;
/**
* 死锁的park/unpark
*/
public void parkUnParkDeadLockTest() throws InterruptedException {
Thread consumeThread = new Thread(() -> {
while (baozidian == null) {
synchronized (this){
System.out.println("进入等待");
//注意这里挂起之后不会释放对象锁
LockSupport.park();
}
}
System.out.println("买到包子,回家");
});
consumeThread.start();
//3秒之后生产包子
Thread.sleep(3000);
baozidian = new Object();
synchronized (this){
LockSupport.unpark(consumeThread);
}
System.out.println("通知消费者");
}
6.4.4 伪唤醒
- 官方建议应该在循环中检查等待条件,原因是处于等待状态的线程可能会收到错误警告或唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。
- 伪唤醒是指线程并非因为notify、notifyall、unpark等api调用而唤醒。
- 以wait/notify举个例子
while (条件判断) {//这里用while代替if判断
synchronized (this) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//...后续操作
}
}
6.4.5 总结
API | 线程挂起后是否会自动释放对象锁 | 调用次序是否影响程序执行 |
---|---|---|
suspend/resume | 否 | 是 |
wait/notify | 是 | 是 |
park/unpark | 否 | 否 |
7.线程封闭
多线程访问共享可变数据时,涉及到线程间数据同步的问题。并不是所有时候,都会用到共享数据,所以线程封闭的概念就提出来了。
- 数据都被封闭在各自的线程之中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭。
- 在Java中线程封闭的具体体现:ThreadLocal、局部变量。
7.1ThreadLocal
- 是Java里一种特殊的变量。
- 它是一个线程级别变量,每个线程都有一个Threadlocal,就是每个线程都拥有了自己独立的一个变量,竞争条件被消除了,在并发模式下是绝对安全的变量。
- 用法:ThreadLocal
var = new ThreadLocal ();会自动在每一个线程上创建一个T的副本,副本之间彼此对立,互不影响。 - 可以在ThreadLocal存储一些参数,以便在线程中多个方法中使用,用来代替方法传参的做法。
- 测试代码
private static ThreadLocal<String> values = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
values.set("主线程存放的数据");
System.out.println("线程-1启动之前主线程:"+values.get());
new Thread(() -> {
values.set("线程-1存放的数据");
System.out.println("线程-1:"+values.get());
}).start();
Thread.sleep(3000);
System.out.println("线程-1启动之后主线程:"+values.get());
}
- 实在难以理解的话,可以理解为,JVM维护了一个Map<Thread,T>,每个线程要用这个T的时候,用当前线程取Map里面取,仅作为一个概念理解。
7.2局部变量
- 局部变量的固有属性之一就是封闭在线程中
- 它们位于执行线程的栈中,其他线程无法访问这个栈
8.线程池
8.1线程是不是越多越好
- 线程在Java中不但是一个对象,还是操作系统的资源,线程创建、销毁需要时间。如果创建时间+销毁时间>执行时间 就很不划算。
- Java对象占用堆内存,线程占用操作系统内存,根据jvm规范,一个线程的默认最大栈大小1M,这个栈空间是需要从系统内存中分配的。线程过多,会消耗很多的内存。
- 操作系统需要频繁切换线程上下文(大家都想被运行),影响性能。
- 线程池的推出,就是为了方便的控制线程数量。
8.2线程池原理 - 概念
- 线程池管理器:用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务;
- 工作线程:线程池中线程,在没有任务时处于等待状态,可以循环地执行任务;
- 任务接口:每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
- 任务队列(仓库):用于存放没有处理的任务,提供一种缓冲机制。
8.3线程池API - 接口定义和实现类
类型 | 名称 | 描述 |
---|---|---|
接口 | Executor | 最上层的接口,定义了执行任务的方法execute |
接口 | ExecuService | 继承了Executor接口,拓展了Callable、Future、关闭方法 |
接口 | ScheduledExecutorService | 继承了ExecutorService,增加了定时任务相关的方法 |
实现类 | ThreadPoolExecutor | 基础、标准的线程池实现 |
实现类 | ScheduledThreadPoolExecutor | 继承了ThreadPoolExecutor,实现了ScheduledExecutorService中相关定时任务的方法 |
- 可以认为ScheduledThreadPoolExecutor是最丰富的实现类。
8.4线程池API - 方法定义
ScheduledExecutorService类
- schedule(...):创建并执行一个一次性任务,过了延迟时间就会被执行。
- scheduleAtFixedRate(...):创建并执行一个周期性任务,过了给定的初始延迟时间,会第一次被执行,执行过程中发生了异常,那么任务就停止。一次任务执行时长超过了周期时间,下一次任务会等到该次任务执行结束后,立即执行,这也是它和scheduleWithFixedDelay的重要区别。
- scheduleWithFixedDelay(...):创建并执行一个周期性任务,过了初始延迟时间,第一次被执行,后续以给定的周期时间执行,执行过程中发生了异常,那么任务就停止。一次任务执行时长超过了周期时间,下一次任务会在该次任务执行结束的时间基础上,计算执行延时。对于超过周期的长时间处理任务的不同处理方式,这是它和scheduleAtFixedRate的重要区别。
8.5 线程池API - Executors工具类
我们可以自己实例化线程池,也可以用Executors创建线程池的工具类,常用方法如下:
- newFixedThreadPool(int nThreads):创建一个固定大小,任务队列容量无界的线程池,核心线程数=最大线程数
- newCachedThreadPool()创建的是一个大小无界的缓冲线程池。它的任务是一个同步队列。任务加入到池中,如果池中有空闲线程,则用空闲线程执行。如无则创建新线程执行。池中的线程空闲超过60秒,将被销毁释放。线程数随任务的多少变化。适用于执行耗时较小的异步任务。池的核心线程数=0,最大线程数 = Integer.MAX_VALUE
- newSingleThreadExecutor()只有一个线程来执行无界任务队列的单一线程池。该线程池确保任务按加入的顺序一个一个依次执行。当唯一的线程因任务异常终止时,将创建一个新的线程来继续执行后续的任务。与newFixedThreadPool(1)的区别在于,单一线程池大小在newSingleThreadExecutor方法中硬编码,不能被改变的。
- newScheduledThreadPool(int corePoolSize):能定时执行任务的线程池。该池的核心线程数由参数指定,最大线程数=Integer.MAX_VALUE
8.6 线程池原理 - 任务execute过程
- 是否达到核心线程数量?没达到,创建一个工作线程来执行任务。
- 工作队列是否已满?没满,则将新提交的任务存储在工作队列里。
- 是否达到线程池最大数量?没达到,则创建一个新的工作线程来执行任务。
- 最后,执行拒绝策略来处理这个任务。
8.7如何确定合适数量的线程?
- 计算型任务:cpu数量的1-2倍
- IO型任务:相对比计算型任务,需多一些线程,要根据具体的IO阻塞时长进行考量决定。如tomcat中默认的最大线程数为:200。
- 也可考虑根据需要在一个最小数量和最大数量间自动增减线程数。
- 生产环境可监测cpu的利用率。上限一般为为80%。
以上是关于今天学了点并发相关的知识的主要内容,如果未能解决你的问题,请参考以下文章