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

【并发】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

posted @ 2022-11-27 01:05  金鳞踏雨  阅读(19)  评论(0编辑  收藏  举报  来源