java并发系列—超详细线程池讲解

线程池

创建线程的方法:()

  1. 继承 thread 类(缺点:线程类已经继承了 Thread 类无法继承其他类了,如果一个类已经有父类)、

  2. 实现 runnable 接口(可以解耦)、使用线程池

3.实现callable接口。FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况

image-20230208005334186

​ 线程池的核心思想:线程复用,同一个线程可以被重复使用,来处理多个任务

池化技术 (Pool) :一种编程技巧,核心思想是资源复用,在请求量大时能优化应用性能,降低系统频繁建连的资源开销

好处

  1. 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 第三:**提高线程的可管理性。**线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  4. 第四:Java1.5 中引入的 Executor 框架把任务的提交和执行进行解耦,只需要定义好任务,然后提交给线程池,而不用关心该任务是如何执行、被哪个线程执行,以及什么时候执行

​ Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService,Executors 是一个框架,ThreadPoolExecutor 是线程池实现的核心类。

​ Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService,Executors 是一个框架,ThreadPoolExecutor 是线程池实现的核心类。

​ 可以使用 Executors 框架中的静态工厂方法之一来创建一个线程池:(如果线程池中抛出异常,内部会被 try,catch 掉。可以通过 future.get()捕获异常

  • newFixedThreadPool:创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化(如果某个线程由于发生了未预期 Exception 而结束,那么线程池会补充一个新的线程)。
  • newCachedThreadPool:创建一个可缓存的线程池,如果线程池当前规模超过了处理需求,那么将回收空间线程,而当需求增加时,可以添加新线程,线程池规模不存在任何限制(不推荐使用,线程池规模不可控)大小为 Integer.MAX_VALUE,2 的 31 次方-1。(线程数量过多可能导致OOM)
  • newSingleThreadExecutor**:是一个单线程的 Executor**,如果这个线程异常结束,会创建另一个线程替代,它能保证依照任务在队列中的顺序串行执行(FIFO, LIFO, 优先级)
  • newScheduledThreadPool:创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于 Timer。
ExecutorService threadPool = Executors.newFixedThreadPool(3);

ExecutorService threadPool = Executors.newCachedThreadPool();

ExecutorService threadPool = Executors.newSingleThreadExecutor();

threadPool.execute(Runnable r);

threadPool.shutdown();//将任务执行完毕后关闭

threadPool.shutdownNow();//**立即关闭,并返回没有执行的任务(List< Runnable >)

​ 对于计算密集型的任务,在拥有 N 个 cpu 的系统上,当线程池的大小为 N+1 时,通常能实现最优利用率。对于 I/O 密集型,通常线程池大小为 2*N

Java.util.concurrent.ThreadPoolExecutor

​ ThreadPoolExecutor 类 是 线 程 池 中 最 核 心 的 一 个 类 , Executors 中 的newFixedThreadPool 等方法就是由 ThreadPoolExecutor 实现。不过在 java doc 中,并不提倡我们直接使用 ThreadPoolExecutor,而是使用 Executors 类中提供的几个静态方法来创 建 线 程 池 。 ThreadPoolExecutor 继 承 AbstractExecutorService 类 , 即ThreadPoolExecutor 中能够调用ExecutorService 的方法

​ ThreadPoolExecutor 中主要方法,execute(Runnable command)、submit(Callable< T >task)、shutdown()、shutdownNow();

execute 和 submit 都属于线程池的方法,对比:

  • execute 只能执行 Runnable 类型的任务**,没有返回值; submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务,底层是封装成FutureTask(FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况),然后调用 execute 执行**
  • execute 会直接抛出任务执行时的异常,submit 会吞掉异常,可通过Future 的 get 方法将任务执行时的异常重新抛出

​ submit 底层还是调用 execute

ThreadPoolExecutor executor = new  ThreadPoolExecutor(5, 10, 200,TimeUnit.MILLISECONDS,new ArrayBlockingQueue< Runnable >(5));
构造方法
public ThreadPoolExecutor(int corePoolSize,
			int maximumPoolSize,
			long keepAliveTime,
			TimeUnit unit,
			BlockingQueue<Runnable> workQueue,
			ThreadFactory threadFactory,
			RejectedExecutionHandler handler)
  • corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者 prestartCoreThread()方法,从这 2 个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize 个线程或者一个线程。**默认情况下,**在创建了线程池后,线程池中的线程数为 0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize 后,就会把到达的任务放到缓存队列当中;

  • maximumPoolSize:最大线程数,当队列中存放的任务达到队列容量时,当前可以同时运行的数量变为最大线程数,创建线程并立即执行最新的任务,与核心线程数之间的差值又叫救急线程数

  • keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于 corePoolSize 时,keepAliveTime 才会起作用,直到线程池中的线程数不大于 corePoolSize,即当线程池中的线程数大于 corePoolSize 时,如果一个线程空闲的时间达到 keepAliveTime,则会终止,直到线程池中的线程数不超过 corePoolSize。但是如果调用了 allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize 时,keepAliveTime 参数也会起作用,直到线程池中的线程数为 0;

  • unit:keepAliveTime 参数的时间单位,

    • 参数 keepAliveTime 的时间单位,有 7 种取值,在 TimeUnit 类中有 7 种静态属性:
      TimeUnit.DAYS; //天
      TimeUnit.HOURS; //小时
      TimeUnit.MINUTES; //分钟
      TimeUnit.SECONDS; //秒
      TimeUnit.MILLISECONDS; //毫秒
      TimeUnit.MICROSECONDS; //微妙
      TimeUnit.NANOSECONDS; //纳秒
  • workQueue:阻塞队列,存放被提交但尚未被执行的任务
    一般来说,这里的阻塞队列有以下几种选择

    • ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;(强制有界)

    • LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为 Integer.MAX_VALUE;

    • synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

      一般使用 LinkedBlockingQueue 和 Synchronou

      主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较:

      • Linked 支持有界,Array 强制有界
      • Linked 实现是链表,Array 实现是数组
      • Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
      • Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
      • Linked 两把锁,Array 一把锁
  • threadFactory:线程工厂,创建新线程时用到,可以为线程创建时起名字

  • handler:拒绝策略,线程到达最大线程数仍有新任务时会执行拒绝策略

    • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常。默认策略

    • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。

    • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)

    • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

      补充:其他框架拒绝策略

      • Dubbo:在抛出 RejectedExecutionException 异常前记录日志,
      • 并 dump 线程栈信息,方便定位问题
      • Netty:创建一个新线程来执行任务
      • ActiveMQ:带超时等待(60s)尝试放入队列
      • PinPoint:它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
使用流程
  1. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NaMijvGx-1679668652094)(https://typora963.oss-cn-hangzhou.aliyuncs.com/JUC-%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86-1673425907078523-167673650029625.png)]

  2. 创建线程池,这时没有创建线程(懒惰),等待提交过来的任务请求,调用 execute 方法才会创建线程

  3. 当调用 execute() 方法添加一个请求任务时,线程池会做如下判断:

    • 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务
    • 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列
    • 如果这时队列满了且正在运行的线程数量还小于 maximumPoolSize,那么会创建非核心线程立刻运行这个任务(救急线程),对于阻塞队列中的任务不公平。这是因为创建每个 Worker(线程)对象会绑定一个初始任务,启动 Worker 时会优先执行
    • 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
  4. 当一个线程完成任务时,会从队列中取下一个任务来执行

  5. 当一个线程空闲超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后最终会收缩到 corePoolSize 大小

实现原理

​ ThreadPoolExecutor 中有一个实现了 Runnable 接口的 Worker 内部类,可以 new Worker 类,构造函数中传入任务,在 Worker 类中创建新线程,Worker 重写 Runnable的 run 方法,while (task != null || (task = getTask()) != null),getTask 方法从workQueue 中取出任务。即循环不断取出任务,while 循环中 run 任务时要 lock当不满足while 循环中的条件,即没有任务,getTask 调用 queue 的 take 方法,会阻塞。

​ 在这个 while循环中【while (task != null || (task = getTask()) != null)中】,会调用 task(即传递进来的 runnable)的 run 方法,而不是 start 开启新线程,因为 while 循环在 worker 类的 run 方法中,已经是一个线程了。

在 addWorker 方法中 new 的 Worker 类。

在 execute()方法中,当小于 corePoolSize 时,调用 addWorker 方法并传入任务。

Executors 框架:(和前面讲的联系起来)

​ newFixedThreadPool 创建的线程池 corePoolSize 和 maximumPoolSize 值是相等的,它使用的 LinkedBlockingQueue;

​ newSingleThreadExecutor 将 corePoolSize 和 maximumPoolSize 都设置为 1,也使用的LinkedBlockingQueue;

​ newCachedThreadPool 将 corePoolSize 设 置 为 0 , 将 maximumPoolSize 设 置 为Integer.MAX_VALUE,使用的 SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过 60 秒,就销毁线程。

​ ScheduledThreadPoolExecutor 继 承 了 ThreadPoolExecutor , 传 入 队 列 是DelayedWorkQueue()。

image-20230206231953595

image-20230206231957447

image-20230206232003132

队列补充()

有界队列和无界队列:

  • 有界队列:有固定大小的队列,比如设定了固定大小的LinkedBlockingQueue,又或者大小为 0
  • 无界队列:没有设置固定大小的队列,这些队列可以直接入队,直到溢出(超过 Integer.MAX_VALUE),所以相当于无界

java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:FIFO队列

  • ArrayBlockQueue:由数组结构组成的有界阻塞队列
  • LinkedBlockingQueue:由链表结构组成的无界(默认大小Integer.MAX_VALUE)的阻塞队列
  • PriorityBlockQueue:支持优先级排序的无界阻塞队列
  • DelayedWorkQueue:使用优先级队列实现的延迟无界阻塞队列
  • SynchronousQueue:不存储元素的阻塞队列,每一个生产线程会阻塞
    到有一个 put 的线程放入元素为止
  • LinkedTransferQueue:由链表结构组成的无界阻塞队列
  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列

​ 与普通队列(LinkedList、ArrayList等)的不同点在于阻塞队列中阻塞添加和阻塞删除方法,以及线程安全:

  • 阻塞添加 put():当阻塞队列元素已满时,添加队列元素的线程会被阻塞,直到队列元素不满时才重新唤醒线程执行
  • 阻塞删除 take():在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般会返回被删除的元素)

​ 对于任务耗时较短的情况下,线程数不宜过多。 对于任务耗时长的情况,分为 I/O 密集和计算密集。I/O 密集的时候,相对来说 CPU 空闲的时间比较多,那可以适当的增加线程数,增加 CPU 的利用率。 计算密集的时候,CPU 一直被占用进行计算,线程数不宜太多。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值