ThreadPool 线程池

为什么使用多线程 ?


  • 以前 : CPU 单核电脑 , CPU 通过快速的切换 , 只在视觉上感觉是在同时运行不同的任务 
  • 现在 : 多核电脑 , 多个线程 各自跑在独立的 CPU 上 , CPU 不用切换 , 效率高

线程池的优势 :


线程池

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

特点 : 线程复用 , 控制最大并发数, 管理线程

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

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

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

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

 

线程池如何使用

1 .  Executors.newFixedThreadPool ( int )

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
 
//newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue

2 .  Executors.newSingleThreadExecutor ( )

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
 
//newSingleThreadExecutor 创建的线程池corePoolSize和maximumPoolSize值都是1,它使用的是LinkedBlockingQueue

3 .  Executors.newCachedThreadPool ( )

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
//newCachedThreadPool创建的线程池将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,
//它使用的是SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。

注意 : 我们可以发现 他们底层都使用的 ThreadPoolExecutor 这个类

ThreadPoolExecutor 底层原理

 

线程池几个重要参数 ( 7大参数 )


线程池的七大参数

  1. corePoolSize : 线程池中的常驻核心线程数
  2. maximunPoolSize : 线程池中能够容纳同时执行的最大线程数, 此值必须大于1
  3. KeepAliveTime : 多余的空闲线程的存活时间 , 当前池中线程数量超过 corePoolSize 时 , 当空闲时间达到 KeepAliveTime时 , 多余的线程会被摧毁掉 , 只剩下corePoolSize个线程为止
  4. unit : keepAliveTime 的单位
  5. workQueue : 任务队列 , 被提交但尚未被执行的任务
  6. threadFactory : 表示生成线程池中工作线程的线程工厂 , 用于创建线程 , 一般默认的即可
  7. handler : 拒绝策略, 当队列满了 , 并且工作线程大于等于线程池的最大线程数时 , 如何来拒绝请求执行的 runnable 策略

 泳道图 :

 

线程池的工作流程

  1. 在创建了线程后, 开始等待请求
  2. 当调用 execute ( ) 方法添加一个请求任务时 线程池会做出如下判断 :
    1. 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务.
    2. 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;注意 : 此时并不会立即扩容创建非核心线程 , 可以看出 阻塞队列相当于缓冲区 , 减小 CPU 的压力.
    3. 如果这个时候队列也满了 , 且正在运行的线程数量还小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这些任务.
    4. 如果 队列满了 且 正在运行的线程数量大于或等于maximumPoolSize ( 线程池也满了 )  ,那么线程池会 启动饱和拒绝策略来执行.
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行.
  4. 当一个线程无事可做超过一定的时间( keepAliveTime )时,线程会判断 :
    1. 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉.
    2. 所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小.

线程池的拒绝策略


等待队列已经满了 , 再也塞不下新任务了 , 同时 线程池中的 max 线程也达到了 , 无法继续为新任务服务......

这个是时候我们就需要拒绝策略机制合理的处理这个问题。


JDK内置的拒绝策略

  1. AbortPolicy (默认):直接抛出 RejectedExecutionException 异常 阻止系统正常运行
    1. 异常 : java并发包的线程池拒绝策略异常

     

  2. CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
    1. 就是说 : 我一共就有5个线程 , 你来了10个 , 我处理不过来 , 没有处理的任务就原路返回 , 注意 : 也是有概率处理完的
  3. DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。
  4. DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。

在工作中 单一的 / 固定数的 / 可变的 这三种创建线程池的方法哪个用的多?


答案是一个都不用,我们工作中只能使用 自定义的

Executors 中JDK已经给你提供了,为什么不用? ( Out Of Memory )

 

如何自定义线程池


根据上面的三个创建线程池的方式 , 我们可以发现 底层都是new了一个叫做 ThreadPool Executor 的线程池对象 , 那么 我们直接自己 new一个 ThreadPool Executor 的线程池对象 , 然后根据业务的需求 , 指定合理的参数就可以了  

package com.threadpool;

import java.util.concurrent.*;

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                2L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        try {
            for (int i = 1; i <= 8; i++) {
                final int tmpI = i;
                threadPool.submit(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 处理业务" + "\t 顾客: " + tmpI);
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }

    }

}

生产中如设置合理线程池大小的设置

根据任务类型来配置线程池大小

如果是 CPU 密集型任务 

  • 处理复杂的逻辑 和 算法的操作 , 可能要处理很久 非常消耗CPU资源

如果是 CPU 密集型任务,那么就意味着 CPU 是稀缺资源,这个时候我们通常不能通过增加线程数来提高计算能力,因为线程数量太多,会导致频繁的上下文切换,一般这种情况下,

建议合理的线程数值是 N(CPU)数 + 1。如果是八核就配置9 ,避免CPU告高速切换

如果是 I/O 密集型任务

  • 简单的业务逻辑处理 , 但是数量巨大 , CPU 的消耗很少 , 大部分都是在等待读写操作

如果是 I/O 密集型任务,就说明需要较多的等待,这个时候可以参考 Brain Goetz 的推荐方法

线程数 = CPU核数 × (1 + 平均等待时间/平均工作时间)。

参考值可以是 N(CPU) 核数 * 2。

具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值