java线程池详解

tips:右侧有导航

什么是线程池

线程池做的工作只是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超过数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行

线程池有什么优点

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗

  2. 提高响应速度,当任务到达时,任务可以不需要等待线程创建就能立即执行。

  3. 提高线程的课管理型。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,是用线程池可以进行统一的分配,调优,和监控

线程池有什么特点

线程复用,控制并发数;管理线程

线程池如何创建

Executor框架

java中的线程池 是通过 Executor 框架实现的,该框架中用到了以下几个类

Executors 是 Executor的工具类,类似于 Arrays是Array的工具类在这里插入图片描述

线程池玩的就是 ThreadPoolExecutor

Executors怎样创建线程池

使用Executors创建线程池常见的有3中方式

Executors.newFixedThreadPool ( int ) ------- 创建固定数量线程的线程池
/**
 * 使用 Executors.newFixedThreadPool(5); 方法创建一个线程
 *
 * 将线程池比喻成一个银行, 银行中共有5个开放的窗口办理业务, 现在有10个人来办理业务
 */
public class ExecutorsFixThread {
    public static void main(String[] args) {
        //创建5个固定数量线程的
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 10; i++) {
            fixedThreadPool.execute(() ->{
                System.out.println(Thread.currentThread().getName() + "\t开始办理");
            });
        }

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

执行结果如下
在这里插入图片描述

  1. 从代码执行结果可以知道 线程池中 线程可以复用

  2. 如果不关闭线程池,线程池会保持连接 不会关闭

使用 Executors.newFixedThreadPool 底层代码
在这里插入图片描述

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待

Executors.newFixedThreadPool 创建的 corePoolSize 和 maximumPoolSize是一样的,使用的是 LinkedBlockingQueue

Executors.newSingleThreadExecutor ( ) ------- 创建单个线程的线程池
public class ExecutorsFixThread {
    public static void main(String[] args) {
        //创建5个固定数量线程的
        ExecutorService fixedThreadPool = Executors.newSingleThreadExecutor();

        for (int i = 0; i < 10; i++) {
            fixedThreadPool.execute(() ->{
                System.out.println(Thread.currentThread().getName() + "\t开始办理");
            });
        }

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

执行结果如下
在这里插入图片描述

Executors.newSingleThreadExecutor 源码如下
在这里插入图片描述

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行

Executors.newSingleThreadExecutor 创建的 corePoolSize 和 maximumPoolSize都是1,使用的是 LinkedBlockingQueue

Executors.newCachedThreadPool ( ) ---- 创建一个带缓冲的线程
public class ExecutorsFixThread {
    public static void main(String[] args) {
        //创建5个固定数量线程的
        ExecutorService fixedThreadPool = Executors.newCachedThreadPool();

        for (int i = 0; i < 10; i++) {
            //try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
            fixedThreadPool.execute(() ->{
                System.out.println(Thread.currentThread().getName() + "\t开始办理");
            });
        }

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

如果注释停顿的代码块
在这里插入图片描述

如果不注释停顿的代码块
在这里插入图片描述

此例可以说明 如果线程够用,就会只使用线程池中的一个线程,如果线程不够用,线程池就会多创建几个线程来用

源码如下:
8574

创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵回收空闲线程,若无可回收,则新建线程

Executors.newCachedThreadPool 将核心线程数设置成0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说 来了任务就创建线程运行,当线程空闲60s后,就销毁线程

工作中使用什么方式创建线程池

不使用Executors工具类创建线程池 阿里巴巴开发手册中说 不允许使用 Executors去创建,而是通过new ThreadPoolExecutor (... ) 的方式创建多线程

原因:

  1. 使用FixedThreadPool 和 SingleThreadPool:

允许请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM

  1. 使用 CachedThreadPool 和 ScheduledThreadPool:

语序的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM

线程池中的7大参数:

  1. corePoolSize:线程池中的常驻核心线程数 比如:银行中 周一到周5每天至少有5个窗口办理业务,常驻核心线程数就是5

  2. maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1,maximumPoolSize = corePoolSize + 其他

  3. keepAliveTime:多余的空闲线程的存活时间 当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到只剩下corePoolSize个线程为止

  4. unit:keepAliveTime的单位

  5. workQueue:任务队列,被提交单上位被执行的任务

  6. threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可

  7. handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数 ( maximumPoolSize ) 时如何来拒绝请求执行的 runnable的策略

解释5:银行共有3个窗口,但是现在来了4个人办理业务,前3个人已经占用的银行的窗口,第4个人就只能待在候客区等待,等待的这个线程 就会被装在 workQueue(阻塞队列) 中

解释6:线程池中存在已经new好的线程,这些线程是怎样来的呢,就是通过 threadFactory 线程工厂 来创建的,一般使用默认的 threadFactory,但是如果想要修改的话 也是可以的,在说白一点 格力空调出厂的时候是白色的,自己可以把这个空调刷成红色,但是没有这个必要,正常情况下使用默认的。

解释7:什么叫做拒绝策略,拒绝策略有几种类型,应该如何使用
是什么

等待队列已经排满了,在也塞不下新任务的同时,maximumPoolSize也达到了最大值,无法继续为新任务服务

此时就应该由拒绝策略机制处理了

共有几种类型

拒绝策略共有4中情况,这4中拒绝策略全部都实现了 RejectedExecutionHandler接口

AbortPolicy ( 默认 ) :直接抛出 RejectedExecutionException异常,阻止系统正常运行
public class ThreadPoolAbortPolicyDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //类似于一池N个工作线程,银行开通了N个受理窗口
        ExecutorService executorService = new ThreadPoolExecutor(
                1
                , 5
                , 10
                , TimeUnit.SECONDS
                , new LinkedBlockingQueue<>(3)
                , Executors.defaultThreadFactory()
                , new ThreadPoolExecutor.AbortPolicy()
        );
        try {
          	// 是否抛出异常由 最大线程数和 阻塞队列联合决定, 当此时线程最大可以承受的压力是 5 + 3 = 8 超过8时会报错
            for (int j = 1; j <= 8; j++) {
                executorService.execute(() ->{
                    // 让线程sleep 5s 的目的是防止当前线程已经执行完成, 返回线程池后又被复用
                    try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
                    System.out.println(Thread.currentThread().getName() + ", 开始执行");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //关闭线程池
            executorService.shutdown();
        }
    }
}

当 j 的值为 8 时,程序正常执行
在这里插入图片描述

当 j 的值为 9 时出现异常
在这里插入图片描述

CallRunsPolicy: 调用者运行,一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量

举个生活例子:开发人员和测试人员一起测试开发提测的功能,开发人员提测了以后就没管了给测试人员测试,测试测到一半的时候发现有bug,但是自己不能写代码不能修改这个bug,只能将通知提测的开发人员,修改这个bug ( 谁调用的,就将这个bug回退给谁,这个bug是一定要处理的 )

代码示例:

public class ThreadPoolCallerRunsPolicyDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //类似于一池N个工作线程,银行开通了N个受理窗口
        ExecutorService executorService = new ThreadPoolExecutor(
                1
                , 5
                , 10
                , TimeUnit.SECONDS
                , new LinkedBlockingQueue<>(3)
                , Executors.defaultThreadFactory()
          // CallRunsPolicy
                , new ThreadPoolExecutor.CallerRunsPolicy()
        );
        try {
            for (int j = 1; j <= 15; j++) {
                executorService.execute(() ->{
                    // 让线程sleep 5s 的目的是防止当前线程已经执行完成, 返回线程池后又被复用
                    try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
                    System.out.println(Thread.currentThread().getName() + ", 开始执行");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //关闭线程池
            executorService.shutdown();
        }
    }
}

从代码可以看到,虽然线程池可以承受的最压力是8,但是我设置的是15, 程序没有抛出异常,而是 将线程池处理不过来的任务 交给了main线程去执行了,

在生活实例中, main线程相当于是开发人员,线程池相当于是测试人员,现在测试人员处理不过来了,那么只能将他们处理不过来的任务交给开发人员处理 (由main线程执行 )
在这里插入图片描述

main线程执行的任务 + 线程池执行的任务 = 15

DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
public class ThreadPoolDiscardOldestPolicyDemo {
    public static void main(String[] args) {
        //类似于一池N个工作线程,银行开通了N个受理窗口
        ExecutorService executorService = new ThreadPoolExecutor(
                1
                , 5
                , 10
                , TimeUnit.SECONDS
                , new LinkedBlockingQueue<>(3)
                , Executors.defaultThreadFactory()
                // CallRunsPolicy
                , new ThreadPoolExecutor.DiscardOldestPolicy()
        );
        try {
            for (int j = 1; j <= 15; j++) {
                int finalJ = j;
                executorService.execute(() ->{
                    // 让线程sleep 5s 的目的是防止当前线程已经执行完成, 返回线程池后又被复用
                    try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
                    System.out.println(finalJ + ", "+Thread.currentThread().getName() + ", 开始执行");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //关闭线程池
            executorService.shutdown();
        }
    }
}

执行结果如下,虽然最后一个值是 15,但是总共执行的任务并没有15个,实际执行的只有8个
在这里插入图片描述

DiscardPolicy:该策略默默的丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略
public class ThreadPoolDiscardPolicyDemo {

    public static void main(String[] args) {
        //类似于一池N个工作线程,银行开通了N个受理窗口
        ExecutorService executorService = new ThreadPoolExecutor(
                1
                , 5
                , 10
                , TimeUnit.SECONDS
                , new LinkedBlockingQueue<>(3)
                , Executors.defaultThreadFactory()
                , new ThreadPoolExecutor.DiscardPolicy()
        );
        try {
            for (int j = 1; j <= 15; j++) {
                int finalJ = j;
                executorService.execute(() ->{
                    // 让线程sleep 5s 的目的是防止当前线程已经执行完成, 返回线程池后又被复用
                    try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                    System.out.println(finalJ + ", "+Thread.currentThread().getName() + ", 开始执行");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //关闭线程池
            executorService.shutdown();
        }
    }
}

执行结果如下
在这里插入图片描述

DiscardPolicy 和 DiscardOldestPolicy的区别就是:DiscardPolicy不管怎样执行,都是丢弃掉 线程池最大压力 ( 8个 ) 之后的那几个任务, 而DiscardOldestPolicy是丢弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务

线程池原理

例子 – 和生活中的实例 ( 银行办理业务 ) 结合在一起:
在这里插入图片描述

核心线程数:目前银行开放的窗口,最大核心数 包含核心线程数 + 其他

最大线程数:银行所有可以开放的窗口

阻塞队列: 等候办理业务的候客区

  1. 在创建了线程池后,开始等待请求 ------ 新开了一家银行后 开始办理业务

  2. 当调用execute() 方法添加一个请求任务时,线程池会做出如下判断 ------ 当有一个人来办理业务的时候开始执行

    1. 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务: ------ 如果当日开放的窗口没有人,那么直接就去办理业务

    2. 如果正在运行的线程数量大于或等于 corePoolSize,将这个任务放入到阻塞队列中 ------ 如果当日开放的所有窗口有人,就去候客区等着(候客区的位子也是有限的)

    3. 如果队列满了,且正在运行的线程数量小于 maximumPoolSize,那么要创建非核心线程立刻运行这个任务

      1. ------ 如果当日开放的窗口满了,阻塞队列也满了,但是银行还有多余的可以开放的窗口,那么此时 开放多余的窗口,但是 新来的线程(不在任务队列,也不在等候区,而是从大门外进来的新用户) 直接去刚刚开放的窗口办理业务
    4. 如果队列满了,且正在运行的线程数量大于等于 maximumPoolSize,那么线程池会启动饱和策略来执行。 ------ 如果最大核心线程数满了,等候区也满了,此时就会启动拒绝策略了

  3. 当一个线程任务完成时,他会从队列中取下一个任务来执行 ------ 当前一个人已经办完业务了,那么窗口就会让等候区的人来办理业务

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

    1. 如果当前运行的线程数大于corePoolSize,那么这个线程就会被停掉,所有线程池的所有任务完成后,他最终会收缩到corePoolSize的大小
    2. ----- 当银行当日客流高峰已经过去了,且今日核心工作人员以外的来加班的人已经无事可做了,那么领导为了节省开销,就会让这些来加班的人回家
验证2.3
public class TestNewThreadWhenBlockingQueueFull {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 1; i <= 8; i++) {
            int finalI = i;
            threadPoolExecutor.execute(() -> {
              	// 目的是让当前线程等待,不然CPU执行速度太快了,可能发现不了效果
                try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(Thread.currentThread().getName()+"号窗口开始服务,"+ finalI +"号顾客");
            });
        }
	      // 关闭线程池
      	threadPoolExecutor.shutdown();
    }
}

运行结果:
在这里插入图片描述

运行结果中 按道理来说,

1号窗口 1号顾客,

2号窗口 2号顾客,

3号窗口如果不是直接执行新来的人的话 就是阻塞队列中的3号顾客,但是 现在是执行新来的人,所以就变成了6号顾客,7,8号顾客同理

最后开始执行 3,4,5号顾客

示意图如下:
在这里插入图片描述

怎样手写一个线程池

按照线程池原理图中的例子来写

ExecutorService executorService = new ThreadPoolExecutor(
                	2
                , 5
                , 10
                , TimeUnit.SECONDS
                , new LinkedBlockingQueue<>(4)
  				//线程工厂都使用默认的 (照着源码抄就完事了)
                , Executors.defaultThreadFactory()
                , new ThreadPoolExecutor.AbortPolicy()
        );

注意:

//LinkedBlockingQueue 一定要附上初始值,如果使用空参数的构造方法阻塞队列中的初始值是 Integer.MAX_VALUE
在这里插入图片描述

赋上初始值 使用的不是一个构造方法
在这里插入图片描述

线程池中的最大线程数 应该如何配置

什么叫做单核CPU,多核CPU

举个例子:

单核CPU就是 一个小丑丢多个球

多核CPU就是 多个小丑 没人丢1个球

怎样通过代码获取cpu的核心线程数
int i = Runtime.getRuntime().availableProcessors();
根据业务类型来区分,业务分为CPU密集型和IO密集型
CPU密集型
什么是CPU密集型

该任务需要大量的运算,而没有阻塞,CPU一直全速运行,

CPU密集型任务只有在真正的多核CPU上才可以得到加速 ( 通过多线程 ) ,在单核CPU上,无论开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些

如何配置

CPU密集型任务配置尽可能少的线程数量

公式:CPU核数 + 1个线程的线程池

IO密集型
是什么:

该任务需要大量的IO,既大量的阻塞

在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待,所以IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间

IO密集型时 大部分线程都阻塞,所以需要多配置线程数

方式1:IO密集型任务线程并不是一直在执行任务,应该配置尽可能多的线程 公式: CPU核数 * 2

方式2:CPU核心数 /(1-阻塞系数) , 阻塞系数一般在0.8~0.9之间

举个例子: 8核CPU: 8/(1 - 0.9) = 80 个线程数

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值