Fork me on Gitee

线程池

线程池——治理线程的法宝

1. 线程池的自我介绍

  • 线程池的重要性

  • 什么是池

    • 软件中的“池”,可以理解为计划经济

    • 如果不使用线程池,每个任务都新开一个线程处理

      • 一个线程
      • for循环创建线程
      • 当任务数量上升到1000
    • 这样的开销太大,我们希望有固定数量的线程,来执行这1000个线程,这样就避免了反复创建并销毁线程所带来的开销问题

为什么要使用线程池

问题一:反复创建线程开销大

问题二:过多的线程会占用太多内存

  • 解决以上两个问题的思路
    • 用少量的线程——避免内存占用过多
    • 让这部分线程都保持工作,且可以反复执行任务——避免生命周期的损耗

线程池的好处

  • 加速响应速度
  • 合理利用CPU和内存(灵活配置达到性能最佳状态)
  • 统一管理

线程池适合应用的场景

  • 服务器接受到大量请求时,使用线程池技术是非常合适的。它可以大大减少线程的创建和销毁次数,提搞服务器的工作效率。

2. 创建和停止线程

线程池构造函数的参数

参数名 类型 含义
corePoolSize int 核心线程数
maxPoolSize int 最大线程数
KeepAliveTime long 保持存活时间
workQueue BlockQueue 任务存储队列
ThreadFactory ThreadFactory 当线程池需要新的线程的时候,会使用threadFactory来生成新的线程
Handler RejectExecutionHandler 由于线程池无法接收新提交的任务的拒绝策略
  • corePoolSize:指的是核心线程数,线程池在初始化后,默认情况下,线程池中并没有任何线程,线程池会等待有任务到来时,在创建新线程去执行任务。
  • maxPoolSize:线程池有可能会在核心线程数的基础上,额外增加一些线程,但是这些新增加的线程数有一个上限,这就是最大量maxPoolSize。
  • KeepAliveTime:如果线程池当前的线程数多于corePoolSize,那么多于的线程空闲时间超过KeepAliveTime,它们就会终止。

image-20240229220555353

如上图所示,core Pool Size为初始化线程池容量大小,随着线程数量的增加,当前线程数会增加,直到等于最大线程数。

添加线程规则

image-20240302232528093

如果用流程图来描述如下图

image-20240229220819847

根据上图可知,是否需要增加线程的判断顺序是:

  • corePoolSize
  • workQueue
  • maxPoolSize

image-20240229221006018

增减线程的特点

  1. 通过设置corePoolSize和maximumPoolSize相同,就可以创建固定大小的线程池。如newFixThreadPool
  2. 线程池希望保持较少的线程数,并且只有在负载变得很大时才增加它。
  3. 通过设置maximumPoolSize为很高的值,例如 Integer.MAX_VALUE。才可以允许线程池容纳任意数量的并发任务。
  4. 只有在队列填满时才创建多于corePoolSize的线程,所以如果使用的是无界队列(例如LinkedBlockQueue),那么线程数就不会超过corePoolSize。

image-20240229221220451

  • ThreadFactory:新的线程是由ThreadFactory创建的,默认使用Executors.defaultThreadFactor(),创建出来的线程都在同一个线程组,拥有同样的NORM PRIORITY优先级并且都不是守护线程。如果自己指定ThreadFactory,那么就可以改变线程名、线程组、优先级、是否是守护线程等。

image-20240229221648319

  • WorkQueue工作队列

    • 直接交换:SynchronousQueue,maxPool需要设置大一些,无队列作为缓冲。
    • 无界队列:LinkedBlockingQueue,若处理速度 < 存放队列速度,一直存放,会OOM。
    • 有界队列:ArrayBlockingQueue,

image-20240302233903257

线程池应该手动创建还是自动创建

  • 正确的创建线程池的方法

    • 根据不同的业务场景,自己设置线程池参数,比如我们的内存有多大,我们想给线程池取什么名字等等。

线程池里的线程数量设定为多少比较合适

  • CPU密集型(加密、计算Hash等):最佳线程数为CPU核心数的1-2倍左右。

  • 耗时IO型(读写数据库、文件、网络读写等):最佳线程数一般会大于cpu核心数很多倍,以IVM线程监控显示繁忙情况为依据,保证线程空闲可以衔接上,参考Brain Goetz推荐的计算方法:

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

停止线程池的正确方式

3. 常见线程池的特点和用法

FixedThreadPool

  • 由于传进去的LinkedBlockingQueue是没有容量上限的,所以当请求越来越多,并且无法及时处理完毕的时候,也就是请求堆积的时候,会容易造成大量的内存,可能会导致OOM。`

正常执行的状态

/**
 * 演示 newFixedThreadPool
 */
public class FixedThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Task());
        }
    }


}

class Task implements Runnable{

    @Override
    public void run() {
        //休眠500毫秒
        try{
            Thread.sleep(500);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
}

执行结果:打印出来的线程编号一直是pool-1-thread-1~4

pool-1-thread-1
pool-1-thread-3
pool-1-thread-2
pool-1-thread-4
pool-1-thread-2
pool-1-thread-4
pool-1-thread-1
pool-1-thread-3
pool-1-thread-4
pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
....

OOM的状态

/**
 * 演示 newFixedThreadPool出错的情况
 */
public class FixedThreadPoolOOM {
    private static ExecutorService executorService  = Executors.newFixedThreadPool(1);

    public static void main(String[] args) {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            executorService.execute(new SubThread());
        }

    }

}


class SubThread implements Runnable{

    @Override
    public void run() {
        try{
            Thread.sleep(1000000000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

一个线程执行Integer.MAX_VALUE次,并且线程睡眠时间长,处理<存储队列中的速度

newSingleThreadExector

  • 可以看出,这里和刚才的newFixedThreadPool的原理基本一样,只不过把线程数直接设置成了1,所以这也会导致同样的问题,也就是当请求堆积的时候,可能会占用大量的内存。

正常状态

public class SingleThreadExecutor {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Task());

        }
    }
}

class Task implements Runnable{

    @Override
    public void run() {
        //休眠500毫秒
        try{
            Thread.sleep(500);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
}

执行结果;只有一个线程一直执行

pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1

image-20240306235543617

核心数和最大线程数均为1,且使用有界队列作为阻塞队列

newCachedThreadPool

  • 可缓存的线程池
  • 特点:无界线程池,具备自动回收多余线程的功能
  • 这里的弊端在于第二个参数maximumPoolSizre被设置为了Integer.MAX_VALUE,这可能会创建非常多的线程,甚至导致OOM。

image-20240307000045919

public class CachedThreadPool {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Task());

        }
    }
}

观察线程编号数量比较随意,会创建非常多的线程

pool-1-thread-15
pool-1-thread-7
pool-1-thread-8
pool-1-thread-11
pool-1-thread-3
pool-1-thread-2
pool-1-thread-1

newScheduledThreadPool

  • 支持定时及周期性任务执行的线程池
/**
 * 跟时间相关,推迟相关的特点
 */
public class ScheduledThreadPoolTest {
    public static void main(String[] args) {
        ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);
        // 每隔5秒执行一次
//        threadPool.schedule(new Task(),5, TimeUnit.SECONDS);
        // 期初执行1次,每隔3秒钟执行
        threadPool.scheduleAtFixedRate(new Task(),1,3,TimeUnit.SECONDS);
    }
}

4种线程池的构造函数的参数

image-20240307001220543

阻塞队列分析

Fix和SIngle的Queue是LinkedBlockingQueue?

新来的数量无法估计,所以将阻塞队列作为无限

CacheThreadPool使用的是SynchronusQueue?

不需要存储,任务过来直接在新线程中执行

ScheduledThreadPool来说,它使用的是延迟队列DelayedWorkQueue

WorkStrelingPool

  • 这个线程池和之前的都有很大不同
  • 子任务
  • 拥有一定的窃取能力,线程之间可以合作 map-reduce

4.任务太多,怎么拒绝

  • 拒绝时机
    • 当Executor关闭时,提交新任务被拒绝
    • 当Executor对最大线程和工作队列使用有限边界并且已经饱和

四种拒绝策略

  • AbortPolicy:直接抛出异常
  • Discardpolicy:默默丢弃,不抛出异常
  • DiscardOldestPolicy:直接丢弃任务,不予处理也不抛出异常
  • CallerRunsPolicy:调用者线程处理

5.钩子方法,给线程池加点料

6.实现原理、源码分析

线程池的状态

  • Running:接受新任务并处理排队任务

  • shutdown:不接受新任务,但处理排队任务

  • stop:不接受新任务,也不处理排队任务,并中断正在进行的任务

  • tidying:整洁。

  • terminated:terminate()运行完成。

    线程流转如下:
    扩展面试题_线程池状态转换.jpg

7. 使用线程池的注意点

  • 避免任务的堆积
  • 避免线程数过度增加
  • 排查线程泄露
posted @ 2024-03-03 23:29  shine-rainbow  阅读(7)  评论(0编辑  收藏  举报