一篇文章给你讲清楚线程池

简介: 什么是线程池?为什么使用线程池?如何使用线程池?素质三连~

思维导图

文章已收录Github精选,欢迎Starhttps://github.com/yehongzhi/learningSummary

前言

在实际开发场景中,我们经常要使用多线程开发应用,比如实现异步操作,或者为了提高程序的效率等等。但是以前我见过有实习生在使用的时候是直接new Runable(),然后start()。没有使用线程池,可能很多初学者对线程池在多线程开发中没有足够的认识,所以我写一篇文章讲讲线程池,希望对大家有所启发。

一、什么是线程池

线程池借鉴了"池化"技术的思想,线程池能够对线程的生命周期进行管理,对线程重复利用,并且能够以一种简单的方式将任务的提交与执行相解耦。

举个例子来说,线程就像是某个公司的客服小姐姐,每天都要接很多客户的电话,如果同时有1000个客户打电话进来咨询,按正常的逻辑,那就需要1000个客服小姐姐,但是在现实中往往需要考虑成本问题,招这么多人费用太多了,于是就可以这样优化,可以招100个人成立一个客服中心,如果同时超过100个人则提示让客户等待,等有空闲的客服小姐姐时就去响应客户。实现效益最大化。这就是一个池化技术在现实生活中类似的例子。

二、为什么使用线程池

一种技术的出现,肯定是要解决存在的问题。如果不用线程池,会怎么样呢?很简单,需要时创建线程,线程跑完销毁,如果频繁去做这两个动作,就会造成比较大的资源消耗。所以线程池主要就是解决这个问题。

因此在《java并发编程的艺术》书中就提到以下几点:

  • 降低资源消耗。通过重复使用已创建的线程,降低线程创建和销毁造成的资源消耗。
  • 提高响应速度。当有任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。使用线程池可以进行统一的分配,调优和监控。

三、Executor

创建线程池主要使用ThreadPoolExecutor这个类,所以我们先看一张类图。

一般来说,遵守面向接口编程的思想,我们都喜欢使用ExecutorService接口接收线程池实例。如下:

public static void main(String[] args) throws Exception {
    //创建线程池
    ExecutorService executor = new ThreadPoolExecutor(10, 10, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<>(10));
}

这里可以看到创建线程池是使用ThreadPoolExecutor构造器来创建。构造器的参数有什么意义呢,继续往下看。

3.1 七个关键参数

/**
* corePoolSize 核心线程数
* maximumPoolSize 最大线程数
* keepAliveTime 线程存活时间
* unit keepAliveTime的时间单位,有日,小时,分钟,秒等等
* workQueue 工作队列
* threadFactory 线程工厂,用于创建线程
* handler 饱和策略
*/
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    //省略...
}

那么这7个参数,在线程池工作时,起到什么作用呢?直接看一张图就明白了。

这里有两个参数需要讲解一下,工作队列workQueue和饱和策略handler。

工作队列的类是BlockingQueue,是一个接口,我们先看看类图,看一下有哪些子类可以使用。

可以看到有很多实现的子类,功能也各有不同。下面讲几个有代表性的。

DelayQueue是无界的队列,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。

LinkedBlockingDeque是基于双向链表实现的双向并发阻塞队列,该阻塞队列同时支持FIFO和FILO两种操作方式,即可以从队列的头和尾同时操作(添加或删除);并且该阻塞队列是支持线程安全。可以指定队列的容量,如果不指定默认容量大小是Integer.MAX_VALUE

ArrayBlockingQueue是基于数组实现的有界阻塞队列,此队列按先进先出的原则对元素进行排序。新元素插入到队列的尾部,获取元素的操作则从队列的头部进行。

PriorityBlockingQueue是带优先级的无界阻塞队列,每次出队都返回优先级最高或者最低的元素(规则可以通过实现Comparable接口自己制定),内部是使用平衡二叉树实现的,遍历不保证有序。

饱和策略只要看RejectedExecutionHandler接口,以及其实现子类。

饱和策略主要有四种,如果要自定义饱和策略也很简单,实现RejectedExecutionHandler接口,重写rejectedExecution()方法即可。下面介绍JDK里的四种饱和策略。

  • AbortPolicy,直接抛出异常,简单粗暴。
  • CallerRunsPolicy,在任务被拒绝添加后,会调用当前线程池的所在的线程去执行被拒绝的任务。
  • DiscardPolicy,什么都不做,既不抛出异常,也不会执行。
  • DiscardOldestPolicy,当任务被拒绝添加时,会抛弃任务队列中最旧的任务(也就是最先加入队列的任务),再把这个新任务添加进去。

3.2 Executors

Executors类提供了四种线程池,根据使用不同的参数去new ThreadPoolExecutor实现。简单介绍一下。

第一种是newFixedThreadPool,这是创建固定大小的线程池,核心线程数和最大线程数都设置相同的值,使用LinkedBlockingQueue作为工作队列,当corePoolSize满了之后就加入到LinkedBlockingQueue队列中。LinkedBlockingQueue默认大小为Integer.MAX_VALUE,所以会有OOM的风险。

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

第二种是newSingleThreadExecutor,创建线程数为1的线程池,并且使用了LinkedBlockingQueue,核心线程数和最大线程数都为1,满了就放入队列中,执行完了就从队列取一个。也就是创建了一个具有缓冲队列的单线程的线程池。跟上面的问题一样,队列的容量默认是Integer.MAX_VALUE,也会有OOM的风险。

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

第三种是newCachedThreadPool,创建可缓冲的线程池,没有大小限制。核心线程数是0,最大线程数是Integer.MAX_VALUE,所以当有新任务时,任务会放入SynchronousQueue队列中,SynchronousQueue只能存放大小为1,所以会立刻新起线程。如果在工作线程在指定时间(60秒)空闲,则会自动终止。

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

第四种是newScheduledThreadPool,支持定时及周期性任务执行的线程池。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

3.3 使用规范

在阿里java开发规范中,是强制不允许使用Executors创建线程池,我们不妨看看。

假如有人头铁不信,那我们写一段代码模拟一下。

public class ThreadTest {
    private static AtomicInteger num = new AtomicInteger();
    public static void main(String[] args) throws Exception {
        //创建线程池
        ExecutorService executor = Executors.newCachedThreadPool();
        while (true) {
            executor.execute(() -> {
                try {
                    System.out.println("线程数:" + num.incrementAndGet());
                    Thread.sleep(10000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

然后设置JVM的参数-Xms5M -Xmx5M,运行一小段时间,就会看到报错了。
在这里插入图片描述
第二个问题是线程数的设置,设置多少线程数比较合适呢?

如果是cpu密集型的应用,cpu密集的意思是执行的任务大部分时间是在做计算和逻辑判断,这种情况显然不能设置太多的线程数,否则花在线程之间的切换时间就变多,效率就会变得低下。所以一般这种情况设置线程数为cpu核数+1即可。

cpu核数可以通过Runtime获取。

Runtime.getRuntime().availableProcessors()

如果是IO密集型的应用,IO密集的意思是执行的任务需要执行大量的IO操作,比如网络IO,磁盘IO,对CPU的使用率较低,因为在IO操作的特点需要等待,那么就可以把CPU切换到其他线程。所以可以设置线程数为CPU核数的两倍+1

絮叨

经过学习之后,我们就要养成使用多线程不能直接new一个Thread,然后start(),要有使用线程池的意识。其次要理解线程池参数的意义,根据实际情况去设置。

并发编程往往是实际开发中比较容易出问题,希望看完这篇文章能减少一些不必要的错误。

觉得有用就点个赞吧,你的点赞是我创作的最大动力~

我是一个努力让大家记住的程序员。我们下期再见!!!
在这里插入图片描述

能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!

相关文章
|
缓存 Java
认真阅读完这篇文章熟练掌握阿里巴巴规范创建Java线程池
认真阅读完这篇文章熟练掌握阿里巴巴规范创建Java线程池
805 0
|
3月前
|
缓存 前端开发 JavaScript
一篇文章助你搞懂java中的线程概念!纯干货,快收藏!
【8月更文挑战第11天】一篇文章助你搞懂java中的线程概念!纯干货,快收藏!
36 0
一篇文章助你搞懂java中的线程概念!纯干货,快收藏!
|
4月前
|
Python Windows
从菜鸟到大神:一篇文章带你彻底搞懂Python并发编程——线程篇与进程篇的深度较量!
【7月更文挑战第10天】Python并发编程对比线程与进程。线程适合IO密集型任务,利用`threading`模块,但GIL限制CPU并行。进程适用于CPU密集型任务,通过`multiprocessing`实现,独立内存空间,启动成本高。例子展示了如何创建和管理线程与进程以提高效率。选择取决于任务类型和资源需求。
32 0
|
5月前
|
NoSQL Redis
Redis系列学习文章分享---第五篇(Redis实战篇--优惠券秒杀,全局唯一id 添加优惠券 实现秒杀下单 库存超卖问题分析 乐观锁解决超卖 实现一人一单功能 集群下的线程并发安全问题)
Redis系列学习文章分享---第五篇(Redis实战篇--优惠券秒杀,全局唯一id 添加优惠券 实现秒杀下单 库存超卖问题分析 乐观锁解决超卖 实现一人一单功能 集群下的线程并发安全问题)
126 0
|
6月前
|
Java 测试技术 开发工具
Android 笔记:AndroidTrain , Lint , build(1),只需一篇文章吃透Android多线程技术
Android 笔记:AndroidTrain , Lint , build(1),只需一篇文章吃透Android多线程技术
|
设计模式 存储 安全
Java的第十三篇文章——JAVA多线程(后期再学一遍)
Java的第十三篇文章——JAVA多线程(后期再学一遍)
|
缓存 Java API
认真阅读完这篇文章熟练掌握高并发多线程——线程池
认真阅读完这篇文章熟练掌握高并发多线程——线程池
156 0
认真阅读完这篇文章熟练掌握高并发多线程——线程池
|
缓存
认真阅读完这篇文章熟练掌握多线程常见锁的基本用法
认真阅读完这篇文章熟练掌握多线程常见锁的基本用法
131 0
认真阅读完这篇文章熟练掌握多线程常见锁的基本用法
|
算法 Java 调度
一篇文章,搞明白异步和多线程的区别
一篇文章,搞明白异步和多线程的区别
977 0
一篇文章,搞明白异步和多线程的区别
|
算法 Java 调度
一篇文章,搞懂异步和多线程的区别
一篇文章,搞懂异步和多线程的区别
351 0
一篇文章,搞懂异步和多线程的区别