【JAVA】:万字长篇带你了解JAVA并发编程【二】

目录

  • 【JAVA】:万字长篇带你了解JAVA并发编程【二】
    • 3. 线程池
      • 池化技术
      • 线程池的概念与作用
        • 什么是线程池?
        • 优点
      • Executor框架
        • Executor框架组成部分
          • 工作任务
          • 抽象接口和类
      • 线程池的生命周期
      • 非核心线程和核心线程
      • ThreadPoolExecutor
      • 线程池的工作流
      • 常见的线程池
        • Executors 的 4 个功能线程有如下`弊端`:
        • 合理设置线程池参数
    • 结语

个人主页: 【⭐️个人主页】
需要您的【 点赞+关注】支持


【JAVA】:万字长篇带你了解JAVA并发编程【二】

3. 线程池

池化技术

线程的创建、销毁都会带来一定的开销

如果当我们需要使用到多线程时再去创建,使用完又去销毁,这样去使用不仅会拉长业务流程,还会增加创建、销毁线程的开销

于是有了池化技术的思想,将线程提前创建出来,放在一个池子(容器)中进行管理

当需要使用时,从池子里拿取一个线程来执行任务,执行完毕后再放回池子

不仅是线程有池化的思想,连接也有池化的思想,也就是连接池

池化技术不仅能复用资源提高响应还方便管理

线程池的概念与作用

什么是线程池?

线程池是一种利用池化技术思想来实现的线程管理技术,主要是为了复用线程、便利地管理线程和任务、并将线程的创建和任务的执行解耦开来。我们可以创建线程池来复用已经创建的线程来降低频繁创建和销毁线程所带来的资源消耗。在JAVA中主要是使用ThreadPoolExecutor类来创建线程池,并且JDK中也提供了Executors工厂类来创建线程池(不推荐使用)。

❗❗❗ 这就是线程池最核心的设计思路,「复用线程,平摊线程的创建与销毁的开销代价」。

优点

相比于来一个任务创建一个线程的方式,使用线程池的优势体现在如下几点:

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

我们先看一张线程池相关接口的类图结构,网上盗来的,但画的还是很全面的
【JAVA】:万字长篇带你了解JAVA并发编程【二】_第1张图片
右上角的几个接口可以先不看,等我们介绍到组合任务的时候会继续说的,我们看左边,Executor、ExecutorService 以及 AbstractExecutorService 都是我们熟悉的,它们抽象了任务执行者的基本模型。

ThreadPoolExecutor 是对线程池概念的抽象,它天生实现了任务执行的相关接口,也就是说,线程池也是一个任务的执行者,允许你向其中提交多个任务,线程池将负责分配线程与调度任务。

至于 Schedule 线程池,它是扩展了基础的线程池实现,提供「计划调度」能力,定时调度任务,延时执行等。

Executor框架

Executor是一套线程池管理框架。是JDK 1.5中引入的一系列并发库中与Executor相关的功能类,其中最核心的类就是常见的ThreadPoolExecutor
Executor将工作任务线程池进行分离解耦

Executor框架组成部分
工作任务

工作任务被分为两种:无返回结果的Runnable有返回结果的Callable

在线程池中允许执行这两种任务,其中它们都是函数式接口,可以使用lambda表达式来实现

Future接口用来定义获取异步任务的结果,它的实现类常是FutureTask

线程池在执行Callable任务时,会将使用FutureTask将其封装成Runnable执行,因此Executor的执行方法入参只有Runnable

FutureTask相当于适配器,将Callable转换为Runnable再进行执行
例子:

  @Test
    public void testFutureTask() throws ExecutionException, InterruptedException {
        FutureTask<String> command = new FutureTask<String>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(5000);
                return "hello world";
            }
        });
        result.execute(command);
        System.out.println("获取异步结果:start");
        String s = command.get();
        System.out.println("获取异步结果:" + s);

    }
抽象接口和类

Executor接口:只有一个execute()方法;
ExecutorService接口: ExecutorService扩展了Executor接口,增加了生命周期的管理方法。
ExecutorService的生命周期包括三种状态:运行关闭终止

创建后便进入运行状态,当调用了shutdown()方法时,便进入关闭状态,此时意味着ExecutorService不再接受新的任务,但它还在执行已经提交了submit()的任务,当已经提交了的任务执行完后,便到达终止状态。

ScheduledExecutorService接口:任务调度的线程池实现,可以在给定的延迟后运行命令或者定期执行命令;

ThreadPoolExecutor:最核心的线程池实现,用来执行被提交的任务;

线程池的生命周期

总共有五种状态:
【JAVA】:万字长篇带你了解JAVA并发编程【二】_第2张图片

  • RUNNING(111) :能接受新提交的任务,并且也能处理阻塞队列中的任务;

  • SHUTDOWN(000):关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。(finalize() 方法在执行过程中也会调用shutdown()方法进入该状态);

  • STOP(001):不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow()方法会使线程池进入到该状态;

  • TIDYING(010):如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。

  • TERMINATED(011):在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。

ThreadPoolExecutor 使用int的高三位表示线程池状态,低29位表示线程数量;

非核心线程和核心线程

什么是“非核心线程”?

核心线程跟创建的先后没有关系,而是跟工作线程的个数有关,如果当前工作线程的个数大于核心线程数,那么所有的线程都可能是“非核心线程”,都有被回收的可能。
一个线程执行完了一个任务后,会去阻塞队列里面取新的任务,在取到任务之前它就是一个闲置的线程。

线程回收策略

取任务的方法有两种,一种是通过 take() 方法一直阻塞直到取出任务,另一种是通过 poll(keepAliveTime,timeUnit) 方法在一定时间内取出任务或者超时,如果超时这个线程就会被回收,请注意核心线程一般不会被回收。

那么怎么保证核心线程不会被回收呢?

还是跟工作线程的个数有关,每一个线程在取任务的时候,线程池会比较当前的工作线程个数与核心线程数:

如果工作线程数小于当前的核心线程数,则使用第一种方法取任务,也就是没有超时回收,这时所有的工作线程都是“核心线程”,他们不会被回收;

如果大于核心线程数,则使用第二种方法取任务,一旦超时就回收,所以并没有绝对的核心线程,只要这个线程没有在存活时间内取到任务去执行就会被回收。

核心线程一般不会被回收,但是也不是绝对的,如果我们设置了允许核心线程超时被回收的话,那么就没有核心线程这种说法了,所有的线程都会通过 poll(keepAliveTime, timeUnit) 来获取任务,一旦超时获取不到任务,就会被回收,一般很少会这样来使用,除非该线程池需要处理的任务非常少,并且频率也不高,不需要将核心线程一直维持着。

非核心线程存活时间

当工作线程数达到 corePoolSize 时,线程池会将新接收到的任务存放在阻塞队列中,而阻塞队列又两种情况:一种是有界的队列,一种是无界的队列。

如果是无界队列,那么当核心线程都在忙的时候,所有新提交的任务都会被存放在该无界队列中,这时最大线程数将变得没有意义,因为阻塞队列不会存在被装满的情况。

如果是有界队列,那么当阻塞队列中装满了等待执行的任务,这时再有新任务提交时,线程池就需要创建新的“临时”线程来处理,相当于增派人手来处理任务。

但是创建的“临时”线程是有存活时间的,不可能让他们一直都存活着,当阻塞队列中的任务被执行完毕,并且又没有那么多新任务被提交时,“临时”线程就需要被回收销毁,在被回收销毁之前等待的这段时间,就是非核心线程的存活时间,也就是 keepAliveTime 属性。

ThreadPoolExecutor

ThreadPoolExecutor 的创建并不复杂,直接 new 就好,只不过构造函数有好久个重载,我们直接看最底层的那个,也就是参数最多的那个。

public ThreadPoolExecutor
(   int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler
)

参数介绍
创建线程池需要以下几个参数,其中有5个是必需的:

  • corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
  • maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
  • keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
  • unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
  • workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
  • threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
    handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。
  • RejectedExecutionHandler 拒绝策略:当线程不够用,并且阻塞队列爆满时如何拒绝任务的策略
拒绝策略 作用
AbortPolicy 默认 抛出异常
CallerRunsPolicy 调用线程来执行任务
DiscardPolicy 不处理,丢弃
DiscardOldestPolicy 丢弃队列中最近一个任务,并立即执行当前任务

线程池的工作流

【JAVA】:万字长篇带你了解JAVA并发编程【二】_第3张图片

任务数 <= 核心线程数时,线程池中工作线程 = 任务数

核心线程数 + 队列容量 < 任务数 <= 最大线程数 + 队列容量时,工作线程数 = 任务数 - 队列容量

常见的线程池

Executors已经为我们封装好了 4 种常见的功能线程池,如下:

  • 定长线程池(FixedThreadPool)

    只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。适用于控制线程最大并发数。

  • 定时线程池(ScheduledThreadPool )

    核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。适用于执行定时或周期性的任务。

  • 可缓存线程池(CachedThreadPool)

    无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。适用于执行大量、耗时少的任务。

  • 单线程化线程池(SingleThreadExecutor)

    只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。不适合并发以及可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等。

Executors 的 4 个功能线程有如下弊端
  • FixedThreadPool 和 SingleThreadExecutor:

    主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,可能会耗费非常大的内存,甚至 OOM。

  • CachedThreadPool 和 ScheduledThreadPool

    主要问题是线程数最大数是 ·Integer.MAX_VALUE·,可能会创建数量非常多的线程,甚至 OOM。

合理设置线程池参数
return new ThreadPoolExecutor(
                Runtime.getRuntime().availableProcessors() + 1,
                1024,
                1, TimeUnit.HOURS,
                new ArrayBlockingQueue<>(1024), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r);
            }
        },
                new RejectedExecutionHandler() {
                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        System.out.println("rejected");
                    }
                }
        );

结语

线程池帮我们提高对线程的管理和监控能力,本身也降低了因为创建线程和线程切换带来的内存资源和cpu资源的消耗,也提高了整体的响应速度。
但是线程池也需要我们自己通过ThreadPoolExecutor自己定义,拒绝直接适用Executors工具提供的线程池类。导致的线程不可控的问题。

你可能感兴趣的:(Java开发知识,java,多线程,并发)