一、基础知识
1. 并发的优缺点
- 优点:更充分发挥CPU性能,方便业务拆分,提高系统的并发性能
- 缺点:遇到并发问题(上下文切换,死锁,线程安全)
2. 并发编程的三要素
- 原子性:不可再分割
- 可见性:一个线程对共享资源的修改,其他线程可以立刻看到
- 有序性:程序执行的顺序按照代码执行的顺序
3. 并行、并发与串行
- 并行:一个UPC核上,多个任务按照自己获取的时间片交替轮流占执行
- 并发:多个CPU核上同时运行多个任务
- 串行:一个CPU核上,顺序执行多个任务
4.进程与线程
- 进程:一个运行的程序
- 线程:一个运行的程序里面的控制单元,负责当前进程的执行
5.多线程
- 什么是多线程:即一个进程可以同时运行多个线程来完成不同的任务
- 它的优缺点:
优点:提高CPU利用率
缺点:
1.线程可以称为轻型进程,一样会占用内存
2.多线程需要花费CPU时间来跟踪管理协调
3.需要解决共享资源竞争问题
6.线程死锁
死锁:多个线程运行时,彼此通信或者竞争资源,一直请求对方占有的资源而又不会放弃自己正在占有的资源,造成的无限等待的过程
7.创建线程的方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 从线程池中获取
8.Runnable与Callable的区别
① 相同点:
- 都是接口
- 都可以创建多线程
- 都用start()启动线程
② 不同点:
- Runnable的run()无返回值,而Callable的call()有返回值,调用FutureTask.get()可以得到执行结果,调用此方法会使主线程阻塞
- run()只能抛出运行时异常,且无法捕获,call()允许抛出异常,且可以捕获
9.run()与start()的区别
- start()用于启动线程,run()用于执行线程运行时的代码
- start()只能调用一次,run()则可以多次调用
- start()启动线程后,线程处于就绪状态,在分配到时间片后开始运行,start()会执行线程的准备工作,然后自动执行run(),是真正的实现了多线程,而run()只是主线程下的一个普通方法,在直接调用它之后只是执行了一个普通方法,没有实现多线程
10.Callable与future
Callable可以产生线程执行的结果,future可以获取这一结果
11.线程的状态和基本操作
① 线程的状态:
- 新建状态(new):由四种创建方式创建
- 就绪状态(Runnable):start()
- 运行状态(running):获取时间片后,自动运行run()
- 阻塞状态(block):wait()、获取synchronized锁失败、sleep()、join()、发出I/O 请求
- 死亡状态(dead):run()、main()执行结束,死亡的线程不可复生
② 基本操作
- wait():使一个线程处于等待(阻塞)状态,并释放锁
- sleep():使一个正在运行的线程休眠,不释放锁,静态方法,需要处理异常
- notify():唤醒一个等待状态的线程,但是不能保证肯定唤醒,只是确定可以唤醒,最终唤醒与否有JVM确定,与优先级无关
- notifyall():唤醒所有等待的线程,让他们去竞争锁
waiting状态属于一种特殊的阻塞状态
③ sleep()与wait()
- sleep()是Thread下的静态方法,wait()是object下的方法
- sleep不释放锁,wait释放
- sleep通常用于暂停执行,wait通常用于线程通信
- sleep调用后,线程可以自己苏醒,wait不能
sleep方法执行后进入阻塞状态,但是它不释放锁,执行完成后,线程进入就绪状态,由于还持有锁,所以在获取到时间片后就可以进入running状态
wait方法执行后,线程释放锁并进入等待池,当它被唤醒以后,会进入就绪状态,但是它需要获取到锁才能进入running状态
④yield()
使当前线程从运行状态变为就绪状态,不释放锁,只是放弃时间片,让其他线程有机会执行
⑤sleep()与yield()
- sleep在给其他线程运行机会时不考虑优先级,yield只会给优先级不低于自己的线程机会
- sleep执行后线程变为阻塞状态,yield执行后线程变为就绪状态
- sleep抛出异常,yield没有
- sleep比yield具有更好的移植性(与CPU调度有关)
⑥退出当前线程
- run()执行完
- stop()
- interrupt()中断线程
⑦interrupt、interrupted、isinterrupted
- interrupt:中断线程
- interrupted:查看当前线程中断信号是true还是false,并清除信号,清除后中断信号为false
- isinterrupted:判断当前中断信号是true还是false
⑧阻塞式方法
指程序会一直等待该方法完成,期间不做任何事
⑨如何唤醒一个阻塞的线程
wait()释放锁,notify()唤醒线程
⑩notify与notifyall
- 如果一个线程调用了wait,则该线程会进入等待池,等待池中的线程不会竞争锁
- notifyall唤醒所有线程,从等待池进入锁池,竞争锁
- notify只会唤醒一个,具体是哪一个线程,由虚拟机决定
二、并发理论
1.Java内存模型
①垃圾回收机制
- 目的:识别和丢弃不再使用的对象来释放和重用资源
- 时机:当一个对象不再有指向它的引用,或者超出自己的作用域时,它会被回收
- 当对象的引用被置为null时,它不会立即被回收,而是在下一次垃圾回收时才释放其内存
- finalize方法调用的时机:当垃圾回收器决定回收某对象时
- finalize方法是一个object类方法,当垃圾回收器要释放一个对象的内存时,会调用该对象的此方法
三、并发关键字
1.synchronized
-
synchronized的作用:用来控制线程同步,多线程环境下,一个synchronized代码块不能被多个线程同时执行
-
使用synchronized的三个地方:
①修饰实例方法:
作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁②修饰静态方法:
作用于当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。③修饰代码块:
指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 -
synchronized实现原理
synchronized是通过对象的监视器完成线程同步(线程互斥)的,每个对象的对象头中都有一个监视器(修饰静态代码块时,锁对象是类,类在jvm中有自己的Class对象),每一个线程进入同步代码块之前都会尝试获取锁对象的监视器,如果获取成功,则可以进入执行,否则就会被阻塞,直到获取锁的线程释放锁,才可以去竞争锁,竞争成功才可以进入同步代码块或同步方法。 -
锁升级
锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗,具体实现如下:- 无锁状态:当只有一个线程访问同步代码的时候,这个线程会直接获取到锁,此时markword值为00,表示无锁状态,任意线程进来都能直接获得锁
- 偏向锁状态:如果一个线程多次访问一个同步代码块同步方法,那么它会尝试获取偏向锁,如果获取偏向锁成功,markword会记录下这个线程ID,并把锁状态改为01,下次这个线程进来时,将直接获取锁;在01状态下,如果有别的线程进来竞争锁,那么偏向锁将会升级为轻量级锁
- 轻量级锁:此时已经有多个线程在竞争锁,markword在锁的状态也变成了00,未竞争到锁的线程会通过CAS的方式尝试获取锁,如果获取失败会自旋等待,当自旋到一定次数仍未成功,锁就会升级为重量级锁
- 重量级锁:此时markword的锁状态值为10,将会通过系统的互斥锁来实现同步
-
synchronized、volatile、CAS区别
①synchronized是悲观锁,属于抢占式,会引起其他线程阻塞
②volatile提供了多线程共享变量可见性和禁止指令重排序优化
③CAS 是基于冲突检测的乐观锁(非阻塞) -
synchronized与lock的区别
①synchronized是Java内置关键字,Lock是个Java类;
②synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
③synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
④通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。 -
synchronized与ReentrantLock的区别
①synchronized是关键字,ReentrantLock是类
②他们都是可重入锁
③ReentrantLock只能修饰代码块
2.volatile
①volatile的作用
- 保证可见性和禁止指令重排
② 原理
当一个线程访问volatile修饰的变量时,不能从自己的工作内存中读取,只能去主内存读取,而当一个线程修改volatile修饰的变量时,需要将修改结果理解同步回主内存,以此来完成线程对共享变量修改时,对其他线程的可见性,但是由于这一系列操作不是原子性的,比如在同步回主内存的过程中,有其他线程读取了主内存变量未修改的值,那么就会造成数据不一致,所以volatile不能保证原子性
②volatile与synchronized的区别
- synchronized表示只有一个线程能获得对象锁,阻塞其他线程
- volatile表示变量必须从主存中获取,保证多线程环境下的变量可见性;禁止指令重排
- volatile是变量修饰符,synchronized可修饰方法,类,变量
- volatile不能保证原子性,synchronized可以
- volatile不会造成线程阻塞
- volatile标记的变量不会被编译器优化,synchronized的会
- volatile是线程同步的轻量级实现
3.final
①不可变对象
- 一个对象一旦被创建,那么它的状态将不会再发生变化
- 它的所有域都是final类型的
- 不可变对象保证了对象的内存可见性
四、Lock体系
1.lock
①lock
- lock:synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁
- lock对比同步的优势:
(1)使锁更公平
(2)使线程在等待的时候响应中断
(3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
(4)可以在不同的范围,以不同的顺序获取和释放锁
②死锁与活锁、饥饿
- 死锁:两个或以上线程在执行时相互等待对方持有资源而陷入无限等待
- 活锁:任务或执行者没有被阻塞,但是由于有些条件没有满足,而一直尝试,然后失败,再尝试再失败
- 饥饿:一个或多个线程由于某些原因,无法获取所需资源,一直无法执行
2.AQS
①AQS
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器
②核心思想
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
3.ReentrantLock(重入锁)实现原理与公平锁非公平锁区别
①ReentrantLock
ReentrantLock重入锁,是实现Lock接口的一个类,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。
在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。
②重入性的实现原理
要想支持重入性,就要解决两个问题:
- 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;
- 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。
ReentrantLock支持两种锁:公平锁和非公平锁。何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO
4.读写锁ReentrantReadWriteLock
- ReadWriteLock 是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术
- ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能
- 读写锁三个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式
(2)重进入:读锁和写锁都支持线程重进入
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
五、并发容器
1.ConcurrentHashMap
①ConcurrentHashMap
ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现。平时涉及高并发如果要用map结构,那第一时间想到的就是它。相对于hashmap来说,ConcurrentHashMap就是线程安全的map。
②底层实现
- JDK1.6
(1)segment继承了ReentrantLock充当锁的角色,为每一个segment提供了线程安全的保障
(2)segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成的链表 - JDK1.8:在分段的基础上,通过CAS操作保证线程安全
2.并发容器
①同步容器
可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。
②并发容器
并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量
③SynchronizedMap 和 ConcurrentHashMap
- SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为 map
- ConcurrentHashMap 使用分段锁来保证在多线程下的性能
- ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶
3.CopyOnWriteArrayList
①在非复合场景下是线程安全的
②优点
是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行
③缺点
- 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc
- 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求
- 由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。
4.ThreadLocal
①ThreadLocal
ThreadLocal 是一个本地线程副本变量工具类,当一个线程调用ThreadLocal对象的方法时,就会在当前线程内部创建了一个 ThreadLocalMap 对象,这个ThreadLocalMap对象使用当前的ThreadLocal作为key,而要存储的值为value,存储到这个ThreadLocalMap中,取的时候,也是通过ThreadLocal取值,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享
② 实现原理
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。当一个线程调用ThreadLocal对象的方法时,就会在当前线程内部创建了一个 ThreadLocalMap 对象,这个ThreadLocalMap对象使用当前的ThreadLocal作为key,而要存储的值为value,存储到这个ThreadLocalMap中,取的时候,也是通过ThreadLocal取值。
public class ThreadLocalExample {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("Hello, ThreadLocal!");
// 此时会调用主线程的ThreadLocalMap,但是这个对象为空,所以第一次的时候会创建一个ThreadLocalMap对象
// 然后将这个ThreadLocal作为key,"Hello, ThreadLocal!"作为值,存储到这个ThreadLocalMap中
Thread thread1 = new Thread(() -> {
System.out.println(threadLocal.get()); // 在子线程中获取ThreadLocal的值
});
thread1.start();
System.out.println(threadLocal.get()); // 在主线程中获取ThreadLocal的值
// 使用ThreadLocal的hash值取主线程的ThreadLocalMap中查找对应值
}
}
// 由于ThreadLocal是线程独立的,所以子线程并不能从主线程的ThreadLocal中获取值
// 最终的输出结果为:
// null
// Hello, ThreadLocal!
③ 内存泄漏问题
使用ThreadLocal会有内存泄漏的风险,原因在于,线程中的ThreadLocalMap会持有ThreadLocal的强引用,如果在ThreadLocal使用完后,没有及时通过remove方法,清楚ThreadLocalMap中ThreadLocal对应的entry,那么会导致ThreadLocal无法被垃圾回收器回收
5.BlockingQueue
①阻塞队列
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列
这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用
②JDK7提供的阻塞队列
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
六、线程池
① 线程池
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销
② 线程池的优点
- 降低资源消耗:重用存在的线程,减少对象创建销毁的开销
- 提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要等到线程创建就能立即执行
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
- 附加功能:提供定时执行、定期执行、单线程、并发数控制等功能
③ Executors类创建四种常见线程池
- newSingleThreadExecutor:(在需要任务顺序执行的时候使用)创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
- newFixedThreadPool:(最常用)创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。
- newCachedThreadPool:(适用于执行时间短的场景)创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
- newScheduledThreadPool:(定时任务线程池)创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
以最常用的fixedThreadPool为例,简单展示如何创建并使用线程池
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomThreadPoolExample {
public static void main(String[] args) {
// 创建一个有界队列,容量为10
ArrayBlockingQueue<Runnable> taskQueue = new ArrayBlockingQueue<>(10);
// 创建一个ThreadPoolExecutor,设置核心线程数为2,最大线程数为4,线程存活时间为60秒,使用有界队列和自定义拒绝策略
ThreadPoolExecutor customThreadPool = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60, // 非核心线程的存活时间,如果非核心线程空闲时间超过这个值,就会被回收
TimeUnit.SECONDS, // 时间单位
taskQueue, // 任务队列
ThreadPoolExecutor.AbortPolicy // 拒绝策略
);
// 提交任务给线程池执行
for (int i = 1; i <= 10; i++) {
final int taskId = i;
customThreadPool.execute(() -> {
System.out.println("Task " + taskId + " is running on thread: " + Thread.currentThread().getName());
});
}
// 关闭线程池
customThreadPool.shutdown();
}
}
这里使用Executors也是可以创建固定线程数线程池的(Executors.newFixedThreadPool(…))
但是这里默认使用的是无界队列,在任务提交的速度大于线程处理速度时,有可能会无限往队列中添加任务,造成内存泄漏,而且这个方法内部其实也是通过new ThreadPoolExecutor来创建线程池的,所以推荐直接使用ThreadPoolExecutor来创建线程池,自己定义详细参数
④ 线程池的状态
- running:这是最正常的状态,接受新的任务,处理等待队列中的任务。
- shutdown:不接受新的任务提交,但是会继续处理等待队列中的任务。
- stop:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
- tidying(整理):所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
- terminated:terminated()方法结束后,线程池的状态就会变成这个。
⑤ 线程池的工作流程
- 判断核心线程数是否已满:任务提交后,首先会判断当前核心线程数是否小于设置的核心线程数,如果小于,那么线程池会新建一个线程来执行新任务
- 判断队列是否已满:如果未满,则放入队列中,当有空余线程了再取出执行(无界队列会一直加任务)
- 判断最大线程数是否已满:如果队列已满,则判断当前线程数是否达到了最大线程数,如果未达到,则在核心线程之外,额外创建线程来执行任务
- 执行拒绝策略:如果已经达到最大线程数,则执行拒绝策略
ThreadPoolExecutor的拒绝策略
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略来处理新任务:
(1)ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理
(2)ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉
(3)ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
⑥ submit() 和 execute() 方法的区别
- 接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务
- 返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有
- 异常处理:submit()方便Exception处理
⑦ThreadPoolExecutor
-
与Executors的区别:
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,规避资源耗尽的风险
Executors 各个方法的弊端:
(1)newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM
(2)newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM
而ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定 -
ThreadPoolExecutor 3 个最重要的参数:
(1)corePoolSize :核心线程数,线程数定义了最小可以同时运行的线程数量。
(2)maximumPoolSize :线程池中允许存在的工作线程的最大数量
(3)workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中