线程池(通俗易懂)

概述

线程池(ThreadPool)是一种用于管理和复用线程的机制,它能够在应用程序中创建一组预先初始化的线程,这些线程可用于执行任务,从而避免了不断创建和销毁线程的开销。Java中的线程池可以通过java.util.concurrent包中的ExecutorService接口及其实现类来实现。

  1. 线程池的作用:
    ○ 提高性能:线程池能够降低线程创建和销毁的开销,提高线程的重用率,从而提高应用程序的性能。
    ○ 控制资源消耗:线程池能够限制同时运行的线程数量,避免因为线程过多导致系统资源不足,从而提高系统的稳定性。
    ○ 提高响应速度:线程池能够减少任务的等待时间,提高任务的响应速度,从而提升用户体验。
  2. 线程池的组成:
    ○ 任务队列(Task Queue):用于存储等待执行的任务。线程池中的线程会从任务队列中取出任务并执行。
    ○ 线程池管理器(ThreadPool Manager):负责线程池的创建、销毁和管理,包括线程的增减、线程池参数的调整等。
    ○ 工作线程(Worker Threads):线程池中的线程,用于执行任务。
  3. 常见的线程池类型:
    ○ FixedThreadPool(固定大小线程池):线程数量固定,任务提交后立即执行,如果所有线程都在工作,新任务会进入任务队列等待。
    ○ CachedThreadPool(缓存线程池):线程数量不固定,可以根据任务的数量动态调整线程的数量。如果线程池中有空闲的线程,则复用空闲线程;否则会创建新的线程。
    ○ ScheduledThreadPool(定时任务线程池):用于执行定时任务或周期性任务,可以设定任务的执行时间和执行周期。
    ○ SingleThreadExecutor(单线程线程池):只有一个工作线程的线程池,保证任务按照提交的顺序依次执行。
  4. 线程池参数:
    ○ 核心线程数(Core Pool Size):线程池中保持存活的线程数量,即使这些线程是空闲的。当任务数量超过核心线程数时,线程池会根据情况动态调整线程数量。
    ○ 最大线程数(Maximum Pool Size):线程池允许的最大线程数量,超过这个数量的任务会被放入任务队列中排队等待执行。
    ○ 任务队列容量(Task Queue Capacity):用于存储等待执行的任务的容量,当任务队列已满时,新提交的任务可能会被拒绝执行。
    ○ 空闲线程存活时间(Keep Alive Time):当线程池中的线程数量超过核心线程数时,空闲线程在多长时间内没有被使用后会被销毁。
    通过合理配置线程池的参数和选择合适的线程池类型,可以有效地管理和优化线程的使用,从而提高应用程序的性能和可靠性。
    线程池和数据库连接池非常类似,可以统一管理和维护线程,减少没有必要的开销。
    在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。
    线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。
    那么,我们应该如何创建一个线程池那?Java中已经提供了创建线程池的一个类:Executor
    而我们创建时,一般使用它的子类:ThreadPoolExecutor
    这些线程池,都是通过JDK中的工具类Executors来构建的,线程池内部的最终实现类是一个叫做
    ThreadPoolExecutor。

为什么要使用线程池和 线程池优点

背景:经常创建和销毁、使用特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处: 提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止。
因为频繁的开启线程或者停止线程,线程需要从新被 cpu 从就绪到运行状态调度,需要发生
cpu 的上下文切换,效率非常低。
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后再线程创建后启动这些任务如果线程的数量超过最大数量,超过数量的线程将排队等候,等其他线程执行完毕,再从队列中取出任务来执行
特点:线程复用,控制最大并发数,管理线程
核心点:复用机制 提前创建好固定的线程一直在运行状态 实现复用 限制线程创建数量。
1.降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
2.提高响应速度:任务到达时,无需等待线程创建即可立即执行。
3.提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因 为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分 配、调优和监控。
4.提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比 如延时定时线程池 ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行

线程池的七大参数

  1. corePoolSize:核心线程数,线程池正常情况下保持的线程数,大户人家“长工”的数量。
  2. maximumPoolSize:最大线程数,当线程池繁忙时最多可以拥有的线程数,大户人家“长工”+“短工”的总数量。
  3. keepAliveTime:空闲线程存活时间,没有活之后“短工”可以生存的最大时间。
  4. TimeUnit:时间单位,配合参数 3 一起使用,用于描述参数 3 的时间单位。
  5. BlockingQueue:线程池的任务队列,用于保存线程池待执行任务的容器。
  6. ThreadFactory:线程工厂,用于创建线程池中线程的工厂方法,通过它可以设置线程的命名规则、优先级和线程类型。
  7. RejectedExecutionHandler(handler):拒绝策略,当任务量超过线程池可以保存的最大任务数时,执行的策略。

四大拒绝策略

线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也
塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
AbortPolicy:为线程池默认的拒绝策略,该策略直接抛异常处理。
DiscardPolicy:直接抛弃不处理。 忽视,什么都不会发生
DiscardOldestPolicy:丢弃队列中最老的任务。 从队列中踢出最先进入队列(最后一个执行)的任务
CallerRunsPolicy:将任务分配给当前执行execute方法线程来处理 执行任务
5.实现 RejectedExecutionHandler 接口,可自定义处理器

如果你提交任务时,线程池队列已满,这时会发生什么

1.如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务
2.如 果 使 用 的 是 有 界 队 列 比 如 ArrayBlockingQueue , 任 务 首 先 会 被 添 加 到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据 maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒,绝策略 RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy

给用户发消息任务超出队列,你用哪个拒绝策略?有其他方法吗

ThreadPoolExecutor.CallerRunsPolicy
无界队列(LinkedBlockingQuene),继续添加任务到阻塞队列中等待执行。
用消息队列存任务数据,线程池慢慢处理。
线程池最大值如何设置
private int cupThreadCount = Runtime.getRuntime().availableProcessors() * 2;
自定义线程池就需要我们自己配置最大线程数 maximumPoolSize,为了高效的并发运行,当 然这个不能随便设置。这时需要看我们的业务是 IO 密集型还是 CPU 密集型。
CPU 密集型 ((CPU 核数+1))
CPU 密集的意思是该任务需要大量的运算,而没有阻塞,CPU 一直全速运行。CPU 密集任 务只有在真正的多核 CPU 上才可能得到加速(通过多线程),而在单核 CPU 上,无论你开几 个模拟的多线程该任务都不可能得到加速,因为 CPU 总的运算能力就那些。 CPU 密集型任务配置尽可能少的线程数量:以保证每个 CPU 高效的运行一个线程。 一般公式:(CPU 核数+1)个 线程的线程池
IO 密集型 (CPU 核数 * 2 )
I0 密集型,即该任务需要大量的 IO,即大量的阻塞。在单线程上运行 I0 密集型的任务会导
致浪费大量的 CPU 运算能力浪费在等待。
所以在 IO 密集型任务中使用多线程可以大大的加速程序运行,即使在单核 CPU 上,这种加
速主要就是利用了被浪费掉的阻塞时间。
I0 密集型时,大部分线程都阻寒,故需要多配置线程数:
公式: CPU 核数 * 2
CPU 核数 / (1 - 阻塞系数) 阻塞系数 在 0.8~0.9 之间
查看 CPU 核数:
System.out.println(Runtime.getRuntime().availableProcessors());

常见的线程池

CachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。
SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行
的任务。
SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。
FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程,不建议

FixedThreadPool

Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去
Java 线程池的实现原理其实就是一个线程集合 workerSet 和一个阻塞队列 workQueue。当用户向线程池提交一个任务(也就是线程)时,线程池会先将任务放入 workQueue 中。
workerSet 中的线程会不断的从 workQueue 中获取线程然后执行。当 workQueue 中没有任务的时候,worker 就会阻塞,直到队列中有任务了就取出来继续执行。
特点:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,如果这个唯一的线程因
为异常结束,那么会有一个新的线程来替代它,他必须保证前一项任务执行完毕才能执行后一项。
保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
缺点:缺点的话,很明显,他是单线程的,高并发业务下有点无力
总结:保证所有任务按照指定顺序执行的,如果这个唯一的线程因为异常结束,那么会有一个新的
线程来替代它

newCachedThreadPool

创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源
CachedThreadPool,是一种可以缓存的线程池
)对最大线程数没有限制,也就是说可以登记线程数可以达到Integer.MAX_VALUE
2) 线程存活时间是60秒
3)阻塞队列用的是SynchronousQueue,这是一种不能存储任何元素的阻塞队列,也就是没
提交一个任务,到这个队列里面都需要分配一个工作线程来处理。
因为CachedThreadPool的最大线程数没有限制,所以它可以用来处理大量的任务。另外,
每个工作线程有可以存活60秒,是的这些工作线程可以缓存起来,去应对更多的任务处理
特点:newCachedThreadPool创建一个可缓存线程池,如果当前线程池的长度超过了处理的需要
时,它可以灵活的回收空闲的线程,当需要增加时, 它可以灵活的添加新的线程,而不会对池的
长度作任何限制
缺点:他虽然可以无线的新建线程,但是容易造成堆外内存溢出,因为它的最大值是在初始化的时
候设置为 Integer.MAX_VALUE,一般来说机器都没那么大内存给它不断使用。当然知道可能出问
题的点,就可以去重写一个方法限制一下这个最大值总结:线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线
程,而不用每次新建线程。

package com.lijie;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestNewCachedThreadPool {
    public static void main(String[] args) {
        // 创建无限大小线程池,由jvm自动回收
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int temp = i;
            newCachedThreadPool.execute(new Runnable() {
                public void run() {
                    try {
                        Thread.sleep(100);
                    } catch (Exception e) {
                    }
                    System.out.println(Thread.currentThread().getName() +
                                       ",i==" + temp);
                }
            });
        }
    }
}

newScheduledThreadPool

创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行
特点:创建一个固定长度的线程池,而且支持定时的以及周期性的任务执行,类似于
Timer(Timer是Java的一个定时器类)
缺点:由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一
个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的
任务都无法继续)。

ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
scheduledThreadPool.schedule(newRunnable(){ 
@Override 
public void run() {
System.out.println("延迟三秒");
}
}, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(newRunnable(){ 
@Override 
public void run() {
System.out.println("延迟 1 秒后每三秒执行一次");
}
},1,3,TimeUnit.SECONDS);

newFixedThreadPool

创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
它的特点就是:核心线程和最大线程数量都是一个固定的值。如果任务比较多工作线程处理不
过来的情况下,就会加入到阻塞队列里面等待执行。
FixedThreadPool 使用的是“无界队列”LinkedBlockingQueue
特点:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的
大小最好根据系统资源进行设置。
缺点:线程数量是固定的,但是阻塞队列是无界队列。如果有很多请求积压,阻塞队列越来越长,
容易导致OOM(超出内存空间)
总结:请求的挤压一定要和分配的线程池大小匹配,定线程池的大小最好根据系统资源进行设置。
如Runtime.getRuntime().availableProcessors()

Runtime.getRuntime().availableProcessors()方法是查看电脑CPU核心数量)
public static ExecutorService newFixedThreadPool(int var0) {

return new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
}
//创建固定大小的线程池
ExecutorService fPool = Executors.newFixedThreadPool(3);
package com.lc.other.thread.threadPool;

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

/**
 * @Author lc
 * @description:
 * @Date 2023/3/8 14:59
 */
public class NewFixedThreadPoolTest {
    public static void main(String[] args) {
//创建一个可重用固定线程数的线程池
        ExecutorService pool = Executors.newFixedThreadPool(2);
//创建实现了 Runnable 接口对象,Thread 对象当然也实现了 Runnable 接口
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();
        Thread t3 = new MyThread();
        Thread t4 = new MyThread();
        Thread t5 = new MyThread();
        //将线程放入池中进行执行
        pool.execute(t1);
        pool.execute(t2);
        pool.execute(t3);
        pool.execute(t4);
        pool.execute(t5);

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

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "正在执行。。。");
    }
/**
 * 运行结果:
 * pool-1-thread-1 正在执行。。。
 * pool-1-thread-1 正在执行。。。
 * pool-1-thread-2 正在执行。。。
 * pool-1-thread-1 正在执行。。。
 * pool-1-thread-2 正在执行。。。
 */
}

从上面的运行来看,我们 Thread 类都是在线程池中运行的,线程池在执行 execute 方法来执行 Thread 类
中的 run 方法。不管 execute 执行几次,线程池始终都会使用 2 个线程来处理。不会再去创建出其他线程来处理
run 方法执行。这就是固定大小线程池。

为什么要使用 Executor 线程池框架

什么是 Executors 框架
Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的 框架。 无限制的创建线程会引起应用程序内存溢出。所以创建一个线程池是个更好的 的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用 Executors 框架可以非常方便的创建一个线程池。

1、每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的。
2、调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
3、直接使用 new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。
1、能复用已存在并空闲的线程从而减少线程对象的创建从而减少了消亡线程的开销。
2、可有效控制最大并发线程数,提高系统资源使用率,同时避免过多资源竞争。
3、框架中已经有定时、定期、单线程、并发数控制等功能。
综上所述使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。

Executor 和 Executors 的区别
Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业 务的需求。 Executor 接口对象能执行我们的线程任务。 ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方 法我们能获得任务执行的状态并且可以获取任务的返回值。 使用 ThreadPoolExecutor 可以创建自定义线程池。 Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算 的完成,并可以使用 get()方法获取计算的结果。

线程池的处理流程

在这里插入图片描述

提交一个任务到线程池中,线程池的处理流程如下:

  1. 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没
    有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个
    流程。
  2. 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队
    列里。如果工作队列满了,则进入下个流程。
  3. 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任
    务。如果已经满了,则交给饱和策略来处理这个任务。
  4. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面
    有任务,线程池也不会马上执行它们。
  5. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要
    创建非核心线程立刻运行这个任务;
    d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池
    会抛出异常 RejectExecutionException。
  6. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  7. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运
    行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它
    最终会收缩到 corePoolSize 的大小。

线程池中,底层启动线程是调用Worker中thread的start方法,执行任务是通过调用阻塞队列中Runnable的run方法
核心线程也是可以回收的,只需设置allowCoreThreadTimeOut=true

,首先线程池判断基本线程池是否已满(< corePoolSize ?)?没满,创建一个工作线程来执行任务。满了,则进入下个流程。
2,其次线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。
3,最后线程池判断整个线程池是否已满(< maximumPoolSize ?)?没满,则创建一个新的工作线程来执行任务,满了,则交给饱和策略来处理这个任务。

总结:线程池优先要创建出基本线程池大小(corePoolSize)的线程数量,没有达到这个数量时,每次提交新任务都会直接创建一个新线程,当达到了基本线程数量后,又有新任务到达,优先放入等待队列,如果队列满了,才去创建新的线程(不能超过线程池的最大数maxmumPoolSize)

线程池状态

RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子
方法 terminated()。
TERMINATED:terminated()方法结束后,该状态表示线程池彻底终止

Executors 关闭

使用 shutdown 和 shutdownNow 可以关闭线程池
两者的区别:
shutdown 只是将空闲的线程 interrupt() 了,shutdown()之前提交的任务可以继续执行直到结束。
shutdownNow 是 interrupt 所有线程, 因此大部分线程将立刻被中断。之所以是大部分,而不是全部 ,
是因为 interrupt()方法能力有限。

线程池submit与execute的区别

相同点:
相同点就是都可以开启线程执行池中的任务。
不同点:
接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和
Callable 类型的任务。
返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有
异常处理:submit()方便Exception处理
得出结论1:如果提交的任务不需要一个结果的话直接用execute()会提升很多性能。
两个方法都可以向线程池提交任务,execute()方法的返回类型是 void,它定 义在 Executor 接口中。
而 submit()方法可以返回持有计算结果的 Future 对象,它定义在 ExecutorService 接口中,它扩展了 Executor 接口,其它线程池类像 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有这些方法
结论二:就是相当于说如果你传的任务是需要结果的,那你就使用你的类去继承Callable接口,然后告诉submit方法就行了,如果你只需要一个特定的结果,就把那个特定的结果告诉submit方法然后把你想要的特定结果也告诉他,它只是帮你完成以前使用Future模式的时候你自己需要做的那些步骤而已,如果你不需要一个结果,那么就老老实实使用execute,如果你需要的是一个空结果,那么submit(yourRunnable)与submit(yourRunnable,null)是等价的!
向线程池提交任务的两种方式:
1)通过execute()方法
ExecutorService threadpool= Executors.newFixedThreadPool(10);
threadpool.execute(new Runnable(){…});
这种方式提交没有返回值,也就不能判断任务是否被线程池执行成功。
2)通过submit()方法

Future<?> future = threadpool.submit(new Runnable(){...});    
    try {    
            Object res = future.get();//获取任务执行结果    
        } catch (InterruptedException e) {    
            // 处理中断异常    
            e.printStackTrace();    
        } catch (ExecutionException e) {    
            // 处理无法执行任务异常    
            e.printStackTrace();    
        }finally{    
            // 关闭线程池    
            executor.shutdown();    
        }    

使用submit 方法来提交任务,它会返回一个Future对象,通过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。
execute():只能执行 Runnable 类型的任务。
submit():可以执行 Runnable 和 Callable 类型的任务。
Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。
submit(Runnable) 方法也要求一个 Runnable 实现类,但它返回一个 Future 对象。这个 Future 对象可
以用来检查 Runnable 是否已经执行完毕。以下是 ExecutorService submit() 示例:
//从 Executors 中获得 ExecutorService

ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(new Runnable() {
    public void run() {
        System.out.println("Asynchronous task");
    }
});

future.get(); //获得执行完 run 方法后的返回值,这里使用的 Runnable,所以这里没有返回值,返回的是 null。
executorService.shutdown();
submit(Callable) 方法类似于 submit(Runnable) 方法,除了它所要求的参数类型之外。Callable 实例除了
它的 call() 方法能够返回一个结果之外和一个 Runnable 很相像。Runnable.run() 不能够返回一个结果。
Callable 的结果可以通过 submit(Callable) 方法返回的 Future 对象进行获取。
以下是一个 ExecutorService Callable 示例:

//从 Executors 中获得 ExecutorService
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(new Callable(){
public Object call() throws Exception {
System.out.println("Asynchronous Callable");
return "Callable Result";
}
});
System.out.println("future.get() = " + future.get());
executorService.shutdown();
输出:
Asynchronous Callable
future.get() = Callable Result

一个应用创建多个线程池 还是多个线程池

这个问题的答案是:取决于应用的实际需求和场景。
创建多个线程池的优点是,可以更好地控制不同类型任务的执行,避免线程池资源被某种类型的任务独占,从而导致其他任务无法得到及时处理的情况。通过将不同类型的任务分配到不同的线程池中,可以有效避免线程池资源的浪费和滥用,提高系统的整体性能和稳定性。
然而,创建多个线程池也会带来一些缺点。首先是线程池的管理成本增加,需要对多个线程池进行管理和维护;其次是线程调度的开销增加,多个线程池之间的调度需要进行切换,会带来一定的性能开销。
因此,具体是创建多个线程池还是一个线程池,需要综合考虑以下几个因素:

  1. 业务需求:根据业务需求和场景,选择最合适的线程池方案。如果应用中有不同类型的任务,且这些任务之间的关联性较小,可以考虑创建多个线程池来处理不同类型的任务;如果应用中的任务类型比较单一,并且任务之间的关联性较强,可以考虑使用一个线程池来处理所有任务。
  2. 线程池管理成本:创建多个线程池会增加线程池的管理成本,包括线程池的创建、销毁、调度、监控等方面。如果线程池的管理成本过高,可以考虑使用一个线程池来处理所有任务。
  3. 线程调度开销:多个线程池之间的切换会带来一定的性能开销。如果应用中需要频繁切换线程池,可以考虑使用一个线程池来处理所有任务,减少线程调度的开销。
    综上所述,选择是否创建多个线程池,需要根据实际业务需求和场景进行综合考虑,并进行合理的设计和优化。
    在设计应用程序时,创建多个线程池或单个线程池取决于多种因素,包括应用程序的性质、负载类型、资源限制以及性能需求等。下面是一些考虑因素:
  4. 任务类型和特性:
    ○ 如果应用程序中存在不同类型或不同优先级的任务,可以考虑为它们创建不同的线程池。例如,可以为CPU密集型任务和IO密集型任务分别创建不同的线程池,以便更好地利用系统资源。
  5. 资源限制:
    ○ 考虑到系统资源的限制,特别是CPU和内存资源,可能需要限制线程池的总大小。如果将所有任务都放入一个单一线程池中,可能会导致资源争用和性能下降。
  6. 任务隔离:
    ○ 有些情况下,为了防止一个任务的问题影响到其他任务,可以考虑为每个任务类型创建一个独立的线程池。这样可以确保一个线程池中的问题不会影响到其他线程池中的任务。
  7. 线程池的管理和调优:
    ○ 如果应用程序中存在不同类型的任务,单个线程池可能难以管理和调优。通过创建多个线程池,可以更好地控制每个线程池的大小、队列大小、超时设置等参数,以满足不同类型任务的需求。
  8. 性能需求:
    ○ 根据应用程序的性能需求和负载特性,选择合适的线程池策略。有时候,为每个任务类型创建一个独立的线程池可以提高系统的吞吐量和响应性能。
    总的来说,根据应用程序的特性和需求,需要综合考虑多种因素来决定是创建多个线程池还是单个线程池。在一些情况下,创建多个线程池可以更好地满足不同类型任务的需求,提高系统的灵活性和性能。

什么是线程组,为什么在 Java 中不推荐使用

ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程
组,组中还可以有线程,这样的组织结构有点类似于树的形式。
线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为
了管理线程的生命周期,复用线程,减少创建销毁线程的开销。
为什么不推荐使用线程组?因为使用有很多的安全隐患吧,没有具体追究,如果需要使用,推荐使
用线程池。

如何让Java的线程池顺序执行任务?

Java中的线程池本身并不提供内置的方式来保证任务的顺序执行的,因为线程池的设计目的是为了提高并发性能和效率,如果顺序执行的话,那就和单线程没区别了。
Java的线程池默认是按照任务提交的顺序执行任务的,但是在使用线程池时,线程的执行是异步的,无法保证任务的执行顺序。不过,您可以通过一些方式来实现线程池中任务的顺序执行:

  1. 使用单线程的线程池:通过创建一个只有一个线程的线程池,可以确保任务按照提交的顺序被执行。例如:
    ExecutorService executor = Executors.newSingleThreadExecutor();
    这样就只有一个线程在池中,任务将依次执行。
  2. 使用有序的队列:可以使用带有排序功能的队列,如PriorityBlockingQueue,来保证任务按照某个特定的顺序执行。在提交任务时,为每个任务设置一个优先级或者指定一个比较器,使得任务按照特定的顺序排列。例如:
ExecutorService executor = new ThreadPoolExecutor(
    1, // 核心线程数
    1, // 最大线程数
    0L, // 线程空闲时间
    TimeUnit.MILLISECONDS, // 时间单位
    new PriorityBlockingQueue<>()); // 使用优先级队列

这样,任务将按照优先级进行执行。
3. 使用Future对象的get()方法:可以使用Future对象来控制任务的顺序执行。通过调用Future对象的get()方法获取任务的返回结果,会阻塞当前线程直到任务完成。可以按照提交任务的顺序依次调用get()方法来实现任务的顺序执行。例如:

ExecutorService executor = Executors.newFixedThreadPool(10);

List<Future<?>> futures = new ArrayList<>();

for (int i = 0; i < 10; i++) {
    final int taskId = i;
    Future<?> future = executor.submit(() -> {
        // 执行任务逻辑
        System.out.println("Task " + taskId + " executed");
    });
    futures.add(future);
}

for (Future<?> future : futures) {
    future.get(); // 等待任务完成
}

executor.shutdown();

通过遍历Future对象的集合,并依次调用get()方法,可以确保任务按照提交的顺序执行。
请注意,以上方法只能保证任务在单个线程池中的顺序执行,如果有多个线程池,无法保证全局的执行顺序。另外,如果任务之间相互依赖或有其他复杂的执行顺序要求,可能需要考虑使用更高级的调度方式,如CompletableFuture等。

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

思静语

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值