线程池

线程池(重点)

线程池(Thread Pool)是一种多线程处理形式,处理过程中将任务提交给线程池,线程池中的线程会异步地执行这些任务。线程池的主要目的是复用线程,减少线程的创建和销毁开销,提高程序的响应速度和吞吐量。

在Java中,java.util.concurrent 包提供了对线程池的支持,包括 ExecutorService 接口和几个实现了该接口的类,如 ThreadPoolExecutorExecutors

优势:

  1. 降低资源消耗:通过复用线程,减少了线程的创建和销毁次数,降低了线程的创建和销毁的开销。
  2. 提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行。
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性。使用线程池可以对线程进行统一分配、调优和监控。

JDK中线程池

线程池的体系结构

image-20240217144728987

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

Executors工具类

创建线程池方法

Executors 是 JDK 所提供的线程池 工具类,在该类中提供了很多的静态方法供我们快速的创建线程池对象。

// 创建一个可缓存线程池,可灵活的去创建线程,并且灵活的回收线程,若无可回收,则新建线程。
ExecutorService newCachedThreadPool()
    
// 初始化一个具有固定数量线程的线程池
ExecutorService newFixedThreadPool(int nThreads)
    
// 初始化一个具有一个线程的线程池
ExecutorService newSingleThreadExecutor()
    
// 初始化一个具有一个线程的线程池,支持定时及周期性任务执行
ScheduledExecutorService newSingleThreadScheduledExecutor()

返回值就是线程池对象ExecutorService,ScheduledExecutorService。

ExecutorService中的常见方法

Future<?> submit(Runnable task): 向线程池提交任务
void shutdown(): 关闭线程池

submit():
Future<T> submit(Callable<T> task):提交一个返回值的任务,返回一个 Future 对象,用于表示任务的执行结果。
Future<?> submit(Runnable task):提交一个不带返回值的任务,返回一个 Future 对象。
    
invokeAll():
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks):提交一组任务并等待它们结束,返回一个 Future 列表。
    
invokeAny():
<T> T invokeAny(Collection<? extends Callable<T>> tasks):提交一组任务并返回第一个成功完成的任务的结果,或抛出异常。
    
shutdown():
void shutdown():关闭ExecutorService,不能再接受新任务,但会继续执行已提交的任务。

shutdownNow():
List<Runnable> shutdownNow():立即关闭ExecutorService,尝试停止所有正在执行的任务,返回待执行的任务列表。

isShutdown():
boolean isShutdown():判断ExecutorService是否已关闭。

isTerminated():
boolean isTerminated():判断所有任务是否已完成执行。
    
awaitTermination():
boolean awaitTermination(long timeout, TimeUnit unit):在关闭之后等待所有任务完成,直到超时或任务完成。

execute():
void execute(Runnable command):执行提交的Runnable任务。

1.newCachedThreadPool

无限大线程池:newCachedThreadPool的线程池大小理论上可以是无限的(int的最大值),因为它没有设置最大线程数。当执行第二个任务时,如果第一个任务的线程已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

public class ExecutorsDemo {

    // 演示Executors中的newCachedThreadPool返回的线程池的特点
    public static void main(String[] args) throws InterruptedException {

        // 获取线程池对象
        ExecutorService threadPool = Executors.newCachedThreadPool();

        // 提交任务
        threadPool.submit(() -> {
            System.out.println( Thread.currentThread().getName() + "---执行了任务");
        });

        // 提交任务
        threadPool.submit(() -> {
            System.out.println( Thread.currentThread().getName() + "---执行了任务");
        });

        // 不使用线程池了,还可以将线程池关闭
        threadPool.shutdown();
    }
}

控制台输出结果

pool-1-thread-2---执行了任务
pool-1-thread-1---执行了任务

2.newFixedThreadPool

创建一个具有固定数量线程的线程池

public class ExecutorsDemo {

    // 演示newFixedThreadPool方法所获取到的线程池的特点
    public static void main(String[] args) {

        // 获取线程池对象,初始化一个具有固定数量线程的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(3);  // 在该线程池中存在3个线程

        // 提交任务
        for(int x = 0 ; x < 5 ; x++) {
            threadPool.submit( () -> {
                System.out.println(Thread.currentThread().getName() + "----->>>执行了任务" );
            });
        }

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

}

控制台输出结果

pool-1-thread-1----->>>执行了任务
pool-1-thread-2----->>>执行了任务
pool-1-thread-2----->>>执行了任务
pool-1-thread-2----->>>执行了任务
pool-1-thread-3----->>>执行了任务

通过控制台的输出结果,我们可以看到5个任务是通过3个线程进行执行的,说明此线程池中存在三个线程对象。

3.newSingleThreadExecutor

创建一个具有一个线程的线程池

public class ExecutorsDemo {

    // 演示newSingleThreadExecutor方法所获取到的线程池的特点
    public static void main(String[] args) {

        // 获取线程池对象,初始化一个具有一个线程的线程池
        ExecutorService threadPool = Executors.newSingleThreadExecutor();

        // 提交任务
        for(int x = 0 ; x < 5 ; x++) {
            threadPool.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "----->>>执行了任务");
            });
        }

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

}

控制台输出结果

pool-1-thread-1----->>>执行了任务
pool-1-thread-1----->>>执行了任务
pool-1-thread-1----->>>执行了任务
pool-1-thread-1----->>>执行了任务
pool-1-thread-1----->>>执行了任务

通过控制台的输出结果,我们可以看到5个任务是通过1个线程进行执行的,说明此线程池中只存在一个线程对象。

4.newSingleThreadScheduledExecutor

支持定时及周期性任务执行

测试1(演示定时执行)

public class ExecutorsDemo {

    // 演示newSingleThreadScheduledExecutor方法所获取到的线程池的特点(支持定时及周期性任务执行)
    public static void main(String[] args) {

        // 获取线程池对象
        ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();

        // 提交任务,10s以后开始执行该任务
        threadPool.schedule( () -> {
            System.out.println(Thread.currentThread().getName() + "---->>>执行了该任务");
        } , 10 , TimeUnit.SECONDS) ;

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

}

测试2(演示周期性执行)

public class ExecutorsDemo {

    // 演示newSingleThreadScheduledExecutor方法所获取到的线程池的特点(支持定时及周期性任务执行)
    public static void main(String[] args) {

        // 获取线程池对象
        ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();

        // 提交任务,10s以后开始第一次执行该任务,然后每隔1秒执行一次
        threadPool.scheduleAtFixedRate( () -> {
            System.out.println(Thread.currentThread().getName() + "---->>>执行了该任务");
        } , 10 ,1, TimeUnit.SECONDS) ;

    }

}

ThreadPoolExecutor创建线程池

ThreadPoolExecutor 是 Java 的 java.util.concurrent 包中的一个重要类,用于管理线程池。线程池是一种设计模式,能够有效地管理和复用线程,减少了线程创建和销毁所带来的性能开销。

ThreadPoolExecutor完整的构造方法:

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

参数说明

corePoolSize:   核心线程的最大值,不能小于0
maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
keepAliveTime:  空闲线程最大存活时间,不能小于0
unit:           时间单位
workQueue:      任务队列,不能为null
threadFactory:  创建线程工厂,不能为null      
handler:        任务的拒绝策略,不能为null    

使用自定义线程池

public class ThreadPoolExecutorDemo {

    // 演示基本使用
    public static void main(String[] args) {

        // 通过ThreadPoolExecutor创建一个线程池对象
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 3, 60, TimeUnit.SECONDS, 
                                                               new ArrayBlockingQueue<Runnable>(3), 
                                                               Executors.defaultThreadFactory(), 
                                                               new ThreadPoolExecutor.AbortPolicy());

        /**
         * 以上代码表示的意思是:核心线程池中的线程数量最大为1,整个线程池中最多存在3个线程,空闲线程最大的存活时间为60,时间单位为秒,阻塞队列使用的是有界阻塞队列,容量为3, 使用默认的线程工厂; 以及默认的任务处理策略
         */

        // 提交任务
        threadPoolExecutor.submit(() -> {
            System.out.println(Thread.currentThread().getName() + "------>>>执行了任务");
        });

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

    }
}

BlockingQueue(阻塞队列)

该队列存储的是未被处理的任务,Runnable实例(任务)

在线程池中,阻塞队列用于存储未被处理的任务,没有被处理的任务将会在阻塞队列中进行等待。

BlockingQueue的特征:

1、当生产者线程试图向BlockingQueue中放入元素时,如果队列已满,则线程被阻塞。

2、当消费者线程试图从BlockingQueue中取元素时,如果队列没有元素,则该线程被阻塞。

和阻塞相关的两个方法:

void put(E e) throws InterruptedException;			// 存数据
E take() throws InterruptedException;				// 取出数据,并移除

BlockingQueue的常用实现类:

ArrayBlockingQueue:基于数组实现的有界阻塞队列,有界指的是队列有大小限制,可以put多少元素,取决于数组长度。
    
LinkedBlockingQueue:基于链表实现的无界阻塞队列,并不是绝对的无界(容量:Integer.MAX_VALUE)

SynchronousQueue:只能进行一次put操作。

我们以ArrayBlockingQueue举例来演示一下阻塞队列的使用

public class ArrayBlockingQueueDemo {

    public static void main(String[] args) throws InterruptedException {

        // 创建一个容量为1的阻塞队列
        ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<String>(1) ;

        // 存储元素
        arrayBlockingQueue.put("java");
        arrayBlockingQueue.put("world");  //因为队列容量为1,当存储第二个元素时,队列会堵塞。

        // 取元素
        System.out.println(arrayBlockingQueue.take());

        System.out.println(arrayBlockingQueue.take());

        // 输出
        System.out.println("程序结束了....");

    }

}

线程池任务的拒绝策略

RejectedExecutionHandler是jdk提供的一个任务拒绝策略接口,它下面存在4个子类。

ThreadPoolExecutor.AbortPolicy: 		   丢弃任务并抛出RejectedExecutionException异常。是默认的策略。

ThreadPoolExecutor.DiscardPolicy: 		   丢弃任务,但是不抛出异常 这是不推荐的做法。

ThreadPoolExecutor.DiscardOldestPolicy:    抛弃队列中等待最久的任务 然后把当前任务加入队列中。

ThreadPoolExecutor.CallerRunsPolicy:       调用任务的run()方法绕过线程池直接执行。

注:明确线程池最多可执行的任务数 = 队列容量 + 最大线程数

演示ThreadPoolExecutor.DiscardPolicy任务处理策略

public class ThreadPoolExecutorDemo {

    public static void main(String[] args) {

        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.DiscardPolicy()) ;

        // 提交5个任务,而该线程池最多可以处理4个任务,当我们使用DiscardPolicy这个任务处理策略的时候,控制台不会报错
        for(int x = 0 ; x < 5 ; x++) {
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
            });
        }

    }
}

控制台输出结果

pool-1-thread-1---->> 执行了任务
pool-1-thread-1---->> 执行了任务
pool-1-thread-3---->> 执行了任务
pool-1-thread-2---->> 执行了任务

控制台没有报错,仅仅执行了4个任务,有一个任务被丢弃了

线程池自定义拒绝策略

import java.util.concurrent.*;  

public class CustomRejectedExecutionHandlerExample {  

public static void main(String[] args) {  
    // 创建一个线程池  
    ThreadPoolExecutor executor = new ThreadPoolExecutor(  
          2, // 核心线程数  
          4, // 最大线程数  
          1, // 空闲线程存活时间  
          TimeUnit.SECONDS,  
          new ArrayBlockingQueue<>(2), // 工作队列  
          (r,executor)->{// 自定义拒绝策略  
                System.out.println("Task " + r.toString() + " has been rejected.");  
            // 这里可以添加自定义的处理逻辑,比如将任务放到另一个队列中  
         } 
    );  

   executor.shutdown();  
}  

    
    
    // 自定义拒绝策略  
static class CustomRejectedExecutionHandler implements RejectedExecutionHandler {  
       @Override  
       public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {  
         System.out.println("Task " + r.toString() + " has been rejected.");  
            // 这里可以添加自定义的处理逻辑,比如将任务放到另一个队列中  
        }  
    }  
}

通过实现自定义的拒绝策略,Java 的线程池可以更加灵活地处理任务提交的情况,确保在负载高的情况下仍然能够以你希望的方式处理业务逻辑。

线程池关闭

关闭线程池的两个方法:

1、shutdown():停止接收新任务,已经提交的任务(包括正在跑的和队列中等待的),会继续执行完成。
2、shutdownNow():停止接收新任务,原来的任务停止执行。
(1)跟 shutdown() 一样,先停止接收新submit的任务;
(2)忽略队列里等待的任务;
(3)尝试将正在执行的任务interrupt中断;
(4)返回未执行的任务列表;
awaitTermination(60, TimeUnit.SECONDS) 等待所有任务完成,如果超过此时间仍有未完成的任务,则会返回false。

优雅关闭线程池的方式:

1、调用 ExecutorService 的 shutdown() 方法,这将禁止提交新任务,但已经提交的任务将继续执行,直到完成为止。

2、调用 awaitTermination(60, TimeUnit.SECONDS) 方法,等待所有任务完成,如果超过此时间仍有未完成的任务,则会返回false。

3、返回false,则调用ExecutorService 的 shutdownNow() 方法来中断所有正在执行的任务。

executor.shutdown();
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
	List<Runnable> runnables = executor.shutdownNow();
}

线程池创建方式选择

在《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。

而线程池不允许使用Executors去创建,而要通过ThreadPoolExecutor方式,这一方面是由于jdk中Executor框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活;使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。

image-20241017183239828

image-20241017183502984

posted @   CH_song  阅读(16)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示