目录
一、初识线程池
简单的使用线程:
static class Task implements Runnable {
@Override
public void run() {
System.out.println("执行了任务");
}
}
Thread thread = new Thread(new Task());
thread.start();
弊端:
ForLoop方式创建线程,开销太大,占用太多内存。我们希望有固定数量的线程,来执行这1000个线程。这样就避免了反复创建并销毁线程所带来的开销问题。
线程池的好处:
加快响应速度、合理利用 CPU 和内存、统一管理。
线程池适合应用的场景:
服务器接收到大量请求时,使用线程池技术是非常合适的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率。
实际上,在开发中,如果需要创建5个以上的线程,那么就可以使用线程池来管理。
二、线程池的应用
2.1 线程增减的时机
创建和停止线程池:
ThreadPoolExecutor 线程池构造方法的参数:
增减线程的特点:
1. 通过设置 corePoolSize 和 maximumPoolSize 相同,就可以创建固定大小的线程池。
2. 线程池希望保持较少的线程数,并且只有在负载变得很大时才增加它。
3. 通过设置 maximumPoolSize 为很高的值,可以允许线程池容纳任意数量的并发任务。
4. 只有在队列填满时才创建多于 corePoolSize 的线程,如果使用的是无界队列,那么线程数就不会超过 corePoolSize。
2.2 线程存活时间和工作队列
keepAliveTime:
如果线程池当前的线程数多于 corePoolSize,那么多余的线程空闲时间超过 keepAliveTime,它们就会被终止。
ThreadFactory 用来创建线程:
默认使用 Executors.defaultThreadFactory(),创建出来的线程都在同一个线程组。
如果自己指定 ThreadFactory,那么就可以改变线程名、线程组、优先级、是否是守护线程等。
workQueue 工作队列:
有3种最常见的队列类型:
1)直接交接:SynchronousQueue(无缓冲队列,maximumPoolSize要设大一点)
2)无界队列:LinkedBlockingQueue(maximumPoolSize无意义)
3)有界的队列:ArrayBlockingQueue(maximumPoolSize有意义)
2.3 暂无
2.4 暂无
2.5 自动创建线程池的风险
手动创建线程池更好,因为这样可以更加明确线程池的运行规则,避免资源耗尽的风险。
自动创建线程池(即直接调用 JDK 封装好的构造方法)可能带来哪些问题?
public class FixedThreadPoolTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(4);
for (int i = 0; i < 1000; i++) {
executorService.execute(new Task());
}
}
}
class Task implements Runnable {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
// 会循环打印1 2 3 4,只有4个线程被创建
}
}
newFixedThreadPool 容易造成大量内存占用,可能会导致 OOM。
2.6 常见线程池的用法演示
newSingleThreadExecutor 当请求堆积的时候,可能会占用大量的内存。
ExecutorService executorService = Executors.newSingleThreadExecutor();
CachedThreadPool: 可缓存线程池
特点:具有自动回收多余线程功能,是直接交接队列。
弊端:在于第二个参数 maximumPoolSize 被设置为了 Integer.MAX_VALUE,这可能会创建数量非常多的线程,甚至导致 OOM。
ExecutorService executorService = Executors.newCachedThreadPool();
ScheduledThreadPool 支持定时及周期性任务执行的线程池
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);
threadPool.schedule(new Task(), 5, TimeUnit.SECONDS); //延迟执行
threadPool.scheduleAtFixedRate(new Task(), 1, 3, TimeUnit.SECONDS); //初始时间点、频率时间
正确的创建线程池的方法:
根据不同的业务场景,设置线程池参数。
比如:内存有多大,给线程取什么名字,任务被拒绝后如何记录日志等等。
线程池里的线程数量设定为多少比较合适?
CPU 密集型(加密、计算hash等):最佳线程数为 CPU 核心数的1-2倍左右。
耗时 IO 型(读写数据库、文件、网络读写等):最佳线程数一般会大于 CPU 核心数很多倍。这种情况,CPU 会等待外设设备,不会满负荷工作。
参考 Brain Goetz 推荐的计算方法:
线程数 = CPU 核心数 * (1 + 平均等待时间 / 平均工作时间 )
2.7 对比各种线程池的特点
阻塞队列分析:
FixedThreadPool 和 SingleThreadExecutor 的 Queue 是 LinkedBlockingQueue ?
CachedThreadPool 使用的 Queue 是 SynchronousQueue ?
ScheduledThreadPool 使用延迟队列 DelayedWorkQueue ?
workStealingPool 是 JDK1.8 加入的:
这个线程池和之前的都有很大不同
子任务(适用于二叉树查找、分矩阵等)
窃取:每个线程之间会合作的,帮助其他线程执行子任务(不要加锁、不保证顺序)
2.8 暂无
2.9 如何正确关闭线程池
shutdown:
初始化整个关闭过程。存量的任务执行完毕后,会拒绝新的任务。
executorService.shutdown();
isShutDown:
返回Boolean,告诉我们线程池是不是进入停止状态了。
executorService.isShutdown()
isTerminated:
返回Boolean,告诉我们线程池是不是完全终止了。包括正在执行的任务 和 队列里的任务 都清空了。
executorService.isTerminated()
awaitTermination:
等一会儿,看线程池停没停后,返回 Boolean。起到检测作用。
boolean b = executorService.awaitTermination(7L, TimeUnit.SECONDS);
shutdownNow:
立刻关闭线程池,会给正在执行的任务发射中断信号,try{}catch(){} 捕获。返回任务队列中的任务。
List<Runnable> runnableList = executorService.shutdownNow();
2.10 暂无
2.11 暂无
2.12 暂停和恢复线程池
拒绝时机:
1. 当 Executor 关闭时,提交新任务会被拒绝。
2. 当 Executor 对最大线程和工作队列容量使用有限边界,并且已经饱和时。
4种拒绝策略:
AbortPolicy:抛出异常
DiscardPolicy: 默默丢弃任务
DiscardOldestPolicy: 默默丢弃最老的任务,腾出位置给新任务
CallerRunsPolicy:让提交新任务者执行新任务。可以降低提交任务速度,负反馈效果。
钩子方法,给线程池加点料:
每个任务执行前后:做点日志、统计
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* 描述: 演示每个任务执行前后放钩子函数
*/
public class PauseableThreadPool extends ThreadPoolExecutor {
private final ReentrantLock lock = new ReentrantLock();
private Condition unpaused = lock.newCondition();
private boolean isPaused;
public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}
public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
}
public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory,
handler);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
lock.lock();
try {
while (isPaused) {
unpaused.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private void pause() {
lock.lock();
try {
isPaused = true;
} finally {
lock.unlock();
}
}
public void resume() {
lock.lock();
try {
isPaused = false;
unpaused.signalAll();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
PauseableThreadPool pauseableThreadPool = new PauseableThreadPool(10, 20, 10l,
TimeUnit.SECONDS, new LinkedBlockingQueue<>());
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("我被执行");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 10000; i++) {
pauseableThreadPool.execute(runnable);
}
Thread.sleep(1500);
pauseableThreadPool.pause();
System.out.println("线程池被暂停了");
Thread.sleep(1500);
pauseableThreadPool.resume();
System.out.println("线程池被恢复了");
}
}
2.13 暂无
2.14 线程池实现复用的原因
实现原理、源码分析
线程池组成部分:
线程池管理器
工作线程
任务队列(要线程安全)
任务接口(Task)
Executor只有一个方法;ExecutorService有暂停等方法;ThreadPoolExecutor用来new线程池;
线程池实现任务复用的原理:
相同线程执行不同任务
2.15 线程池状态和使用注意点
execute方法:
根据是否到达corePool、线程是否停止 执行相关代码。感觉“线程池”是对“线程”和“任务”的封装。
使用线程池的注意点:
避免任务堆积(如:用FixedThreadPool)
避免线程数过度增加(如:用CachedThreadPool )
排查线程泄漏(线程执行完毕却未被回收,可能任务逻辑不对,任务始终结束不了)
三、课程总结
1. 线程池的自我介绍
2. 创建和停止线程池
3. 常见线程池的特点和用法
4. 任务太多,怎么拒绝
5. 钩子方法,给线程池加点料
6. 实现原理、源码分析
7. 使用线程池的注意点