含源码解析,深入Java 线程池原理

从池化技术到底层实现,一篇文章带你贯通线程池技术。

1、池化技术简介

在系统开发过程中,我们经常会用到池化技术来减少系统消耗,提升系统性能。
在编程领域,比较典型的池化技术有:
线程池、连接池、内存池、对象池等。

对象池通过复用对象来减少创建对象、垃圾回收的开销;连接池(数据库连接池、Redis连接池和HTTP连接池等)通过复用TCP连接来减少创建和释放连接的时间。线程池通过复用线程提升性能。简单来说,池化技术就是通过复用来提升性能。

线程、内存、数据库的连接对象都是资源,在程序中,当你创建一个线程或者在堆上申请一块内存的时候都涉及到很多的系统调用,也是非常消耗CPU的。如果你的程序需要很多类似的工作线程或者需要频繁地申请释放小块内存,在没有对这方面进行优化的情况下,这部分代码很可能会成为影响你整个程序性能的瓶颈。

如果每次都是如此的创建线程->执行任务->销毁线程,会造成很大的性能开销。复用已创建好的线程可以提高系统的性能,借助池化技术的思想,通过预先创建好多个线程,放在池中,这样可以在需要使用线程的时候直接获取,避免多次重复创建、销毁带来的开销。

(1)线程池的优点

  • 线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。
  • 可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。

(2)线程池的风险

虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁,它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。

  • 死锁

任何多线程应用程序都有死锁风险。当一组进程或线程中的每一个都在等待一个只有该组中另一个进程才能引起的事件时,我们就说这组进程或线程 死锁了。死锁的最简单情形是:线程 A 持有对象 X 的独占锁,并且在等待对象 Y 的锁,而线程 B 持有对象 Y 的独占锁,却在等待对象 X 的锁。除非有某种方法来打破对锁的等待(Java 锁定不支持这种方法),否则死锁的线程将永远等下去。

  • 资源不足

线程池的一个优点在于:相对于其它替代调度机制(有些我们已经讨论过)而言,它们通常执行得很好。但只有恰当地调整了线程池大小时才是这样的。

线程消耗包括内存和其它系统资源在内的大量资源。除了
Thread 对象所需的内存之外,每个线程都需要两个可能很大的执行调用堆栈。除此以外,JVM 可能会为每个 Java
线程创建一个本机线程,这些本机线程将消耗额外的系统资源。最后,虽然线程之间切换的调度开销很小,但如果有很多线程,环境切换也可能严重地影响程序的性能。

如果线程池太大,那么被那些线程消耗的资源可能严重地影响系统性能。在线程之间进行切换将会浪费时间,而且使用超出比您实际需要的线程可能会引起资源匮乏问题,因为池线程正在消耗一些资源,而这些资源可能会被其它任务更有效地利用。

除了线程自身所使用的资源以外,服务请求时所做的工作可能需要其它资源,例如 JDBC 连接、套接字或文件,这些也都是有限资源,有太多的并发请求也可能引起失效,例如不能分配 JDBC 连接。

  • 并发错误

线程池和其它排队机制依靠使用
wait() 和 notify()
方法,这两个方法都难于使用。如果编码不正确,那么可能丢失通知,导致线程保持空闲状态,尽管队列中有工作要处理。使用这些方法时,必须格外小心;即便是专家也可能在它们上面出错。而最好使用现有的、已经知道能工作的实现,例如在
util.concurrent 包。

  • 线程泄漏

各种类型的线程池中一个严重的风险是线程泄漏,当从池中除去一个线程以执行一项任务,而在任务完成后该线程却没有返回池时,会发生这种情况。发生线程泄漏的一种情形出现在任务抛出一个 RuntimeException 或一个 Error 时。

如果池类没有捕捉到它们,那么线程只会退出而线程池的大小将会永久减少一个。当这种情况发生的次数足够多时,线程池最终就为空,而且系统将停止,因为没有可用的线程来处理任务。

  • 请求过载

仅仅是请求就压垮了服务器,这种情况是可能的。在这种情形下,我们可能不想将每个到来的请求都排队到我们的工作队列,因为排在队列中等待执行的任务可能会消耗太多的系统资源并引起资源缺乏。在这种情形下决定如何做取决于您自己;在某些情况下,您可以简单地抛弃请求,依靠更高级别的协议稍后重试请求,您也可以用一个指出服务器暂时很忙的响应来拒绝请求。

2、 如何配置线程池大小配置

一般需要根据任务的类型来配置线程池大小:

  • 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1
  • 如果是IO密集型任务,参考值可以设置为2*NCPU

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

3、线程池的底层原理

(1)线程池的状态

线程池和线程一样拥有自己的状态,在ThreadPoolExecutor类中定义了一个volatile变量runState来表示线程池的状态,线程池有四种状态,分别为RUNNING、SHURDOWN、STOP、TERMINATED。

  • 线程池创建后处于RUNNING状态。
  • 调用shutdown后处于SHUTDOWN状态,线程池不能接受新的任务,会等待缓冲队列的任务完成。
  • 调用shutdownNow后处于STOP状态,线程池不能接受新的任务,并尝试终止正在执行的任务。
  • 当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    // runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

    // Packing and unpacking ctl
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    private static int workerCountOf(int c)  { return c & CAPACITY; }
    private static int ctlOf(int rs, int wc) { return rs | wc; }

其中ctl这个AtomicInteger的功能很强大,其高3位用于维护线程池运行状态,低29位维护线程池中线程数量

RUNNING:-1<

SHUTDOWN:0<

STOP:1<

TIDYING:2<

TERMINATED:3<

这些状态均由int型表示,大小关系为 RUNNING

Original: https://www.cnblogs.com/binyue/p/12273165.html
Author: 邴越
Title: 含源码解析,深入Java 线程池原理

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/539210/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Java8新特性之Stream

    Java8新特性之StreamStream将要处理的元素集合看作一种流,在流的过程中,借助Stream API对流中的元素进行操作,比如:筛选、排序、聚合等。Stream可以由数组…

    Java 2023年5月29日
    064
  • shell 编程

    shell 编程 Shell 是一个命令行解释器,它为用户提供了一个向 Linux 内核发送请求以便运行程序的界面系统级程序,用户可以用 Shell 来启动、挂起、停止甚至是编写一…

    Java 2023年6月5日
    077
  • 一文搞懂Flink Window机制

    Windows是处理无线数据流的核心,它将流分割成有限大小的桶(buckets),并在其上执行各种计算。 窗口化的Flink程序的结构通常如下,有分组流(keyed streams…

    Java 2023年6月6日
    0109
  • 并行流的线程安全问题(parallelStream)

    并行流的线程安全问题(parallelStream) 问题代码 Map madData = new HashMap(); List list = paramVo.getList()…

    Java 2023年6月7日
    079
  • Spring-Boot-9-Header参数获取

    Spring 获取Header参数1. 方法一:对应的部分加入@RequestHeader获取@Controllerpublic class HelloController {@R…

    Java 2023年5月30日
    064
  • 在vue中使用echarts

    1.引入echarts 先通过npm安装echarts npm run echarts–save 2.在main.js中 import * as echarts fro…

    Java 2023年6月7日
    074
  • 用Java实现在123456789中随机插入+或者-,使表达式的结果为100

    这里我的思路是暴力算法。就是不断试错,不限循环次数,直到找到正确的情况为止。通过走随机,不断控制在 123456789中添加 +或者 -,再通过正则表达式筛选出所有的数字(包含正负…

    Java 2023年6月7日
    063
  • java运行时创建对象

    使用JDK自带的反射(java.lang.reflect)或者自省(java.beans.Introspector)运行时创建对象。 有很多场景需要运行时创建对象,比如Copy对象…

    Java 2023年6月5日
    055
  • vue.js和node.js的关系

    在学习vue的时候最先安装的就是node.js环境。那么没有node.js环境,vue.js能不能运行呢? 首先说一下node.js 就前端来说nodejs具有划时代的意义, 做前…

    Java 2023年6月5日
    070
  • Mybatis-Plus初步上手!!

    1.简介 1.1、特性 2.快速开始 3.配置日志 4.CRUD拓展 4.1、插入 4.2、更新 4.3、查询 4.4、删除 5.性能分析插件 6.条件构造器Wrapper 7.代…

    Java 2023年6月8日
    085
  • 抽象 类乐器 期末代码

    abstract class YueQi{ //表示发出声音的功能 public abstract void sound(); } //子类钢琴 class Piano exten…

    Java 2023年6月9日
    061
  • 学习狂神Spring5_课堂笔记(更新中)

    Spring 简介 spring,英文单词为春天,表示为软件行业带来了春天。 2002年,首次推出了Spring框架的雏形:interface21框架。官网为:www.interf…

    Java 2023年6月5日
    079
  • 文字与段落标记

    一、 标题字标记 1. 标题字标记h 说明:HTML文档中包含有各种级别的标题,各种级别的标题由 h1 到 h6 元素来定义。其中,h1 代表最高级别的标题,依次递减,h6 级别最…

    Java 2023年6月5日
    096
  • 初读鸟哥的linux私房菜的收获

    搞了十几年开发,一直是在windows下面搞.net开发,有一些不甘心,所以转行去搞java开发,当然也少不了要学习一下linux嘛。前同事波神是这方面的高手,给我推荐去读《鸟哥的…

    Java 2023年6月7日
    073
  • Day11

    package com.oop.demo02;//学生类public class Student { //属性:字段 String name; int age; //方法 publ…

    Java 2023年6月5日
    071
  • Oracle备份与还原(实用版)

    Oracle备份与还原 EXP&#x548C;IMP&#x662F;&#x5BA2;&#x6237;&#x7AEF;&#x5DE5;…

    Java 2023年6月8日
    075
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球