Java 多线程与并发——Thread 和 ThreadPool
Java 对操作系统提供的功能进行了封装,包括进程和线程,运行一个程序会产生一个进程,进程包含至少一个线程,每个进程对应一个 JVM 实例,多个线程共享 JVM 里的堆、方法区,每个线程拥有自己的虚拟机栈、本地方法栈、程序计数器,这 3 个区域随线程而生,随线程而灭。
看下 HotSpot VM 运行时数据区:
进程是资源分配的最小单位,线程是 CPU 调度的最小单位,线程的执行是抢占式的。Java 线程采用的是单线程编程模型,程序会自动创建主线程,主线程可以创建子线程,主线程原则上要后于子线程完成执行。需要注意的是,JVM 线程是多线程的,JVM 实例在创建的时候会同时创建很多线程,例如垃圾收集器的线程等。
由于进程有独立的地址空间,而线程没有,所以多进程的程序比多线程的程序要健壮,但是进程的切换比线程的切换开销大,所以多线程比多进程拥有更高的性能。
并发性和并行性的区别:
并发性:同一时刻只有一条指令执行,多个进程指令被快速轮换执行。
并行性:在同一时刻有多条指令在多个处理器上同时执行。
使用多线程编程的好处:
进程之间不能共享内存,但线程之间可以;使用多线程来实现多任务并发比多进程的效率高;Java内置了多线程功能支持。
1.多线程三大特性
多线程编程中三大特性:原子性,可见性,有序性。
1、原子性
原子性即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
举个最简单的例子,假如为一个 32 位的变量赋值过程不具备原子性的话,会发生什么后果?
i = 9;
假若一个线程执行到这个语句时,我暂且假设为一个 32 位的变量赋值包括两个过程:为低 16 位赋值,为高 16 位赋值。
那么就可能发生一种情况:当将低 16 位数值写入之后,突然被中断,而此时又有一个线程去读取 i 的值,那么读取到的就是错误的数据。
2、可见性
可见性即当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。举个简单的例子,看下面这段代码:
// 线程1执行的代码
int i = 0;
i = 10;
// 线程2执行的代码
j = i;
假若执行线程 1 的是 CPU1,执行线程 2 的是 CPU2。由上面的分析可知,当线程 1 执行 i =10 这句时,会先把 i 的初始值加载到 CPU1 的高速缓存中,然后赋值为 10,那么在 CPU1 的高速缓存当中 i 的值变为 10 了,却没有立即写入到主存当中。
此时线程 2 执行 j = i,它会先去主存读取 i 的值并加载到 CPU2 的缓存当中,注意此时内存当中 i 的值还是0,那么就会使得 j 的值为0,而不是 10。
这就是可见性问题,线程 1 对变量 i 修改了之后,线程 2 没有立即看到线程 1 修改的值。
当 CPU 执行 int i = 0; i = 10; 这段代码时,会从主存获取 i 的初始值,然后把 i 的初始值加载到 CPU1 的高速缓存中,然后赋值为 10,最后重新写入到主存当中。
3、有序性
有序性即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:
int i = 0;
boolean flag = false;
i = 1; // 语句1
flag = true; // 语句2
上面代码定义了一个 int 型变量,定义了一个 boolean 类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句 1 是在语句 2 前面的,那么 JVM 在真正执行这段代码的时候会保证语句 1 一定会在语句 2 前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。
下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
比如上面的代码中,语句 1 和语句 2 谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句 2 先执行而语句 1 后执行。但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:
int a = 10; // 语句1
int r = 2; // 语句2
a = a + 3; // 语句3
r = a*a; // 语句4
这段代码有4个语句,那么可能的一个执行顺序是:语句 2 -> 语句 1 -> 语句 3 -> 语句 4
那么可不可能是这个执行顺序呢: 语句 2 -> 语句 1 -> 语句 4 -> 语句 3
不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令 Instruction 2 必须用到 Instruction 1 的结果,那么处理器会保证 Instruction 1 会在 Instruction 2 之前执行。
虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:
// 线程1:
context = loadContext(); // 语句1
inited = true; // 语句2
// 线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
上面代码中,由于语句 1 和语句 2 没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程 1 执行过程中先执行语句 2,而此是线程 2 会以为初始化工作已经完成,那么就会跳出 while 循环,去执行 doSomethingwithconfig(context) 方法,而此时 context 并没有被初始化,就会导致程序出错。
从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
2.Thread的使用
1.Thread的创建
优点 | 缺点 | |
---|---|---|
继承Thread类 | 编写简单,访问当前线程简单,获取当前线程直接使用this即可 | 不能继承其他父类 |
实现Runnable接口或Callable接口 | 共享一个target对象,适合多个线程来处理一个资源的情况 | 编程相对复杂,获取当前线程必须使用Thread.currentThread();方法 |
// new MyThread().start();
public class MyThread extends Thread {
@Override
public void run() { // 线程执行逻辑
}
}
// MyThread2 thread2 = new MyThread2();
// new Thread(thread2, "新线程1").start();
public class MyThread2 implements Runnable {
@Override
public void run() { // 线程执行逻辑
}
}
// 通过FutureTask获取子线程的返回值
// MyThread3 thread3 = new MyThread3();
// FutureTask<String> task = new FutureTask<String>(thread3);
// new Thread(task, "有返回值的线程").start();
// String value = task.get(); // get方法会阻塞,直到子线程执行结束才返回,带超时: task.get(500, TimeUnit.MILLISECONDS);
//
// 通过线程池获取子线程的返回值
// Future<String> future = threadPool.submit(thread3);
// String value = future.get();
public class MyThread3 implements Callable<String> {
@Override
public String call() throws Exception { // 线程执行逻辑,可以有返回值
return "hello";
}
}
常见的问题:
1、Thread 中 start 和 run 方法的区别?
分析 JDK 源码后得知,主线程调用 start 方法,会调用 JVM 的 StartThread 方法去创建一个新的子线程,然后去执行这个子线程 run 方法里的内容。因此区别是调用 start 方法会创建一个新的子线程并启动,而 run 只是 Thread 的一个普通方法的调用。这两个方法不存在可比性。
2、Thread 和 Runnable 是什么关系?
Thread 是实现了 Runnable 接口的类,通过 start 方法给 Runnable 接口的 run 方法赋予了多线程的特性,由于 Java 类的单一继承原则,提升系统的可扩展性,推荐多使用 Runnable 接口。
3、如何给 run 方法传参?
实现方式主要有构造函数传参、成员变量传参、回调函数传参。
4、如何实现处理线程的返回值?
- 主线程等待法,建立 while 循环,如果子线程还没执行完,那么 Thread.currentThread().sleep(100);,直至子线程处理完毕,缺点是需要自己实现循环等待的逻辑,代码臃肿,需要等待的时间也是不确定的,不能做到精准的控制。
- 使用 Thread 类的 join 阻塞当前线程以等待子线程处理完毕,缺点是粒度不够细,没法做到多个子线程之间的等待关系;
- 通过 Callable 接口实现,通过 FutureTask 或者线程池获取。
2.Thread的状态
阅读源码 java.lang.Thread.State 可知,线程的状态分为:
说明 | |
---|---|
NEW(新建状态) | 创建后尚未启动的线程的状态。 |
RUNNABLE(运行状态) | 包含 Running 和 Ready,主线程调用 start 方法后处于 Running,处于 Running 状态的线程位于可运行线程池中,等待被调度选中,获取 CPU 的使用权;处于 Ready 状态的线程位于线程池中,等待被线程调度选中,获取 CPU 的使用权;而在 Ready 状态的线程获取 CPU 执行时间后就会变成 Running 状态的线程。 |
BLOCKED(阻塞状态) | 等待获取排他锁 |
WAITING(无限期等待) | 不会被分配 CPU 执行时间,需要显式被唤醒。 |
TIMED_WAITING(限期等待) | 在一定时间后会由系统自动唤醒。 |
TERMINATED(结束状态) | 已终止线程的状态,线程已经结束运行。 |
线程的状态转换图如下:
- 如果程序调用子线程的 start() 后子线程立即执行,程序可以使用 Thread.sleep(1) 来让当前运行的线程(主线程)睡眠1毫秒;
- 直接掉用线程的 stop() 来结束该线程,容易导致死锁,通常不推荐使用;
- 测试线程是否死亡可以用 isAlive(),当线程处于就绪、运行、阻塞三种状态时,该方法返回 true;当线程处于新建、死亡两种状态时返回 false;
- 不要对处于死亡的线程调用 start(),否则会引发 IllegalThreadStateException。
常见的问题:
1、什么情况会让线程陷入 WAITING(无限期等待)状态?
没有设置 Timeout 参数的 Object.wait() 方法、没有设置 Timeout 参数的 Thread.join() 方法和 LockSupport.park() 方法会让线程陷入 WAITING 状态。
2、什么情况会让线程陷入 TIMED_WAITING (限期等待)状态?
Thread.sleep() 方法、设置了 Timeout 参数的 Object.wait() 方法、设置了 Timeout 参数的 Thread.join() 方法、LockSupport.parkNanos() 方法和 LockSupport.parkUntil() 方法会让线程陷入 TIMED_WAITING 状态。
3、阻塞状态与等待状态的区别?
阻塞状态等待获取排他锁,这个事件将在另外一个线程放弃锁的时候发生。而等待状态是在等待一段时间或者有唤醒动作的时候发生。
3.Thread的静态方法
1.sleep方法
静态方法 Thread.sleep(long millis); 用来暂停程序的执行,暂停 millis 毫秒,暂停期间进入阻塞状态。
public class SleepTask implements Runnable {
@Override
public void run() {
System.out.println("sleepTask is do");
Thread.sleep(2000);
System.out.println("sleepTask is done");
}
}
2.yield方法
当调用 Thread.yield(); 方法时,会给线程调度器一个当前线程愿意让出 CPU 使用的暗示,但是线程调度器可能会忽略这个暗示。
public class YieldTask implements Runnable {
@Override
public void run() {
System.out.println("yieldTask is do");
Thread.yield();
System.out.println("yieldTask is done");
}
}
4.Thread的实例方法
1.join方法
方法 join 让一个线程等待另一个线程完成的方法。
SleepTask sleepTask = new SleepTask();
Thread t1 = new Thread(sleepTask, "sleepTask");
t1.start();
t1.join();
System.out.println("task is done");
2.interrupt方法
停止线程之前我们可以调用 Thread 实例对象的 stop() 方法,stop() 方法是由一个线程去停止另一个线程,这种方式太过暴力且是不安全的,所以这个方法在新的 JDK 版本中已经被抛弃了。类似还有 Thread 实例对象的 suspend() 和 resume() 方法,也同样都被废弃了。
目前停止线程使用的是 interrupt() 方法,通知线程应该中断了。
如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态并抛出一个 InterruptedException;如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,被设置中断标志的线程将继续正常运行,不受影响。
interrupt() 并不能中断线程,中断需要被调用的线程配置中断才行。也就是说一个线程如果有被中断的需求,需要做到:
- 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程;
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,被设置中断标志的线程将继续正常运行,不受影响。
public class InterruptTask implements Runnable {
@Override
public void run() {
try {
// 检查中断标志位, 如果被设置了中断标志就自行停止线程
while (!Thread.currentThread().isInterrupted()) {
// 业务逻辑
}
} catch (InterruptedException e) {
logger.error("{} ({}) catch InterruptedException", Thread.currentThread().getName(),Thread.currentThread().getState());
// 正确处理异常, 例如catch异常后就结束线程
}
}
}
调用 interrupt() 方法通知线程应该中断了:
@Test
public void test() throws Exception {
InterruptTask interruptTask = new InterruptTask(lock);
Thread t1 = new Thread(interruptTask, "waitTask");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
4.其他方法
1、setDaemon 方法
调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程。如果前台线程都死亡,后台线程都死亡。isDaemon()方法用于判断指定线程是否为后台线程,主线程默认为前台线程。
2、setPriority 方法
调用 setPriority 方法 改变线程优先级
设置成最高优先级:.setPriority(Thread.MAX_PRIORITY);
设置成最低优先级:.setPriority(Thread.MIN_PRIORITY);
改变主线程的优先级:Thread.currentThread().setPriority(6);
5.Object的wait/notify机制
对于 JVM 中运行程序的每个 Object 来说,都有两个池,锁池 EntryList 和等待池 WaitSet,而这两个池与 Object 类的 wait()、notify()、notifyAll() 三个方法以及 synchronized 相关,wait() 会让出 CPU,释放已经占有的同步锁,使线程进入无限期等待,除非调用 notify() 或 notifyAll() 唤醒,使等待的线程继续运行。
锁池和等待池都是针对对象而言的:
说明 | |
---|---|
锁池 EntryList | 假设线程 A 已经拥有了某个对象(不是类)的锁,而其他线程 B、C 想要调用这个对象的某个 synchronized 方法(或者块),由于 B、C 线程在进入对象的 synchronized 方法(或者块)之前必须先获得该对象锁的拥有权,而恰好该对象的锁目前正被线程 A 所占用,此时 B、C 线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池。 |
等待池 WaitSet | 假设线程 A 调用了某个对象的 wait() 方法,线程 A 就会释放该对象的锁,同时线程 A 就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁。当调用 notify/notifyAll 时,这时被环境的对象将会进入到该对象的锁池中,竞争该对象的锁。 |
Object 类的方法:
说明 | |
---|---|
wait() | 使当前执行代码的线程进行等待,将当前线程置入 “预执行队列” 中,并且在 wait() 所在的代码行处停止执行,直到接到通知或被中断为止。只能在同步方法或同步代码块中调用 wait() 方法,在执行 wait() 方法后,当前线程释放锁。在 wait() 返回前,线程与其他线程竞争重新获取锁。 |
wait(long) | 带一个参数的表示等待某一时间内是否有线程对锁进行唤醒,如果超过这个时间则自动唤醒。 |
notify() | 用来通知那些可能等待该对象的对象锁的其他线程。只能在同步方法或同步代码块中调用 notify() 方法,在执行 notify() 方法后,当前线程不会马上释放锁,呈 wait 状态的线程也不能马上获取该对象锁,要等到 notify() 方法的线程将程序执行完,也就是退出 synchronized 代码块后,当前线程才会释放锁,而呈 wait 状态所在线程才可以获取该对象锁。 |
public class WaitTask implements Runnable {
private final Object lock; // 这里的lock就是上面所说的某个对象(不是类)的锁
public WaitTask(Object lock){
super();
this.lock = lock;
}
@Override
public void run() {
System.out.println("notifyTask is do");
synchronized (lock) {
System.out.println("waitTask get lock");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("waitTask is done");
}
}
}
public class NotifyTask implements Runnable {
private final Object lock;
public NotifyTask(Object lock){
super();
this.lock = lock;
}
@Override
public void run() {
System.out.println("notifyTask is do");
synchronized (lock) {
System.out.println("notifyTask get lock");
lock.notify();
System.out.println("notifyTask end");
}
}
}
测试类:
@Test
public void test() throws Exception {
Object lock = new Object();
WaitTask waitTask = new WaitTask(lock);
NotifyTask notifyTask = new NotifyTask(lock);
Thread t1 = new Thread(waitTask, "waitTask");
Thread t2 = new Thread(notifyTask, "notifyTask");
t1.start();
Thread.sleep(1000);
t2.start();
}
程序运行结果:
waitTask get lock
notifyTask get lock
notifyTask end
waitTask end
常见的问题:
1、sleep 和 wait 的区别?
sleep 是 Thread 类的方法,可以在任何地方使用;wait 是 Object 类中定义的方法,只能在 synchronized 方法或者 synchronized 块中使用。
最主要的本质区别是 Thread.sleep 只会让出 CPU,不会导致锁行为的改变;Object.wait 不仅会让出 CPU,还会释放已经占有的同步资源锁,这也是只能在 synchronized 方法或者 synchronized 块中使用的原因,只有获取锁了才能释放锁。
2、notify 和 notifyAll 的区别?
notifyAll 会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会,没有获取到锁的而已经待在锁池中的线程只能等待其他机会去获取锁,而不能再主动回到等待池中;
notify 只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会。
3.线程池
使用线程池可以降低资源消耗,提高线程的可管理性。
可以利用 JUC 包(java.util.concurrent)下的 Executors 的静态方法创建不同的线程池:
方法 | 说明 |
---|---|
newFixedThreadPool(int nThreads) | 指定工作线程数量的线程池。 |
newCachedThreadPool() | 处理大量短时间工作任务的线程池; 1、试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程; 2、如果线程闲置的时机超过阈值,则会被终止并移除缓存; 3、系统长时间闲置的时候,不会消耗什么资源。 |
newSingleThreadExecutor() | 创建唯一的工作者线程来执行任务,如果线程异常结束,会有另一个线程取代它。 |
newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize) | 定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程。 |
newWorkStealingPool() | 内部会构建 ForkJoinPool,利用 working-stealing 算法,并行地处理任务,不保证处理顺序。JDK1.7 及以上版本 Java 提供了 Fork/Join 框架,Fork/Join 是把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架 。 |
看下这几个方法的具体实现:
package java.util.concurrent;
public class Executors {
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()); // 基于链表的阻塞队列
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>())); // 基于链表的阻塞队列
}
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
// 省略了部分代码
}
生产环境中不建议使用 Executors 的静态方法创建线程池,建议根据业务自定义 ThreadPoolExecutor ,这里我们封装了一个基于链表的阻塞队列的线程池:
public class LinkedBlockingThreadPool extends ThreadPoolExecutor {
/**
* 线程池 基于链表的阻塞队列
*
* @param corePoolSize 核心线程数
* @param maximumPoolSize 最大线程数 maximumPoolSize >= corePoolSize
* @param keepAliveTime 线程存活时间, 秒
* @param blockingQueueCapacity LinkedBlockingQueue的容量
*/
public LinkedBlockingThreadPool(int corePoolSize, int maximumPoolSize,
long keepAliveTime, int blockingQueueCapacity) {
super(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
// 当任务队列是LinkedBlockingQueue, 会将超过核心线程的任务放在任务队列中排队
new LinkedBlockingQueue<Runnable>(blockingQueueCapacity),
Executors.defaultThreadFactory());
}
}
LinkedBlockingQueue 一定需要指定大小,因为 LinkedBlockingQueue 的默认容量是 Integer.MAX_VALUE,不指定的话极端情况下会造成大量积压,进而引发 OOM,业务不可用。
ThreadPoolExecutor 构造函数的七个参数:
public class ThreadPoolExecutor extends AbstractExecutorService {
// ctl的高3位用来保存线程池的运行信息, 另外的低29位保存线程池内有效线程的数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 线程不够用时能够创建的最大线程数 maximumPoolSize>=corePoolSize
long keepAliveTime, // 核心线程数之外的空闲线程的存活时间, 超时后线程销毁
TimeUnit unit, // 核心线程数之外的空闲线程的存活时间单位 TimeUnit.SECONDS:秒
BlockingQueue<Runnable> workQueue, // 任务等待队列
ThreadFactory threadFactory), // 创建新线程的线程工厂
RejectedExecutionHandler handler) { // 线程池饱和策略
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
// 省略了部分代码
}
public abstract class AbstractExecutorService implements ExecutorService {
// 省略代码
}
public interface ExecutorService extends Executor {
// 省略代码
}
public interface Executor {
void execute(Runnable command);
}
ThreadFactory 参数表示创建新线程的线程工厂,一般使用默认的 Executors.defaultThreadFactory(),用这个线程工厂创建的新线程具有相同的优先级且是非守护线程。
RejectedExecutionHandler 参数表示线程池饱和策略,如果阻塞队列满了,并且没有空闲线程,这时如果继续提交任务,这时就需要采取一种策略处理该任务,线程池提供了四种策略:
- AbortPolicy:直接抛出异常,默认策略;
- CallerRunsPolicy:用调用者所在的线程来执行任务;
- DiscardOldestPolicy:丢弃队列中靠最前的任务,并执行当前任务;
- DiscardPolicy:直接丢弃任务。
除此之外,也可以通过实现 RejectedExecutionHandler 接口的自定义 handler。
ExecutorService 的工作流程:
新任务提交 execute 执行后的判断流程图:
线程池的状态:
- RUNNING:能接受新提交的任务,并且也能处理 workQueue 中的任务;
- SHUTDOWN:terminated() 方法执行完后进入该状态。不再接受新提交的任务,但可以处理存量任务;
- STOP:不再接受新提交的任务,也不处理存量任务;
- TIDYING:所有的任务都已终止;
- TERMINATED:terminated() 方法执行完后进入该状态。
线程池的状态转换图:
JUC 的三个 Executor 接口:
- Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦;
- ExecutorService:具备管理执行器和任务生命周期的方法,提交任务机制更完善;
- ScheduledExecutorService:支持 Future 和定期执行任务。
如何设置合理的线程池大小?
首先通过公式预估所需线程池大小:
I/O 密集型应用: 线 程 数 = c p u 核 数 ∗ ( 1 + 线 程 平 均 等 待 时 间 / 线 程 平 均 执 行 时 间 ) 线程数 = cpu 核数*(1 + 线程平均等待时间/线程平均执行时间) 线程数=cpu核数∗(1+线程平均等待时间/线程平均执行时间)
CPU 密集型应用: 线 程 数 = c p u 核 数 + 1 线程数 = cpu 核数 + 1 线程数=cpu核数+1
这两个公式是前人根据大量的经验得出的较为合理的计算方式,然后再通过压测调优,找到最合适的线程池大小。
创建线程池一般不建议使用 Executors 的静态方法去创建,而是应该通过直接构造 ThreadPoolExecutor 的方式,这样的处理方式可以更加明确线程池的运行规则,规避资源耗尽的风险。
线程池配置记录:
机器配置:8 核 16 G,QPS:500,IO 密集型,业务比较重。
Service 层一级并发线程池配置:
核心线程数 corePoolSize:100
最大线程数 maximumPoolSize:200
存活时间 keepAliveTime:5 秒
任务等待队列 workQueue:1500
Service 层二级并发线程池配置:
核心线程数 corePoolSize:200
最大线程数 maximumPoolSize:300
存活时间 keepAliveTime:10 秒
任务等待队列 workQueue:2000
更多推荐
所有评论(0)