多线程【线程池】
一、什么是线程池
线程池:指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程,
一旦任务已经完成了,线程回到池子中并等待下一次分配任务。
二、使用线程池的好处
1)控制最大并大数。
2)降低资源消耗。通过重复利用已创建的线程来降低线程创建和销毁造成的消耗。
3)提高响应速度。当任务到达时,任务不需要等到线程创建,而是可以直接使用线程池中的空闲线程。
4)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、延时执行、调优和监控等。
三、涉及到的类和接口
常用的线程池接口和类都在 java.util.concurrent包下,大致为:
Executor:线程池的顶级接口
ExecutorService:线程池接口,可通过submit()方法提交任务代码
ExecutorService接口的实现类最常用的为以下两个:
ThreadPoolExecutor
ScheduledThreadPoolExecutor
和 Array -> Arrays、Collection -> Collections 一样,线程池的创建也是有工具类可以使用的:
Executors工厂类:通过此类可以创建一个线程池
四、线程池种类
在 JDK 8 以后,一共有 5 种线程池,分别为:
固定线程数的线程池
只有一个线程的线程池
可根据任务数动态扩容线程数的线程池
可调度的线程池
具有抢占式操作的线程池
这些线程池都能由 Executors 工具类来进行创建,分别对应以下方法:
1)newFixedThreadPool:创建指定的、固定个数的线程池
2)newCachedThreadPool:创建缓存线程池(线程个数根据任务数逐渐增加,上线为 Integer.MAX_VALUE)
3)newSingleThreadExecutor:创建单个线程的线程池
4)newScheduledThreadPool:创建可调度的线程池 调度:定时、周期执行5)newWorkStealingPool:创建具有抢占式操作的线程池
对于 newWorkStealingPool 的补充:
newWorkStealingPool,这个是 JDK1.8 版本加入的一种线程池,stealing 翻译为抢断、窃取的意思,它实现的一个线程池和上面4种都不一样,用的是 ForkJoinPool 类。
newWorkStealingPool 适合使用在很耗时的操作,但是 newWorkStealingPool 不是 ThreadPoolExecutor 的扩展,它是新的线程池类 ForkJoinPool 的扩展,但是都是在统一的一个 Executors 类中实现,由于能够合理的使用 CPU 进行任务操作(并行操作),所以适合使用在很耗时的任务中。
参考文章:
https://blog.csdn.net/qq_38428623/article/details/86689800
https://blog.csdn.net/tjbsl/article/details/98480843
五、如何使用线程池
(一)使用步骤
1)创建线程池对象
2)创建线程任务
3)使用线程池对象的 submit() 或者 execute() 方法提交要执行的任务
4)使用完毕,可以使用shutdown()方法关闭线程池
(二)案例代码
需求:使用线程池管理线程来简单的模拟买票程序。
public class Demo(){
public static void main(String[] args) {
test();
}
public static void test(){
//1、创建线程池对象
ExecutorService pool = Executors.newFixedThreadPool(4);
//2、创建任务
Runnable runnable = new Runnable(){
private int tickets = 100;
@Override
public void run() {
while (true){
if(tickets <= 0){
break;
}
System.out.println(Thread.currentThread().getName()+"卖了第"+tickets+"张票");
tickets--;
}
}
};
//3、将任务提交到线程池(需要几个线程来执行就提交几次)
for (int i=0; i<5; i++){
pool.submit(runnable);
}
//4、关闭线程池
pool.shutdown();
}
补充:
shutdown:启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务
shutdownNow:尝试停止所有正在执行的任务,停止等待任务的处理,并返回正在等待执行的任务列表。
execute() 和 submit() 的区别:
1)参数:execute 只能传递 Runnable;submit 既可以传递 Runnable,也可以传递 Callable
2)返回值:execute 没有返回值;submit 有返回值,可以获取 Callable 的返回结果(当然一般是用来获取线程执行的结果如何)
3)异常处理:execute 无法捕获异常;submit 可以通过返回的 Future 对象的 get() 方法捕获异常
4)submit 方法的底层其实也是调用了 execute 方法
六、线程池底层源码查看
newFixedThreadPool
newCachedThreadPool
newSingleThreadExecutor
newScheduledThreadPool
七、线程池7大参数
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;
}
corePoolSize:线程池中的常驻核心线程数
maximumPoolSize:线程池中能够容纳同时指向的最大线程数,此值必须大于等于1
keepAliveTime:多余空闲线程的存活时间
(若线程池中当前线程数超过corePoolSize时,且空闲线程的空闲时间达到keepAliveTime时,多余空闲线程会被销毁,直到只剩下corePoolSize个线程为止)
TimeUnit:keepAliveTime 的时间单位
workQueue:任务队列,被提交但尚未被执行的任务
ThreadFactory:线程工厂,用于创建线程,一般用默认的即可
RejectedExecutionHandler:拒绝策略,当任务太多来不及处理,如何拒绝任务
八、线程池底层工作原理
1)线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2)当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会执行拒绝策略。
3)当一个线程完成任务时,它会从队列中取下一个任务来执行。
4)当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
九、线程池的4大拒绝策略
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK 内置的拒绝策略如下:
1)AbortPolicy : 直接抛出 RejectedExecutionException 异常,阻止系统正常运行。
2)CallerRunsPolicy : 该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用线程。
3)DiscardOldestPolicy : 丢弃队列中等待最久的线程,然后把当前任务加入队列中尝试再次提交当前任务。
4)DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。
以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler 接口。
十、线程池的实际使用
通过查看 Executors 提供的默认线程池的底层源码后,我们会发现其有如下弊端:
1)FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会堆积大量的线程,从而导致 OOM。
并且在《阿里巴巴Java开发手册》中也有指出,线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式手动创建,这样的处理方式能让程序员更加明确线程池的允许规则,从而规避资源耗尽的风险。
小结:在实际开发中不会使用 Executors 创建,而是手动创建,自己指定参数。
十一、线程池的手动创建
以上的参数是随手写的,实际开发中参数的设置要根据业务场景以及服务器配置来进行设置。
十二、线程池配置合理线程数
设置线程池的参数时,需要从以下 2 个方面进行考虑:
系统是 CPU 密集型?
系统是 IO 密集型?
(一)CPU 密集型
CPU 密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
那么这种情况下,应该尽可能配置少的线程数量,从而减少线程之间的切换,让其充分利用时间进行计算。
一般公式为:CPU核数 + 1 个线程的线程池。
可以通过以下代码来查看服务器的核数:
Runtime.getRuntime().availableProcessors()
(二)IO 密集型
IO 密集型的意思是该任务需要大量的 IO,即大量的阻塞。
那么这种情况下会导致有大量的 CPU 算力浪费在等待上,所以需要多配置线程数。
在 IO 密集型情况下,了解到有两种配置线程数的公式:
公式一:CPU核数/(1-阻塞系数),其中阻塞系数在 0.8-0.9 之间
如:8核CPU,可以设置为 8/(1-0.9)=80 个线程
公式二:CPU核数 * 2
线程数的设置参考文章:
Java新手,若有错误,欢迎指正!