JUC基础——线程池(1)

简介: JUC基础——线程池

前言

本文提要:介绍线程池,了解线程池的参数及使用,深入理解线程池工作原理


学习java,JUC是绕不过去的一部分内容,JUC也就是java.util.concurrent包,为开发者提供了大量高效的线程并发工具,方便我们可以开发出更高性能的代码。而juc其实涉及到的内容有很多,主要包含以下部分:


线程池

并发集合

同步器

原子变量

并发工具类

今天我们就来讲其中比较核心和常用的一个内容——线程池


一、线程池是什么

线程池是一种线程使用模式,我们都知道线程的创建与销毁会消耗资源,为了减少无谓的消耗而引入了池化技术。而线程池、连接池、对象池等池化技术都有一个共同的特征:重复利用。换句话说,存放在线程池里的线程在执行完一个任务后,可能还会存活,等待着下一次执行任务,这样就避免每一个任务都需要进行一次线程的创建和销毁。


二、管理线程池

1. 线程池种类


5bfc723650084a089c3523571ab2a8a3.png

我们可以看到预置的线程池有三种:


  • ThreadPoolExecutor
  • 最常见的线程池
  • ScheduledThreadPoolExecutor
  • 定时线程池,主要用于执行周期性任务
  • ForkJoinPool

拆分合并线程池,把一个大任务切分为若干个子任务并行地执行,最后合并得到这个大任务的结果

更细分的每种线程池也有预置的构建方法,这些构建方法在Executors类下,一般情况下,我们创建线程池会使用这些预置的构建方法来获取

14bbfb4695d6421e9e2e335733d04e6e.png

cad32c711e7e40138c388acbe013e2df.png


2. 线程池参数

线程池不单单有存放线程的作用,还具备“管理”的作用,比如我们的任务提交给线程池,线程池会帮我们找到合适的线程来执行;又比如没有任务或任务太多时,线程池也会自动“休息”或“满负荷运行”。这些管理功能需要我们进行参数的配置,不同配置出来的线程池自然效果不同ff73281349b3441a99c932dab9c0b584.png


可以看到,我们最常用的线程池类就是ThreadPoolExecutor,而它的构造方法支持7个参数,这也就是所谓的线程池的七大参数:


public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
(1)corePoolSize:线程池中常驻核心线程数
(2)maximumPoolSize:线程池能够容纳同时执行的最大线程数
(3)keepAliveTime:多余的空闲线程存活时间
(4)unit:keepAliveTime的时间单位
(5)workQueue:任务队列,被提交但尚未执行的任务
(6)threadFactory:表示生成线程池中的工作线程的线程工厂
(7)handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何拒绝

我们先知道这七大参数的意思即可,更具体的影响我们会在后面原理阶段细说。

3. 创建线程池

我们前面说了,Executors类下为我们预置了不同种类线程池的预建方法,我们简单介绍下Executors类下常用的五种线程池


newSingleThreadExecutor (单线程化)

// 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,
// 保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

newFixedThreadPool (定长)

// 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
// 定长线程池;适用于执行负载重,cpu使用频率高的任务;
// 这个主要是为了防止太多线程进行大量的线程频繁切换,得不偿失;
// 比如同时很多人进行商品秒杀。
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

newScheduledThreadPool (可定期)

// 创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

newCachedThreadPool (可缓存)

// 创建一个可缓存线程池,如果线程池长度超过处理需要,
// 可灵活回收空闲线程,若无可回收,则新建线程
// 适用于执行大量(并发)短期异步的任务;注意,任务量的负载要轻;
// 比如同时给很多人发送从磁盘读取的消息通知
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

newWorkStealingPool(分治-任务窃取)

// 分治线程池,线程之间会窃取任务来执行
public static ExecutorService newWorkStealingPool() {
    return new ForkJoinPool
        (Runtime.getRuntime().availableProcessors(),
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}


但是使用预置的方式创建线程池并非没有弊端,比如


newFixedThreadPool和newSingleThreadExecutor,主要问题是使用的无界队列,堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。

newCachedThreadPool和newScheduledThreadPool设置的线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。


基于这种考虑,所以很多公司的代码规范会强制要求程序员手动创建线程池,如下例子

ThreadPoolExecutor tp = new ThreadPoolExecutor(3, 5, 2000L,
      TimeUnit.SECONDS, new ArrayBlockingQueue<>(200),
      new ThreadFactory() {
          private AtomicInteger threadNumber = new AtomicInteger(1);
          @Override
          public Thread newThread(Runnable r) {
              return new Thread(r, "money caculate thread: " + threadNumber.getAndAdd(1));
          }
      },
      new ThreadPoolExecutor.CallerRunsPolicy()
);

注意,在上面的创建里,

我们使用了线程安全的有界队列,并设定了长度200;

我们自定义了线程工厂,这样该线程池创建的线程都有特殊的线程名;

我们选择了CallerRunsPolicy 拒绝策略,一旦线程池满负荷,再往里提交任务就由提交任务的线程自己去运行


其实拒绝策略一共有好几种:


  • AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出异常 RejectedExecutionException (属于RuntimeException),让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
  • DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
  • DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
  • CallerRunsPolicy,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务,这样做任务可以保证不丢失。

而关于核心线程、最大线程的设置是一个经验问题,因此一个常见的经验配置如下,但实际上因为可能因为一个项目包含不止一个线程池,所以数目设置仅供参考


任务特性 特点 常用线程池
CPU密集型任务 应配置尽可能小的线程 cpu数 +1
IO密集型任务 并不是一直在执行任务,则应配置尽可能多的线程 2*cpu数
混合型的任务 可拆分成cpu密集型任务和IO密集型任务


另外,很多框架也会提供创建线程池的方式,比如Spring的ThreadPoolTaskExecutor,但因为我们这里主要还是说JUC,所以不再展开。


目录
相关文章
|
11天前
|
存储 Java 数据安全/隐私保护
【JUC】ThreadLocal 如何实现数据的线程隔离?
【1月更文挑战第15天】【JUC】ThreadLocal 如何实现数据的线程隔离?ThreadLocal 导致内存泄漏问题?
|
11天前
|
安全 算法 Java
剑指JUC原理-19.线程安全集合(上)
剑指JUC原理-19.线程安全集合
27 0
|
11天前
|
Java Linux 调度
剑指JUC原理-7.线程状态与ReentrantLock(中)
剑指JUC原理-7.线程状态与ReentrantLock
36 0
|
11天前
|
存储 安全 Java
剑指JUC原理-4.共享资源和线程安全性(上)
剑指JUC原理-4.共享资源和线程安全性
50 1
|
11天前
|
监控 Java 应用服务中间件
剑指JUC原理-3.线程常用方法及状态(下)
剑指JUC原理-3.线程常用方法及状态
63 0
|
11天前
|
Java Linux API
剑指JUC原理-2.线程
剑指JUC原理-2.线程
44 0
|
11天前
|
Java
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
30 0
|
11天前
|
存储 安全 Java
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(下)
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(下)
45 0
|
11天前
|
存储 安全 Java
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(上)
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)
38 0
|
11天前
|
安全 Java C++
JUC(java.util.concurrent)的常见类(多线程编程常用类)
JUC(java.util.concurrent)的常见类(多线程编程常用类)