深入理解线程池原理及使用

3 篇文章 0 订阅
2 篇文章 0 订阅

并发工具类-分类

  1. 为了并发安全∶互斥同步、非互斥同步、无同步方案
  2. 管理线程、提高效率
  3. 线程协作

线程池的自我介绍

为什么要使用线程池

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

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

解决以上两个问题的思路

  • 用少量的线程 ⇒ 避免内存占用过多

  • 让这部分线程都保持工作,且可以反复执行任务 ⇒ 避免生命周期的损耗

线程池的重要性

  • 治理线程的最大法宝 - 线程池

  • 优点:复用线程,避免反复创建和销毁线程带来的开销问题,减少JVM回收垃圾的压力

线程池的好处

  • 加快响应速度

  • 合理利用CPU和内存

  • 统一管理线程

线程池应用场景

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

  • 实际上,在开发中,如果需要创建5个以上的线程,那么就可以使
    用线程池来管理

创建和停止线程池

问题

  1. 线程池应该手动创建还是自动创建
  2. 线程池里的线程数量设定为多少比较合适?

线程池构造函数的参数

参数名类型含义
corePoolSizeint核心线程数
maxPoolSizeint最大线程数
keepAliveTimelong保持存活时间
workQueueBlockingQueue任务存储队列
threadFactoryThreadFactory当线程池需要新的线程的时候,会使用threadFactory来生成新的线程
HandlerRejectedExecutionHandler由于线程池无法接受你所提交的任务的拒绝策略
  • corePoolSize:指的是核心线程数,线程池在完成初始化后,默认
    情况下,线程池中并没有任何线程,线程池会等待有任务到来时,
    再创建新线程去执行任务

  • maxPoolSize:线程池有可能会在核心线程数的基础上,额外增加一些线程,但是这些新增加的线程数有一个上限

  • keepAliveTime:如果线程池当前的线程数多于 corePoolSize,那么如果多余的线程空闲时间超过 keepAliveTime,它们就会被终止

  • workQueue:工作队列

    有3种最常见的队列类型∶

  1. 直接交接:SynchronousQueue,需要将 maximumPoolSize 设置的大一些,因为这种队列本身不会进行任务缓存

  2. 无界队列:LinkedBlockingQueue,无需设置 maximumPoolSize,因为这种队列是一种无界队列,永远不会满,如果处理任务速度跟不上添加队列的速度,会导致队列中任务挤压严重

  3. 有界的队列:ArrayBlockingQueue,按需设置即可,maximumPoolSize 大于 corePoolSize

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

  2. 通常我们用默认的ThreadFactory就可以了

线程池-线程数存储

在这里插入图片描述

  • 添加线程规则
  1. 如果线程数小于 corePoolSize,即使其他工作线程处于空闲状态,也会创建一个新线程来运行新任务。

  2. 如果线程数等于(或大于) corePoolSize 但少于 maximumPoolSize,则将任务放入队列。

  3. 如果队列已满,并且线程数小于 maxPoolSize,则创建一个新线程来运行任务。

  4. 如果队列已满,并且线程数大于或等于 maxPoolSize,则拒绝该任务。

线程池工作原理

在这里插入图片描述

  • 是否需要增加线程的判断顺序是:
  1. corePoolSize
  2. workQueue
  3. maxPoolSize

举例说明

  • 线程池∶核心池大小为5,最大池大小为10,队列为100。
  1. 因为线程中的请求最多会创建5个,然后任务将被添加到队列中,直到达到100
  2. 当队列已满时,将创建最新的线程 maxPoolSize,最多到10个线程,如果再来任务,就拒绝。

增减线程的特点

  1. 通过设置 corePoolSize 和 maximumPoolSize 相同,就可以创建固定大小的线程池。

  2. 线程池希望保持较少的线程数,并且只有在负载变得很大时才增加它。

  3. 通过设置 maximumPoolSize 为很高的值,例如 Integer.MAX_VALUE,可以允许线程池容纳任意数量的并发任务。

  4. 只有在队列填满时才创建多于 corePoolSize 的线程,所以如果你使用的是无界队列(例如LinkedBlockingQueue),那么线程数就不会超过corePoolSize。

线程池的创建方式-线程池应该手动创建还是自动创建

手动创建更好,因为这样可以让我们更加明确线程池的运行规则,避免资源耗尽的风险。

让我们来看看自动创建线程池(也就是直接调用JDK封装好的构造函数)可能带来哪些问题

  • FixedThreadPool

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

测试演示,修改启动配置:-Xmx8m -Xms8m

线程池-任务处理

在这里插入图片描述

  • SingleThreadExecutor
  1. 单线程的线程池:它只会用唯一的工作线程来执行任务

  2. 它的原理和 FixedThreadPool 是一样的,但是此时的线程数量被设置为了1

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

  • CachedThreadPool
  1. 可缓存线程池

  2. 特点:无界线程池,具有自动回收多余线程的功能

  3. 缺点:这里的弊端在于第二个参数 maximumPoolSize 被设置成 Integer.MAX_VALUE,这可能会创建数量非常多的线程,甚至导致OOM。

在这里插入图片描述

  • ScheduledThreadPool

    支持定时及周期性任务执行的线程池

常见线程池的特点和用法

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

在这里插入图片描述

阻塞队列分析

  • FixedThreadPool 和SingleThreadExecutor 的 Queue 是LinkedBlockingQueue

  • CachedThreadPool 使用的 Queue 是 SynchronousQueue

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

  • workStealingPool 是JDK1.8加入的

    1. 这个线程池和之前的都有很大不同
    2. 子任务:当前线程中嵌套其他任务
    3. 窃取:如果有线程处于空闲状态,会从其他线程的子任务中窃取线程来执行,但是不保证执行的顺序
正确的创建线程池的方法
  • 根据不同的业务场景,自己设置线程池参数,比如我们的内存有多大,我们想给线程取什么名字等等
线程池里的线程数量设定为多少比较合适?
  • CPU密集型(加密、计算hash等):最佳线程数为CPU核心
    数的1-2 倍左右。

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

停止线程池的正确方法

  • shutdown
  1. 停止接收新的任务,如果往队列中添加任务时,会拒绝并抛出异常

  2. 将线程池中所有线程任务及队列中的任务全部执行完

不会立即停止线程池中所有任务

  • isShutdown

    判断当前线程池是否停止,无法判断线程池中的任务是否执行完毕

  • isTerminated

    判断当前线程池任务是否执行完毕

  • awaitTermination

    等待一定时间后判断当前线程池中任务是否执行完成

  1. 如果线程池任务执行完毕会返回True
  2. 如果线程池中任务没有执行完,且已经超过了等待时间,返回False
  3. 如果被中断,则会抛异常
  • shutdownNow

    立即停止正在执行的线程,并返回队列中未执行的任务

拒绝策略-任务太多,怎么拒绝?

拒绝时机

  1. 当 Executor 关闭时,提交新任务会被拒绝。

  2. 以及当 Executor 对最大线程和工作队列容量使用有限边界并且已经饱和时

在这里插入图片描述

4种拒绝策略

只有队列和线程池均满了,配置策略才会执行

  • AbortPolicy:直接抛出异常

  • DiscardPolicy:丢弃一些队列中的任务

  • DiscardOldestPolicy:丢弃最老未执行的任务

  • CallerRunsPolicy:反馈到提交任务线程,由提交任务线程去执行

钩子方法-给线程池加点料

  • 每个任务执行前后
  • 日志、统计
  • 代码演示
package com.jensuper.prc.juc.threadpool;

import java.util.concurrent.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author jichao
 * @version V1.0
 * @description: 在线程池中,线程执行前后可以添加函数处理
 * @date 2020/08/30
 */
public class PauseableThreadPoolTest extends ThreadPoolExecutor {

    /**
     * 锁
     */
    private ReentrantLock lock = new ReentrantLock();

    private Condition unPaused = lock.newCondition();

    /**
     * 暂停状态
     */
    private boolean isPaused;


    public PauseableThreadPoolTest(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    public PauseableThreadPoolTest(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }

    public PauseableThreadPoolTest(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }

    public PauseableThreadPoolTest(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        super.beforeExecute(t, r);
        // 执行之前需要判断暂停状态是否为 true
        lock.lock();
        try {
            while (isPaused) {
                unPaused.await();
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    /**
     * 设置暂停方法
     */
    private void pause() {
        lock.lock();
        try {
            isPaused = true;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 取消暂停方法
     */
    private void resume() {
        lock.lock();
        try {
            isPaused = false;
            unPaused.signalAll();
        } finally {
            lock.unlock();
        }
    }


    public static void main(String[] args) {
        PauseableThreadPoolTest threadPoolTest =
                new PauseableThreadPoolTest(10, 20, 6L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread().getName() + " => 执行方法");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        for (int i = 0; i < 10000; i++) {
            threadPoolTest.execute(runnable);
        }

        // 等待一定时间,设置线程状态为暂停
        try {
            Thread.sleep(1500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadPoolTest.pause();
        System.out.println(Thread.currentThread().getName()+" => 线程被暂停了");
        try {
            Thread.sleep(1500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadPoolTest.resume();
        System.out.println(Thread.currentThread().getName()+" => 线程被唤醒了");
    }
}

实现原理、源码分析

线程池组成部分

  1. 线程池管理器:创建线程、停止线程

  2. 工作线程:从队列中取出任务,用线程去执行任务

  3. 任务列队:存放未被线程执行的任务,需要支持并发

  4. 任务接口(Task ):每一个需要线程去执行的任务,这种任务最终要被放在队列中

在这里插入图片描述

Executor家族

线程池、ThreadPoolExecutor、ExecutorService、Executor、Executors等这么多和线程池相关的类,大家都是什么关系?

  • Executor:只有一个执行任务的方法

  • ExecutorService:继承了Executor,扩展了一些线程池的用法

  • Executors:工具类

在这里插入图片描述

线程池实现任务复用的原理

  1. 相同线程执行不同任务
  2. 源码分析
  1. 先判断工作线程小于核心线程数,则添加工作线程
  2. 每一个工作线程有 runWorker 方法,这个方法会循环从 task 队列中获取任务,并调用 task 的 run 方法,task 本身是 Runable

使用线程池的注意点

  • 线程池状态
  1. RUNNING:接受新任务并处理排队任务

  2. SHUTDOWN:不接受新任务,但处理排队任务

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

  4. TIDYING:中文是整洁,理解了中文就容易理解这个状态了,所有任务都已终止,workerCount 为零时,线程会转换到 TIDYING 状态,并将运行 terminate() 钩子方法。

  5. TERMINATED :terminate( ) 运行完成

  • 注意点:
  1. 避免任务堆积
  2. 避免线程数过度增加
  3. 排查线程泄漏
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值