线程池

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

为解决此问题,我们需要使用线程池。一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。

java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,下面主要来围绕这个类对线程池进行介绍。

必看博文:Java线程池实现原理及其在美团业务中的实践,这篇写的太好了,从线程池如何维护自身状态,线程池如何管理任务,线程池如何管理线程三个方面介绍线程池的运行机制,各种图把流程描述的很清晰!

另外,线程池的参数是支持线程池运行时设置的,这篇博文中也有讲到。要注意到的时,任务队列是定长的,而若想动态修改队列容量,则可在LinkedBlockingQueue的基础上自定义可变容量的队列,可参考线程池的参数动态调整 

 

线程池的创建

7个核心参数+4种丢弃策略

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

corePoolSize:  表示常驻核心线程数。如果等于0,则执行完任务之后,没有任何请求进入时销毁线程池的线程;如果大于0,即使本地任务执行完,核心线程也不会被销毁。这个值非常关键,设置过大会浪费资源,过小会导致线程频繁的创建或销毁。

maximumPoolSize: 表示线程池能够同时执行的最大线程数,这个值必须大于1,如果执行的线程数大于此值,则需要借助第5个参数的帮助,缓存在队列中。如果maximumPoolSize与corePoolSize相等,即是固定大小的线程池。

keepAliveTime: 表示线程池中的线程空闲时间,当空闲时间达到keepAliveTime值时,线程会被销毁,直至剩下corePoolSize个线程为止,避免浪费内存和句柄资源。在默认情况下,当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用。但是当ThreadPoolExecutor的allowCoreThreadTimeOut变量值设为true时,核心线程超时后也会被收回。

unit: 表示时间单位。keepAliveTime 的时间单位通常时TimeUnit.SECONDS.

workQueue: 表示缓存队列。当请求的线程数大于maximumPoolSize时,线程进入BlockingQueue阻塞队列。

threadFactory: 表示线程工厂。他用来生产一组相同任务的线程。线程池的命名是通过给这个factory增加组名前缀名来实现的。在虚拟机栈分析时就可以知道线程任务是有哪个线程工厂来产生的。

handler: 表示执行拒绝策略的对象。当超过第5个参数workQueue的任务缓存区上限时,就可以通过该策略处理对象,这是一种简单的限流保护。

拒绝策略主要包含:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 

由这些参数可知,线程池的工作过程如下:

1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面 有任务,线程池也不会马上执行它们。

2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:   

  a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;   

  b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;   

  c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要 创建非核心线程立刻运行这个任务;   

  d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池 会抛出异常 RejectExecutionException。

3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。

4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,   

  如果当前运 行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它 最终会收缩到 corePoolSize 的大小。

线程池类型

①newSingleThreadExecutor 单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务

②newFixedThreadExecutor(n) 固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行

③newCacheThreadExecutor(推荐使用) 可缓存线程池, 当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。

④newScheduleThreadExecutor 大小无限制的线程池,支持定时和周期性的执行线程

线程的创建

在java中如果要创建线程的话,一般有三种方式:

1)继承Thread类;

class MyThread extends Thread { 
    @Override 
    public void run() { 
        System.out.println(Thread.currentThread().getName()
            +"\t"+Thread.currentThread().getId()); 
    } 
} 

// 启动方式为: 
public class Main { 
    public static void main(String[] args) { 
        new MyThread().start(); 
    } 
}                     

 

https://blog.csdn.net/vbirdbest/article/details/81282163

2)实现Runnable接口; 

public class Main {
    public static void main(String[] args) {
         // 将Runnable实现类作为Thread的构造参数传递到Thread类中,然后启动Thread类
        MyRunnable runnable = new MyRunnable();
        new Thread(runnable).start();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
    }
}

 

3)实现Callable接口;

public class Main {
    public static void main(String[] args) throws Exception {
         // 将Callable包装成FutureTask,FutureTask也是一种Runnable
        MyCallable callable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();

        // get方法会阻塞调用的线程
        Integer sum = futureTask.get();
        System.out.println(Thread.currentThread().getName() + Thread.currentThread().getId() + "=" + sum);
    }
}


class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() 
        + "\t" + new Date() + " \tstarting..."); int sum = 0; for (int i = 0; i <= 100000; i++) { sum += i; } Thread.sleep(5000); System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId()
        + "\t" + new Date() + " \tover..."); return sum; } }

三种方式比较:

- Thread: 继承方式, 不建议使用, 因为Java是单继承的,继承了Thread就没办法继承其它类了,不够灵活

- Runnable: 实现接口,比Thread类更加灵活,没有单继承的限制

- Callable: Thread和Runnable都是重写的run()方法并且没有返回值,Callable是重写的call()方法并且有返回值并可以借助FutureTask类来判断线程是否已经执行完毕或者取消线程执行

- 当线程不需要返回值时使用Runnable,需要返回值时就使用Callable,一般情况下不直接把线程体代码放到Thread类中,一般通过Thread类来启动线程

- Thread类是实现Runnable,Callable封装成FutureTask,FutureTask实现RunnableFuture,RunnableFuture继承Runnable,所以Callable也算是一种Runnable,所以三种实现方式本质上都是Runnable实现

线程池相关类间的关系

线程池的创建和提交

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3); 

fixedThreadPool.execute(new Runnable() {  
                public void run() {  
                    try {  
                        System.out.println("hello");  
                        Thread.sleep(10);  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }  
            });  

Future<String> future = fixedThreadPool.submit(callable); 

while(true) {
            //idDone:如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,
        //在所有这些情况中,此方法都将返回 true。 if(future.isDone()) { System.out.println("任务执行完成:" + future.get()); break; } }

fixedThreadPool.shutdown();

execute和submit的区别:

  • execute只能提交Runnable类型的任务,无返回值。submit既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null。
  • execute在执行任务时,如果遇到异常会直接抛出,而submit不会直接抛出,只有在使用Future的get方法获取返回值时,才会抛出异常。

shutdown()和shutdownNow()的区别:

- shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务

- shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

4个参数的设计

1:核心线程数(corePoolSize) 核心线程数的设计需要依据任务的处理时间和每秒产生的任务数量来确定,例如:执行一个任务需要0.1秒,系统百分之80的时间每秒都会产生100个任务,那么要想在1秒内处理完这100个任务,就需要10个线程,此时我们就可以设计核心线程数为10;当然实际情况不可能这么平均,所以我们一般按照8020原则设计即可,既按照百分之80的情况设计核心线程数,剩下的百分之20可以利用最大线程数处理;

2:任务队列长度(workQueue) 任务队列长度一般设计为:核心线程数/单个任务执行时间*2即可;例如上面的场景中,核心线程数设计为10,单个任务执行时间为0.1秒,则队列长度可以设计为200;

3:最大线程数(maximumPoolSize) 最大线程数的设计除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定:例如:上述环境中,如果系统每秒最大产生的任务是1000个,那么,最大线程数=(最大任务数-任务队列长度)*单个任务执行时间;既: 最大线程数=(1000-200)*0.1=80个;

4:最大空闲时间(keepAliveTime) 这个参数的设计完全参考系统运行环境和硬件压力设定,没有固定的参考值,用户可以根据经验和系统产生任务的时间间隔合理设置一个值即可; https://blog.csdn.net/qq_43061290/article/details/106911277 ThreadPoolTaskExecutor是spring core包中的,而ThreadPoolExecutor是JDK中的JUC。ThreadPoolTaskExecutor是对ThreadPoolExecutor进行了封装处理。

posted @ 2021-12-12 20:21  Hyacinth-Yuan  阅读(67)  评论(0编辑  收藏  举报