Java线程池原理彻底弄懂

参考:
https://blog.csdn.net/lxk_1993/article/details/90672654
https://blog.csdn.net/f641385712/article/details/80832636

附、Java中创建线程的四种方法

1)继承Thread类创建线程

2)实现Runnable接口创建线程

3)使用Callable和Future创建线程

4)使用线程池例如用Executor框架

一、继承Thread类创建线程

通过继承Thread类来创建并启动多线程的一般步骤如下:

  • 定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。
  • 创建Thread子类的实例,也就是创建了线程对象
  • 启动线程,即调用线程的start()方法
public class MyThread extends Thread{//继承Thread类
  public void run(){
  //重写run方法
  }
}
public class Main {
  public static void main(String[] args){
    new MyThread().start();//创建并启动线程
  }
}

二、实现Runnable接口创建线程

通过实现Runnable接口创建并启动线程步骤如下:

  • 定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体

  • 创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象

  • 第三部依然是通过调用线程对象的start()方法来启动线程

public class MyThread2 implements Runnable {//实现Runnable接口
  public void run(){
  //重写run方法

  }

}
public class Main {
  public static void main(String[] args){
    //创建并启动线程

    MyThread2 myThread=new MyThread2();

    Thread thread=new Thread(myThread);

    thread().start();

    //或者    new Thread(new MyThread2()).start();

  }
}

三、使用Callable和Future创建线程

  • 创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从Java8开始可以直接使用Lambda表达式创建Callable对象)。
  • 使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
  • 使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
  • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
public class T01 {
    public static void main (String[] args) {
        // 创建Callable对象
        T01 rt= new T01();
        // 先使用Lambda表达式创建Callable<Integer>对象
        // 使用FutureTask来包装Callable对象
        FutureTask<Integer> task = new  FutureTask<Integer>((Callable<Integer>)() -> {
            int i = 0;
            for ( ; i < 30 ; i++ )
            {
                System.out.println("1"+Thread.currentThread().getName()
                        + " 的循环变量i的值:" + i);
            }
            // call()方法可以有返回值
            return i;
        });
        for (int i = 0 ; i < 50 ; i++)
        {
            System.out.println("2"+Thread.currentThread().getName()
                    + " 的循环变量i的值:" + i);
            if (i == 20)
            {
                // 实质还是以Callable对象来创建、并启动线程
                new Thread(task , "3有返回值的线程").start();
            }
        }
        try
        {
            // 获取线程返回值
            System.out.println("子线程的返回值:" +  task.get());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

输出结果:
在这里插入图片描述
说明:

理论上是:

程序先使用使用Lamda表达式创建一个Callable对象,然后将该实例包装成一个FutureTask对象。主线程中当循环变量i等于20时,程序启动以FutrueTask对象为target的线程。程序最后调用FutrueTask对象的get()方法来返回call()方法的返回值——该方法将导致主线程被阻塞,直到call()方法结束并返回为止。

实际上:(水平有限,待完善解释) 由于多线程问题

四、使用线程池例如用Executor框架

一、什么是线程池?

线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。

二、如何创建线程池?

java.util.concurrent.Executors提供了一个java.util.concurrent.Executor接口的实现用于创建线程池

三、为什么要用线程池?
  • 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  • 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

总结:线程池作用就是限制系统中执行线程的数量。据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了;否则进入等待队列。

四、关于线程池几个重要的类
ExecutorService真正的线程池接口。
ScheduledExecutorService能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
ThreadPoolExecutorExecutorService的默认实现。
ScheduledThreadPoolExecutor继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。
五、配置线程池

在Executors类里面提供了一些静态工厂,生成一些常用的线程池。

一、SingleThreadExecutor
  • 创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

源码实现

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService(
            new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
    );
}
  • SingleThreadExecutor的corePoolSize和maximumPoolSize被设置为1。其他参数与FixedThreadPool相同。
  • SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。
  • SingleThreadExecutor使用无界队列作为工作队列对线程池带来的影响与FixedThreadPool相同,这里就不赘述了。

核心代码实例:

class MyThread extends Thread {

    @Override
    public void run() {
        // TODO Auto-generated method stub
//        super.run();
        System.out.println(Thread.currentThread().getName()+"正在执行....");
    }
}

public class singleThreadExecutorTest{
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        //创建一个可重用固定线程数的线程池
        ExecutorService pool=Executors.newSingleThreadExecutor();
        //创建实现了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();
    }
}

运行结果:

pool-1-thread-1正在执行…
pool-1-thread-1正在执行…
pool-1-thread-1正在执行…
pool-1-thread-1正在执行…
pool-1-thread-1正在执行…

附:谈一波run()方法

上面代码是创建了一个单线程的线程池,其中execute()方法就是执行一个线程使用的,相当于一个start()方法

-1. 而run方法又是一个什么样的方法?run方法与start方法有什么关联?

run()方法当作普通方法的方式调用 run( )其实是一个普通方法,只不过当线程调用了start()方法后,一旦线程被CPU调度,处于运行状态,那么线程才会去调用这个run()方法;

  • 2.run()方法的执行是不是需要线程调用start()方法

上面说了,run()方法是一个普通的对象方法,因此,不需要线程调用start()后才可以调用的。可以线程对象可以随时随地调用run方法。
实例

Example1:
  Thread t1 = new Thread(new MyTask(1));
  Thread t2 = new Thread(new MyTask(2));
     t1.run();
     t2.run();

上面的输出结果是固定的:
count的值:1
count的值:2
再看另一个实例
 Thread t1 = new Thread(new MyTask());
 Thread t2 = new Thread(new MyTask());
 t1.start();
 t2.start();
 
这个输出结果不是固定的,因为线程的运行没法预测。运行结果可能不一样。
二、newFixedThreadPool
  • 建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大的大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

源码实现

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

当线程池中的线程数大于corePoolSize时,keepAliveTime为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止。这里把keepAliveTime设置为0L,意味着多余的空闲线程会被立即终止。

FixedThreadPool的execute()方法的运行示意图如下图所示。
在这里插入图片描述

  • 图中1:如果当前运行的线程数少于corePoolSize,则创建新线程来执行任务。
  • 图中2:在线程池完成预热之后(当前运行的线程数等于corePoolSize),将任务加入LinkedBlockingQueue。
  • 图中3:线程执行完1中的任务后,会在循环中反复从LinkedBlockingQueue获取任务来执行。

FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。

注:使用无界队列作为工作队列会对线程池带来如下影响:(内存耗尽)

  • 当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize。
  • 由于上一点,使用无界队列时maximumPoolSize将是一个无效参数。
  • 由于前面两点,使用无界队列时keepAliveTime将是一个无效参数。
  • 由于使用无界队列,运行中的FixedThreadPool(未执行方法shutdown()或shutdownNow())不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution方法)。

核心代码实例:

public class singleThreadExecutorTest{
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        //创建一个可重用固定线程数的线程池
        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();
    }
}

结果:
pool-1-thread-1正在执行....
pool-1-thread-1正在执行....
pool-1-thread-1正在执行....
pool-1-thread-1正在执行....
pool-1-thread-2正在执行....
三、 newCachedThreadPool
  • 创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

源码实现:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>());
}

CachedThreadPool的corePoolSize被设置为0,即corePool为空;maximumPoolSize被设置为Integer.MAX_VALUE,即maximumPool是无界的。
这里把keepAliveTime设置为60L,意味着CachedThreadPool中的空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会被终止。

FixedThreadPool和SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列。

  • CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPool的maximumPool是无界的。
  • 这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。
  • 极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。

CachedThreadPool的execute()方法的执行示意图如下图所示:
在这里插入图片描述

  • 上图1: 首先执行SynchronousQueue.offer(Runnable
    task)。如果当前maximumPool中有空闲线程正在执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成;否则执行下面的步骤2。
  • 上图2:当初始maximumPool为空,或者maximumPool中当前没有空闲线程时,将没有线程执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤1将失败。此时CachedThreadPool会创建一个新线程执行任务,execute()方法执行完成。
  • 上图3:.在步骤2中新创建的线程将任务执行完后,会执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这个poll操作会让空闲线程最多在SynchronousQueue中等待60秒钟。如果60秒钟内主线程提交了一个新任务(主线程执行步骤1),那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于空闲60秒的空闲线程会被终止,因此长时间保持空闲的CachedThreadPool不会使用任何资源。
    //创建一个可重用固定线程数的线程池
    ExecutorService pool=Executors.newCachedThreadPool();
    //创建实现了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();
   
结果:
pool-1-thread-2正在执行....
pool-1-thread-1正在执行....
pool-1-thread-3正在执行....
pool-1-thread-4正在执行....
pool-1-thread-5正在执行....
四、 newScheduledThreadPool

适用于需要单个后台线程执行周期任务,同时需要保证顺序地执行各个任务的应用场景。

  • 创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。有三种提交任务的方式:
    • schedule:延迟多长时间之后只执行一次;
    • scheduledAtFixedRate:延迟指定时间后执行一次,之后按照固定的时长周期执行;
    • scheduledWithFixedDelay:延迟指定时间后执行一次,之后按照:上一次任务执行时长 + 周期的时长 的时间去周期执行;

ScheduledThreadPoolExecutor的运行机制
在这里插入图片描述
DelayQueue是一个无界队列,所以ThreadPoolExecutor的maximumPoolSize在ScheduledThreadPoolExecutor中没有什么意义(设置maximumPoolSize的大小没有什么效果)。

ScheduledThreadPoolExecutor的执行主要分为两大部分。

  • 当调用ScheduledThreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法时,会向ScheduledThreadPoolExecutor的DelayQueue添加一个实现了RunnableScheduledFutur接口的ScheduledFutureTask。
  • 线程池中的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务。

ScheduledThreadPoolExecutor为了实现周期性的执行任务,对ThreadPoolExecutor做了如下的修改。

  • 使用DelayQueue作为任务队列。
  • 获取任务的方式不同(后文会说明)。
  • 执行周期任务后,增加了额外的处理(后文会说明)。
ScheduledThreadPoolExecutor的实现

前面我们提到过,ScheduledThreadPoolExecutor会把待调度的任务(ScheduledFutureTask)放到一个DelayQueue中。

ScheduledFutureTask主要包含3个成员变量,如下。

  • long型成员变量time,表示这个任务将要被执行的具体时间。
  • long型成员变量sequenceNumber,表示这个任务被添加到ScheduledThreadPoolExecutor中的序号。
  • long型成员变量period,表示任务执行的间隔周期。

DelayQueue封装了一个PriorityQueue,这个PriorityQueue会对队列中的ScheduledFutureTask进行排序。
排序时,time小的排在前面(时间早的任务将被先执行)。

如果两个ScheduledFutureTask的time相同,就比较sequenceNumber,sequenceNumber小的排在前面(也就是说,如果两个任务的执行时间相同,那么先提交的任务将被先执行)。

首先,让我们看看ScheduledThreadPoolExecutor中的线程执行周期任务的过程。下图是ScheduledThreadPoolExecutor中的线程1执行某个周期任务的4个步骤。

在这里插入图片描述

 public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
 
        pool.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("延迟执行");
            }
        },1, TimeUnit.SECONDS);
 
        /**
         * 这个执行周期是固定,不管任务执行多长时间,还是每过3秒中就会产生一个新的任务
         */
        pool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                //这个业务逻辑需要很长的时间,定时任务去统计一张数据上亿的表,财务财务信息,需要30min
                System.out.println("重复执行1");
            }
        },1,3,TimeUnit.SECONDS);
 
        /**
         * 假设12点整执行第一次任务12:00,执行一次任务需要30min,下一次任务 12:30 + 3s 开始执行
         */
        pool.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                //30min
                try {
                    Thread.sleep(60000 * 30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("" + new Date() +"重复执行2");
            }
        },1, 3, TimeUnit.SECONDS);
 }

附: Executors可以创建2种类型的ScheduledThreadPoolExecutor:

  • 包含若干个线程的ScheduledThreadPoolExecutor。 适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程的数量的应用场景。

  • SingleThreadScheduledExecutor。只包含一个线程的ScheduledThreadPoolExecutor。

六、Executor框架

在HotSpot VM的线程模型中,Java线程(java.lang.Thread)被 一对一映射为本地操作系统线程
Java线程启动时会创建一个本地操作系统线程;当该Java线程终止时,这个操作系统线程也会被回收。操作系统会调度所有线程并将它们分配给可用的CPU。
在上层,Java多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器(Executor框架)将这些任务映射为固定数量的线程在底层,操作系统内核将这些线程映射到硬件处理器上。这种两级调度模型的示意图下面有介绍。

从下图中可以看出,应用程序通过Executor框架控制上层的调度;而下层的调度由操作系统内核控制,下层的调度不受应用程序的控制。

一、Executor框架的结构
  • 任务。包括被执行任务需要实现的接口:Runnable接口 或 Callable接口。
  • 任务的执行。包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口(ThreadPoolExecutor
    和 ScheduledThreadPoolExecutor)。
  • 异步计算的结果。包括接口Future和实现Future接口的FutureTask类。
    在这里插入图片描述
    下面是这些类和接口的简介:
  • Executor是一个接口,它是Executor框架的基础,它将任务的提交与任务的执行分离开来。
  • ThreadPoolExecutor 是线程池的核心实现类,用来执行被提交的任务。
  • ScheduledThreadPoolExecutor
    是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。ScheduledThreadPoolExecutor比Timer更灵活,功能更强大。
  • Future接口和实现Future接口的FutureTask类,代表异步计算的结果。
  • Runnable接口和Callable接口的实现类,都可以被ThreadPoolExecutor
    或ScheduledThreadPoolExecutor执行。

Executor框架的使用示意图如下:
在这里插入图片描述

七、ThreadPoolExecutor详解

Executor框架最核心的类是ThreadPoolExecutor,它是线程池的实现类

一、ThreadPoolExecutor的重要参数

corePoolSize:核心线程数

  • 核心线程会一直存活,即使没有任务需要执行
  • 当线程数小于核心线程数时(还未满,就会一直增),即使有线程空闲,线程池也会优先创建新线程处理
  • 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭

queueCapacity:任务队列容量(阻塞队列)

  • 当核心线程数达到最大时,新任务会放在队列中排队等待执行

maxPoolSize:最大线程数

  • 当线程数>corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务,直到线程数量达到maxPoolSize
  • 当线程数已经=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常

keepAliveTime:线程空闲时间

  • 当线程空闲时间达到keepAliveTime时,线程会被销毁,直到线程数量=corePoolSize(因为核心线程会一直存活) 如果allowCoreThreadTimeout=true,则会直到线程数量=0(这个特性需要注意)

rejectedExecutionHandler:任务拒绝处理器(用户可以自定义拒绝后的处理方式)
两种情况会拒绝处理任务:

  • 当线程数已经达到maxPoolSize,且任务队列已满时,会拒绝新任务

  • 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务(并不是立马停止,而是执行完再停止)。若拒绝后,此时,线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置,默认值是AbortPolicy,会抛出异常hreadPoolExecutor类有几个内部实现类来处理这类情况:

    • AbortPolicy 丢弃任务,抛运行时异常
    • CallerRunsPolicy 执行任务(这个策略重试添加当前的任务,他会自动重复调用 execute() 方法,直到成功) 如果执行器已关闭,则丢弃.
    • DiscardPolicy 对拒绝任务直接无声抛弃,没有异常信息
    • DiscardOldestPolicy 对拒绝任务不抛弃,而是抛弃队列里面等待最久的(队列头部的任务将被删除)一个线程,然后把拒绝任务加到队列(Queue是先进先出的任务调度算法,具体策略会咋下面有分析)(如果再次失败,则重复此过程)
    • 实现RejectedExecutionHandler接口,可自定义处理器(可以自己实现然后set进去)
二、ThreadPoolExecutor处理任务的顺序、原理

一个任务通过 execute(Runnable) 方法被添加到线程池,任务就是一个 Runnable 类型的对象,任务的执行方法就是 Runnable 类型对象的 run() 方法。

当一个任务通过 execute(Runnable) 方法欲添加到线程池时,线程池采用的策略如下(即添加任务的策略):

  • 如果此时线程池中的数量小于 corePoolSize ,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  • 如果此时线程池中的数量等于 corePoolSize ,但是缓冲队列 workQueue 未满,那么任务被放入缓冲队列。
  • 如果此时线程池中的数量大于 corePoolSize ,缓冲队列 workQueue
    满,并且线程池中的数量小于maximumPoolSize ,建新的线程来处理被添加的任务。
  • 如果此时线程池中的数量大于 corePoolSize ,缓冲队列 workQueue
    满,并且线程池中的数量等于maximumPoolSize ,那么通过 handler 所指定的策略来处理此任务。

任务处理的优先级(顺序)为:

核心线程 corePoolSize 、任务队列 workQueue 、最大线程 maximumPoolSize ,如果三者都满了,使用
handler处理被拒绝的任务。当线程池中的线程数量大于 corePoolSize 时,如果某线程空闲时间超过 keepAliveTime
,线程将被终止。这样,线程池可以动态的调整池中的线程数。

线程池处理流程图:
在这里插入图片描述

public ThreadPoolExecutor(int corePoolSize,
                           int maximumPoolSize,
                           long keepAliveTime,
                           TimeUnit unit,
                           BlockingQueue<Runnable> workQueue,
                           RejectedExecutionHandler handler) {
     this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
          Executors.defaultThreadFactory(), handler);
 }
 发现我们可以指定workQueue和handler。当然还有其余的构造函数,有类似的效果

线程池构造函数7大参数解释:

  • corePoolSize:线程池中存在的核心线程数,也就是线程池中不会被释放的线程数量,有网友可能会有疑问,如果线程不会被释放,那不会一直被占着资源?不会,存活的线程不会跟其它线程抢占资源,相当于处于一个假死状态,有任务过来就会唤醒存活的线程执行任务

  • maximumPoolSize:线程池中允许的最大线程数,也就是任务数 > corePoolSize并且阻塞队列也已经放满了任务,这是最大线程数就会起作用,则创建新的线程执行任务,前提是任务数 < maximumPoolSize

  • keepAliveTime:线程空闲时的存活时间,大于corePoolSize数量被创建的线程在没有任务执行的情况下的存活时间,一旦达到存活时间线程则会被回收释放

  • unit:空闲线程的保留时间单位。

  • BlockingQueue workQueue:用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。

1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务

2.LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务

3.SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态

4.priorityBlockingQuene:具有优先级的无界阻塞队列

5.DelayQueue: 支持延时获取元素的无界阻塞队列,在创建元素时可以指定多久才能从队列中获取当前元素

6.LinkedTransferQueue: 由一个链表结构组成的无界阻塞TransferQueue队列

7.LinkedBlockingDeque: 由一个链表结构组成的双向阻塞队列,可以从队列的两端插入和移出元素
  • threadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程做些更有意义的事情,比如设置daemon和优先级等等

  • handler:线程池的淘汰策略,当过来的任务数 > corePoolSize, 阻塞队列已满,> maximumPoolSize, 这时handler就会起作用,会采用一种策略来处理过来的任务数,JDK也提供了4中淘汰策略:

     1. AbortPolicy:直接抛出异常,默认策略
    
     2. CallerRunsPolicy:用调用者所在的线程来执行任务
    
     3. DiscardOldestPolicy: 丢弃队列中最近的一个任务,并执行当前任务
    
     4. DiscardPolicy: 不处理,直接丢弃掉
    

    当然我们也可以自定义自己的淘汰策略,需要实现接口RejectedExecutionHandler,重写rejectedExecution方法,可以自定义加一些日志或者持久化不能处理的任务

八、自定义线程池

我们将代码跟入深一点,就会发现四种线程池最终调用的还是new
ThreadPoolExecutor(),只不过传入参数不同而已。

如:public static ExecutorService newCachedThreadPool() {
       return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
 }

在《阿里巴巴开发手册》中明确指出不建议我们直接Executors创建现成线程池,我们可以看到上面的四种线程池它们有一个参数也就是要传入的阻塞队列。

  • newSingleThreadPool()传入的阻塞队列LinkedBlockingQueue()基于链表实现的阻塞队列,队列最大长度为Integer.MAX_VALUE,也就是这个队列可以承载2^31大小的线程,这时候服务器肯定会承受不住这么多的线程,会造成服务器崩溃的场景

  • newFixedThreadPool()跟newSingleThreadPool()一样,传入的都是相当于无界的一个队列

  • newCachedThreadPool()和newScheduledThreadPool()可以看到传入的一个最大线程数为Integer.MAX_VALUE,也就是会一直创建新的线程去执行任务,这时候服务器也是承受不住的,会造成服务器崩溃。

上述原因也就是阿里开发手册为什么不建议我们直接使用创建好的线程池使用,如果开发者稍有一不注意,就会造成服务器线程崩溃。所以我们应该使用底层的ThreadPoolExecutor()类创建线程池,这样自己能够清楚的知道自己设置线程池和队列大小

这四种不同类型的线程池,因为都是通过 ThreadPoolExecutor这个核心类来创建的,如果我们要自定义线程池,那么也是通过这个类来实现的。

以下是自定义线程池,使用了有界队列,自定义ThreadFactory和拒绝策略的demo:

public class ThreadTest {

    public static void main(String[] args) throws InterruptedException, IOException {
        int corePoolSize = 2;
        int maximumPoolSize = 4;
        long keepAliveTime = 10;
        TimeUnit unit = TimeUnit.SECONDS;
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
        ThreadFactory threadFactory = new NameTreadFactory();
        RejectedExecutionHandler handler = new MyIgnorePolicy();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit,
                workQueue, threadFactory, handler);
        executor.prestartAllCoreThreads(); // 预启动所有核心线程
        
        for (int i = 1; i <= 10; i++) {
            MyTask task = new MyTask(String.valueOf(i));
            executor.execute(task);
        }

        System.in.read(); //阻塞主线程
    }

    static class NameTreadFactory implements ThreadFactory {

        private final AtomicInteger mThreadNum = new AtomicInteger(1);

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r, "my-thread-" + mThreadNum.getAndIncrement());
            System.out.println(t.getName() + " has been created");
            return t;
        }
    }

    public static class MyIgnorePolicy implements RejectedExecutionHandler {

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            doLog(r, e);
        }

        private void doLog(Runnable r, ThreadPoolExecutor e) {
            // 可做日志记录等
            System.err.println( r.toString() + " rejected");
//          System.out.println("completedTaskCount: " + e.getCompletedTaskCount());
        }
    }

    static class MyTask implements Runnable {
        private String name;

        public MyTask(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            try {
                System.out.println(this.toString() + " is running!");
                Thread.sleep(3000); //让任务执行慢点
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        public String getName() {
            return name;
        }

        @Override
        public String toString() {
            return "MyTask [name=" + name + "]";
        }
    }
}

输出结果如下:
在这里插入图片描述
其中线程线程1-4先占满了核心线程和最大线程数量,然后4、5线程进入等待队列,7-10线程被直接忽略拒绝执行,等1-4线程中有线程执行完后通知4、5线程继续执行。

总结,通过自定义线程池,我们可以更好的让线程池为我们所用,更加适应我的实际场景。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值