Java入门3.2---线程池

一、Java线程与系统内核线程

  Java虚拟机使用的是KLT线程模型。Java线程创建依赖于系统内核,通过JVM调用系统库创建内核线程,内核线程与Java-Thread是1:1的映射关系。

  1. 并发:CPU在多个线程之间来回切换调度。
  2. 并行:多核CPU同时处理多个线程。

 

 

 

 

二、线程池

  线程是稀缺资源,它的创建与销毁时一个相对偏重且耗资源的操作,而Java线程依赖于内核线程,创建线程需要进行操作系统状态切换,为避免资源过度消耗需要设法重用线程执行多个任务。线程池就是一个线程缓存,负责对线程进行统一分配、调优与监控。线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。

1.为什么要用线程池?什么时候用线程池?

多线程的优点:

  1. 减少阻塞,tomcat、Ajax
  2. 避免空转,IO操作
  3. 提升性能,多核CPU情况下 可以对任务拆分并发处理

线程池的优点:

  1. 重用存在的线程,减少线程创建,消亡的开销,提高性能。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性,可统一分配、调优和监控。

当单个任务处理时间比较短或者需要处理的任务数量很大时,建议使用线程池。比如网购商品秒杀、云盘文件上传和下载、12306网上购票系统等。

2.线程池的5种状态

RUNNING

  1. 该状态的线程池会接收新任务,并处理阻塞队列中的任务;
  2. 调用线程池的shutdown()方法,可以切换到SHUTDOWN状态;
  3. 调用线程池的shutdownNow()方法,可以切换到STOP状态;

SHUTDOWN

  1. 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
  2. 队列为空,并且线程池中执行的任务也为空,进入TIDYING状态;

STOP

  1. 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
  2. 线程池中执行的任务为空,进入TIDYING状态;

TIDYING

  1. 该状态表明所有的任务已经运行终止,记录的任务数量为0。
  2. terminated()执行完毕,进入TERMINATED状态

TERMINATED

  1. 该状态表示线程池彻底终止

3.线程池怎么用?

3.1 工作流程

  1. 提交一个任务,线程池里存活的核心线程数小于线程数corePoolSize时,线程池会创建一个核心线程去处理提交的任务。
  2. 如果线程池核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行。
  3. 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建一个非核心线程执行提交的任务。
  4. 如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用拒绝策略处理。

补充说明:

阻塞队列:在任意时刻,不管并发有多高,永远只有一个线程能够进行队列的入队或者出队操作,线程安全的队列(有界|无界)

  1. 队列满,只能进行出队操作,所有入队的操作必须等待,也就是被阻塞。
  2. 队列空,只能进行入队操作,所有出队的操作必须等待,也就是被阻塞。

3.2 线程池的使用步骤

  1. 使用Executors工厂类的静态方法,创建线程池对象;
  2. 编写Runnable或Callable实现类的实例对象;
  3. 利用ExecutorService的submit方法或SchedudExecutorService的schedule方法提交并执行线程任务;
  4. 如果有执行结果,则处理异步执行结果(Future);
  5. 调用shutdown()方法,关闭线程池。

3.3 创建线程池

  1. ExecutorService: 真正的线程池接口。
  2. ScheduledExecutorService: 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
  3. ThreadPoolExecutor: ExecutorService的默认实现。
  4. ScheduledThreadPoolExecutor: 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

3.3.1 通过构造方法ThreadPoolExecutor创建线程池(默认)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * 用给定的初始参数创建一个新的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.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

(1)corePoolSize

  需要依据任务的处理时间和每秒产生的任务数量来确定,例如:执行一个任务需要0.1秒,系统百分之80的时间每秒产生100个任务,那么要想在1秒内处理完这100个任务,就需要10个线程,此时corePoolSize=10。当然实际情况不可能这么平均,所以一般按照8020原则,按照百分之80的情况设计核心线程数,剩下的百分之20可以利用最大线程数处理。(根据实际情况最大不超过最大线程数。)

(2)maximumPoolSize

  需要参照corePoolSize和每秒产生的最大任务数。假如系统每秒产生最大任务是1000,那么最大线程数=(最大任务数-任务队列长度)*每个任务执行时间:最大线程数=(1000-200)*0.1=80个

  1. CPU密集型:CPU的核数
  2. IO密集型:IO任务的个数

(3)keepAliveTime

  临时创建出来的非核心线程当空闲超过设置的时间将被销毁,根据实际需要设定。

(4)unit

  有days、hours、minutes、milliseconds、microseconds、nanoseconds。

(5)workQueue

  一定要设置长度,具体长度根据业务大概流量计算。核心线程数/单个任务执行时间*2。假如corePoolSize=10,单个任务执行时间为0.1秒,则workQueue=200。

  1. ArrayBlockingQueue(有界队列):是一个用数组实现的有界阻塞队列,按FIFO排序量。
  2. LinkedBlockingQueue(可设置容量队列):基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列
  3. DelayQueue(延迟队列):是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
  4. PriorityBlockingQueue(优先级队列):是具有优先级的无界阻塞队列。
  5. SynchronousQueue(同步队列):一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。

(6)threadFactory

  用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。

(7)handler拒绝策略

  1. ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常(默认)。
  2. ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
  3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)。
  4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。

举例1:综合案例-秒杀商品

功能:某商场上架10部新手机,免费送给客户体验,要求所有参与活动的人员在规定的时间同时参与秒杀争抢,假设有20个人同时参与了该活动,请使用线程池模拟这个场景,保证前10人秒杀成功,后10人秒杀失败。

要求:1.使用线程池创建线程 2.解决线程安全问题

3.3.2 ExecutorService创建线程池

Executors 返回线程池对象的弊端如下:

  • FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。

这几种方法内部实际上是调用了ThreadPoolExecutor的构造方法。

(1)newCachedThreadPool

  1. static ExecutorService newCachedThreadPool():创建一个默认的线程池对象,里面的线程可重用且在第一次使用时才创建线程池对象
  2. static ExecutorService newCachedThreadPool(ThreadFactory threadFactory):允许自定义线程的创建,线程池中的所有线程都使用ThreadFactory来创建,这样的线程无需手动启动,自动执行;

特点:

  1. 核心线程数为0
  2. 最大线程数为Integer.MAX_VALUE
  3. 阻塞队列是SynchronousQueue
  4. 非核心线程空闲存活时间为60秒

  当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。

原理:

  1. 提交任务
  2. 因为没有核心线程,所以任务直接加到SynchronousQueue队列。
  3. 判断是否有空闲线程,如果有,就去取出任务执行。
  4. 如果没有空闲线程,就新建一个线程执行。
  5. 执行完任务的线程,还可以存活60秒,如果在这期间,接到任务,可以继续活下去;否则,被销毁。

使用场景:用于并发执行大量短期的小任务。

(2)newFixedThreadPool

  1. static ExecutorService newFixedThreadPool(int nThreads):创建一个可重用固定线程数的线程池;
  2. static ExecutorService newFixedThreadPool(int nThreads,ThreadFactory threadFactory):创建一个可重用固定线程数的线程池线程池中的所有线程都使用ThreadFactory来创建

特点:

  1. 核心线程数和最大线程数大小一样
  2. 没有所谓的非空闲时间,即keepAliveTime为0
  3. 阻塞队列为无界队列LinkedBlockingQueue

原理:

  1. 提交任务
  2. 如果线程数少于核心线程,创建核心线程执行任务
  3. 如果线程数等于核心线程,把任务添加到LinkedBlockingQueue阻塞队列
  4. 如果线程执行完任务,去阻塞队列取任务,继续执行。

缺点:

  1. 使用无界队列的线程池会导致内存飙升:newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长,会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终导致OOM。

使用场景:FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务

(3)newSingleThreadExecutor

  1. static ExecutorService newSingleThreadExecutor():创建一个使用单个worker线程的Executor,以无界队列方式来运行该线程;
  2. static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory):创建一个使用单个worker线程的Executor,且线程池中的所有线程都使用ThreadFactory来创建

特点:

  1. 核心线程数为1
  2. 最大线程数也为1
  3. 阻塞队列是LinkedBlockingQueue
  4. keepAliveTime为0

原理:

  1. 提交任务
  2. 线程池是否有一条线程在,如果没有,新建线程执行任务
  3. 如果有,讲任务加到阻塞队列
  4. 当前的唯一线程,从队列取任务,执行完一个,再继续取,一个人(一条线程)夜以继日地干活。

使用场景:适用于串行执行任务的场景,一个任务一个任务地执行。

(4)提交任务方法

  1. <T>Future<T>submit(Callable<T> task):停止所有正在执行的任务,返回一个Future对象。
  2. Future<?>submit(Runnable task):执行Runnable任务,并返回一个表示该任务的Future。
  3. <T>Future<T>submit(Runnable task,T result):执行Runnable任务,并返回一个表示该任务的Future。

举例1:对比6种创建线程池的方法

newCachedThreadPool:线程数量不限制,任务优先模式,前提是服务器性能支持。 1.创建默认的线程池对象
2.创建自定义的线程
newFixedThreadPool:自定义线程数量,适用于服务器硬件一般和对性能要求不高的情况。 1.固定线程池中的线程数量
 2.固定线程数量+自定义线程的创建  
newSingleThreadExecutor:单线程线程池,适用于追求绝对的安全(e.g. 银行),不考虑性能。 1.单个线程完成所有任务  
2.自定义单个线程完成所有任务  

举例2:ATM取款

功能:设计一个程序,使用两个线程模拟在两个地点同时从一个账号中取钱,假如卡中一共有1000元,每个线程取800元,要求演示如果一个线程取款成功,剩余200元,另一个线程取款失败,余额不足。

3.3.3 ScheduledExecutorService创建需要重复执行的线程池

  ScheduledExecutorService是ExecutorService的子接口,具备了延迟运行或定期执行任务的能力,和Timer/TimerTask类似,适用于需要重复执行任务的情况

(1)newScheduledThreadPool

  1. static ScheduledExecutorService newScheduledThreadPool(int corePoolSize):创建一个可重用固定线程数的线程池且允许延迟运行或定期执行任务;
  2. static ScheduledExecutorService newScheduledThreadPool(int corePoolSize,ThreadFactory threadFactory):创建一个可重用固定线程数的线程池且线程池中的所有线程都使用ThreadFactory来创建,且允许延迟运行或定期执行任务

特点:

  1. 最大线程数为Integer.MAX_VALUE
  2. 阻塞队列是DelayedWorkQueue
  3. keepAliveTime为0
  4. scheduleAtFixedRate() :按某种速率周期执行
  5. scheduleWithFixedDelay():在某个延迟后执行

 原理:

  1. 添加一个任务
  2. 线程池中的线程从 DelayQueue 中取任务
  3. 线程从 DelayQueue 中获取 time 大于等于当前时间的task
  4. 执行完后修改这个 task 的 time 为下次被执行的时间
  5. 这个 task 放回DelayQueue队列中

使用场景:周期性执行任务的场景,需要限制线程数量的场景

(2)newSingleThreadScheduledExecutor

  1. static ScheduledExecutorService newSingleThreadScheduledExecutor():创建一个单线程执行程序,它允许在给定延迟后运行命令或者定期地执行。
  2. static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory):创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行

(3)提交任务方法

  1. <V>ScheduledFuture<V>schedule(Callabel<V>callable,long delay,TimeUnit unit):延迟时间单位是unit,数量是delay的时间后执行callable;
  2. ScheduledFuture<?>schedule(Runnable command,long delay,TimeUnit unit):延迟时间单位是unit,数量是delay的时间后执行command;
  3. ScheduledFuture<?>scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit):延迟时间单位是unit,数量是initialDelay的时间后,每间隔period时间重复执行一次command。
  4. ScheduledFuture<?>scheduleWithFixedDelay(Runnable command,long initialDelay,long period,TimeUnit unit):创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。

举例1:对比几种不同的延迟创建线程池的方法

1.newScheduledThreadPool+schedule

Over先输出,2s之后线程开始执行。

 

 

 

2.newScheduledThreadPool+scheduledAtFixedRate

 

Over先输出,初始是等待1s开始执行任务,每个任务执行1.5s,每隔2s执行一个任务(这里只设置了1个任务),实际上每间隔2s输出一句“自定义线程X执行了任务1”。

  

  

3.newSingleScheduledExecutor+scheduledWithFixedDelay  

首先输出Over,每个任务执行2s,间隔2s,实际上每隔4s输出“自定义线程1执行了任务1”。

  

3.3.4 异步计算Future

  通过Future对象获取线程计算的结果。

常用方法:

  1. boolean cancel(boolean mayInterruptRunning):试图取消对此任务的执行(只能取消尚未完成的任务),返回值表示任务是否取消成功,取消成功返回true。
  2. V get()如有必要,等待计算完成,然后获取其结果。
  3. V get(long timeout,TimeUnit unit):如有必要,最多等待为使计算完成所给定的时间之后,获取其结果。
  4. boolean isCancelled():如果在任务正常完成前将其取消,则返回true。
  5. boolean isDone():如果任务已完成,则返回true。
正常执行任务 取消任务

3.4 关闭线程池

  通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来终端线程,所以无法响应中断的任务可能永远无法终止。

void shutdown():shutdown将线程池的状态设置为SHUTWDOWN状态,并不会立即停止。

  1. 停止接收外部submit的任务
  2. 内部正在跑的任务和队列里等待的任务,会执行完
  3. 内部正在跑的任务和队列里等待的任务,会执行完

List<Runnable> shutdownNow():内部正在跑的任务和队列里等待的任务,会执行完。

  1. 停止接收外部submit的任务
  2. 忽略队列里等待的任务
  3. 尝试中断正在跑的任务
  4. 返回未执行的任务

举例1:对比shutdownNow和shutdown

1.shutdownNow

线程1在shutdownNow命令发起后,需要停下所有正在执行和等待的任务。

2.shutdown

线程1在shutdown命令发起后已经缓存了所有的任务,需要继续执行。

3.5 线程池异常处理

  在使用线程池处理任务的时候,任务代码可能抛出RuntimeException,抛出异常后,线程池可能捕获它,也可能创建一个新的线程来代替异常的线程,我们可能无法感知任务出现了异常,因此我们需要考虑线程池异常情况。

3.5.1 try-catch捕获异常

3.5.2 submit执行,Future.get接受异常

3.5.3 重写ThreadPoolExecutor.afterExecute方法,处理传递的异常应用

jdk的demo

3.5.4 实例化时传入ThreadFactory,设置Thread.UncaughtExceptionHandler处理未检测的异常。

为工作者线程设置UncaughtExceptionHandler,在uncaughtException方法中处理异常。

 

三、一些疑问

1.实现Runnable接口和Callable接口的区别

  Runnable自Java 1.0以来一直存在,但Callable仅在Java 1.5中引入,目的就是为了来处理Runnable不支持的用例。Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。

  工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。

1
2
3
Executors.callable(Runnable task)
Executors.callable(Runnable task,Object resule))

Runnable.java

1
2
3
4
5
6
7
@FunctionalInterface
public interface Runnable {
   /**
    * 被线程执行,没有返回值也无法抛出异常
    */
    public abstract void run();
}

Callable.java

1
2
3
4
5
6
7
8
9
@FunctionalInterface
public interface Callable<V> {
    /**
     * 计算结果,或在无法这样做时抛出异常。
     * @return 计算得出的结果
     * @throws 如果无法计算结果,则抛出异常
     */
    V call() throws Exception;
}

2.执行execute()方法和submit()方法的区别

  1. execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  2. submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

以AbstractExecutorService接口中的一个 submit 方法为例子来看看源代码:

上面方法调用的 newTaskFor 方法返回了一个 FutureTask 对象。 

execute()方法:

3.线程池对比

举例1:以下哪种方式创建的线程池适合使用在很耗时的任务 (C)

A. Executors.newCachedThreadPool()

B. Executors.newFixedThreadPool()

C. Executors.newWorkStealingPool()

D. Executors.newSingleThreadExecutor()

举例2:以下说法正确的是 (ACD)

A. 调用Thread.interrupt() 用于请求另外一个线程中止执行,而不是直接中止

B. 推荐使用Thread.current().isInterrupted(),而不是Thread.interrupted()检查自己是否被interrupt

C. 检测到当前线程被interrupt后,应抛出InterruptedException,并在finally或try-with-resource中清理执行状态

D. 调用线程的interrupt方法,只有当线程走到了sleep, wait, join等阻塞这些方法的时候,才会抛出InterruptedException

 

 

参考文献:

【1】ThreadPoolExecutor_Java API中文文档 - itmyhome(http://itmyhome.com)

【2】面试必备:Java线程池解析

【3】Java线程池深入浅出

【4】JavaGuide/java线程池学习总结.md at master · Snailclimb/JavaGuide

【5】大白话之必会 Java Atomic | 线程一点也不安全(一):比自增和 synchronized 更快速、靠谱的原 - 链滴

posted @   nxf_rabbit75  阅读(438)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
历史上的今天:
2019-06-04 seaborn关联图---散点线性图replot、散点图scatterplot、线形图lineplot
2019-06-04 seaborn---样式控制/调色板
一、Java线程与系统内核线程二、线程池1.为什么要用线程池?什么时候用线程池?2.线程池的5种状态3.线程池怎么用?3.1 工作流程3.2 线程池的使用步骤3.3 创建线程池3.3.1 通过构造方法ThreadPoolExecutor创建线程池(默认)举例1:综合案例-秒杀商品3.3.2 ExecutorService创建线程池举例1:对比6种创建线程池的方法举例2:ATM取款3.3.3 ScheduledExecutorService创建需要重复执行的线程池举例1:对比几种不同的延迟创建线程池的方法3.3.4 异步计算Future3.4 关闭线程池举例1:对比shutdownNow和shutdown3.5 线程池异常处理3.5.1 try-catch捕获异常3.5.2 submit执行,Future.get接受异常3.5.3 重写ThreadPoolExecutor.afterExecute方法,处理传递的异常应用3.5.4 实例化时传入ThreadFactory,设置Thread.UncaughtExceptionHandler处理未检测的异常。三、一些疑问1.实现Runnable接口和Callable接口的区别2.执行execute()方法和submit()方法的区别3.线程池对比
点击右上角即可分享
微信分享提示