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,所以不再展开。


目录
相关文章
|
6月前
|
存储 Java 数据安全/隐私保护
【JUC】ThreadLocal 如何实现数据的线程隔离?
【1月更文挑战第15天】【JUC】ThreadLocal 如何实现数据的线程隔离?ThreadLocal 导致内存泄漏问题?
|
6月前
|
安全 算法 Java
剑指JUC原理-19.线程安全集合(上)
剑指JUC原理-19.线程安全集合
51 0
|
1月前
|
Java C++
【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
33 0
|
2月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
2月前
|
监控 Java 调度
【Java学习】多线程&JUC万字超详解
本文详细介绍了多线程的概念和三种实现方式,还有一些常见的成员方法,CPU的调动方式,多线程的生命周期,还有线程安全问题,锁和死锁的概念,以及等待唤醒机制,阻塞队列,多线程的六种状态,线程池等
131 6
【Java学习】多线程&JUC万字超详解
|
3月前
|
算法 Java
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
该博客文章综合介绍了Java并发编程的基础知识,包括线程与进程的区别、并发与并行的概念、线程的生命周期状态、`sleep`与`wait`方法的差异、`Lock`接口及其实现类与`synchronized`关键字的对比,以及生产者和消费者问题的解决方案和使用`Condition`对象替代`synchronized`关键字的方法。
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
|
3月前
|
设计模式 Java 调度
JUC线程池: ScheduledThreadPoolExecutor详解
`ScheduledThreadPoolExecutor`是Java标准库提供的一个强大的定时任务调度工具,它让并发编程中的任务调度变得简单而可靠。这个类的设计兼顾了灵活性与功能性,使其成为实现复杂定时任务逻辑的理想选择。不过,使用时仍需留意任务的执行时间以及系统的实际响应能力,以避免潜在的调度问题影响应用程序的行为。
78 1
|
3月前
|
Java API 调度
JUC线程池: FutureTask详解
总而言之,FutureTask是Java并发编程中一个非常实用的类,它在异步任务执行及结果处理方面提供了优雅的解决方案。在实现细节方面可以搭配线程池的使用,以及与Callable接口的配合使用,来完成高效的并发任务执行和结果处理。
42 0
|
3月前
|
Java 程序员 容器
【多线程面试题二十四】、 说说你对JUC的了解
这篇文章介绍了Java并发包java.util.concurrent(简称JUC),它是JSR 166规范的实现,提供了并发编程所需的基础组件,包括原子更新类、锁与条件变量、线程池、阻塞队列、并发容器和同步器等多种工具。
|
5月前
|
存储 安全 Java
Java多线程编程--JUC
Java多线程编程