线程池
一、为什么需要线程池
优点:
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁带来的消耗。
- 提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性:线程池可以统一进行线程分配、调度和监控。
二、创建线程池的两种方式
1、Executors
第一种Executors: 具体的4种常用的线程池实现如下:(返回值都是ExecutorService)
public class Test1 {
// 创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程。
private ExecutorService pool1 = Executors.newFixedThreadPool(4);
// 可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用。
// 如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务
// 总结:可伸缩的,遇强则强,遇弱则弱
private ExecutorService pool2 = Executors.newCachedThreadPool();
// 创建一个定长的线程池,支持定时及周期性任务执行
private ExecutorService pool3 = Executors.newScheduledThreadPool(3);
// 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,
// 保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
private ExecutorService pool4 = Executors.newSingleThreadExecutor();
}
知道了第一种的线程的创建方式,怎么使用呢,接着看下面的代码!
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test2 {
public static void main(String[] args) {
// ExecutorService pool = Executors.newSingleThreadExecutor();
// ExecutorService pool = Executors.newFixedThreadPool(5);
// ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
ExecutorService pool = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 10 ; i++) {
// 为什么需要定义这个变量?这个其实就是一个临时变量
// 因为在后面new Runnable的代码块中不能直接使用到i,所以创建了这个临时的变量
final int temp = i;
// 使用线程池创建线程
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":"+ temp);
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 线程池用完需要关闭
pool.shutdown();
}
}
}
知道了如何使用,下面我们就来看看第一种利用Executors创建方式的源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
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,//21亿
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
通过上面源码,我们发现这几种创建线程池本质就是:调用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;
}
知道了创建线程的根本方法就是ThreadPoolExecutor(),下面我们就来详细看看这种创建方式,也就是创建线程池的第二种方法!
2、ThreadPoolExecutor
第二种: 线程池的返回值ThreadPoolExecutor简介:
private ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,3,0, TimeUnit.MICROSECONDS,
new LinkedBlockingDeque<>(),new ThreadPoolExecutor.AbortPolicy();// 这里有6个参数
2.1、解释六个参数:
第1个参数: 线程池维护线程的最少数量 (core : 核心)
第2个参数: 线程池维护线程的最大数量
第3个参数: 线程池维护线程所允许的空闲时间
第4个参数: 线程池维护线程所允许的空闲时间的单位 。
如果超过3这个数量,4这个时间单位,2-1(最大线程数-核心线程数)这些线程就会关闭
第5个参数: 线程池所使用的缓冲队列
第6个参数: 线程池对如果超出工作队列的长度,任务要处理的方式,即拒绝任务的处理策略 (有四种)
2.2、四种策略
-
CallerRunsPolicy:谁指派的任务,让他自己去执行(哪里来的去哪里!)
-
AbortPolicy:始终抛出一个异常,让任务处理类处理
-
DiscardPolicy:丢弃最新的任务(队列满了,丢掉任务,不会抛出异常!)
-
DiscardOldestPolicy:丢弃最久的最老的任务(队列满了,尝试和最早的竞争,也不会抛出异常!)
2.3、画图解释
解释一下这个图:
有一家银行,有五个柜台,一般情况下都是都只开启两个柜台(1,2柜台),其他三个柜台(3,4,5柜台)都不开门,有一天来了两个人来办理业务,很自然的就坐到了前两个柜台。这个时候又来了3个人,一看开着的两个柜台都有人,这个时候他们就会在候客区等待(就是阻塞队列第5个参数),这个时候又来了一个人,因为1,2柜台都有人,候客区也满了,这个时候银行就会开启新的柜台(之前没开启的3,4,5可能就会开启),来处理业务,过了一会,1,2,3,4,5柜台都满了,候客区也满了,这时候又进来了一个人,这个时候就会启动拒绝策略了(第6个参数),拒绝策略共有四种,前面已经介绍过了。等到3,4,5柜台都没有人了,候客区也没有人了,3、4、5这几个临时柜台等待超过3这个数量,4这个时间单位的时间后,还没有人来,3、4、5柜台就会关闭窗口,关闭释放!
2.4、自定义线程池
import java.util.concurrent.*;
public class Test3 {
public static void main(String[] args) {
// 自定义线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,5,3, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(3),
Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy()); // 银行满了,好有人进来,不处理这个人的,抛出异常
try {
// 最大承载:队列 + max ,即 3 + 5
// 超出就会抛出 RejectedExecutionException
for (int i = 1; i <= 9; i++) {
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" ok");
}
});
}
}catch (Exception e ) {
e.printStackTrace();
}finally {
threadPoolExecutor.shutdown();
}
}
}
2.5、聊聊关闭线程池
关闭线程池有两种方式:1)shotdown();2)shutdownNow()
- shutdown:新传入的任务不在接收,但是目前所有的任务(所有线程中的任务+工作队列种的任务)还要执行完毕
- shutdownNow:新传入的任务不再接收,目前的任务(所有线程中的任务)判断是否可以停止,如果可以停止,就结束任务,如果不能停止,就执行完在停止;工作队列中的任务直接丢弃。
2.6、小结和扩展
池的最大大小如何设置?
两种方法:
1、CPU 密集型:几核就是几,可以保证CPU的效率最高
2、IO 密集型 > 判断你的程序中十分消耗IO的线程// 获取CPU的核数
// 获取CPU的核数
System.out.println(Runtime.getRuntime().availableProcessors());