java线程池ThreadPoolExecutor
目录
1.ExecutorService接口的实现类ThreadPoolExecutor自己定义线程池
2.Executors(线程池的工具类)方法newFixedThreadPool(int 线程数量)
说到临时线程,临时线程什么时候被调用呢?上面的代码中可以看到一共出书了三次线程池对象,拿到了4个数据的变化,
一、概念
线程池就是一个可以复用线程的技术,线程池可以解决线程冗余的问题,如果每个用户都要创建一个新线程来处理,当数量多了会影响系统的性能。这篇文章主要说了线程池的执行流程,临时线程什么时候开始工作,创建线程池的方式。
二、工作原理
线程池由线程池管理器、任务队列和一组工作线程组成。 比如有3个线程对6个任务,那么为了复用线程,一个线程处理完一个任务以后不会销毁,而是处理下一个任务。
1. 初始化线程池
线程池管理器创建一定数量的工作线程,并将它们放入线程池中等待任务。
ThreadPoolExecutor tp = new ThreadPoolExecutor(
3,//线程数
10,//最大线程数=线程数+临时线程
1000,//临时线程的保留时间
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(6),//等待队列长度 6
Executors.defaultThreadFactory(),//生产线程的工厂
new ThreadPoolExecutor.AbortPolicy()
);
2. 提交任务
当有任务需要执行时,应用程序通过将任务提交给线程池管理器来执行。任务可以是一个实现Runnable或Callable接口的对象。
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"~~");
}
}
MyRunnable mr = new MyRunnable();
tp.execute(mr);
3.ThreadPoolExecutor的参数列表
new ThreadPoolExecutor(
corePoolSize //核心线程数
maximumPoolSize //任务队列容量(阻塞队列)//最大线程数=线程数+临时线程
keepAliveTime //临时线程的保留时间
TimeUnit.时间 //可以是NANO(纳秒),MICRO(微秒),MILLI(毫秒),秒,分, 时,天做为临时线程的存活时间。
BlockingQueue<Runnable> //等待队列长度(阻塞队列)
ThreadFactory //生产线程的工厂
RejectedExecutionHandler //超出任务队列容量+阻塞队列长度的处理方式);
处理方式有4种
- AbortPolicy(默认):直接抛出RejectedExecutionException异常,阻止任务的提交,表示无法处理新任务。
- CallerRunsPolicy:将任务返回给提交任务的线程执行。这意味着提交任务的线程将执行该任务,从而降低了新任务的提交速度。
- DiscardPolicy:默默地丢弃无法处理的任务,而不抛出任何异常。如果线程池已满,新任务将被丢弃。
- DiscardOldestPolicy:丢弃最早的未处理任务,然后尝试将新任务重新加入队列。这保证了任务的执行,但也可能丢失一些任务。
处理方式总结
- AbortPolicy可用于确保任务不会丢失但会导致异常;
- CallerRunsPolicy可用于确保任务一定会执行但可能导致执行线程变慢;
- DiscardPolicy适用于对任务丢失不敏感的场景;
- DiscardOldestPolicy适用于快速执行新任务而不关心旧任务的场景。
三、创建线程池的两种方案
1.ExecutorService接口的实现类ThreadPoolExecutor自己定义线程池
用Runnable接口的实体类来接
- new ThreadPoolExecutor对象tp,设置各个参数。
- 创建实现Runnable接口的实体类MyRunnable类的对象mr,重写run方法
- 把mr放进tp的execute方法的参数列表中执行任务
用Callable接口的实体类来接
- new ThreadPoolExecutor对象tp,设置各个参数。
- 创建实现Runnable接口的实体类MyRunnable类的对象mr,重写Call方法
- 把mr放进tp的submit方法的参数列表中,返回Future对象f
- 通过f.get();执行任务
ThreadPoolExecutor tp = new ThreadPoolExecutor(
3,
10,
100,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
Future<String> f1 = tp.submit(new MyCallable(100));
Future<String> f2 = tp.submit(new MyCallable(222));
Future<String> f3 = tp.submit(new MyCallable(1121));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
2.Executors(线程池的工具类)方法newFixedThreadPool(int 线程数量)
- ExecutorService pool = Executors.newFixedThreadPool(线程数量);
拿到线程池对象后面的步骤和上面一样。
ExecutorService pool = Executors.newFixedThreadPool(10);
MyRunnable2 mr2 = new MyRunnable2();
pool.execute(mr2);//用Runnable的实现类
Future<String> f1 = pool.submit(new MyCallable(100));//用Callable的实现类
Future<String> f2 = pool.submit(new MyCallable(220));
Future<String> f3 = pool.submit(new MyCallable(1110));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
两种方式的优缺点
用工具类Executors,创建线程对象对象比较方便。但是存在比较大的弊端
- Executors提供的三种创建线程池对象的方法FixedThreadPool、SinglePool、CachedThreadPool.
- FixedThreadPool、SinglePool允许的阻塞队列长度为MAX_VALUE,可能会导致堆积大量的请求,从而导致OOM。
- CachedThreadPool允许的创建线程数量为MAX_VALUE,可能会导致创建大量的线程,从而导致OOM。
四、两种终结线程池的方法
shutdown()
强行终止线程池,但会等候所有任务执行完毕
shutdownNow()
强行终止线程池,清空等候区的任务,但是执行中的会继续执行完。
五、来聊聊一些小细节
在上面的内容已经初始化的线程池,三个核心线程+7个临时线程+6个阻塞队列 我们来运行看看能得到什么。
如果直接输出线程池对象我们能得到下面几个数据
[Running, pool size = , active threads = , queued tasks = , completed tasks = ]
- Runing代表线程池正在运行。
- pool size代表线程池的大小有多少个任务进了线程池。
- active threads代表正在执行任务的线程数量。
- queued tasks代表阻塞队列(也就是等待队列)数量。
- completed tasks代表已完成的任务数量。
MyRunnable mr = new MyRunnable();
System.out.println(tp);
tp.execute(mr);
tp.execute(mr);
tp.execute(mr);
System.out.println(tp);
//[Running, pool size = 3, active threads = 3, queued tasks = 0, completed tasks = 0]
tp.execute(mr);
tp.execute(mr);
tp.execute(mr);
tp.execute(mr);
tp.execute(mr);
tp.execute(mr);
tp.execute(mr);//10
System.out.println(tp);
//[Running, pool size = 4, active threads = 4, queued tasks = 6, completed tasks = 0]
//因为超过了6个队列,所以调用了一个临时线程
//3个线程执行中,其他线程要如果没有超过6个等待队列的话,不会调用临时线程
tp.execute(mr);
tp.execute(mr);
tp.execute(mr);
tp.execute(mr);
tp.execute(mr);
tp.execute(mr);
tp.shutdown();//强行终止线程池,但会等候所有任务执行完毕
//tp.shutdownNow();//强行终止线程池,结束掉所有在等候区的任务,但是还在执行的线程会继续执行
System.out.println(tp);
//[Running, pool size = 10, active threads = 10, queued tasks = 6, completed tasks = 0]
说到临时线程,临时线程什么时候被调用呢?上面的代码中可以看到一共出书了三次线程池对象,拿到了4个数据的变化,
第一次只进行了3个任务,先调用了3个核心线程去处理。
第二次又进来了7个任务,但是在工作的线程只有4个,因为临时线程只有在阻塞队列溢出的时候才开始工作,进来7个任务阻塞队列的容量是6,所以溢出了一个,就调用了一个1临时线程去处理溢出的任务。
第三次又进来了6个任务,这时候因为阻塞队列在刚刚已经装满溢出了,所以剩下的6个临时线程也参与处理任务,所以这时候线程池大小是10,正在执行任务的线程数也是10.
总结
- 线程池对象有两种创建方式,一是通过 new ThreadPoolExecutor自定义线程池的参数,二是通过工具类Executors的三种方法创建。
- 线程池对象同样有两种方式执行,一是通过Runnable接口的实体类来执行,二是通过Callable接口的实体类来执行。
- 线程池的执行流程是,先执行核心线程,后进入队列等待,队列等待溢出执行临时线程,当临时线程也不够用了就交给处理方式处理。
小白第一次写博客,如果有不对的地方,各位大佬敬请指正,多多关照!(抱拳)