1、使用线程池的好处
主要目的:避免对线程频繁创建和销毁带来的性能消耗。
通过一个Demo可以直观感受下 :
执行同一个功能:往list容器存放10万个元素,将所耗时间进行对比 ->
不使用线程池:
public class ThreadTest {
public static void main(String[] args) throws Exception {
final long start = System.currentTimeMillis();
final Random random = new Random();
ArrayList<Integer> list = new ArrayList<>();
for (int i=0;i<100000;i++){
Thread thread = new Thread(() -> list.add(random.nextInt()));
thread.start();
thread.join();
}
System.out.println(list.size()+"----"+(System.currentTimeMillis()-start));
}
}
- 使用线程池:
public class ThreadPoolTest {
public static void main(String[] args) throws InterruptedException {
final long start = System.currentTimeMillis();
final Random random = new Random();
ArrayList<Integer> list = new ArrayList<>();
//创建线程池
// ExecutorService executorService = Executors.newSingleThreadExecutor();
ExecutorService executorService = Executors.newCachedThreadPool();
// ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i=0;i<100000;i++){
executorService.submit(() -> {
list.add(random.nextInt());
});
}
executorService.shutdown();
executorService.awaitTermination(1,TimeUnit.DAYS);
System.out.println(list.size()+"----"+(System.currentTimeMillis()-start));
}
}
可以看到,执行大任务量使用线程池,对性能有很大的提升。
2、线程池的任务调度原理
核心线程数占满时,放在工作队列中进行阻塞等待,队列占满,就由非核心线程执行任务,当超过设置的最大线程数时,会有饱和策略处理,简单初步了解4种饱和策略(通过RejectedExecutionHandler类):
- AbortPolicy: 直接抛出异常
- CallerRunsPolicy:等待,调用所在线程执行任务
- DiscardOldestPolicy:丢去队列里最近的一个任务,并执行当前任务
- DiscardPolicy: 不执行任务,直接丢弃。
3、分类
- newSingleThreadExecutor : 单线程线程池,只有一个线程串行执行任务,一个个执行队列中任务。
- newFixedThreadExecutor: 固定大小线程池
- newCachedThreadPool: 无界线程池。工作线程不够就一直创建,有空闲就复用(60秒存活期)。
4、使用选择
Fixed 是单队列多线程,会有锁竞争的时间开销,如果存在慢请求(比如2s),比较适合,底层实现上使用链表阻塞对列,任务的存和取可以并行执行,这样吞吐量更高。因为是并行,一个线程执行时间较长,还有其他线程等着被使用,不影响这些线程的执行时间。
single 多队列单线程,刚好相反,如果多个慢请求堆积在队列中,显然客户端可能会请求超时。
cached 由于可根据任务量情况大小灵活伸缩,空闲没任务就没有占用内存,适合轻负载服务器。
注意:线程池要单例使用,避免多例创建太多线程池
5、线程池策略
需要进行业务隔离,比如系统中请求有 user模块 、订单模块 ,如果都是在一个线程池,那么显然是所有鸡蛋放一个篮子,一台机器崩,业务全部崩,这就是需要每个模块分别穿件一个线程池的初衷,也是业务隔离的思想体现。