1. 线程池的重要性
1.1 对于操作系统来说
在操作系统创建线程、切换线程状态、终结线程都要进行CPU调度,进行上下文切换,这是一个耗费时间和系统资源的事情。
大多数实际场景中是这样的:处理某一次请求的时间是非常短暂的,但是请求数量是巨大的。这种技术背景下,如果我们为每一个请求都单独创建一个线程,那么物理机的所有资源基本上都被操作系统创建线程、切换线程状态、销毁线程这些操作所占用,用于业务请求处理的资源反而减少了
所以最理想的处理方式是,将处理请求的线程数量控制在一个范围,既保证后续的请求不会等待太长时间,又保证物理机将足够的资源用于请求处理本身。
另外,一些操作系统是有最大线程数量限制的。当运行的线程数量逼近这个值的时候,操作系统会变得不稳定。这也是我们要限制线程数量的原因
总结来说:
① 反复创建线程开销大
② 过多的线程会占用太多的内存
解决以上两个问题的思路
① 用少量的线程 - 避免内存占用过多
② 让这部分线程都保持工作,且可以反复执行任务 - 避免生命周期的损耗
1.2 对于服务器
服务器应用程序中经常出现的情况是:单个任务处理的时间很短而请求的数目却是巨大的,为每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源要比花在处理实际的用户请求的时间和资源更多
除了创建和销毁线程的开销之外,活动的线程也消耗系统资源(线程的生命周期!)。在一个JVM 里创建太多的线程可能会导致系统由于过度消耗内存而用完内存或“切换过度”。为了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻处理的请求数目
1.3 线程池的好处
- 加快反应速度,消除线程创建和销毁的开销
- 利用线程池很好的掌控线程的数量,合理的利用CPU和内存
- 利用线程池统一管理线程
1.4 线程上下文切换
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行 线程自己调用了
sleep、yield、wait、join、park、synchronized、lock
等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生会影响性能
2 线程池构造函数的参数
public ThreadPoolExecutor(int corePoolSize,
//corePoolSize 核心线程数目 (最多保留的线程数)
int maximumPoolSize,
//maximumPoolSize 大线程数目
long keepAliveTime,
//keepAliveTime 救急线程的生存时间
TimeUnit unit,
//unit 时间单位 - 针对救急线程
BlockingQueue<Runnable> workQueue,
//workQueue 存放任务的阻塞队列
ThreadFactory threadFactory,
//threadFactory 线程工厂 - 可以为线程创建时起个好名字
RejectedExecutionHandler handler
//handler 线城池的饱和策略事件 拒绝策略
)
2.1corePoolSize
&maximumPoolSize
&RejectedExecutionHandler
jdk提供的线程池里面分了两种线程,核心线程和救急线程(救急线程的前提是配合有界队列使用),他们的工作方式是这样的
① 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务(并不是线程池一创建就把线程池中的线程都创建,而是来任务了再创建)
② 当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排队,直到有空闲的线程
③ 如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急,执行新加的任务。
④ 如果线程到达maximumPoolSize 或者线程池已经关闭了仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现
-
AbortPolicy
让调用者抛出RejectedExecutionException
异常,这是默认策略 -
CallerRunsPolicy
让调用者运行任务 -
DiscardPolicy
放弃本次任务 -
DiscardOldestPolicy
放弃队列中最老的任务,本任务取而代之 -
以上是jdk提供的策略,第三方也提供了一些策略
-
Dubbo
的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方 便定位问题 -
Netty
的实现,是创建一个新线程来执行任务 -
ActiveMQ
的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略 -
PinPoint
的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
⑤ 当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由 keepAliveTime 和 unit 来控制(救急线程有生存时间,核心线程没有生存时间)
为什么达到corePoolSize后先往阻塞队列中放置任务,等阻塞队列满了再创建 maximumPoolSize - corePoolSize 数目的线程来救急?
因为线程池希望保持较少的线程数,并且只有再负载变得很大的时候才增加他
2.2 keepAliveTime
救急线程的生存时间
如果线程池当前的线程数多于corePoolSize,那么多余的线程在空闲时间超过keepAliveTime,他们就会被终止回收
2.3 ThreaFactory
新的线程是由ThreaFactory创建的,默认使用Executors.defaultThreadFactory(),创建出来的线程都在同一个线程组,拥有同样的NORM_PRIORITY优先级并且都不是守护线程;
如果自己指定ThreaFactory,那么就可以改变线程名,线程组,优先级,是否是守护线程等
/**
* The default thread factory
*/
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
2.4 工作队列
首先阻塞队列就是一个在任意时刻,不管并发多高,保证永远只有一个任务可以入队或出队,也就是线程安全
① ArrayBlockingQueue
ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序
② LinkedBlockingQueue
LinkedBlockingQueue(可设置容量队列)基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列
LinkedBlockingQueue导致的OOM问题
因为即使是无边界的阻塞队列,所以当请求数越来越多,并且无法及时处理完毕的时候,也就是请求堆积的时候,会容易造成占用大量的内存,可能导致OOM
③ DelayQueue
DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序 newScheduledThreadPool线程池使用了这个队列
④ PriorityBlockingQueue
PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列
⑤ SynchronousQueue
SynchronousQueue(同步队列)一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列
3. 使用Executors
创建线程池
因为上面的构造函数参数太多对使用者并不友好,所以提供了一些方法来创建不同类型的线程池,但是方法的实现还是那个构造函数
//创建一个不限制线程个数的线程池
ExecutorService executor = Executors.newCachedThreadPool();
//创建一个固定线程个数的线程池
ExecutorService executor = Executors.newFixedThreadPool(10);
3.1 newFixedThreadPool
获得固定大小的线程池,没有救急线程
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads,
nThreads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
特点
- 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
- 阻塞队列是无界的,可以放任意数量的任务,既然是无界的,那么就不存在放满,那么也就不会使用到救急线程
public static void main(String[] args) {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
public void run() {
try {
System.out.println(index);
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
优点:
具有线程池提高程序效率和节省创建线程时所耗的开销。
缺点:
① 在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源
② newFixedThreadPool
使用了无界的阻塞队列LinkedBlockingQueue
,如果线程获取一个任务后,任务的执行时间比较长(比如,上面demo设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终导致OOM
使用场景:
适用于任务量已知,相对耗时的任务
FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务
3.2 newCachedThreadPool
可缓存线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0,
Integer.MAX_VALUE,
60L,
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>()); }
特点:
① 核心线程数是 0
② 最大线程数是 Integer.MAX_VALUE
,救急线程的空闲生存时间是 60s,意味着全部都是救急线程(60s 后可以回收)且救急线程可以无限创建
③ 队列采用了 SynchronousQueue
实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)
整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
public void run() {
System.out.println(index);
}
});
}
}
弊端
第二个参数maximumPoolSize被设置为Integer.MAX_VALUE,这可能会导致创建线程的数量过多而导致OOM
3.3 newSingleThreadExecutor
单线程的线程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(
new ThreadPoolExecutor(1,
1,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>())); }
特点:
① 核心线程数为1
② 最大线程数也为1
③ 阻塞队列是LinkedBlockingQueue
④ keepAliveTime为0
希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放
那我为什么不自己创建单线程来串行的执行任务呢
- 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一 个线程,保证池的正常工作
Executors.newFixedThreadPool(1) 初始时为1不也可以实现么?
不用Executors.newFixedThreadPool(1)是因为以后还可以修改对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改,而对于newSingleThreadExecutor线程个数始终为1,不能修改 FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
public static void main(String[] args) {
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
3.4 任务调度线程池
延迟执行/周期执行
3.4.1 任务调度线程池加入前 - Timer
在任务调度线程池加入之前可以使用java.util.Timer来实现定时的功能,Timer的优点在于简简单易用,但是由于所有的任务都是同一个线程来调度的,因此所有的任务都是串行执行,同一时间只能有一个任务在执行,且前一个任务的延迟或异常会影响到后面的任务
public static void main(String[] args) {
Timer timer = new Timer();
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
System.out.println("tast1");
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
TimerTask timerTask2 = new TimerTask() {
@Override
public void run() {
System.out.println("tast2");
}
};
//使用timer添加两个任务,希望他们都在1s后执行
//但是由于timer内只有一个线程来顺序执行队列中的任务,因此任务1的延迟会影响任务2的执行
timer.schedule(timerTask,1000);
timer.schedule(timerTask2,1000);
}
3.4.2 ScheduledThreadPoolExecutor
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
特点
① 最大线程数为Integer.MAX_VALUE
② 阻塞队列是DelayedWorkQueue
③ keepAliveTime为0
④ scheduleAtFixedRate()
:按某种速率周期执行
⑤ scheduleWithFixedDelay()
:在某个延迟后执行
延迟执行
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
// 添加两个任务,希望它们都在 1s 后执行
executor.schedule(() -> {
System.out.println("任务1,执行时间:" + new Date());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}, 1000, TimeUnit.MILLISECONDS);
executor.schedule(() -> {
System.out.println("任务2,执行时间:" + new Date()); },
1000,TimeUnit.MILLISECONDS);
前一个任务的延迟或异常不会影响到之后的任务
反复执行
使用scheduleAtFixedRate可以反复执行任务
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1); log.debug("start...");
pool.scheduleAtFixedRate(() -> {
log.debug("running..."); },
1, 1, TimeUnit.SECONDS);
但是如果任务的本身时间较长,超过了时间间隔则会影响之后的执行
规定每个任务和每个任务的间隔时间
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.debug("start...");
pool.scheduleWithFixedDelay(()-> {
log.debug("running...");
sleep(2); },
1, 1, TimeUnit.SECONDS);
使用场景
周期性执行任务的场景,需要限制线程数量的场景
4. 提交任务
关于向线程池中提交任务有下面几种方法
① 执行任务
// 执行任务
void execute(Runnable command);
② 提交任务 task,用返回值 Future 获得任务执行结果
// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);
//使用
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> future = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "ok";
}
});
System.out.println(future.get());
③ 提交 tasks 中所有任务
// 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)throws InterruptedException;
④ 提交 tasks 中所有任务,带超时时间
// 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout,
TimeUnit unit)throws InterruptedException;
⑤ 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)throws InterruptedException, ExecutionException
⑥ 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)throws InterruptedException,ExecutionException, TimeoutException;
5. 线程池中的线程数量设置为多少合适
① CPU密集型
对于CPU密集型(加密,计算hash等),因为所有的线程都要使用cpu,争夺cpu,这个时候cpu是满负荷高效运转的,最佳线程数设计为CPU核心数的1-2倍即可
耗时IO型
(读写文件,数据库,网络读写等),因为大部分时间线程处于等待状态,这个时候最佳线程数一般会大于cpu核心数的很多倍,以JVM线程监控显示繁忙情况为依据保证线程空闲可以衔接上
Brain Goetz推荐的计算方法
线程数 = CPU核心数*(1+平均等待时间/平均工作时间)
实际上想要更精准的计算应该对不同的场景进行压测选择最优方案
6. 停止线程
//线程池状态变为 SHUTDOWN
//- 不会接收新任务 - 但已提交任务会执行完 - 此方法不会阻塞调用线程的执行
void shutdown();
/* 线程池状态变为 STOP - 不会接收新任务 - 会将队列中的任务返回 - 并用 interrupt 的方式中断正在执行的任务 */
List<Runnable> shutdownNow();
//指定时间内线程池中的线程是不是都运行完毕了
boolean awaitTermination(long timeout, TimeUnit unit)
7. 钩子方法
可以在每个任务的执行前后加上自己的方法
/**
* 描述: 演示每个任务执行前后放钩子函数
*/
public class PauseableThreadPool extends ThreadPoolExecutor {
private final ReentrantLock lock = new ReentrantLock();
private Condition unpaused = lock.newCondition();
private boolean isPaused;
//...构造方法
@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("线程池被恢复了");
}
}
8. 异常处理
在上面的使用中,如果线程池中的线程出现了异常并不会被抛出来,我们要合理的处理这些异常
8.1 主动捉异常
ExecutorService pool = Executors.newFixedThreadPool(1);
pool.submit(() -> {
try {
log.debug("task1");
int i = 1 / 0;
} catch (Exception e) {
log.error("error:", e);
}
})
8.2 使用 Future
ExecutorService pool = Executors.newFixedThreadPool(1);
Future<Boolean> f = pool.submit(() -> {
log.debug("task1");
int i = 1 / 0;
return true;
});
log.debug("result:{}", f.get())
8.3 为工作者线程设置UncaughtExceptionHandler
ExecutorService threadPool = Executors.newFixedThreadPool(1, r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler(
(t1, e) -> {
System.out.println(t1.getName() + "线程抛出的异常"+e);
});
return t;
});
threadPool.execute(()->{
Object object = null;
System.out.print("result## " + object.toString());
});
8.4 重写ThreadPoolExecutor的afterExecute方法,处理传递的异常引用
class ExtendedExecutor extends ThreadPoolExecutor {
// 这可是jdk文档里面给的例子。。
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t == null && r instanceof Future<?>) {
try {
Object result = ((Future<?>) r).get();
} catch (CancellationException ce) {
t = ce;
} catch (ExecutionException ee) {
t = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // ignore/reset
}
}
if (t != null)
System.out.println(t);
}
}}
9. 线程池的状态
Running:接受新任务并处理排队任务
ShutDown:不接受新任务但处理排队任务
Stop:不接受新任务也不处理排队任务,并中断正在进行的任务
Tidying:所有的任务都已终止,并运行terminate()钩子方法
Terminated:terminate()运行完毕