美文网首页
线程池学习(深入)

线程池学习(深入)

作者: 西谷haul | 来源:发表于2021-02-26 17:40 被阅读0次

一、为什么要用线程池?

线程池做的工作只是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超过数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行
它的主要特点是:线程复用;控制最大并发数;管理线程

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
第三:提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

二、图解

image.png

三、应用举例

public class ThreadPool {

    public static void main(String[] args) {
        //一池三个线程,类似于银行中有五个柜台
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        //一池一个线程,类似于银行有一个柜台
        ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
        //一池N个线程,类似于银行有N个柜台
        ExecutorService threadPool3 = Executors.newCachedThreadPool();

        try {
            for (int i = 0; i < 10; i++) {
                threadPool.execute(() -> {
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在受理");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}
执行结果

以上三个其实他们指向的是同一个类,ThreadPoolExecutor

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }


四、线程池的七大参数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

int corePoolSize:线程中的常驻核心线程数(即为银行中常驻的柜台)
int maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1
long keepAliveTime:多余的空间线程的存活时间,当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到只剩下corePoolSize线程为止
TimeUnit unit:keepAliveTime的单位
BlockingQueue<Runnable> workQueue:任务队列,被提交但尚未被执行的任务。
ThreadFactory threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认即可。
RejectedExecutionHandler handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝请求执行的runnable的策略。

线程池中阻塞队列的作用?为什么是先添加列队而不是先创建最大线程?

  • 1、一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用cpu资源。
  • 2、在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。
    就好比一个企业里面有10个(core)正式工的名额,最多招10个正式工,要是任务超过正式工人数(task > core)的情况下,工厂领导(线程池)不是首先扩招工人,还是这10人,但是任务可以稍微积压一下,即先放到队列去(代价低)。10个正式工慢慢干,迟早会干完的,要是任务还在继续增加,超过正式工的加班忍耐极限了(队列满了),就的招外包帮忙了(注意是临时工)要是正式工加上外包还是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)。

五、线程池底层工作原理

image.png 流程讲解

线程池中线程复用原理

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对Thread 进行了封装,并不是每次执行任务都会调用Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的 run 方法串联起来。

在工作中单一的/固定的/可变的三种创建线程池的方法哪个用的多?
答案:都不用!!原因如下(阿里巴巴java开发手册):

image.png

如下:newFixedThreadPool的队列允许的最大长度是Integer.MAX_VALUE,SingleThreadPool则是同理。

newFixedThreadPool LinkedBlockingQueue

如下:newCachedThreadPool:允许创建的最大线程数是Integer.MAX_VALUE

newCachedThreadPool 拒绝策略
public class ThreadPool {

    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        try {
            for (int i = 0; i < 10; i++) {
                threadPool.execute(() -> {
                    try {
                        Thread.sleep(1000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //模拟10个顾客过来办理业务,目前最多有5个柜台来进行处理
                    System.out.println(Thread.currentThread().getName() + "办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}
运行结果

运行结果如上,因为常驻线程数是2,最大线程数是5,阻塞队列大小是3,所以最多可以处理5+3个顾客,多余的就被拒绝策略进行处理了。直接抛出RejectedExecutionException异常阻止系统正常运行。

六、创建自定义线程池,最大线程数该如何设置?

首先要区别CPU密集型和IO密集型

一:CPU密集型:

定义:
CPU密集型也是指计算密集型,大部分时间用来做计算逻辑判断等CPU动作的程序称为CPU密集型任务。该类型的任务需要进行大量的计算,主要消耗CPU资源。 这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。

特点:
(1)CPU 使用率较高(也就是经常计算一些复杂的运算,逻辑处理等情况)非常多的情况下使用
(2)针对单台机器,最大线程数一般只需要设置为CPU核心数的线程个数就可以了
(3)这一类型多出现在开发中的一些业务复杂计算和逻辑处理过程中。

二:IO密集型:

定义:
IO密集型任务指任务需要执行大量的IO操作,涉及到网络、磁盘IO操作,对CPU消耗较少,其消耗的主要资源为IO。 我们所接触到的 IO ,大致可以分成两种:磁盘 IO和网络 IO。

特点:
(1):磁盘 IO ,大多都是一些针对磁盘的读写操作,最常见的就是文件的读写,假如你的数据库、 Redis 也是在本地的话,那么这个也属于磁盘 IO。
(2):网络 IO ,这个应该是大家更加熟悉的,我们会遇到各种网络请求,比如 http 请求、远程数据库读写、远程 Redis 读写等等。IO 操作的特点就是需要等待,我们请求一些数据,由对方将数据写入缓冲区,在这段时间中,需要读取数据的线程根本无事可做,因此可以把 CPU 时间片让出去,直到缓冲区写满。既然这样,IO 密集型任务其实就有很大的优化空间了(毕竟存在等待):

CPU 使用率较低,程序中会存在大量的 I/O 操作占用时间,导致线程空余时间很多,所以通常就需要开CPU核心数两倍的线程。当线程进行 I/O 操作 CPU 空闲时,线程等待时间所占比例越高,就需要越多线程,启用其他线程继续使用 CPU,以此提高 CPU 的使用率;线程 CPU 时间所占比例越高,需要越少的线程,这一类型在开发中主要出现在一些计算业务频繁的逻辑中。

三、分析:

1:高并发、任务执行时间短的业务,线程池线程数可设置为CPU核数+1,减少线程上下文的切换
2:并发不高、任务执行时间长的业务这就需要区分开看了

  • (1)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以适当加大线程池中的线程数目,让CPU处理更多的业务。
  • (2)假如是业务时间长集中在计算操作上,也就是计算密集型任务,线程池中的线程数设置得少一些,减少线程上下文的切换。

3:并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,我们的项目使用的是redis作为缓存(这类非关系型数据库还是挺好的)。增加服务器是第二步(一般政府项目的首先,因为不用对项目技术做大改动,求一个稳,但前提是资金充足),至于线程池的设置,设置参考 2 。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件(任务时间过长的可以考虑拆分逻辑放入队列等操作)对任务进行拆分和解耦。

四、总结:

1、一个计算为主的程序(CPU密集型程序),多线程跑的时候,可以充分利用起所有的 CPU 核心数,比如说 8 个核心的CPU ,开8 个线程的时候,可以同时跑 8 个线程的运算任务,此时是最大效率。但是如果线程远远超出 CPU 核心数量,反而会使得任务效率下降,因为频繁的切换线程也是要消耗时间的。因此对于 CPU 密集型的任务来说,线程数等于 CPU 数是最好的了。

2、如果是一个磁盘或网络为主的程序(IO密集型程序),一个线程处在 IO 等待的时候,另一个线程还可以在 CPU 里面跑,有时候 CPU 闲着没事干,所有的线程都在等着 IO,这时候他们就是同时的了,而单线程的话此时还是在一个一个等待的。我们都知道 IO 的速度比起 CPU 来是很慢的。此时线程数等于CPU核心数的两倍是最佳的。

相关文章

  • [第三篇]深入学习线程池之优雅的关闭线程池

    通过 《深入学习线程池之线程池简介及工作原理》、《深入学习线程池之通过ThreadPoolExecutor创建线程...

  • 线程池学习(深入)

    一、为什么要用线程池? 线程池做的工作只是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些...

  • 线程池

    线程池的文章:JDK线程池(一):体系结构JDK线程池(二):ThreadPoolExecutor深入分析java...

  • 深入理解Java线程池

    深入理解Java线程池 线程池初探 所谓线程池,就是将多个线程放在一个池子里面(所谓池化技术),然后需要线程的时候...

  • 线程池的原理

    参考 深入Java源码理解线程池原理 线程池是对CPU利用的优化手段 线程池使用池化技术实现,替他的实现还有连接池...

  • 【学习笔记】java线程池

    深入理解Java之线程池

  • 深入线程池

    关于线程池的连环21问 1、为什么会有线程池?2、简单手写一个线程池?3、为什么要把任务先放在任务队列里面,而不是...

  • 最近研究的MySQL线程池问题,都整理在这了

    最近出现多次由于上层组件异常导致DB雪崩的情况,将部分监控DB启用了线程池功能。在使用线程池的过程中不断的深入学习...

  • Java 线程池 实现原理与源码深度解析

    正文 史上最清晰的线程池源码分析 鼎鼎大名的线程池。不需要多说!!!!! 这篇博客深入分析 Java 中线程池的实...

  • 线程池源码解读

    深入分析java线程池的实现原理 ps: 用一个AtomicInteger记录 线程池状态和其中的线程个数, 其中...

网友评论

      本文标题:线程池学习(深入)

      本文链接:https://www.haomeiwen.com/subject/pdrmfltx.html