java线程池ThreadPoolExecutor

目录

一、概念

二、工作原理

1. 初始化线程池

2. 提交任务

3.ThreadPoolExecutor的参数列表

处理方式有4种

处理方式总结

三、创建线程池的两种方案

1.ExecutorService接口的实现类ThreadPoolExecutor自己定义线程池

用Runnable接口的实体类来接

用Callable接口的实体类来接

2.Executors(线程池的工具类)方法newFixedThreadPool(int 线程数量)

两种方式的优缺点

四、两种终结线程池的方法

shutdown()

shutdownNow()

五、来聊聊一些小细节

说到临时线程,临时线程什么时候被调用呢?上面的代码中可以看到一共出书了三次线程池对象,拿到了4个数据的变化,

总结


一、概念

线程池就是一个可以复用线程的技术,线程池可以解决线程冗余的问题,如果每个用户都要创建一个新线程来处理,当数量多了会影响系统的性能。这篇文章主要说了线程池的执行流程,临时线程什么时候开始工作,创建线程池的方式。

二、工作原理

线程池由线程池管理器、任务队列和一组工作线程组成。 比如有3个线程对6个任务,那么为了复用线程,一个线程处理完一个任务以后不会销毁,而是处理下一个任务。

1. 初始化线程池

线程池管理器创建一定数量的工作线程,并将它们放入线程池中等待任务。

ThreadPoolExecutor tp = new ThreadPoolExecutor(
        3,//线程数
        10,//最大线程数=线程数+临时线程
        1000,//临时线程的保留时间
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(6),//等待队列长度 6
        Executors.defaultThreadFactory(),//生产线程的工厂
        new ThreadPoolExecutor.AbortPolicy()
);

2. 提交任务

当有任务需要执行时,应用程序通过将任务提交给线程池管理器来执行。任务可以是一个实现Runnable或Callable接口的对象。

class MyRunnable implements Runnable{
    @Override
    public void run() {
            System.out.println(Thread.currentThread().getName()+"~~");
    }
}
MyRunnable mr = new MyRunnable();
tp.execute(mr);

3.ThreadPoolExecutor的参数列表

new ThreadPoolExecutor(
    corePoolSize   //核心线程数
    maximumPoolSize  //任务队列容量(阻塞队列)//最大线程数=线程数+临时线程
    keepAliveTime   //临时线程的保留时间
    TimeUnit.时间   //可以是NANO(纳秒),MICRO(微秒),MILLI(毫秒),秒,分,                                    时,天做为临时线程的存活时间。
    BlockingQueue<Runnable>   //等待队列长度(阻塞队列)
    ThreadFactory  //生产线程的工厂
    RejectedExecutionHandler  //超出任务队列容量+阻塞队列长度的处理方式

);

处理方式有4种
  1. AbortPolicy(默认):直接抛出RejectedExecutionException异常,阻止任务的提交,表示无法处理新任务。
  2. CallerRunsPolicy:将任务返回给提交任务的线程执行。这意味着提交任务的线程将执行该任务,从而降低了新任务的提交速度。
  3. DiscardPolicy:默默地丢弃无法处理的任务,而不抛出任何异常。如果线程池已满,新任务将被丢弃。
  4. DiscardOldestPolicy:丢弃最早的未处理任务,然后尝试将新任务重新加入队列。这保证了任务的执行,但也可能丢失一些任务。
处理方式总结
  1. AbortPolicy可用于确保任务不会丢失但会导致异常;
  2. CallerRunsPolicy可用于确保任务一定会执行但可能导致执行线程变慢;
  3. DiscardPolicy适用于对任务丢失不敏感的场景;
  4. DiscardOldestPolicy适用于快速执行新任务而不关心旧任务的场景。

三、创建线程池的两种方案

1.ExecutorService接口的实现类ThreadPoolExecutor自己定义线程池

用Runnable接口的实体类来接
  1. new ThreadPoolExecutor对象tp,设置各个参数。
  2. 创建实现Runnable接口的实体类MyRunnable类的对象mr,重写run方法
  3. 把mr放进tp的execute方法的参数列表中执行任务
用Callable接口的实体类来接
  1. new ThreadPoolExecutor对象tp,设置各个参数。
  2. 创建实现Runnable接口的实体类MyRunnable类的对象mr,重写Call方法
  3. 把mr放进tp的submit方法的参数列表中,返回Future对象f
  4. 通过f.get();执行任务
ThreadPoolExecutor tp = new ThreadPoolExecutor(
        3,
        10,
        100,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(4),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy()
);

Future<String> f1 = tp.submit(new MyCallable(100));
Future<String> f2 = tp.submit(new MyCallable(222));
Future<String> f3 = tp.submit(new MyCallable(1121));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());

2.Executors(线程池的工具类)方法newFixedThreadPool(int 线程数量)

  1. ExecutorService pool = Executors.newFixedThreadPool(线程数量);
    拿到线程池对象后面的步骤和上面一样。
ExecutorService pool = Executors.newFixedThreadPool(10);
MyRunnable2 mr2 = new MyRunnable2();
pool.execute(mr2);//用Runnable的实现类
Future<String> f1 = pool.submit(new MyCallable(100));//用Callable的实现类
Future<String> f2 = pool.submit(new MyCallable(220));
Future<String> f3 = pool.submit(new MyCallable(1110));

System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
两种方式的优缺点

用工具类Executors,创建线程对象对象比较方便。但是存在比较大的弊端

  1. Executors提供的三种创建线程池对象的方法FixedThreadPool、SinglePool、CachedThreadPool.
  2. FixedThreadPool、SinglePool允许的阻塞队列长度为MAX_VALUE,可能会导致堆积大量的请求,从而导致OOM。
  3. CachedThreadPool允许的创建线程数量为MAX_VALUE,可能会导致创建大量的线程,从而导致OOM。

四、两种终结线程池的方法

shutdown()

强行终止线程池,但会等候所有任务执行完毕

shutdownNow()

强行终止线程池,清空等候区的任务,但是执行中的会继续执行完。

五、来聊聊一些小细节

在上面的内容已经初始化的线程池,三个核心线程+7个临时线程+6个阻塞队列 我们来运行看看能得到什么。
如果直接输出线程池对象我们能得到下面几个数据
[Running, pool size = , active threads = , queued tasks = , completed tasks = ]

  1. Runing代表线程池正在运行。
  2. pool size代表线程池的大小有多少个任务进了线程池。
  3. active threads代表正在执行任务的线程数量。
  4. queued tasks代表阻塞队列(也就是等待队列)数量。
  5. completed tasks代表已完成的任务数量。
MyRunnable mr = new MyRunnable();
        System.out.println(tp);
        tp.execute(mr);
        tp.execute(mr);
        tp.execute(mr);
        System.out.println(tp);
//[Running, pool size = 3, active threads = 3, queued tasks = 0, completed tasks = 0]
        tp.execute(mr);
        tp.execute(mr);
        tp.execute(mr);
        tp.execute(mr);
        tp.execute(mr);
        tp.execute(mr);
        tp.execute(mr);//10
        System.out.println(tp);
//[Running, pool size = 4, active threads = 4, queued tasks = 6, completed tasks = 0]
//因为超过了6个队列,所以调用了一个临时线程
//3个线程执行中,其他线程要如果没有超过6个等待队列的话,不会调用临时线程
        tp.execute(mr);
        tp.execute(mr);
        tp.execute(mr);
        tp.execute(mr);
        tp.execute(mr);
        tp.execute(mr);
        tp.shutdown();//强行终止线程池,但会等候所有任务执行完毕
//tp.shutdownNow();//强行终止线程池,结束掉所有在等候区的任务,但是还在执行的线程会继续执行
        System.out.println(tp);
//[Running, pool size = 10, active threads = 10, queued tasks = 6, completed tasks = 0]
说到临时线程,临时线程什么时候被调用呢?上面的代码中可以看到一共出书了三次线程池对象,拿到了4个数据的变化,

第一次只进行了3个任务,先调用了3个核心线程去处理。
第二次又进来了7个任务,但是在工作的线程只有4个,因为临时线程只有在阻塞队列溢出的时候才开始工作,进来7个任务阻塞队列的容量是6,所以溢出了一个,就调用了一个1临时线程去处理溢出的任务。
第三次又进来了6个任务,这时候因为阻塞队列在刚刚已经装满溢出了,所以剩下的6个临时线程也参与处理任务,所以这时候线程池大小是10,正在执行任务的线程数也是10.

总结

  • 线程池对象有两种创建方式,一是通过 new ThreadPoolExecutor自定义线程池的参数,二是通过工具类Executors的三种方法创建。
  • 线程池对象同样有两种方式执行,一是通过Runnable接口的实体类来执行,二是通过Callable接口的实体类来执行。
  • 线程池的执行流程是,先执行核心线程,后进入队列等待,队列等待溢出执行临时线程,当临时线程也不够用了就交给处理方式处理。

小白第一次写博客,如果有不对的地方,各位大佬敬请指正,多多关照!(抱拳)