1.多线程
1、线程的定义和创建
Runnable里的run方法就看作是共享的类的方法 里面的数据就是共享数据
线程的三个基本状态
1、就绪 获得了除处理机之外的所有资源 只有获得处理机 立即可以执行
2、阻塞 某一执行态的线程请求IO或者进入睡眠状态等停止运行,等到获取资源后再次进入就绪态
3、运行 某一就绪态的线程被处理机调度运行
线程的调度及优先级
CPU一般采用时间片轮转算法执行程序
抢占式:高优先级的线程抢占CPU
Java的调度方法
1、对于同等优先级线程组成的就绪队列,使用时间片轮转
2、对于高优先级,使用优先调度的抢占式策略
3、优先级高的线程不一定比优先级低的线程先调度 只是从概率上讲高优先级的线程被调度,并不意味着当高优先级的线程执行结束后才调度低优先级的
线程的优先级
public final static int MIN_PRIORITY = 1;
// -- 默认的优先级是 5
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
如何获取和设置当前线程的优先级
Thread thread = new Thread();
thread.getPriority(); 获取线程的优先级
thread.setPriority(int p) 设置线程的优先级
Thread类与Runnable接口
Thread类的属性
1、private volatile String name
这个是线程的名字 可以手动的设置(有get set方法) 在每次new Thread时默认就设置上了
2、private Runnable target
这个是一个Runnable接口类型的属性,可以在构造器里选择传入,那么start方法调用的就是传入的runnable的run方法
3、private long tid
线程的id唯一 使用了静态的方法为其自增且是加锁安全的 只能获取 不能手动设置(私有的)
// 源码中的id加1的代码
private static synchronized long nextThreadID() {
return ++threadSeqNumber;
}
Thread类常用方法
1、currentThread
//是一个静态的本地方法 返回的是当前正在执行的线程对象
//也就是说写在哪个线程中 就返回这个线程的对象 静态方法直接使用Thread类调
public static native Thread currentThread()
Thread thread = Thread.currentThread(); //返回当前正在执行的线程 静态方法
thread.setName("Thread-name"); //设置当前线程的名字
String name = thread.getName();//获取当前线程的名字
Thread.currentThread.getId() //返回当前线程的id
System.out.println(name);
2、yield
public static native void yield();
放弃当前线程占用的处理机,让所有线程去竞争处理机,但是处理机可以忽视
_暂停当前正在执行的线程,把执行机会让给优先级相同或者更高的线程
_若准备队列中没有同优先级的线程,忽略此方法
3、join
public final void join() throws InterruptedException {}
在一个线程A执行时,调用了另一个线程(B)的join方法,那么正在执行的线程A立刻停止运行,进入阻塞状态 ,等到B线程执行结束后A线程才能执行 (低优先级的线程也可以获得执行)
场景:A线程在执行过程中需要某些线程提供数据,这时就调用其他线程的join方法,等待其他全部结束后拿到数据,继续往下执行
4、stop
强制结束线程,不推荐使用
5、sleep
public static native void sleep(long millis) throws InterruptedException;
// 参数是 毫秒 1秒=1000毫秒
是当前线程进入休眠状态(阻塞),释放处理机,但是不释放锁,等待休眠状态结束后进入就绪队列等待处理机调度再次运行
6、isAlive
判断某个线程是否存活(是否已经执行结束)
public final native boolean isAlive();
7、run方法
Thread类的run方法
我们来看一下Thread类的run方法的源码
@Override
public void run() {
if (target != null) {
target.run();
}
}
Thread thread = new Thread();
thread.start(); //1、开启一个线程 2、调用对象run方法
//使用Thread的空参构造器创建一个线程 因为我们没有传入Runnable的实现类,所以说在run方法里的if 就是null 也就不执行任何的操作
所以说线程的第一种创建方式就产生了
你原生创建的Thread类的对象的run方法(如果没有传入Runnable实现类)没有方法体,那我就继承Thread类,重写一下run方法 子类在调一下start方法就可以启动线程并且执行重写的run方法了
------------------------------------------------------------
Thread thread = new Thread(Runnable target);
thread.start();
//调用 参数是一个Runnable类型的 这时的target属性就不为null
//所以在调用start时调的就是我们传入的Runnable实现类的run方法
即创建线程的第二种方式就产生了
我们可以创建一个类去实现runnable接口并重写run方法,然后去
new Thread()时在构造器中传入这个类对象(接口的多态性) 那么我这时调用Thread对象的
start()方法调用的还是原生的run方法,但是不同的是,我们传入了一个Runnable实现类对象,也就是说Thread类的 即我们为target属性赋了值 target这时不等于null 那么就调用我们传入的runnable实现类的run方法
//源码判断
// if (target != null) {
// target.run();
// }
8.中断相关的方法
线程中断详解
/*
* 一、
* 测试某个线程是否已被中断。 根据传递的 ClearInterrupted 值是否重置中断状态。
* 此方法是一个native方法。
*/
private native boolean isInterrupted(boolean ClearInterrupted);
/*
* 二、
* 测试此线程是否已被中断。 线程的中断状态不受此方法的影响。调用的还是上面的本地方法,传入的
* ClearInterrupted为false,即不清楚中断标志位
*/
public boolean isInterrupted() {
return isInterrupted(false);
}
/*
* 三、
* 测试当前线程是否被中断,调用该方法会清除线程的中断状态,底层调用的也是方法一,传入的参数
* 为true,换句话说,如果这个方法被连续调用两次,第二次调用将返回false。
*/
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
/*
中断此线程。
除非当前线程正在中断自己,这总是被允许的,否则调用此线程的checkAccess方法,这可能会导致抛出SecurityException 。
1、如果此线程在调用Object类的wait() 、 wait(long)或wait(long, int)方法或join() 、 join(long) 、 join(long, int)被阻塞、 sleep(long)或sleep(long, int) ,此类的方法,则其中断状态将被清除并收到InterruptedException
2、如果此线程在InterruptibleChannel的 I/O 操作中被阻塞,则通道将关闭,线程的中断状态将被设置,线程将收到java.nio.channels.ClosedByInterruptException 。
3、如果该线程在java.nio.channels.Selector被阻塞,则该线程的中断状态将被设置,并且它将立即从选择操作中返回,可能具有非零值,就像调用了选择器的wakeup方法一样。
如果前面的条件都不成立,则将设置此线程的中断状态(仅仅修改中断标志位)
中断一个不活跃的线程不需要有任何影响。
*/
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
// native Method Just to set the interrupt flag
interrupt0();
b.interrupt(this);
return;
}
}
interrupt0();
}
线程的创建
/**
* 多线程的创建方式一
* 1.继承Thread类
* 2.重写run()方法 (将此线程执行的操作写在方法中)
* 3.创建对象 调用start()方法 启动线程
*/
public class CreateThreadOne extends Thread {
@Override
public void run() {
System.out.println("线程 "+Thread.currentThread().getName()+" 开启");
}
public static void main(String[] args) {
System.out.println("主线程");
CreateThreadOne ct = new CreateThreadOne();
ct.start(); // 开启一个线程 异步执行run方法
// ct.start(); // 报错 一个对象只能调用一次start方法
CreateThreadOne ctt = new CreateThreadOne(); //重新new对象开启线程
ctt.start();
}
}
一个(对象)线程的start方法只能调用一次 否则 抛出异常
if (threadStatus != 0)
throw new IllegalThreadStateException();
总结
1、想要定义线程中执行的方法 就必须实现Runnable接口并重写run()方法
2、 想要启动一个线程 那么必须调用Thread类的start()方法
start方法的另外一个作用就是调用Thread类对象的run()方法 也就是说在启动一个线程的时候就执行run方法的内容
3、Thread类实现了Runnable接口 即在创建Thread子类时就可以重写run方法 所以我们为了方便就可以使用匿名类的方式创建Thread类的子类对象并且重写run方法
class Test{
public static void main(String[] args){
//这里用到了Thread类的多态 因为我们后面创建的是一个Thread类子类的的匿名类的对象
Thread thread = new Thread(){
@Override
public void run(){
xxxx....
}
}
}
}
4、Thread类中有一个成员属性就是Runnable类型,有一个构造器参数是 Runnable类型的 即可以通过构造器传入一个Runnable的实现类,所以我们也可以不必创建Thread的子类对象,我们直接new Thread时传入一个Runnable的实现类,然后我们调start方法是就会调我们的Runnable实现类重写的方法
public class Exec {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("xxx");
}
};
Thread thread = new Thread(runnable); // 不必创建子类 直接传入runnable实现 // 类即可
// 实现不同功能时 只需传入不同的 //runnable实现类即可
thread.start();
***** //**************演变 接口的匿名实现类对象****************************
// 因为是一个参数类型是一个接口 我们就可以直接传一个接口的匿名实现类对象(接口的多态性)
// 注意 前面的Thread类型接收不是多态 因为我们创建的就是一个Thread类
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
xxx...
}
});
***** //演变*****lambda表达式****************************************
因为Runnable是一个函数式接口
// 使用lambda表达式 创建函数式接口的匿名实现类对象,并重写接口中的方法
// Thread thread = new Thread(Runnable runnable);
Thread thread = new Thread(()->{
System.out.println();
....
});
thread.start();
}
}
创建线程的本质
- 就是继承Thread类,(本质就是使此类变成Thread类),然后创建Thread对象时传入Runnable实现类对象或者继承Thread类时重写run方法,最后调用Thread对象的start方法开启一个线程,(自动调用run方法)
- 只要new 了一个Thread类(及其子类)就相当于创建了一个线程,执不执行另说(调用start方法)
- 两种创建方式的共同点 都需要重写run方法 覆盖Thread类的run方法
- 开发中绝大多数都是使用的第二种,因为接口可以多实现,而类只能单继承,并且可以通过实现类来操纵实现类的公共资源
线程的分类
1、守护线程
2、用户线程
-
它们在几乎每个方面都是相同的,唯一的区别就是判断JVM何时离开
-
守护线程是来服务用户线程的,通过在start()方法前调用,thread.setDaemon(true) 可以把一个用户线程变为守护线程
-
JVM的GC就是一个典型的守护线程
-
若JVM中都是守护线程,当前JVM将退出
-
守护线程依赖于用户线程
-
Java应用程序至少有两个线程,main GC
2、线程的生命周期(状态)
3、线程的同步
- 在Java中 通过同步机制来解决线程的安全问题
- -多个线程操作一个数据才会产生线程安全问题
- 加锁会导致效率低并且可能造成死锁
方式一:同步代码块
作用:保证了代码块中的原子性,即整段代码不执行结束其他线程必须等待,只有当本线程执行结束后释放了锁其他线程才能继续争抢锁,这就保证了线程是安全的,
synchronized(同步监视器){
//需要被同步的代码
}
// 说明:操作共享数据的代码,即为需要被同步的的代码
// 共享数据:多个线程共同操作的变量 比如:ticket
同步监视器俗称 锁
要求:锁可以是任意一个类的对象,但是如果要达到线程的同步,必须要使用同一把锁,即同一个对象,内存中的那一个
public class TicketWindow {
Object object = new Object();
private int ticketNum = 1000;
public int getTicketNum() {
return ticketNum;
}
public void saleTicket() {
synchronized (Class.class) { //可以锁住 因为一个类的Class对象只有一个
//卖票 推荐写这种 基本不会出现问题 默认是当前类的类对象
}
synchronized (object) { //同一个对象只有一个object属性 可以锁住
//卖票
}
synchronized (this) { //同一个对象的安全问题 这个对象只有一个
//卖票
}
synchronized(new Object()){ //不是同一把锁 锁不住
}
}
}
方式二:同步方法
1、如果操作共享数据的代码完整的生命在一个方法中,就可以使用同步方法
2、同步方法仍然有同步监视器(锁),只是不需要显示的声明
- 非静态的同步方法默认的锁是 this
- 静态的同步方法默认的锁是 当前类的Class对象
public synchronized void method(){
//非静态的同步方法默认的锁是 this
}
public static synchronized void method(){
//静态的同步方法默认的锁是 当前类的Class对象
}
方式三、Lock锁机制(JDK5.0)
Lock(是一个接口)是手动的加锁解锁操作,在加锁之后必须手动解锁,synchronized无需手动解锁,执行完同步方法自动解锁
class A{
//先 new一个锁
Lock lock = new ReentrantLock(); //默认是false 非公平锁 构造器写true是公平锁(先来先服务)
private void getTicket() {
while (true) {
//操作代码写在try-finally代码块里,方便下面的解锁
try {
lock.lock(); //手动加锁
// synchronized (Test.class) {
业务操作。。。
} finally {
lock.unlock(); //手动解锁 必须解锁不然后面线程全部处于阻塞
}
}
}
}
4、volatile关键字及内存详解
- 并发编程的三大特性:
可见行(volatile)
有序性(volatile) (指令重排)
原子性(锁机制)
volatile保证可见行
和有序性
,但是不保证原子性
,保证原子性需要借助synchronized这样的锁
JMM内存模型及指令规则
线程主存规则原则
通过Java的内存模型可知 ,每个线程在操作共享变量是,并不是直接对主存中的数据进行操作,而是会先去read
主存中的数据,然后load
加载到自己线程的工作内存中去,这相当是变量的一个副本,然后线程的执行引擎就会 use(操作)工作内存中的数据,操作完后assign到工作内存中,然后通过store
传送到主存中,最后通过write
最终写入到主存中
volatile测试代码
public class VolatileTest {
private volatile static int a = 0;
public static void main(String[] args) {
new Thread(() -> {
while (a == 0) {
}
System.out.println("obj被其他线程赋了值");
}).start();
// 主线程睡眠 确保下面的赋值操作在上面循环后执行
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//主线程为其赋值
a = 2;
System.out.println("我已为obj赋了值");
}
}
测试发现
1、当我们不加volatile时 循环的线程感知不到主线程已经修改了a的值 所以一直是死循环
2、当为变量加上了volatile时,再次测试发现只要主线程修改了a的值,循环的线程立刻可以感知到a的值变化了,就退出了循环
原因
通过上面的JMM我们可以知道为什么当其他线程修改了数据后,其他线程无法感知,因为每个线程在操作数据的时候并不是从主存中获取的数据,都是基于自己线程中的工作内存来操作数据的,即使别的线程修改了数据并写到了主存中,线程也并不能感知到
加了volatile为什么就可以当线程修改时别的线程可以立刻感知到数据被修改了?
1、 变量加了volatile后,那么线程对这个变量的修改就发生了变化,即当一个线程对数据进行了修改,那么 立刻 会把数据更新到主存,注意:立刻更新的意思不是说直接use后直接就store write,这是严重错误,JMM不允许这样做
,详情参照上面图中的线程主存的工作原则,立刻的意思是比如我的线程的一个方法有很多行都用到了这个变量,只要我在某一行对数据进行了修改,立刻就把数据assign到工作内存,然后store write到主存,因为在正常情况下,一个线程改完数据什么时候更新到主存是不确定的(但是线程执行结束后一定会更新这是确定的),所以加了volatile后对数据进行修改立刻就刷回主存
2、通过缓存一致性协议,其他线程感知到了主存中的数据发生了变化,会令其他线程的工作内存中的这个变量副本立即失效
,即读取的时候直接读取主存中的值
3、还有一点就是加了volatile的变量,底层加了一个lock前缀,保证了当我们数据更新到主存时是加锁的,
通过上面两条我们就可以知道volatile的一个作用就是修改数据直接刷新主存,其他线程强制的直接去主存中读数据,保证数据的可见行
volatile可见行原理
底层主要是通过汇编的lock前缀指令,会锁定这块内存区域的缓存(缓存行锁定)并回写到主存
IA-32和Intel 64架构软件开发者手册对lock指令的解释:
-
会将当前处理器缓存行的数据立即写回到系统内存
-
这个写回内存的操作会引起在其他CPU里缓存的该内存地址的数据失效(MESI协议)
-
提供内存屏障,使lock前后指令不能重排序(禁止指令重排)
指令重排与内存屏障:
参考资料:
PDF文档
重排序遵守的原则
1、as-if-serial原则:
-
不管如何重排序(编译器为了提高并行度,更加利于CPU的执行),==(单线程)==程序的结果是不能改变的,编译器,runtime和处理器都必须遵守as-if-serial语义
-
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行的结果,但是,如果代码之间并不存在数据的依赖关系,这些操作就
有可能
被编译器和处理器重排序2、happens-before原则
只靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,从JDK 5开始,Java使用新的UJSR-133内存模型,提供了happens-before原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下
- 程序顺序原则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
- 锁规则:解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
- volatile规则: volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
- 线程启动规则:线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
- 传递性:A先于B,B先于C那么A必然先于C
- 线程终止规则:线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
- 对象终结规则:对象的构造函数执行,结束先于finalize()方法
如何禁止重排序-内存屏障
可见性和禁止重排序都是内存屏障的
-
内存屏障或内存栅栏(Memory Barrier),是让一个CPU处理单元中的内存状态对其它处理单元可见的一项技术。
-
内存屏障有两个能力:
1、就像一套栅栏分割前后的代码,阻止栅栏前后的没有数据依赖性的代码进行指令重排序,保证程序在一定程度上的有序性。2、强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效,保证数据的可见性。
-
内存屏障有三种类型和一种伪类型:
1、lfence:即读屏障(Load Barrier),在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据,以保证读取的是最新的数据。
2、sfence:即写屏障(Store Barrier),在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存,以保证写入的数据立刻对其他线程可见。
3、mfence,即全能屏障,具备ifence和sfence的能力。
4、Lock前缀:Lock不是一种内存屏障,但是它能完成类似全能型内存屏障的功能
所以说:可见性和禁止重排序都是内存屏障的
在Java中:实现了内存屏障的技术有volatile。volatile就是用Lock前缀方式的内存屏障伪类型来实现的(具体实现另述)
-
不同的CPU硬件对于JVM的内存屏障规范实现指令不一样
-
JVM底层简化了内存屏障硬件指令的实现,它只加了lock前缀 lock指令不是一种内存屏障,但是它能完成类似内存屏障的功能
单例模式之懒汉式(双重检查)
主要内容
1、volatile在DCL的单例模式中主要解决了指令重排的问题
对象的创建过程对象的对应的字节码文件主要有三行(按照顺序)
1、new 开辟空间默认初始化
2、init方法对对象初始化
3、将对象的引用赋值给一个变量
但是 由于2 3没有依赖关系符合指令重排序的规则,那么在真正执行的过程中就有可能被重排序为 1 3 2 那么这时的单例模式就出了大问题
同时几个线程进入getInstance方法,第一层判断都是null,进入同步代码块只有一个线程拿到锁去执行同步代码块里的内容,这个线程判断对象是null,就去创建对象,刚好被重排序了,按照了1 3 2的顺序进行了执行,这时执行完了1 3,instance这时实际上已经不为null了,但是这个线程还没来得及执行完毕释放锁更新主存中的数据,后来的线程经过第一层判断发现不为null了,这时直接就返回了对象,但是这时的对象还没经过init的初始化,即这个对象是半初始化对象,那么后续就会出现大问题
解决:加上volatile禁止重排序,保证第一个进入同步代码块的线程的创建对象一定完全经过初始化才为instance赋了值的,就能解决对象半初始化问题
class Bank {
private Bank() {}
//加上volatile 数据可见性 强制的每次都去主存中读取数据 防止指令重排
private volatile static Bank instance = null;
//加 synchronized 同步 静态方法默认的锁是类的Class对象
public static Bank getInstance() {
if (instance == null) { // 双重检查 提高效率 后面的线程无需排队直接取走对象
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}
5、线程的死锁
什么是死锁?
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续执行
6、线程的通信
例题:两个线程交替打印数字
涉及到的三个方法:必须只能写在同步代码块中或者同步方法中
同步代码块的监视器必须和调用wait方法的对象是一个 否则报异常
- wait() 一旦执行此方法,当前线程就进入阻塞状态,
并且释放已获得的锁
意味着其他的线程可以进入同步代码块 - notify() 执行此方法,会随机唤醒
一个wait()
的线程,如果有多个线程被wait,就唤醒优先级高的 - notifyAll() 唤醒所有正在wait()的进行
这三个方法都是定义在java.lang.Object中的 且不能使用在lock锁的代码里面
面试题:sleep和wait方法的异同
相同点: 一旦执行方法都可以使当前线程进入阻塞状态
不同点:
- 1、sleep和wait定义的位置不同 sleep()在Thread类中,wait()是Object类中的方法
- 2、sleep()是静态方法,wait()方法是非静态方法
- 3、调用范围不同,sleep是在任何需要的场景下调用,wait()只能在同步方法或者同步代码块中调用
- 4、sleep()调用不会释放同步监视器(锁),wait()会释放同步监视器
生产者消费者问题
JUC
7、JDK5新增的两种线程创建方式
实现callable接口
通过上图可以知道 FutureTask就是Runnable的一个实现类,我们只需创建一个Thread对象在构造器中传入一个FutureTask的对象就可启动一个线程,并且FutureTask类中依赖了Callable接口,我们在创建FutureTask对象时传入一个Callable的实现类对象重写call方法即可。
public class Test2 {
public static void main(String[] args) {
// 1、创建一个Callable接口的实现类 重写call方法
Callable<Object> callable = new Callable<Object>() {
// call方法有返回值 可以抛出异常
@Override
public Object call() throws Exception {
System.out.println("callable的实现类对象");
return null;
}
};
//2、 创建一个FutureTask对象,传入Callable的实现类 因为它实现了Runnable接口
//就可用多态表示
Runnable runnable = new FutureTask<Object>(callable);
//传入runnable 启动线程
new Thread(runnable).start(); //启动线程
try {
// FutureTask独有的方法 可以拿到返回值 阻塞保证线程执行完成(类似join)
Object o = ((FutureTask) runnable).get();
System.out.println(o);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
callable接口的好处
1、call()方法有返回值 通过FutureTask类的get方法阻塞拿到返回值
即异步任务可以拿到返回值
2、call()方法可以抛出异常,被外面的线程捕获,也是通过get()方法
3、callable接口支持泛型,是一个函数式接口 这个泛型规定了call()方法的返回值
线程池
提前创建好多个线程,放入到线程池中,使用时直接获取,使用完放回池中
,可以避免频繁的创建销毁的开销,实现重复利用
,避免资源耗尽,系统崩溃
好处:
- 提高响应速度,减少了创建新线程的时间,
- 降低资源消耗(重复利用,不需要每次都创建)
- 便于管理
详细细节见JUC笔记