【并发】Java并发线程池底层原理详解与源码分析(上)

news2025/1/21 8:55:06

【并发】Java并发线程池底层原理详解与源码分析(上)

线程池与线程对比 

使用线程的方式运行

使用线程池的方式运行

分析

单线程、多线程、线程池效率问题

详细代码 

结果分析 

单线程为什么是最快的?

单线程都这么快了,我们是不是就不再需要多线程、线程池了呢?

三种常用的线程池 

代码描述 

运行结果 

(1)newCachedThreadPool 分析 

(2)newFixedThreadPool 分析

(3)newSingleThreadExecutor  

总结

手动创建线程池

阿里巴巴Java开发手册关于线程池的建议

代码实现

ThreadPoolExecutor 源码参数

【并发】Java并发线程池底层原理详解与源码分析(下)


【并发】Java并发线程池底层原理详解与源码分析(上)

线程池与线程对比 

我们先来看看下面这个例子,我们用一个小Demo,来演示多线程线程池的运行情况

使用线程的方式运行

/**
 * 使用线程的方式去执行程序
 */
public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {
        Long start = System.currentTimeMillis();
        final Random random = new Random();
        final List<Integer> list = new ArrayList<Integer>();
        for (int i = 0; i < 100000; i++) {
            Thread thread = new Thread() {
                @Override
                public void run() {
                    list.add(random.nextInt());
                }
            };
            thread.start();
            thread.join();
        }
        System.out.println("时间:" + (System.currentTimeMillis() - start));
        System.out.println("大小:" + list.size());
    }
}

运行结果 

使用线程池的方式运行

/**
 * 线程池执行
 */
public class ThreadPoolTest {

    public static void main(String[] args) throws InterruptedException {
        Long start = System.currentTimeMillis();
        final Random random = new Random();
        final List<Integer> list = new ArrayList<Integer>();
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 100000; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    list.add(random.nextInt());
                }
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.DAYS);
        System.out.println("时间:"+(System.currentTimeMillis() - start));
        System.out.println("大小:"+list.size());
    }
}

运行结果  

分析

线程:创建了10w个对象,10w+1个线程,但是由于业务逻辑很简单,每个线程运行一会就要切换,上下文切换非常浪费cpu资源!所以该案例的时间基本上都用来做线程切换了!!!

线程池:创建了10w个对象,但是只创建了2个线程(main线程 + 线程池里的一个核心线程),由于这里的“业务”非常简单,实际上一个线程就可以搞定!线程池是用到了线程复用的机制,从而避免了频繁的上下文切换。

注:这里的线程池里只有一个核心线程,是因为Executors.newSingleThreadExecutor(),这种方式导致的,详情如下,具体的我们后面细说

但是线程池就一定快吗?答案肯定是否定的!这个问题我们要从线程机制的根源上来解析!我们可以再来看一个例子,通过分析单线程、多线程、线程池三者来说明这个问题!!!

单线程、多线程、线程池效率问题

废话不多说,直接上代码! 

详细代码 

public class singleMultiplePool{
    public static void main(String[] args) throws InterruptedException {
        List<Integer> list = new LinkedList<>();
        /**
         * newFixedThreadPool 线程池
         */
        ThreadPoolExecutor tp = (ThreadPoolExecutor) Executors.newFixedThreadPool(1);
        Random r = new Random();
        long startTime = System.currentTimeMillis();
        for(int i=0;i<20000;i++){
            tp.execute(new Runnable() {
                @Override
                public void run() {
                    list.add(r.nextInt());
                }
            });
        }
        tp.shutdown();
        try{
            tp.awaitTermination(1, TimeUnit.DAYS);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("thread pool time:"+(System.currentTimeMillis() - startTime) + " size  " + list.size());
        // 重置list
        list.clear();

        /**
         * 多线程
         */
        startTime = System.currentTimeMillis();
        for(int i=0;i<20000;i++){
            Thread t = new Thread(){
                public void run(){
                    list.add(r.nextInt());
                }
            };
            t.start();
            try{
                t.join();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        System.out.println("multiple thread time:"+(System.currentTimeMillis() - startTime) + " size  " + list.size());
        // 重置list
        list.clear();

        /**
         * 单线程
         */
        startTime = System.currentTimeMillis();
        for(int i=0;i<20000;i++){
            list.add(r.nextInt());
        }
        System.out.println("single thread time:"+(System.currentTimeMillis() - startTime) + " size  " + list.size());
    }
}

运行结果 

结果分析 

从这个案例中可以看出使用线程池的效率远远比采用多线程的方式要高得多。但是用单线程却是最快的。

单线程为什么是最快的?

这是由于在这段代码中,业务处理部分是非常的简单的,没有什么耗时的操作(eg:磁盘IO、socket)。 而多线程之所以慢,是因为时间都花在了上下文切换的环节上面。这个案例中的线程池也是只有1个线程,但是它比单线程的慢的原因是因为它有阻塞队列,向阻塞队列里添加任务也是需要时间的!!!

单线程都这么快了,我们是不是就不再需要多线程、线程池了呢?

肯定也不是这样的。从技术发展的角度上来看,先有了单线程,之后才有多线程,再到后面有了线程池,而技术肯定是不断的在进步的。 这边之所以单线程最快是因为处理的业务过于简单,线程切换的成本要高于多个线程同时工作带来的收益! 而线程池的出现,相当于是对多线程做了封装和优化,节省了一些多线程中不必要的时间开销。

三种常用的线程池 

(1)newCachedThreadPool  缓存线程池

这个线程池在创建时并没有规定线程的数量,这个线程池接到任务时,会优先寻找空闲的线程,如果有空闲的线程,就把任务交给它,如果没有再创建一个线程来执行任务。当线程很久没有执行任务时就会回收该线程,该线程实现了线程的动态创建和销毁。

(2)newFixedThreadPool  多线程线程池

这个线程池在创建时可以指定池内线程的数量

(3)newSingleThreadExecutor  单线程线程池

这个线程池只会调用一个线程来进行执行任务,实际上和自己创建一个Thread对象然后start没什么区别,然是它内部的线程是可以复用的

代码描述 

public class ThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService executorService1 = Executors.newCachedThreadPool();//快
        ExecutorService executorService2 = Executors.newFixedThreadPool(10);//慢
        ExecutorService ExecutorService3 = Executors.newSingleThreadExecutor();//最慢

        for (int i = 1; i <= 100; i++) {
            // 分别用executorService1、ExecutorService2、ExecutorService3 来测试
            executorService1.execute(new MyTask(i));
        }
    }
}

/***
 * 项目
 */
class MyTask implements Runnable {
    int i = 0;

    public MyTask(int i) {
        this.i = i;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "程序员做第" + i + "个项目");
        try {
            Thread.sleep(3000L);//业务逻辑
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果 

newCachedThreadPool  最快,一次性全部打印出来(100行)

newFixedThreadPool 慢一点,一次打印10行数据,然后隔3秒再打印10行....

newSingleThreadExecutor 最慢,一次打印1行数据,然后隔3秒再打印1行....

这是为什么呢?这时候我们要结合源码来分析!!!

(1)newCachedThreadPool 分析 

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
            60L, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>());
}

这个对应的是第一种情况。根据这种线程池的参数来看,我们可以知道,它里面没有指定核心线程数,但是最大线程数是没有上限的。

然后,阻塞队列使用的是SynchronousQueue,这是一个不存储元素的阻塞队列!每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。

所以,这种方案的运行结果,为什么是我们上述的那样(一次性打印出100行)?

这个问题应该已经很清楚了,由于我们这边是有100个任务,但是阻塞队列又不能存储元素。

一有任务线程池就要开一个线程来处理(如果池中没有空闲线程的话)。

但是,又由于它每个业务需要阻塞三秒钟,故当前线程需要三秒后才会空闲,但是100个任务在三秒内肯定就运行完了。

所以,这里根本就没有机会触发线程复用。 

如果我们要想在这个案例中,实现线程复用,其实也很简单。只要将 Thread.sleep(3000L),这一行代码注释即可!

显然,这里面出现了重复的线程! 

(2)newFixedThreadPool 分析

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());
}

从上面的源码中,我们可以得知,它的最大线程数核心线程数是相等的,换而言之,也就是说临时线程数0

那在这个案例中,它的最大线程数不再是无穷大了,线程池不能随心所欲的创建线程。 

阻塞队列是LinkedBlockingQueue,一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。

我们指定newFixedThreadPool(10),所以这也很好解释了为什么输出结果是10个10个地打印,中间会隔三秒钟了 

(3)newSingleThreadExecutor  

public static ExecutorService newSingleThreadExecutor() {
    return new Executors.FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>()));
}

通过上面两个分析, 相信这个,无须多言,大家应该也知道。 这里面的最大线程数是1,所以它会一条记录,一条记录的打印,中间间隔三秒钟。 

总结

介绍了这么多种线程池,它们各有各的特性,面对不同的业务需求,不同的案例场景,我们所选择的方案不同,其性能可能会千差万别!!!

手动创建线程池

在实际开发中,这3种方式其实很少用!阿里巴巴关于线程池方面给出了建议,就是禁止使用Executors 

因为,很容易出现CPU100%问题,OOM问题(Out Of Memory)! 

但是,如果业务场景的并发没有那么高,这3种其实都可以使用!

阿里巴巴Java开发手册关于线程池的建议

代码实现

public class ThreadPoolDemo {
    public static void main(String[] args) {

        // 自定义线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20,
                0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(10));

        for (int i = 1; i <= 100; i++) {
            threadPoolExecutor.execute(new MyTask(i));
        }
    }
}

/***
 * 项目
 */
class MyTask implements Runnable {
    int i = 0;

    public MyTask(int i) {
        this.i = i;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "程序员做第" + i + "个项目");
        try {
            Thread.sleep(3000L);//业务逻辑
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果

看到这个结果,相信初学的小伙伴肯定有很多疑问,关于这些问题,我们需要分析ThreadPoolExecutor源码 才可以知道!我会在下一节的文章中给出解答!

ThreadPoolExecutor 源码参数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
            Executors.defaultThreadFactory(), defaultHandler);
}

各个参数的含义 

  1. corePoolSize: 线程池的最大核心线程数
  2. maximumPoolSize: 线程池的最大线程数
  3. keepAliveTime: 线程池中空闲线程的存活时长
  4. TimeUnit unit: 上一个参数的时间单位
  5. BlockingQueue<Runnable> workQueue: 存放任务的队列,这里使用阻塞队列上面是最简单的一个构造方法的参数,下面都是其他构造方法中有的
  6. ThreadFactory threadFactory: 线程池创建线程的工厂
  7. RejectedExecutionHandler handler: 线程池的控制器,用来处理任务队列和最大线程数都达到最大值,仍然有任务要加入任务队列的情况

【并发】Java并发线程池底层原理详解与源码分析(下)

【并发】Java并发线程池底层原理详解与源码分析(下)_面向鸿蒙编程的博客-CSDN博客这里只会打印会前30个任务(10+10+10=30),由于在3s内核心线程和临时线程都在忙碌中,队列也满了,按照ThreadPoolExecutor默认的策略会抛出异常!按照线程池的工作顺序,会先分配10个核心线程(1~10),再装满队列(11~20),最后分配临时线程(21~30);执行逻辑是核心线程和临时线程会先把“手头上”的任务处理完,才会去处理队列里的任务,这就是队列里的任务(11~20)最后打印的原因!!!https://blog.csdn.net/weixin_43715214/article/details/128068255

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/40753.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第四周 牛背山云海——拍拍大场景,自拍延时片段

目录4.1 面对广阔云海拍张全景照片&#xff0c;再来一组延时片段4.2 认识镜头滤镜4.3 补充技术基础知识&#xff1a;白平衡、色温4.4 小博士课堂——延时摄影课件4.1 面对广阔云海拍张全景照片&#xff0c;再来一组延时片段 云海 雪山 雅安境内的牛背山拍摄云海 牛背山可以看到…

java刷题day 04

一. 单选题&#xff1a; 解析&#xff1a;队列&#xff1a;先到先服务解析&#xff1a;final不能修饰接口&#xff0c;成员变量可以用final修饰 解析&#xff1a; A&#xff1a; 算法是指解题方案的准确而完整的描述&#xff0c;算法不等于程序&#xff0c;也不等于计算方法 BC…

Linux 时间同步 ntpdchrony 内网

Linux 时间同步 ntpd&chrony 在任何服务器集群中&#xff0c;为了更好的协同工作&#xff0c;除了一些互信机制外&#xff0c;需要的就是时间同步功能&#xff0c;如果时间不同步&#xff0c;就好比让在中国的同事与美国的同事进行沟通&#xff0c;会造成各种奇奇怪怪的时…

excel笔记

1.快速填充 当需要提取出电子邮件里的名称&#xff0c;可以在第一行里手动输入Nancy后&#xff0c;直接按快捷键ctrl e进行快速填充&#xff0c;可以直接获得预期数据 如果不用快捷键&#xff0c;也可以点击指定位置的图标&#xff0c;实现这一功能 快速填充不仅可以将进行…

大数据中的R语言——中国大学MOOC课程笔记

第一章 R语言简介 1.1 简介 R的特性&#xff1a; • 免费的 • 一个全面的统计研究平台 • 拥有顶尖水准的制图功能 • 一个可进行交互式数据分析和探索的强大平台 • 轻松地从各种类型的数据源导入数据&#xff0c;包括文本文件、数据库管理系统、统计软件&#xff0c;乃…

rabbitMQ 消息顺序性、消息幂等性、消息不丢失、最终一致性、补偿机制、消息队列设计

一、消息顺序性 消息队列中的若干消息如果是对同一个数据进行操作&#xff0c;这些操作具有前后的关系&#xff0c;必须要按前后的顺序执行&#xff0c;否则就会造成数据异常。 举例&#xff1a;   比如通过mysql binlog进行两个数据库的数据同步&#xff0c;由于对数据库的…

内存和函数

程序的内存布局 Linux默认情况下将高地址的1GB空间分配给内核&#xff0c;用户进程使用剩下2GB或者3GB的内存空间。在用户空间里&#xff0c;也有很多地址区间有特殊的地位&#xff0c;一般来讲&#xff0c;应用程序使用的内存空间里有如下"默认"的区域 1、栈&#…

疫情可视化part2

前言 这是疫情可视化最开始的文章&#xff0c;有需要了解的可前往查看&#xff1a;https://blog.csdn.net/xi1213/article/details/126824752。 本来说有时间就把这个项目完结了的&#xff0c;结果后面一直有事拖着&#xff0c;直到现在十一月份了才搞完。老样子&#xff0c;先…

F1. 生活在树上(easy version)树,dfs

题目链接 F1. 生活在树上&#xff08;easy version&#xff09; 题目背景 本题是 B 组的最后一题&#xff0c;是 F2 题的简单版本&#xff0c;两道题目的解法略有不同。本题和 F2 题在题意上的区别在于本题给定树上的边权&#xff0c;而不是点权。 小智生活在「传智国」&am…

汽车 Automotive > SOME/IP VS DDS调研和未来方向

参考&#xff1a;JASPAR, General incorporated association&#xff1a;What is the conqueror in the SOA platform for the future in-vehicle networks? 目录 SOME/IP介绍参考 DDS介绍 SOME/IP VS DDS 研究方向 SOME/IP介绍参考 汽车Automotive &#xff1e; SOME/…

MAC安全(防MAC泛洪攻击)

一、MAC地址表项分类&#xff1a; 1.1 动态表项&#xff1a;通过对帧内的源MAC进行学习而来&#xff0c;有老化时间 1.2 静态表项&#xff1a;由管理员手工配置&#xff0c;不会老化 1.3 黑洞表项&#xff1a;丢弃特定源MAC或目的MAC,不会老化 静态和黑洞表项不会被动态表项…

类与对象(下篇)

类与对象&#xff08;下&#xff09;再谈构造函数回顾构造函数初始化列表explicit 关键字拷贝构造函数也具有初始化列表友元 friend友元函数输入输出流的重载友元类static 成员内部类再谈构造函数 回顾构造函数 在上一篇博客中提到了构造函数&#xff0c;构造函数其主要目的是…

类与对象(中篇)

类中六个默认成员函数构造函数基本概念构造函数特性析构函数基本概念析构函数特性拷贝构造函数基本概念拷贝构造函数特性赋值运算符重载概念引入运算符重载函数的特性部分运算符的重载函数判等赋值前置 、前置--后置、后置--const 成员函数取地址只要生成一个类 &#xff0c;那…

iOS_Custom Transition Animation 自定义转场动画

文章目录1、push-pop 动画协议2、present-dismiss 动画协议3、实现转场动画协议3.1 动画时长3.2 push or present animation (显示动画)3.3 动画结束3.4 pop or dismiss animation (消失动画)4、UIPresentationController4.1 设置presentVC的frame4.2 present 动画4.3 dismiss …

Docker快速安装Oracle 12c

【Oracle系列3】Docker快速安装Oracle 12c 背景 现在还很多企业用12c&#xff0c;以这个版本为例&#xff0c;介绍docker快速启动Oracle并做实验 步骤 1、docker环境的安装&#xff08;略&#xff09; 2、查询镜像&#xff0c;挑选镜像 docker search oracle结果 StoneM…

阿里P8架构师都在学习参考的SpringCloud微服务实战文档

我一直在使用Spring Boot、Spring Data等框架来进行开发工作。 作为一名Spring系列的忠实粉丝&#xff0c;我自然希望能够有更多的开发者参与进来&#xff0c;于是自己坚持写Spring Cloud相关的文章&#xff0c;并且将文章涉及的代码整理后放在GitHub上分享。 这使我得到了很…

【Hack The Box】Linux练习-- Luanne

HTB 学习笔记 【Hack The Box】Linux练习-- Luanne &#x1f525;系列专栏&#xff1a;Hack The Box &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f4c6;首发时间&#xff1a;&#x1f334;2022年11月24日&#x1f334; &#x1f3…

零基础搭建基于知识图谱的电影问答系统

零基础搭建基于知识图谱的电影问答系统一、项目准备二、项目数据三、训练问题分类器四、准备问答模板五、搭建webapp六、问题预处理一、项目准备 首先需要一款python编译器&#xff0c;本人选用的是PyCharm&#xff0c;搭建好Python环境&#xff1b;安装第三方依赖库&#xff…

【Hack The Box】linux练习-- Delivery

HTB 学习笔记 【Hack The Box】linux练习-- Delivery &#x1f525;系列专栏&#xff1a;Hack The Box &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd; &#x1f4c6;首发时间&#xff1a;&#x1f334;2022年11月17日&#x1f334; &#x1…

黄佳《零基础学机器学习》chap1笔记

黄佳 《零基础学机器学习》 chap1笔记 这本书实在是让我眼前一亮&#xff01;&#xff01;&#xff01; 感觉写的真的太棒了&#xff01; 文章目录黄佳 《零基础学机器学习》 chap1笔记第1课 机器学习快速上手路径—— 唯有实战1.1 机器学习族谱1.2 云环境入门实践&#xff1a;…