线程池
目录线程
- 线程是操作系统能够进行运算调度的最小单位,它包含在进程之中,是进程中的实际运作单位。
- 一个线程是由 线程的ID 当前指令指针PC 寄存器集合 和堆栈组成 每个线程都是系统调度和分派的基本单位,拥有一些必要的运行资源,但与同一进程中的其他线程共享整个进程的资源。线程是进程中一个单一的顺序控制流,负责当前进程中程序的执行。一个进程中可以并发多个线程,每条线程并行执行不同的任务
java 线程的状态
- 创建NEW状态
- 运行RUN状态
- 死亡Terminated
- 线程退出的方式
- 正常退出
- 程序异常退出
- 使用Interrupt中断线程抛出异常
- 跳出循环状态 捕获break
- 使用共享变量
- 线程退出的方式
- 阻塞
- 处于运行中的线程由于某种原因放弃对CPU的使用权,处于阻塞状态,直到其进入就绪状态,才有机会再次被CPU调用进入运行状态。
- 等待阻塞
- 运行状态中的线程执行wait方法,进入等待队列 等待阻塞 Java虚拟机就会把线程放到这个对象的等待池中
- 同步阻塞
- 线程获取同步锁失败(因为锁被其他线程占用),Java虚拟机就会把这个线程放到这个对象的锁池中
- 其他阻塞
- 通过调用sleep方法或者join方法或者发出I/O请求时,线程会进入阻塞状态,当sleep()状态超时,或者join()等待线程终止或者超时,或者I/O处理完毕,线程重新转入就绪状态
- 超时阻塞
线程的基本方法
- 线程等待 wait
- 释放锁 Object 方法 常于notify方法成对出现
- 线程睡眠 sleep
- 线程自身的方法 不释放锁
- 线程让步 yield
- 让当前线程让出CPU 与其他线程一起重新竞争cpu时间片,一般情况下,优先级高的先得到,但也不一定,有的系统对优先级不敏感
- 线程中断 interrupt
- 如果一个sleep或者wait的线程,调用interrupt() ,方法则抛出InterruptedException( InterruptedException表示一个阻塞被中断了),线程的中断标志位会被复位成false;相当于用异常响应了这个中断,所以释放中断标志位
- 等待其他线程终止 join
- 当前线程调用join()方法 则线程转为阻塞状态
- eg:A线程中插入了B.join(),则B先执行,执行完,A线程继续执行;常见的是主线程生成并启动了子线程,需要用到子线程返回结果的场景
- 线程唤醒 notify
- Object类中的notify唤醒在此对象监视器上等待的单个线程;notifyAll唤醒在此对象监视器上等待的所有线程;
- 其他常用方法
- isAlive 判断一个线程是否存活
- activeCount 程序中活跃的线程数
- currentThread 得到当前的线程
- setPriority 设置一个线程的优先级
- getPriority 获取一个线程的优先级
- isDaemon 线程是否为守护线程
线程池(ThreadPoolExecutor)
- ctl 用来表示线程池的状态以及线程数量 使用Int
- 那么ctl的值 高三位就是用来表示了线程池的状态,余下的所有低位数就是用来标识线程池线程数量,默认值是Running 线程个数是0
- 最大线程池个数 1<< (Integer.SIZE -3 ) -1
- 线程池的状态以及流转
- RUNNING
- 线程池状态为运行
- SHUTDOWN
- 拒接新任务,只处理阻塞队列中余下的任务
- STOP
- 拒接新任务,也不处理阻塞队列中余下的任务
- TIDYING
- 包含阻塞队列中的所有任务都已执行完毕,当前活动线程数为0,即将调用Terminaled方法
- TERMINATED
- 终止方法,terminaled方法调用后线程池的状态
- RUNNING
- 流转
- RUNNING >>> SHUTDOWN: 显示调用了shutdown()或隐式的调用了finalize()方法中的shutdown()
- RUNNING或SHUTDOWN >>> STOP:显示的调用了shutdownNow()
- SHUTDOWN >>> TIDYING: 线程池活跃线程为0以及队列排队任务为空
- TIDYING >>> TERMINATED: 当terminated()中钩子方法执行完成时
- 线程池执行过程
- 提交一个任务给线程池 判断此时是否有空余的核心数线程;未满则创建一个新的线程,且将当前任务执行
- 核心线程数满了后,会尝试将任务丢入阻塞队列,如果队列未满,那么任务则是入列成功,自会有线程不断尝试从队列拉取任务后执行
- 如果队列已满,则判断线程池的线程数量是否已达到最大线程数(线程数小于设置的maximunPoolSize 视作为未满),未满则会创建一个新的线程,且将当前任务执行
- 如果最大线程也满了,则会根据设置的拒绝策略执行不同的拒绝措施(丢弃当前任务、抛出异常、丢弃队列中未执行的且最早提交的任务、使用调用者线程执行当前任务)
- 源码理解
- ThreadPoolExecutor 实现的是一个生产消费模型(与其他的池化技术不太一致,仅有复用特性满足) 用户提交任务至池中,就是生产者生产任务。池中的workes就是消费者角色,不断的直接执行或从workQueue拉取任务进行消费
- execute(执行任务)
- 获取当前线程池数量是否小于核心线程
- 如果小于核心线程,添加核心线程并执行该任务
- 如果等于核心线程了添加到队列中。再次检查线程池状态如果非运行状态质检执行拒绝当前任务
- 如果入队失败,核心线程也慢了 那就创建worker 如何失败执行拒绝逻辑
- addWorker(添加工作线程)
- for 一个死循环
- 获取当前线程池的状态 当线程池状态大于SHUTDOWN(stop trdying terminated);线程池状态为SHUTDOWN 且有了第一个任务; 程池状态为 SHUTDOWN 且对列为空 直接返回false
- for一个嵌套死循环 尝试增加线程数 断是否超过线程池可容纳最大线程数量,以及根据传入参数会灵活判断核心线程或最大线程
- cas尝试增加线程数 跳出内部循环 更新到hashSet<>(works)
- 日常中如何使用线程池
1.使用线程池 最好隔离,即不同业务之间如果要是用线程池的话,那么就各自定义。
2.根据任务的IO密集型还 CPU密集型组合型不同任务定义不同的核心线程与最大线程。
3.根据任务类型选择阻塞队列(合理使用防止把内存撑爆)
4.设置线程工厂方便我们不同线程池定义不同的线程名称
5.设置拒绝策略- 拒绝策略一般根据任务重要性来选择,如果任务重要且不能丢,要么队列设置大一点,但又要注意内存问题;要么就是使用CallRunsPolicy,但使用CallRunsPolicy 如果程序处理太慢太慢,可能会渐渐将所有调用者线程阻塞住导致无法处理WEB请求;
- 我们也可以覆写拒绝策略,使其打印日志或者做一个被拒绝的数据存储,然后编写一个定时任务,查询后进行再次提交至线程池进行消费
- 线程池的弊端
- 提升了使用成本,如果不清楚线程池原理的人,胡乱设置一通线程池参数可能会给程序和服务器带来灾难
- 任务顺序性被打乱,按顺序提交的任务,也可能不会按顺序被执行,具体可以回顾下线程池的执行流程(阻塞队列),worker执行流程(第一优先级任务)
- execute提交任务注意异常捕获,否则也是达不到线程复用的目的 这一点可以回顾addWorker那里
常见面试题
- 线程交替打印
- 线程通信
- synchronized + wait-notify/notifyAll
- 声明一个共享变量 打印值
- 声明一个共享变量 当做锁
- 每个线程循环判断共享变量是否到自己可以打印的条件,如果是打印更新变量值,唤醒下一个线程;
- reentrantLock + condition条件
- ReentrantLock 是java中的一个类,用于实现可重入的互斥锁,是AQS的一种实现
- Condition 是ReentrantLock的一个接口,用于实现线程间的条件等待和唤醒。ReentrantLock可以创建多个Condition对象,每个Condition对象可以绑定一个或多个线程,实现对不同线程的精确控制。
- 声明一个共享变量 打印值
- 声明一个ReentrantLock
- 声明三个condition
- 每个线程获取锁 并判断共享变量值是否是自己执行的条件 不是则等待 如果是 修改共享变量值,并唤醒下一个线程
- 使用信号量
- 使用CAS自旋
- 线程通信
- 线程池参数如何设置的,线程数量设置多少合理?
- CPU密集型(即计算密集型任务,它们的特点是需要大量的CPU运算)
- 对于CPU密集型任务 -> 通常设置为CPU核心数加1,这样可以使CPU的利用率最大化,同时避免由于线程上下文切换带来的额外开销。
- IO密集型(即这类任务可能会阻塞,并在执行期间等待IO操作,如数据库操作、文件操作或网络通信)
- 可以设置得更高,因为IO密集型任务并不是一直在进行计算。一个常用的配置是将线程数设置为CPU核心数的两倍加1。但具体数值还需要根据实际IO的等待时间和响应时间来调整
- 混合型任务(既有CPU密集型又有IO密集型)
- 能需要通过实际的性能测试来确定最佳的线程数
考虑系统资源(如内存)。大量的线程可能会消耗大量内存。如果系统资源有限,或者是在多应用共享环境下(如服务器上运行多个应用),则需要降低线程池大小
核心线程数和最大线程数的设置还需要考虑任务队列的容量和任务拒绝策略。如果任务队列很大,可以容纳许多任务,那么线程池就可以设置得小一些
无论如何设置,都需要通过监控和性能测试来验证设置是否合适。监控线程池的运行情况,如队列大小、活跃线程数、最大线程数和拒绝的任务等 - CPU密集型(即计算密集型任务,它们的特点是需要大量的CPU运算)
- 如何确定一个线程池中的任务已经完成了?
- isTerminated 方式执行了shutdown() 关闭线程池后 判断是否所有任务已经完成。
- 优点 :操作简单。
- 缺点 :需要关闭线程池。并且日常使用是将线程池注入到Spring容器,然后各个组件中统一用同一个线程池,不能直接关闭线程池。
- ThreadPoolExecutor的 getCompletedTaskCount() 方法,判断完成任务数和全部任务数是否相等。
- 优点 :不必关闭线程池,避免了创建和销毁带来的损耗。
- 缺点 :使用这种判断存在很大的限制条件;必须确定在循环判断过程中没有新的任务产生。
- CountDownLatch计数器,使用闭锁计数来判断是否全部完成。
- 优点 :代码优雅,不需要对线程池进行操作。
- 缺点 :需要提前知道线程数量;性能较差;还需要在线程代码块内加上异常判断,否则在 countDown之前发生异常而没有处理,就会导致主线程永远阻塞在 await。
- 手动维护一个公共计数 ,原理和闭锁类似,就是更加灵活
- 优点 :手动维护方式更加灵活,对于一些特殊场景可以手动处理。
- 缺点 :和CountDownLatch相比,一样需要知道线程数目,但是代码实现比较麻烦。
- 使用submit向线程池提交任务,Future判断任务执行状态。
- 优点:使用简单,不需要关闭线程池。
- 缺点:每个提交给线程池的任务都会关联一个Future对象,这可能会引入额外的内存开销。如果需要处理大量的任务,可能会占用较多的内存。
- isTerminated 方式执行了shutdown() 关闭线程池后 判断是否所有任务已经完成。
- 为什么不建议使用JAVA自带的Executors创建线程池?
- newFixedThreadPool: 无界队列过多的任务导致程序崩溃,线程池无法根据并发动态扩容
- newSingleThreadExecutor:无界队列过多的任务导致程序崩溃,线程池无法根据并发动态扩容
- newCachedThreadPool:同步队列,过大的并发将会创建很多线程,且线程无上限,可能导致程序崩溃
- newScheduledThreadPool:无界队列过多的任务导致程序崩溃,线程池无法根据并发动态扩容,且有可能导致任务延时
默认都是继承 ThreadPoolExecutor 构造方法中使用的事默认的拒绝策略(直接丢弃)会导致任务丢失
- 线程池里面的阻塞队列设置多长合理?