干翻全岛蛙蛙 发表于 2024-5-18 00:27:26

线程池的运行逻辑与你想象的不一样,它是池族中的异类

只要是 web 项目,程序都会直接或间接利用到线程池,它的利用是如此频繁,以至于像氛围一样,大多数时候被我们无视了。但有时候,我们会相称然地认为线程池与其它对象池(如:数据库毗连池)一样,要用的时候向池子索取,用完后归还给它即可。然后事实上,线程池独树一帜、佼佼不群,它与普通的对象池就是不同。本文本将先叙述这种差异,接着用最简单的代码实现一个线程池,末了再对 JDK 中与线程池相干的 Executor 体系做一个全面介绍。
线程池与普通资源池的差异

提到 pool 这个设计思想,第一反映是如许的:从一个资源容器中获取空闲的资源对象。假如容器中有空闲的,就直接从空闲资源中取出一个返回,假如容器中没有空闲资源,且容器空间未用尽,就新创建一个资源对象,然后再返回给调用方。这个容器就是资源池,它看起来就像如许:
https://img2024.cnblogs.com/blog/2536945/202404/2536945-20240410142143167-615830131.png
图中的工人队伍里,有3人是空闲的,工头(资源池的管理者)可以任选两人来提供劳务服务。同时,队队伍尚未饱和,还可以容纳一名工人。假如雇主要求一次性提供4名劳工服务,则工头需要再招纳一名工人参加队伍,然后再向雇主提供服务。此时,这个团队(资源池)已达到饱和,不能再对外提供劳务服务了,除非某些工人完成了工作。
以上是一个范例资源池的根本特点,那么线程池是否也同样如此呢。至少第一感觉是没问题的,大概应该也是如许吧,究竟拿从池中取出一个线程,再让它执行对应的代码,这听上去很科学嘛。等等,总感觉哪里不对呢,线程这东西能像普通方法调用那样,让我们在主程序里随意支配吗?没错,问题就在这里,线程一旦运行起来,就完全闭关锁国了,除了按照运行前约定好的方式进行数据通信外,再也不能去打扰它老人家了。因此,线程池有点像发动机,池中的各个线程就对应发动机的各个汽缸。整个发动机一旦启动(线程池激活),各个汽缸中的活塞便按照预定的设计,不绝地来回运动,永久也不绝止,直到燃油耗尽,或人为地关闭油门。在此期间,我们是不能控制单个汽缸的活动方向的。就如同我们不能控制正在运行的线程,让其停止正在执行的代码,转而去执行其它代码一样(利用 Thread.interrpt() 方法也达不到此目标,而 Thread.stop() 更是直接停止了线程)①。
https://tukuimg.bdstatic.com/scrop/7c2206a3278746aa4fb494610bb4d956.gif
既然不能直接给线程池里的单个线程明确指派使命,那线程池的意义何在呢?意义就在于,虽然不能一对一精确指派使命,但可以给整个线程池提交使命,至于这些使命由池中的哪个线程来执行,则是不可控的。此时,可以把线程池看作是生产流水线上的单个工序。这里以给「老干妈香辣酱」的玻璃瓶加盖子为例,给瓶子加盖就是要执行的使命,最初该工序上只设置了一个机器臂,加盖子也顺序利用的。但单个机器臂忙不过来,后来又加了一个机器臂,如许服从就提高了。瓶子被加盖的顺序也是不确定的,但终极所有瓶子都会被加盖。
手动编写一个浅易的线程池

如上小节所述,线程池与其它池类组件不一样,调用方不可能直接从池中取出一个线程,然后让它执行一段使命代码。因为线程一旦启动起来,就会在自己的频轨道内独立运行,不受外部控制。要让这些线程执行外部提交的使命,需要提供一个数据通道,将使命打包成一个数据结构通报过去。而这些运行起来的线程,他们都执行一个相同的循环利用:读取使命 → 执行使命 → 读取使命 → ...... ②
      ┌──────────┐    ┌──────────────┐
┌─→ │Take Task │ -→ │ Execute Task │ ─┐
│   └──────────┘    └──────────────┘│
└─────────────────────────────────────┘这个读取使命的数据通道就是队列,池中的所有线程都不停地执行 ② 处的循环逻辑,这便是线程池运行的根本原理。
相对于线程池这个叫法,实际上「执行器 Executor」这个术语在实践中利用得要更多些。因为在 jdk 的 java.util.concurrent 包下,有一个 Executor 接口,它只有一个方法:
public interface Executor {
    void execute(Runnable command);
}这便是执行器接口,顾名思义,它接受一个 Runnable 对象,并可以大概执行它。至于如何执行,交由具体的实现类负责,目前至少有以下四种执行方式 ③

[*]在当前线程中同步执行
[*]总是新开线程来异步执行
[*]只利用一个线程来异步串行执行
[*]利用多个线程来并发执行
本小节将以一个浅易的线程池方式来实现 Executor。
编写只有一个线程的线程池

这是线程池的最简形式,实当代码也非常简单,如下所示
public class SingleThreadPoolExecutor implements Executor {
    // 任务队列
    private final Queue<Runnable> tasks = new LinkedBlockingDeque<>();

    // 直接将任务添加到队列中
    @Override
    public void execute(Runnable task) {
      tasks.offer(task);
    }

    public SingleThreadPoolExecutor() {
      // 在构造函数中,直接创建一个线程,作为为线程池的唯一任务执行线程
      // 它将在被创建后立即执行,执行逻辑为:
      // 1. 从队列中获取任务
      // 2. 如果获取到任务,则执行它,执行完后,返回第1步
      // 3. 如果未获取到任务,则简短休息,继续第1步
      Thread taskRunner = new Thread(() -> {
            Runnable task;
            while (true) {
                task = tasks.poll();
                if (task != null) {
                  task.run();
                  continue;
                }
                try {
                  TimeUnit.MILLISECONDS.sleep(10);
                } catch (InterruptedException e) {
                  e.printStackTrace();
                  break;
                }
            }
      });
      taskRunner.start();
    }
}上述的单线程执行器实现中,执行使命的线程是永久不会停止的,获取到使命时,就执行它,没有获取到,就不停不停的获取。下面是这个执行器的测试代码:
public class SingleThreadPoolTest {    public static void main(String[] args) throws InterruptedException {      SingleThreadPoolExecutorstp stp= new SingleThreadPoolExecutor();      // 连续添加 5 个使命      for (int i = 1; isubmit(Runnable task);
页: [1]
查看完整版本: 线程池的运行逻辑与你想象的不一样,它是池族中的异类