文章目录
- 一、概述以及大体框架
- 二、ThreadPoolExecutor
- 参数分析
- 阿里推荐使用ThreadPoolExecutor
- 三、线程池运行流程
- 四、线程池的一些常用方法
- Runnable 与 Callable
- 回顾下线程创建的几种方法
- execute() 与 submit()
- 停止线程的几种方法
- 五、如何设置线程数量
- CPU密集型(N+1)
- I/O密集型(2N)
- 六、为什么线程池里面的线程能够进行复用
- 七、线程池的状态
- 参考
一、概述以及大体框架
为什么需要线程池?池化技术?
因为如果每来一个任务,都创建的线程、执行任务、线程,这样重复的操作都带来不少性能开销,因此使用池化技术,在开始执行期间事先准备好若干线程,如果任务来了就从池子里获取线程来执行。
大体框架:
Executor只有定义了一个execute方法。
ExecutorService定义了一系列方法,比如shutdown,isshutdown等等方法。
AbstractExecutorService是个抽象类,定义了一系列通用方法。
而ThreadPoolExecutor和ScheduledThreadPoolExecutor为具体线程池的实现类。
二、ThreadPoolExecutor
线程池实现类ThreadPoolExecutor 是 Executor最核心的类。
参数分析
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;
}
- corePoolSize:线程池核心线程数量
- maxinumPoolSize:线程池最大线程数量
- keepLiveTime:非核心线程的存活时间
- timeunit:存活时间的单位
- workQueue:存放任务的阻塞队列
- threadFactory:线程工厂
- rejectHandler:拒绝策略
其中workQueue阻塞队列ArrayBlockingQueue、LinkBlockingQueue。
threadFactory可以用默认的threadFactory。或者可以用自己创建的线程工厂,线程的名字由自己定义,这样可以跟踪问题。
拒绝策略:当线程池的线程数量等于maxNumPoolSize最大线程数时,并且阻塞队列已满时,再添加任务时会执行拒绝策略:
- AbortPolicy:拒绝任务,抛出RejectExecutionException异常。
- DiscardPolicy:直接抛弃,不抛出异常。
- DiscardOldestPolicy:抛弃等待最久的任务,并把任务添加到任务队列里。
- CallerRunPolicy:让调用者,也就是主线程去执行这个任务,如果选这个拒绝策略的话,会较低任务提交的速度,会影响程序的性能,但如果程序接受延迟执行任务的话选这个也可以。
阿里推荐使用ThreadPoolExecutor
因为使用Executors也可以创建线程,但是不推荐这样做,在阿里开发者手册有提到:
第一类:
- newFixedThreadPoolExecutor
- newSingleThreadPoolExecutor
这两个方法创建的线程池的任务队列都是使用LinkBlockingQueue,LinkBlockingQueue没有设置容量,也就是说我可以向程序不断提交任务,导致任务挤压,最终导致OOM。
第二类:
- newCachedThreadPoolExecutor
- newScheduledThreadPool
这两个方法创建的线程池的参数中,他们的最大线程池数量为Interger.max_value,如果不断提交任务,可能会不断创建大量线程,因此也会导致OOM。
三、线程池运行流程
线程池分为核心线程跟非核心线程
一开始提交任务的时候,由于当前线程池的线程数量小于corePoolSize,所以每来一个任务都会创建线程来执行任务,直到线程池的线程数量等于corePoolSize。
如果这个时候再来任务,此时线程数量等于corePoolSize,因此就会加入workQueue阻塞队列,直到workQueue满。
如果此时再来任务,workQueue已经满了,会去判断当前的线程数量是不是小于maxnumPoolSize,如果小于的话,会创建非核心线程处理任务。
如果任务队列满了,并且当前线程数量也等于maxNumPoolSize了,就会执行拒绝策略拒绝任务。
常见的拒绝策略有四种之多,默认是使用AbortPolicy,拒绝执行任务并抛出RejectExecutionException异常。
四、线程池的一些常用方法
Runnable 与 Callable
最重要的特征就是一个有返回值,另外一个没有返回值。
补一个FutureTask的简单实用
回顾下线程创建的几种方法
1.继承Thread,并重写run方法
class MyThread extends Thread{
@Override
public void run() {
System.out.println("继承Thread来实现创建线程");
}
}
2.传入Runnable接口实现类,并用Thread.start来创建线程
Thread myThread = new Thread(() -> System.out.println("hello"));
3.使用FutureTask和Callable接口,并用Thread.start(futureTask)来创建线程
FutureTask futureTask = new FutureTask(new Callable() {
@Override
public Object call() throws Exception {
return null;
}
});
Thread thread = new Thread(futureTask);
thread.start();
//通过futureTask来获取Callable的返回值
futureTask.get();
4.使用线程池submit Callable或者execute Runnable
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(); //通过future来获取返回值
executorService.execute();
execute() 与 submit()
execute用于线程池提交任务,但是没有返回值
如果使用submit的话可以使用Future获取返回值
List<Future<String>> futureList = new ArrayList<>();
Callable<String> callable = new MyCallable();
for (int i = 0; i < 10; i++) {
//提交任务到线程池
Future<String> future = executor.submit(callable);
//将返回值 future 添加到 list,我们可以通过 future 获得 执行 Callable 得到的返回值
futureList.add(future);
}
for (Future<String> fut : futureList) {
try {
System.out.println(new Date() + "::" + fut.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
停止线程的几种方法
- shutdown:进入shutdown状态,此时线程池还在运行,会把队列面的任务执行完,但是此时不能添加任务了,任务来了会抛出RejectExecutionException异常。
- isShutdown:判断是否在shutdown状态。
- shutdownNow:暴力立马关闭线程池。
- isTerminated:检查线程池是否已经停止了。
五、如何设置线程数量
如果线程数量少,那么会导致任务执行速度慢,导致任务挤压,可能会因为任务队列出现OOM的情况。
如果线程数量太多的话,线程的竞争大,会导致大量上下文切换,因为CPU是分配给时间片给线程来处理任务的,时间片一到,那么线程就得保存当前任务,等待下一次CPU分配时间片,那么下一次线程获取到CPU时间片之后,会重新加载任务来处理,保存到加载的过程就称之为上问下切换。
CPU密集型(N+1)
如果程序的计算量特别高,那么就属于CPU密集型,线程数量可以设置为N(CPU的核心数)+1。
I/O密集型(2N)
如果程序的是网络传输或者I/O操作比较多,那么线程池应该设置为2N。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EKyDHGf6-1600421470606)(22CD8B13C8FF401D8A396024D59B7920)]
六、为什么线程池里面的线程能够进行复用
粗略源码分析:
当前线程数少于核心线程数时会添加worker,worker实际上就是线程池里面对线程的封装。
可以看到worker是ThreadPoolExecutor的一个内部类,里面包含了线程变量,以及一个firstTask,firstTask就是一个runnable的对象了。
主要的方法逻辑是在runworker中。
可以看到runWorker会获取当前线程,work里面的firstTask任务,能够轮转的原因就是因为这个while循环了,while循环会去getTask,从任务队列里面获取Task,如果Task不为空,则会执行run方法。
注意这里是run方法,因为start方法是从底层开启一个线程来执行run方法,但是这里面是用的是worker里面的thread来执行,因此没有用到start方法。
七、线程池的状态
- Running:代表线程池正在运行。
- Shutdown:使用shutdown()方法,代表线程池会进入shutdown状态,会继续处理队列中的任务,但是不会接受新任务了。
- Stop:使用shutdownNow会进入这个状态,停止目前线程池里面的所有任务,并且会返回一个List 表示在队列里面还没有执行的任务。会随后进入到teminated状态。
- Tidying:整洁,代表此时所有线程都空闲了,并且也没有任务了。
- Teminated:线程池停止了。