多线程【线程池】

一、什么是线程池

线程池:指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程,

一旦任务已经完成了,线程回到池子中并等待下一次分配任务。

二、使用线程池的好处

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

线程数的设置参考文章:

http://mp.weixin.qq.com/s__biz=MzI5MzYzMDAwNw==&mid=2247488456&idx=4&sn=80ee015180d46f2bd5b26c166b7dab0a&chksm=ec6e6a90db19e3867f8ea9fd5da01c3378431d6dcf940eb9820c4ab917e05851471102515d17&scene=0&xtrack=1#rd

http://mp.weixin.qq.com/s__biz=MzU1MzUyMjYzNg==&mid=2247484319&idx=1&sn=6a22ad5e324562c900a66624239cc6eb&chksm=fbf0c73ccc874e2a5d0a9c9d8e030e426104a52247b11a0069662a3d41a775f8c5332aba3981&mpshare=1&scene=24&srcid=&sharer_sharetime=1591622673427&sharer_shareid=5d06ca706f31f4d058e964b8b7ccfcc9#rd

Java新手,若有错误,欢迎指正!

posted @ 2021-02-20 20:26  跑调大叔!  阅读(561)  评论(0编辑  收藏  举报