详解线程池的使用

news2024/11/16 18:06:35

一、线程池基础

1.1、线程池的思想

image-20240805163241722

我们首先理解什么是池化技术:

池化技术指的是提前准备一些资源,在需要时可以重复使用这些预先准备的资源。
池化技术的优点主要有两个:提前准备和重复利用。

下面是池化技术常见的应用场景:

  • 连接池
    连接池是池化技术的一个典型应用场景。数据库连接池、线程池和连接池等资源能够帮助系统实现可伸缩性和高并发,提高系统的吞吐量。
  • 对象池
    在面向对象的编程中,对象池可以有效降低对象的创建和销毁开销,提高系统的性能。例如,内存池将一块连续的内存分割成多个固定大小的对象块,通过对这些对象进行复用,减少了频繁的内存分配与释放。
  • 线程池
    线程池是一个管理线程的池。通过维护一组可用线程,线程池可以高效地执行并发任务,减少创建和销毁线程的开销,提高系统对并发请求的响应能力。
  • 缓存池
    缓存池是将常用的计算结果、数据或资源存储在内存中,以加快对这些数据的访问速度。通过缓存池,系统可以减少对慢速存储介质的访问,提高系统的响应速度和性能。

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,线程也属于宝贵的系统资源。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

在Java中可以通过线程池来达到这样的效果。今天我们就来详细讲解一下Java的线程池。

1.2、什么是线程池

线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象;

我们其实可以这样理解:

**线程池:**其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多赘述。我们通过一张图来了解线程池的工作原理:

img

1.3、为什么使用线程池

使用线程池最大的原因就是可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行运行压力;当然了,使用线程池的原因不仅仅只有这些,我们可以从线程池自身的优点上来进一步了解线程池的好处;

1.4、使用线程池有哪些优势

线程池是一种用于管理和复用线程的机制,它提供了一种执行大量异步任务的方式,并且可以在多个任务之间合理地分配和管理系统资源。
线程池的主要优点包括:

  • 改善了资源利用率,降低了线程创建和销毁的开销。
  • 提高了系统响应速度,因为线程池已经预先创建好了一些线程,可以更加快速地分配资源以响应用户请求。
  • 提高了代码可读性和可维护性,因为线程池将线程管理和任务执行进行了分离,可以更加方便地对其进行调整和优化。
  • 可以设置线程数目上限,避免了缺乏控制的线程创建造成的系统无法承受的负载压力。

1.5 、线程池应用场景

只要有并发的地方、任务数量大或小、每个任务执行时间长或短的都可以使用线程池;只不过在使用线程池的时候,注意一下设置合理的线程池大小即可;

二、线程池的基本使用

2.1、线程池的总体设计

Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,ExecutorService,ThreadPoolExecutor这几个类。

img

Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

image-20240805175519199

2.2、 Executors工具类快速创建线程池

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线池。

image-20240805180038895

package com.thread.threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorsDemo {

    public static void main(String[] args) {
        // 创建单一线程的连接池
        // ExecutorService threadPool = Executors.newSingleThreadExecutor();
        // ExecutorService threadPool = Executors.newFixedThreadPool(3);
        ExecutorService threadPool = Executors.newCachedThreadPool();

        try {
            for (int i = 0; i < 5; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName() + "执行了业务逻辑");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

2.3、Executors原理

上述案例中的三个方法的本质都是ThreadPoolExecutor的实例化对象,只是具体参数值不同。

image-20240805180615724

image-20240805180647224

image-20240805180706876

2.4、Executors创建线程池种类

在Java中,常见的线程池类型主要有四种,都是通过工具类Excutors创建出来的。

image-20240805190042828

2.4.1、newFixedThreadPool

创建使用固定线程数的线程池

image-20230505221959259

  • 核心线程数与最大线程数一样,没有救急线程

  • 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE

  • 适用场景:适用于任务量已知,相对耗时的任务

  • 案例:

package com.thread.threadpool.typesofthreadpools;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolCase {

    static class FixedThreadDemo implements Runnable{
        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            for (int i = 0; i < 2; i++) {
                System.out.println(name + ":" + i);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 创建一个固定大小的线程池,核心线程数和最大线程数都是3
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 5; i++) {
            executorService.submit(new FixedThreadDemo());
            Thread.sleep(10);
        }

        executorService.shutdown();
    }

}

2.4.2、newSingleThreadExecutor

单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO)执行

image-20230505222050294

  • 核心线程数和最大线程数都是1

  • 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE

  • 适用场景:适用于按照顺序执行的任务

  • 案例:

package com.thread.threadpool.typesofthreadpools;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NewSingleThreadCase {

    static int count = 0;

    static class Demo implements Runnable {
        @Override
        public void run() {
            count++;
            System.out.println(Thread.currentThread().getName() + ":" + count);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 单个线程池,核心线程数和最大线程数都是1
        ExecutorService exec = Executors.newSingleThreadExecutor();

        for (int i = 0; i < 10; i++) {
            exec.execute(new Demo());
            Thread.sleep(5);
        }
        exec.shutdown();
    }

}

2.4.2、newCachedThreadPool

可缓存线程池:

image-20230505222126391

  • 核心线程数为0

  • 最大线程数是Integer.MAX_VALUE

  • 阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。

  • 适用场景:适合任务数比较密集,但每个任务执行时间较短的情况

  • 案例:

package com.thread.threadpool.typesofthreadpools;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPoolCase {

    static class Demo implements Runnable {
        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            try {
                // 修改睡眠时间,模拟线程执行需要花费的时间
                Thread.sleep(100);

                System.out.println(name + "执行完了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 创建一个缓存的线程,没有核心线程数,最大线程数为Integer.MAX_VALUE
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            exec.execute(new Demo());
            Thread.sleep(1);
        }
        exec.shutdown();
    }

}

2.4.3、newScheduledThreadPool

提供了“延迟”和“周期执行”功能

image-20240805191314920

  • 适用场景:有定时和延迟执行的任务

  • 案例:

package com.thread.threadpool.typesofthreadpools;

import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPoolCase {

    static class Task implements Runnable {
        @Override
        public void run() {
            try {
                String name = Thread.currentThread().getName();

                System.out.println(name + ", 开始:" + new Date());
                Thread.sleep(1000);
                System.out.println(name + ", 结束:" + new Date());

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 按照周期执行的线程池,核心线程数为2,最大线程数为Integer.MAX_VALUE
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
        System.out.println("程序开始:" + new Date());

        /**
         * schedule 提交任务到线程池中
         * 第一个参数:提交的任务
         * 第二个参数:任务执行的延迟时间
         * 第三个参数:时间单位
         */
        scheduledThreadPool.schedule(new Task(), 0, TimeUnit.SECONDS);
        scheduledThreadPool.schedule(new Task(), 1, TimeUnit.SECONDS);
        scheduledThreadPool.schedule(new Task(), 5, TimeUnit.SECONDS);

        Thread.sleep(5000);

        // 关闭线程池
        scheduledThreadPool.shutdown();

    }

}

2.5、为什么不建议用Executors创建线程池

参考阿里开发手册《Java开发手册-嵩山版》

image-20240805181015752

三、ThreadPoolExecutor类

线程池实现类 ThreadPoolExecutorExecutor 框架最核心的类。

3.1、ThreadPoolExecutor基本使用

package com.thread.threadpool;

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.RejectedExecutionHandler;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建线程工厂
        ThreadFactory threadFactory = Executors.defaultThreadFactory();

        // 拒绝策略
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();

        // 创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                5,                      // corePoolSize: 核心线程数
                10,                     // maximumPoolSize: 最大线程数
                60,                     // keepAliveTime: 非核心线程存活时间
                TimeUnit.SECONDS,       // unit: 非核心线程存活时间单位
                new LinkedBlockingQueue<>(100),  // workQueue: 等待队列
                threadFactory,          // threadFactory: 创建线程使用工厂
                handler                 // handler: 饱和拒绝策略
        );

        // 提交任务给线程池
        for (int i = 0; i < 20; i++) {
            executor.submit(new Task());
        }

        // 关闭线程池
        executor.shutdown();
    }

    //向线程池提交了 20 个任务,执行完后关闭线程池。每个任务会在执行时打印当前线程的名称,并睡眠 2 秒。
    static class Task implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " is executing task.");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

image-20240805170400234

3.2、线程池的核心参数详解

image-20240805170616652

public ThreadPoolExecutor(int corePoolSize, //核心线程数量
                              int maximumPoolSize,//     最大线程数
                              long keepAliveTime, //       最大空闲时间
                              TimeUnit unit,         //        时间单位
                              BlockingQueue<Runnable> workQueue,   //   任务队列
                              ThreadFactory threadFactory,    // 线程工厂
                              RejectedExecutionHandler handler  //  饱和处理机制
	) 
{ ... }

我们可以通过下面的场景理解ThreadPoolExecutor中的各个参数:

  • a客户(任务)去银行(线程池)办理业务,但银行刚开始营业,窗口服务员还未就位(相当于线程池中初始线程数量为0),于是经理(线程池管理者)就安排1号工作人员(创建1号线程执行任务)接待a客户(创建线程);
  • 在a客户业务还没办完时,b客户(任务)又来了,于是经理(线程池管理者)就安排2号工作人员(创建2号线程执行任务)接待b客户(又创建了一个新的线程);假设该银行总共就2个窗口(核心线程数量是2);
  • 紧接着在a,b客户都没有结束的情况下c客户来了,于是经理(线程池管理者)就安排c客户先坐到银行大厅的座位上(空位相当于是任务队列)等候,并告知他: 如果1、2号工作人员空出,c就可以前去办理业务;
  • 此时d客户又到了银行,(工作人员都在忙,大厅座位也满了)于是经理赶紧安排临时工(新创建的线程)在大堂站着,手持pad设备给d客户办理业务;
  • 假如前面的业务都没有结束的时候e客户又来了,此时正式工作人员都上了,临时工也上了,座位也满了(临时工加正式员工的总数量就是最大线程数),
  • 于是经理只能按《超出银行最大接待能力处理办法》(饱和处理机制)拒接接待e客户;
  • 最后,进来办业务的人少了,大厅的临时工空闲时间也超过了1个小时(最大空闲时间),经理就会让这部分空闲的员工人下班.(销毁线程)
  • 但是为了保证银行银行正常工作(有一个allowCoreThreadTimeout变量控制是否允许销毁核心线程,默认false),即使正式工闲着,也不得提前下班,所以1、2号工作人员继续待着(池内保持核心线程数量);

image-20240805171032242

  1. corePoolSize
    此值是用来初始化线程池中核心线程数,当线程池中线程数< corePoolSize时,系统默认是添加一个任务才创建一个线程池。当线程数 = corePoolSize时,新任务会追加到workQueue中。

  2. maximumPoolSize

    表示允许的最大线程数 = (非核心线程数+核心线程数),当BlockingQueue也满了,但线程池中总线程数 < maximumPoolSize时候就会再次创建新的线程。

  3. keepAliveTime
    非核心线程 =(maximumPoolSize - corePoolSize ) ,非核心线程闲置下来不干活最多存活时间。

  4. 线程池中非核心线程保持存活的时间的单位

    • TimeUnit.DAYS;天
    • TimeUnit.HOURS;小时
    • TimeUnit.MINUTES;分钟
    • TimeUnit.SECONDS;秒
    • TimeUnit.MILLISECONDS; 毫秒
    • TimeUnit.MICROSECONDS; 微秒
    • TimeUnit.NANOSECONDS; 纳秒
  5. workQueue

    等待队列是存放提交但尚未执行的任务的队列。线程池的工作流程如下:

    1. 当一个新的任务提交到线程池时,如果当前线程池中运行的线程数小于核心线程数(corePoolSize),则会创建一个新的线程来执行任务。

    2. 如果当前线程池中运行的线程数已经达到核心线程数,但小于最大线程数(maximumPoolSize),则任务会被放入等待队列中。

    3. 当核心线程都在忙碌,且等待队列已满时,如果线程数小于最大线程数,线程池会尝试创建新的线程来执行任务。

    4. 如果线程数已达到最大线程数,并且等待队列也满了,则根据拒绝策略处理这个新任务。

  6. threadFactory
    创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等。

  7. handler
    corePoolSize、workQueue、maximumPoolSize都不可用的时候执行的饱和策略。

总结:

  • corePoolSize 核心线程数目

  • maximumPoolSize 最大线程数目 = (核心线程+救急线程的最大数目)

  • keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放

  • unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等

  • workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务

  • threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等

  • handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略

3.3、线程池的工作流程

image-20240805172217009

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    • 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    • 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    • 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    • 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会根据拒绝策略来对应处理。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

3.4、线程池中任务队列

栈与队列简单回顾:

栈:先进后出,后进先出

队列:先进先出

java.util.concurrent 包里的 BlockingQueue是一个接口,继承Queue接口,Queue接口继承 Collection。

image-20240805173228971

image-20240805174209473

  • 有界队列(ArrayBlockingQueue):是一个用数组实现的有界阻塞队列,按FIFO(先进先出队列)排序。
  • 无界队列(LinkedBlockingQueue):是基于链表结构的阻塞队列,按FIFO排序,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,因此在任务数量很大且任务执行时间较长时,无界队列可以保证任务不会被丢弃,但同时也会导致线程池中线程数量不断增加,可能会造成内存溢出等问题。
  • 延迟队列(DelayQueue):是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。
  • 优先级队列(PriorityBlockingQueue):是具有优先级的无界阻塞队列。与无界队列类似,优先级队列可以保证所有任务都会被执行,但不同的是优先级队列可以对任务进行管理和排序,确保高优先级的任务优先执行。
  • 同步队列(SynchronousQueue):是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于无界队列。

用的最多是ArrayBlockingQueue和LinkedBlockingQueue:

LinkedBlockingQueueArrayBlockingQueue
默认无界,支持有界强制有界
底层是链表底层是数组
是懒惰的,创建节点的时候添加数据提前初始化 Node 数组
入队会生成新 NodeNode需要是提前创建好的
两把锁(头尾)一把锁

左边是LinkedBlockingQueue加锁的方式,右边是ArrayBlockingQueue加锁的方式

  • LinkedBlockingQueue读和写各有一把锁,性能相对较好
  • ArrayBlockingQueue只有一把锁,读和写公用,性能相对于LinkedBlockingQueue差一些

image-20240805174450522

3.5、线程池的拒绝策略

一般我们创建线程池时,为防止资源被耗尽,任务队列都会选择创建有界任务队列,但这种模式下如果出现任务队列已满且线程池创建的线程数达到你设置的最大线程数时,这时就需要你指定ThreadPoolExecutor的RejectedExecutionHandler参数即合理的拒绝策略,来处理线程池"超载"的情况。

ThreadPoolExecutor自带的拒绝策略如下:

  1. AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
  2. CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
  3. DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中 尝试再次提交当前任务。
  4. DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。 如果允许任务丢失,这是最好的一种策略。

以上内置的策略均实现了RejectedExecutionHandler接口,也可以自己扩展RejectedExecutionHandler接口,定义自己的拒绝策略

public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 自定义的拒绝策略处理逻辑
    }
}

3.6、线程池提交execute和submit有什么区别?

在Java中,线程池中一般有两种方法来提交任务:execute() 和 submit()

  1. execute() 用于提交不需要返回值的任务
  2. submit() 用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个 future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值
package com.thread.threadpool;

import java.util.concurrent.*;

public class SubmitExample {
    public static void main(String[] args) {
        // 创建一个包含5个线程的线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);

        // 使用execute()提交不需要返回值的任务
        executor.execute(new RunnableTask());

        // 使用submit()提交需要返回值的任务
        Future<Integer> future = executor.submit(new CallableTask());

        try {
            // 从Future对象中获取返回值
            Integer result = future.get();
            System.out.println("Callable task result: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        // 关闭线程池
        executor.shutdown();
    }

    // Runnable任务,不需要返回值
    static class RunnableTask implements Runnable {
        @Override
        public void run() {
            System.out.println("Runnable task is running.");
        }
    }

    // Callable任务,需要返回值
    static class CallableTask implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("Callable task is running.");
            return 42; // 返回一个计算结果
        }
    }
}

3.7、优雅的关闭线程池

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

shutdown:将线程池状态置为shutdown,并不会立即停止:

  1. 停止接收外部submit的任务
  2. 内部正在跑的任务和队列里等待的任务,会执行完
  3. 等到第二步完成后,才真正停止

shutdownNow:将线程池状态置为stop。一般会立即停止,事实上不一定:

  1. 和shutdown()一样,先停止接收外部提交的任务
  2. 忽略队列里等待的任务
  3. 尝试将正在跑的任务interrupt中断
  4. 返回未执行的任务列表

shutdown 和shutdownnow区别如下:

  • shutdownNow:能立即停止线程池,正在跑的和正在等待的任务都停下了。这样做立即生效,但是风险也比较大。
  • shutdown:只是关闭了提交通道,用submit()是无效的;而内部的任务该怎么跑还是怎么跑,跑完再彻底停止线程池。
package com.thread.threadpool;

import java.util.List;
import java.util.concurrent.*;

public class ThreadPoolShutdownExample {
    public static void main(String[] args) {
        // 创建一个包含5个线程的线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);

        // 提交一些任务到线程池
        for (int i = 0; i < 10; i++) {
            executor.submit(new Task());
        }

        // 使用shutdown方法关闭线程池
        System.out.println("尝试使用shutdown方法关闭线程池...");
        executor.shutdown();
        try {
            if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
                System.out.println("线程池未能在指定时间内终止。");
                List<Runnable> droppedTasks = executor.shutdownNow();
                System.out.println("线程池被立即关闭。有 " + droppedTasks.size() + " 个任务未执行。");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 重新创建一个线程池
        executor = Executors.newFixedThreadPool(5);

        // 提交一些任务到线程池
        for (int i = 0; i < 10; i++) {
            executor.submit(new Task());
        }

        // 使用shutdownNow方法关闭线程池
        System.out.println("尝试使用shutdownNow方法关闭线程池...");
        List<Runnable> notExecutedTasks = executor.shutdownNow();
        System.out.println(notExecutedTasks.size() + " 个任务未执行。");

        try {
            if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
                System.out.println("线程池未能在指定时间内终止。");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("线程池已关闭。");
    }

    static class Task implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " 正在执行任务。");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " 被中断。");
            }
        }
    }
}

image-20240805185503332

3.8、线程池异常处理

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

常见的异常处理方式:

image-20240805193737713

package com.threadpool;

import java.util.List;
import java.util.concurrent.*;

public class ThreadPoolExceptionHandling {
    public static void main(String[] args) {
        // 使用自定义的ThreadFactory设置UncaughtExceptionHandler
        ThreadFactory threadFactory = new ThreadFactory() {
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                    public void uncaughtException(Thread t, Throwable e) {
                        System.out.println(t.getName() + " 捕获到未检测的异常: " + e.getMessage());
                    }
                });
                return t;
            }
        };

        // 创建自定义ThreadPoolExecutor
        ExecutorService executor = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(), threadFactory) {
            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                super.afterExecute(r, t);
                if (t != null) {
                    System.out.println("任务执行时抛出的异常: " + t.getMessage());
                }
                if (r instanceof Future<?>) {
                    try {
                        ((Future<?>) r).get();
                    } catch (CancellationException ce) {
                        System.out.println("任务被取消: " + ce.getMessage());
                    } catch (InterruptedException ie) {
                        System.out.println("任务被中断: " + ie.getMessage());
                    } catch (ExecutionException ee) {
                        System.out.println("任务执行时抛出的异常: " + ee.getCause().getMessage());
                    }
                }
            }
        };

        // 提交Runnable任务使用try-catch捕获异常
        executor.execute(new Runnable() {
            public void run() {
                try {
                    throw new RuntimeException("Runnable任务异常");
                } catch (RuntimeException e) {
                    System.out.println("Runnable任务捕获到异常: " + e.getMessage());
                }
            }
        });

        // 提交Callable任务,使用Future.get()接受异常
        Future<Integer> future = executor.submit(new Callable<Integer>() {
            public Integer call() throws Exception {
                throw new Exception("Callable任务异常");
            }
        });

        try {
            future.get();
        } catch (InterruptedException | ExecutionException e) {
            System.out.println("Callable任务捕获到异常: " + e.getCause().getMessage());
        }

        // 关闭线程池
        executor.shutdown();
        try {
            if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
                System.out.println("线程池未能在指定时间内终止。");
                List<Runnable> droppedTasks = executor.shutdownNow();
                System.out.println("线程池被立即关闭。有 " + droppedTasks.size() + " 个任务未执行。");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3.9、线程池的状态

线程池有这几个状态:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED

 //线程池状态
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

线程池各个状态切换图:

img

RUNNING

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

SHUTDOWN

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

STOP

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

TIDYING

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

TERMINATED

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

3.10、线程池的参数如何设置

首先线程池中有7个参数,其中最重要的 是核心线程数和最大线程池数,当然这些参数没有固定的一个数值, 不同业务场景线程任务肯定参数不一样,可以根据你的业务类型来针对性回答:

如果任务主要是IO密集型的, 比如存在大量读写操作 像(大量文件读写、 大量网络请求, 频繁的数据库访问等操作)
那核心线程数就可以设置为CPU核心数的两倍左右 ,因为涉及的 I/O 操作通常会导致 线程大部分的时间会处于阻塞状态,所以线程数就应当设置稍大一些。
那 最大线程数可以设置为CPU核心数的四倍左右以便在出现大量并发 I/O 操作时能够有足够的线程来处理。

如果任务是CPU密集型的, 像 一些计算操作, 通常就需要大量的计算资源,因为计算操作会长时间占用 CPU 资源,线程过多会导致cpu占用高,所以参数不宜设置过大 可以将:

核心线程数设置为 CPU 核心数+1,以充分利用 CPU 资源。
最大线程数可以设置为CPU核心数的2倍左右,因为 CPU 密集型任务不会涉及 I/O 等待。

当然,以上是一些经验数值,实际情况我们需要根据服务器的资源占用情况,结合运行时的 cpu、内存等指标进行调整, 还要结合压测工具得到一个比较合理的范围。

总结:

在设置核心线程数之前,需要先熟悉一些执行线程池执行任务的类型

  • IO密集型任务

一般来说:文件读写、DB读写、网络请求等

推荐:核心线程数大小设置为2N+1 (N为计算机的CPU核数)

  • CPU密集型任务

一般来说:计算型代码、Bitmap转换、Gson转换等

推荐:核心线程数大小设置为N+1 (N为计算机的CPU核数)

java代码查看CPU核数

image-20240805195719237

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1984030.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot+Redis 发布与订阅

两个应用都引入 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artif…

机器学习入门之无监督学习(聚类、降维(主成分分析)、关联度分析(Apriori 算法))

无监督学习是机器学习的一种类型&#xff0c;它处理没有显式标签的数据。与监督学习不同&#xff0c;无监督学习的目标是探索数据的内在结构和模式&#xff0c;而不是从标注数据中学习一个特定的映射到预定标签的函数。这使得无监督学习特别适用于探索性数据分析、找寻隐藏模式…

关于加载水印PDF、图片以及压缩包格式文件【把博客当工作记录】

写这篇文章的目的是让大家都可以学到东西,核心代码中列出了处理思维和调用方法,业务代码已经过滤掉了,希望大家不要做crud程序员!!要思考。。。该博客不懂时可联系下方。 1、流程图如下 2、策略描述 实现方式: 设计模式:父策略调动子策略 业务理念:在不影响原有业务…

【电脑基础硬件】从零开始认识主板

从零开始认识主板 一、主板及其元器件二、主板上的各个部件通信三、主板的选择 注意&#xff1a; 以下以Z790电脑主板为例进行说明。 一、主板及其元器件 IO接口&#xff1a; CPU模块 CPU是电脑的大脑&#xff0c;控制并指挥着电脑的各个模块协同工作。CPU通过主板上的插槽固定…

电路中电阻,电容和电感作用总结

电阻作用 1&#xff0c;上拉电阻 电阻的连接一般是一端接上拉的电源&#xff08;一般与芯片信号的电压值相匹配&#xff09;&#xff0c;另一端连接芯片引脚所对应的信号大概如下图 功能&#xff1a;一、预置某些引脚的功能&#xff0c;例如复位信号拉高&#xff08;失能&…

疯传!平台培养“斗士”的背后,是 AI 智能名片与 O2O 商城小程序在搞鬼

摘要&#xff1a;本文探讨了在搭建用户聚集平台以筛选和培养“斗士”的过程中&#xff0c;如何融入 AI 智能名片和 O2O 商城小程序。详细阐述了贴吧或论坛作为初始平台的优势&#xff0c;以及如何通过适当的管理和引导活动筛选出“储备斗士”。同时&#xff0c;分析了 AI 智能名…

浅谈 Mybatis 框架

文章目录 一、什么是MyBatis?1.2、JDBC 二、使用Mybatis2.1、配置MyBatis开发环境2.1.1、配置连接字符串2.1.2、配置MyBatis中的XML路径 2.2、使用MyBatis模式和语法操作数据库 三、使用 Mybatis 进行增删改查操作的要点3.1、ResultMap的用法 四、Mybatis操作难点4.1、#{ } 和…

Modbus-TCP详解

目录 Modbus-TCP Modbus TCP协议的帧结构如下&#xff1a; 实现Modbus-Tcp 使用EasyModbusTCP第三方实现Modbus-Tcp EasyModbusTCP的使用 设置IP 设置端口 设置设备地址 开启通讯 读取寄存器 写入寄存器 实例 Modbus-TCP Modbus TCP是Modbus协议的一种变体&#xff0c;它使用T…

Java多线程的单例设计模式 多种实现方法

目录 前言 饿汉式 懒汉式 Double_check volatile double_check Holder方式 枚举 前言 单例设计模式GOF23中设计模式中最常用的设计模式之一, 单例设计模式提供了多线程环境下的保证唯一实例性的解决方案, 虽然简单, 但是实现单例模式的方式多种多样, 因此需要从多个维度去…

【云原生】恰当运用kubernetes中三种探针,确保应用程序在Kubernetes集群中保持健康、可用和可靠

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

Unity热更——ILRuntime安装接入

一、ILRuntime相关地址 1、官网文档地址 2、GitHub上开源的ILRuntime项目工程 3、GitHub上的官方ILRuntime-Unity实例工程 4、官方视频教程-Unity中文课堂&#xff08;需付费&#xff09; 5、ILRuntime入门笔记-赵青青-博客园 6、ILRuntime 的学习-简书 二、ILRuntime…

Python | Leetcode Python题解之第324题摆动排序II

题目&#xff1a; 题解&#xff1a; def quickSelect(a: List[int], k: int) -> int:seed(datetime.datetime.now())shuffle(a)l, r 0, len(a) - 1while l < r:pivot a[l]i, j l, r 1while True:i 1while i < r and a[i] < pivot:i 1j - 1while j > l an…

自动生成数据:Navicat 16 让数据测试更高效

文章目录 前言一、Navicat是什么&#xff1f;二、Navicat 16 新功能1. 自动生成数据2. 改进的用户界面3. 云同步 三、 安装指南Windows 版安装macOS 版安装Linux 版安装 四、使用示例&#xff1a;自动生成数据1. 创建连接2. 选择表3. 打开数据生成器4. 设置数据规则5. 生成数据…

top命令实时监测Linux进程

top命令可以动态实时显示Linux进程信息&#xff0c;方便观察频繁换进换出的内存的进程变化。 top命令执行示例如下&#xff1a; 其中&#xff0c;第一行表示系统当前时间、系统的运行时间、登录的用户数目、系统的平均负载&#xff08;最近1分钟&#xff0c;最近5分钟&#xff…

springboot艺体培训机构业务管理系统--论文源码调试讲解

第2章 开发环境与技术 开发艺体培训机构业务管理系统需要搭建编程的环境&#xff0c;也需要通过调查&#xff0c;对各个相关技术进行分析&#xff0c;选取适合本系统开发的技术与工具。 2.1 MySQL数据库 MySQL是一种具有安全系数、安全系数、混合开发性、高效化等特征的轻量…

基于Raft算法的分布式KV数据库:五、剩余部分

github地址&#xff1a;https://github.com/1412771048/Raft CPPRaft系列-剩余部分 首先我们看下第五章的架构图&#xff0c;图中的主要部分我们在前几张讲解完毕了&#xff0c;目前还剩下clerk和k-v数据库&#xff0c;而本篇的重点在于补全版图&#xff0c;完成&#xff1a;…

SQL注入sqli-labs-master关卡一

本文环境搭建使用的是小皮&#xff0c;靶机压缩包&#xff1a;通过百度网盘分享的文件&#xff1a;sqli-labs-php7-master.zip 链接&#xff1a;https://pan.baidu.com/s/1xBfsi2lyrA1QgUWycRsHeQ?pwdqwer 提取码&#xff1a;qwer 下载解压至phpstudy的WWW目录下即可。 第一…

关于Redis的面试题

一、为什么要使用Redis 内存数据库,速度很快工作单线程worker,串行化,原子操作,IO线程是多线程的。避免上下文切换使用 IO模型,天生支撑高并发kv模型,v具有类型结构具有本地方法,计算数据移动二进制安全,value值最大为512MB二、Redis是多线程还是单线程 Redis在6.0版本…

(免费领源码)java#SSM#MYSQL私家车位共享APP 51842-计算机毕业设计项目选题推荐

目 录 摘要 1 绪论 1.1 课题的研究背景 1.2研究内容与研究目标 1.3ssm框架 1.4论文结构与章节安排 2 2 私家车位共享APP系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1 数据增加流程 2.2.2 数据修改流程 2.2.3数据删除流程 2.3 系统功能分析 2.3.1功能性分析 2…

计算机网络(TCP报文段首部格式中序号和确认号)

TCP首部格式中的序号和确认号并不总是同时出现。 TCP首部的序号和确认号是根据TCP通信的不同阶段和目的来决定的。在建立连接的过程中&#xff0c;序号用于标识发送数据的起始位置&#xff0c;而确认号用于表示接收方期望接收的下一个数据的起始位置。这两个字段在TCP通信的不同…