线程池概念和实例

线程池的学习

线程池的概念 :线程池首先会创造一些线程,它们的集合称为线程池。线程池在系统开始的时候,西安创造一堆线程,系统将一个任务传给线程池,线程池会启动一对线程来执行这个任务,任务结束之后,该线程不会死亡,而是再次返回线程池中成为空闲状态,等待下一个任务。

常见的线程池

  • ExecutorService

  • BlockingQueue //堵塞队列

  • 自定义线程池ThreadPoolExecutor

阿里手册上说不准创建显示的线程既 Thread,只让用线程池来运行线程,其中只用 ThreadPoolExecutor

1.1. 为什么要用线程池?

池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

线程池提供了一种限制和管理资源(包括执行一个任务)的方式。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。

这里借用《Java 并发编程的艺术》提到的来说一下使用线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

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

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

工具类 Executors 可以实现将 Runnable 对象转换成 Callable 对象。(Executors.callable(Runnable task)Executors.callable(Runnable task, Object result))。

Runnable.java

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

Callable.java

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

1.3. 执行 execute()方法和 submit()方法的区别是什么呢?

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

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

public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}Copy to clipboardErrorCopied

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

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    return new FutureTask<T>(runnable, value);
}Copy to clipboardErrorCopied

我们再来看看execute()方法:

public void execute(Runnable command) {
  ...
}

1.4 如何创建线程池

阿里手册上说不准创建显示的线程既 Thread,只让用线程池来运行线程,其中只用 ThreadPoolExecutor。而且强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式

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

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

方式一:通过构造方法实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YMWlYwjQ-1647269255563)(G:\技术积累\线程池的学习.assets\ThreadPoolExecutor构造方法.png)]

方式二:通过 Executor 框架的工具类 Executors 来实现

我们可以创建三种类型的 ThreadPoolExecutor:

  • FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
  • SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
  • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

对应 Executors 工具类中的方法如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-whSO7ezd-1647269255564)(G:\技术积累\线程池的学习.assets\Executor框架的工具类.png)]

1.5 ThreadPoolExecutor 类分析

ThreadPoolExecutor 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。

/**
 * 用给定的初始参数创建一个新的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;
}Copy to clipboardErrorCopied

下面这些对创建 非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。

[1.5.1 ThreadPoolExecutor构造函数重要参数分析]

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize : 核心线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

ThreadPoolExecutor其他常见参数:

  1. keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
  2. unit : keepAliveTime 参数的时间单位。
  3. threadFactory :executor 创建新线程的时候会用到。
  4. handler :饱和策略。关于饱和策略下面单独介绍一下。
[4.5.2 ThreadPoolExecutor 饱和策略]

ThreadPoolExecutor 饱和策略定义:

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy 抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy 调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy 不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy 此策略将丢弃最早的未处理的任务请求。

举个例子: Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 ThreadPoolExecutor 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了)

ThreadPool实例

ThreadPoolExecutirDemo

package threadPool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo {

    private static final int CORE_POOL_SIZE = 5;//核心线程池
    private static final int MAX_POOL_SIZE = 10; //最大线程数10
    private static final int QUEUE_CAPACITY = 100; //队列容量
    private static final Long KEEP_ALIVE_TIME = 1L; //等待时间
    public static void main(String[] args) {

        //使用阿里巴巴推荐的创建线程池的方式
        //通过ThreadPoolExecutor构造函数自定义参数创建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
            //创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
            Runnable worker = new MyRunnable("" + i);
            //执行Runnable
            executor.execute(worker);
        }

        //终止线程池
        executor.shutdownNow();

        while (!executor.isTerminated()) {
        }
        System.out.println("Finished all threads");
    }
}

Runnable

package threadPool;


import java.util.Date;

/**
 * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
 * @author shuang.kou
 */
public class MyRunnable implements Runnable {

    private String command;

    public MyRunnable(String s) {
        this.command = s;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
        processCommand();
        System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
    }

    private void processCommand() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        return this.command;
    }
}

用线程的两种关闭

executor.shutdown(); //等所线程都运行完了再结束 

executor.shutdownnow();    //直接关闭线程          

executor.shutdown(); 输出

threadPool.ThreadPoolExecutorDemo
pool-1-thread-1 Start. Time = Mon Mar 14 22:06:54 CST 2022
pool-1-thread-2 Start. Time = Mon Mar 14 22:06:54 CST 2022
pool-1-thread-3 Start. Time = Mon Mar 14 22:06:54 CST 2022
pool-1-thread-5 Start. Time = Mon Mar 14 22:06:54 CST 2022
pool-1-thread-4 Start. Time = Mon Mar 14 22:06:54 CST 2022
pool-1-thread-1 End. Time = Mon Mar 14 22:06:59 CST 2022
pool-1-thread-1 Start. Time = Mon Mar 14 22:06:59 CST 2022
pool-1-thread-2 End. Time = Mon Mar 14 22:06:59 CST 2022
pool-1-thread-2 Start. Time = Mon Mar 14 22:06:59 CST 2022
pool-1-thread-5 End. Time = Mon Mar 14 22:06:59 CST 2022
pool-1-thread-4 End. Time = Mon Mar 14 22:06:59 CST 2022
pool-1-thread-5 Start. Time = Mon Mar 14 22:06:59 CST 2022
pool-1-thread-3 End. Time = Mon Mar 14 22:06:59 CST 2022
pool-1-thread-4 Start. Time = Mon Mar 14 22:06:59 CST 2022
pool-1-thread-3 Start. Time = Mon Mar 14 22:06:59 CST 2022
pool-1-thread-1 End. Time = Mon Mar 14 22:07:04 CST 2022
pool-1-thread-2 End. Time = Mon Mar 14 22:07:04 CST 2022
pool-1-thread-5 End. Time = Mon Mar 14 22:07:04 CST 2022
pool-1-thread-4 End. Time = Mon Mar 14 22:07:04 CST 2022
pool-1-thread-3 End. Time = Mon Mar 14 22:07:04 CST 2022
Finished all threads

Process finished with exit code 0

executor.shutdownnow();的输出情况,直接就关闭线程,并且爆出了sleep未执行完的错误

pool-1-thread-1 Start. Time = Mon Mar 14 22:15:55 CST 2022
pool-1-thread-1 End. Time = Mon Mar 14 22:15:55 CST 2022
pool-1-thread-5 Start. Time = Mon Mar 14 22:15:55 CST 2022
pool-1-thread-5 End. Time = Mon Mar 14 22:15:55 CST 2022
pool-1-thread-3 Start. Time = Mon Mar 14 22:15:55 CST 2022
pool-1-thread-4 Start. Time = Mon Mar 14 22:15:55 CST 2022
pool-1-thread-4 End. Time = Mon Mar 14 22:15:55 CST 2022
pool-1-thread-2 Start. Time = Mon Mar 14 22:15:55 CST 2022
pool-1-thread-2 End. Time = Mon Mar 14 22:15:55 CST 2022
pool-1-thread-3 End. Time = Mon Mar 14 22:15:55 CST 2022
Finished all threads
java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at threadPool.MyRunnable.processCommand(MyRunnable.java:27)
	at threadPool.MyRunnable.run(MyRunnable.java:21)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:834)
java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at threadPool.MyRunnable.processCommand(MyRunnable.java:27)
	at threadPool.MyRunnable.run(MyRunnable.java:21)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:834)
java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at threadPool.MyRunnable.processCommand(MyRunnable.java:27)
	at threadPool.MyRunnable.run(MyRunnable.java:21)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:834)
java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at threadPool.MyRunnable.processCommand(MyRunnable.java:27)
	at threadPool.MyRunnable.run(MyRunnable.java:21)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:834)
java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at threadPool.MyRunnable.processCommand(MyRunnable.java:27)
	at threadPool.MyRunnable.run(MyRunnable.java:21)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:834)

Process finished with exit code 0

遇到的问题:

使用 ScheduledExecutorService 线程池的 shutdownnow 不是真正的关闭

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值