一、池化技术
在系统开发过程中,我们经常会用到池化技术来减少系统消耗,提升系统性能。
说人数: 简单点来说,就是提前保存大量的资源,以备不时之需,池化技术就是通过复用来提升性能。
常见池:
- 对象池 通过复用对象来减少创建对象、垃圾回收的开销;
- 连接池 (数据库连接池、Redis连接池和HTTP连接池等)通过复用TCP连接来减少创建和释放连接的时间
- 线程池通过复用线程提升性能
使用内存池的优点
-
降低资源消耗。这个优点可以从创建内存池的过程中看出,当我们在创建内存池的时候,分配的都是一块块比较规整的内存块,减少内存碎片的产生。
-
提高相应速度。这个可以从分配内存和释放内存的过程中看出。每次的分配和释放并不是去调用系统提供的函数或操作符去操作实际的内存,而是在复用内存池中的内存。
-
方便管理。
使用内存池的缺点
缺点就是很可能会造成内存的浪费,因为要使用内存池需要在一开始分配一大块闲置的内存,而这些内存不一定全部被用到。
二、创建线程池的四个方法
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。
Executors 工具类中3大方法(详见API):
public static ExecutorService newSingleThreadExecutor()
//创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
//newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,它使用的LinkedBlockingQueue。
public static ExecutorService newFixedThreadPool(int nThreads)
//创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
//newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue。
public static ExecutorService newCachedThreadPool()
//创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
//newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
public static ExecutorService newScheduledThreadPool()
public class TestMethods {
public static void main(String[] args) {
// ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 单个线程:此时只有pool-1-thread-1
// ExecutorService threadPool = Executors.newFixedThreadPool(5);
// 创建一个固定的线程池的大小: 此时最多有pool-1-thread-5 ok
ExecutorService threadPool = Executors.newCachedThreadPool();
// 可伸缩的,遇强则强,遇弱则弱 : 此时最多开启到pool-1-thread-31 ok 去执行
try {
for (int i = 0; i < 100; i++) {
// 使用了线程池之后,使用线程池来创建线程
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
但是,阿里巴巴 Java 开发手册中 对线程池的规范:
不推荐使用这四种方法来创建线程池,因为这几个方法中设置的某些默认值过大,在某些情况下,会出现内存溢出的风险。
三、线程池7大参数入门简介
3.1 参数介绍
corePoolSize:线程池中的常驻核心线程数
- 在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程。
- 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
keepAliveTime:多余的空闲线程的存活时间。
- 当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
unit:keepAliveTime的单位。
workQueue:任务队列,被提交但尚未被执行的任务。
threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可。
handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数( maximumPoolSize)。
源码分析:
//创建单一线程的线程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
//创建固定线程数的线程池
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
//创建代缓存的线程池:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
前面说过的几种创建线程池的方法,由上述源码可以了解到,其本质都是调用本质ThreadPoolExecutor创建线程池,也是 阿里巴巴规范中提及的方法:
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大核心线程池大小
long keepAliveTime, //超时了没有人调用就会释放
TimeUnit unit, //超时单位
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,// 线程工厂:创建线程的,一般不用动
RejectedExecutionHandler handler/*拒绝策略*/) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
3.2 线程池工作原理
创建一个线程池:
public class TestDefPool {
public static void main(String[] args) {
// 自定义线程池!工作 ThreadPoolExecutor
ExecutorService threadPool = new ThreadPoolExecutor(
2, //当天值班员工(核心线程池大小)
5, //柜台总数:最大核心线程池大小
3, //临时工的超时等待:超时了没有人调用就会释放
TimeUnit.SECONDS, //超时等待单位
new LinkedBlockingDeque<>(3),//候客区:阻塞队列
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()); //满了后告诉客人办不了业务了:抛异常RejectedExecutionException
try {
// 最大承载:Deque + max 此处为:5+3=8
// 超过 RejectedExecutionException
for (int i = 1; i <= 9; i++) {
// 使用了线程池之后,使用线程池来创建线程
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " ok");
});
}
} catch (
Exception e)
{
e.printStackTrace();
} finally
{
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
1、在创建了线程池后,等待提交过来的任务请求。
2、当调用execute()方法添加一个请求任务时,线程池会做如下判断:
-
如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
-
如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列; 如果这时候队列满了且正在运行的线程数量还小于
maximumPoolSize,那么就创建非核心线程立刻运行这个任务; -
如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
- 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。
四、线程池关闭时机
参考:https://www.cnblogs.com/east7/p/15679058.html
首先我们需要了解线程池在什么情况下会自动关闭。ThreadPoolExecutor 类(这是我们最常用的线程池实现类)的源码注释中有这么一句话:
A pool that is no longer referenced in a program and has no remaining
threads will be shutdown automatically.没有引用指向且没有剩余线程的线程池将会自动关闭。
那么什么情况下线程池中会没有剩余线程呢?先来看一下 ThreadPoolExecutor 参数最全的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) { ... ... }
这里我们只关心与线程存活状态最紧密相关的两个参数,也就是corePoolSize和keepAliveTime,上述代码块也包含了这两个参数的源码注释和中文翻译。keepAliveTime参数指定了非核心线程的存活时间,非核心线程的空闲时间一旦达到这个值,就会被销毁,而核心线程则会继续存活,只要有线程存活,线程池也就不会自动关闭。聪明的你一定会想到,如果把corePoolSize设置为0,再给keepAliveTime指定一个值的话,那么线程池在空闲一段时间之后,不就可以自动关闭了吗?没错,这就是线程池自动关闭的第一种情况。
4.1 情况一:核心线程数为 0 并指定线程存活时间
代码示例:
public class ThreadPoolTest {
public static void main(String[] args) {
// 重点关注 corePoolSize 和 keepAliveTime,其他参数不重要
ThreadPoolExecutor executor = new ThreadPoolExecutor(0, 5,
30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(15));
for (int i = 0; i < 20; i++) {
executor.execute(() -> {
// 简单地打印当前线程名称
System.out.println(Thread.currentThread().getName());
});
}
}
}
线程打印开始
… …
pool-1-thread-2
pool-1-thread-3
pool-1-thread-4
pool-1-thread-5
pool-1-thread-1
打印结束,程序等待30s后正常退出
Process finished with exit code 0 # 小知识:exit code 0 说明程序是正常退出,非强行中断或异常退出
通过以上代码和运行结果可以得知,在corePoolSize为0且keepAliveTime设置为 60s 的情况下,如果任务执行完毕又没有新的任务到来,线程池里的线程都将消亡,而且没有核心线程阻止线程池关闭,因此线程池也将随之自动关闭。
而如果将corePoolSize设置为大于0的数字,再运行以上代码,那么线程池将一直处于等待状态而不能关闭,因为核心线程不受keepAliveTime控制,所以会一直存活,程序也将一直不能结束。运行效果如下 (corePoolSize设置为5,其他参数不变):
线程打印开始
… …
pool-1-thread-5
pool-1-thread-1
pool-1-thread-3
pool-1-thread-4
pool-1-thread-2
打印结束,但程序无法结束
4.2 情况二:Executors.newCachedThrteadPool() 创建线程池
Executors 是 JDK 自带的线程池框架类,包含多个创建不同类型线程池的方法,而其中的newCachedThrteadPool()方法也将核心线程数设置为了0并指定了线程存活时间,所以也可以自动关闭。其源码如下:
public class Executors {
... ...
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
... ...
}
如果用这个线程池运行上面的代码,程序也会自动退出,效果如下:
线程打印开始
… …
pool-1-thread-7
pool-1-thread-5
pool-1-thread-4
pool-1-thread-1
pool-1-thread-9
打印结束,程序等待60s后退出
Process finished with exit code 0
4.3 通过 allowCoreThreadTimeOut 控制核心线程存活时间
通过将核心线程数设置为0虽然可以实现线程池的自动关闭,但也存在一些弊端,新到来的任务若发现没有活跃线程,则会优先被放入任务队列,然后等待被处理,这显然会影响程序的执行效率。那你可能要问了,有没有其他的方法来自己实现可自动关闭的线程池呢?答案是肯定的,从 JDK 1.6 开始,ThreadPoolExecutor 类新增了一个allowCoreThreadTimeOut字段:
/**
* If false (default), core threads stay alive even when idle.
* If true, core threads use keepAliveTime to time out waiting
* for work.
* 默认为false,核心线程处于空闲状态也可一直存活
* 如果设置为true,核心线程的存活状态将受keepAliveTime控制,超时将被销毁
*/
private volatile boolean allowCoreThreadTimeOut;
这个字段值默认为false,可使用allowCoreThreadTimeOut()方法对其进行设置,如果设置为 true,那么核心线程数也将受keepAliveTime控制,此方法源码如下:
public void allowCoreThreadTimeOut(boolean value) {
// 核心线程存活时间必须大于0,一旦开启,keepAliveTime 也必须大于0
if (value && keepAliveTime <= 0)
throw new IllegalArgumentException("Core threads must have nonzero keep alive times");
// 将 allowCoreThreadTimeOut 值设为传入的参数值
if (value != allowCoreThreadTimeOut) {
allowCoreThreadTimeOut = value;
// 开启后,清理所有的超时空闲线程,包括核心线程
if (value)
interruptIdleWorkers();
}
}
既然如此,接下来我们就借助这个方法实现一个可自动关闭且核心线程数不为0的线程池,这里直接在第一个程序的基础上进行改进:
public class ThreadPoolTest {
public static void main(String[] args) {
// 这里把corePoolSize设为5,keepAliveTime保持不变
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5,
30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(15));
// 允许核心线程超时销毁
executor.allowCoreThreadTimeOut(true);
for (int i = 0; i < 20; i++) {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName());
});
}
}
}
可以看到,程序在打印结束后等待了30s,然后自行退出,说明线程池已自动关闭,也就是allowCoreThreadTimeOut()方法发挥了作用。这样,我们就实现了可自动关闭且核心线程数不为0的线程池。
超详细的线程池执行流程图
五、四种拒绝策略
等待队列也已经排满了,再也塞不下新任务了同时,线程池中的max线程也达到了,无法继续为新任务服务。
这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK拒绝策略:
- AbortPolicy(默认):直接抛出 RejectedExecutionException异常阻止系统正常运知。
- CallerRunsPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。
以上内置拒绝策略均实现了RejectedExecutionHandler接口。
/**
1. new ThreadPoolExecutor.AbortPolicy()
// 银行满了,还有人进来,不处理这个人的,抛出异常:RejectedExecutionException
//丢弃任务并抛出RejectedExecutionException异常
2.new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
//公司叫你来银行办业务,银行满人办不了,回公司找人办
//新开辟的线程搞不定调用主线程
//由调用线程(提交任务的线程)处理该任务
3.new ThreadPoolExecutor.DiscardPolicy()
//银行办不了了,不办你业务
//队列满了,丢掉任务,不会抛出异常!
4.new ThreadPoolExecutor.DiscardOldestPolicy()
//丢弃队列最前面的任务,然后重新提交被拒绝的任务
*/
这里只是简单介绍,想详细了解,可以看看这篇文章:
https://blog.csdn.net/suifeng629/article/details/98884972
六、小结
根据上述参数对线程池调优:主要针对线程池大小调优
IO密集型
一般来说:文件读写、DB读写、网络请求等。
这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
CPU密集型
一般来说:计算型代码、Bitmap转换、Gson转换等。
这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
// 获取CPU的核数
System.out.println(Runtime.getRuntime().availableProcessors());