并发编程-线程池详解(ThreadPoolExecutor源码分析)

线程池详解

一、为什么要有线程池?

当只有线程的时候,会有几个很明显的缺点:
1、来一个任务要创建一个线程,导致线程创建的数量不可控;
2、频繁的创建和销毁线程,会消耗很多资源;

所以,就有了 “线程池” 。

它的优势:
1、限流 -> 可以通过参数控制线程的数量,合理设置线程池大小避免出现资源瓶颈;
2、降低频繁创建和销毁线程;
3、对于任务的响应速度更快;(不再需要创建线程,而是复用已有的线程,所以就更快啦);

在这里插入图片描述

二、JAVA中提供的线程池

2.1 创建线程池的五种方式
	// 创建线程池的方式
    // 第一种
    ExecutorService executorService1 = Executors.newFixedThreadPool();

    // 第二种
    ExecutorService executorService2 = Executors.newCachedThreadPool();

    // 第三种
    ExecutorService executorService3 = Executors.newSingleThreadExecutor();

    // 第四种 -- 定时任务用的比较多
    ExecutorService executorService4 = Executors.newScheduledThreadPool();

    // 第五种 -- JDK8 增加
    ExecutorService executorService5 = Executors.newWorkStealingPool();
2.2 五种方式的区别
2.2.1 newFixedThreadPool - 生成固定线程数的线程池

运行测试:

package com.exercise.exercise_1126;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @description: 测试newFixedThreadPool
 * @author: annecheng,2019-11-26 20:53
 */
public class AppNewFixed implements Runnable{

    // 固定线程数的线程池
    static ExecutorService service = Executors.newFixedThreadPool(3);

    public static void main(String[] args) {
        for (int i = 0; i<100;++i) {
            service.execute(new AppNewFixed());
        }
        service.shutdown();
    }


    @Override
    public void run() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
}

结果如下:
在这里插入图片描述
所以说呢,newFixedThreadPool生成的是固定线程数的线程池。
如果任务数 > 线程数量,那么会把剩余的任务放在任务队列里,然后通过线程不断从任务队列里取任务执行;

2.2.2 newCachedThreadPool - 伸缩性,可以根据实际情况动态调整线程数

不会限制最大的线程数
回收机制:每60s会回收一次(也就是说空闲线程60s以后会回收)

2.2.3 newSingleThreadExecutor - 只有一个核心线程的线程池
package com.exercise.exercise_1126;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @description: 测试newSingleThreadExecutor
 * @author: annecheng,2019-11-26 20:53
 */
public class AppTest implements Runnable{

    // 固定线程数的线程池
    static ExecutorService service = Executors.newSingleThreadExecutor();

    public static void main(String[] args) {
        for (int i = 0; i<100;++i) {
            service.execute(new AppTest());
        }
        service.shutdown();
    }


    @Override
    public void run() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
}

结果如下:
在这里插入图片描述所以说呢,newSingleThreadExecutor生成的是一个核心线程的线程池。

2.3 总结

在这里插入图片描述

2.4 源码分析,其底层都是调用ThreadPoolExecutor
 public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    
 public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

LinkedBlockingQueue:双向链表的阻塞队列
SynchronousQueue:不存储元素的阻塞队列

// 底层

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

三、ThreadPoolExecutor 详解

3.1 构造 - 参数详解

在这里插入图片描述

3.2 execute方法 详解

先上一个大纲图叭~
在这里插入图片描述

3.3 execute 源码分析
 public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
3.3.1 addWorker(command, true) 源码分析

参数:当前任务 以及 是否是核心线程的标志

做两件事
1、添加工作线程数
在这里插入图片描述
2、构建工作线程在这里插入图片描述

3.3.2 Worker 源码分析

问题:为什么不用ReentrantLock,而要重写AQS?
理由:ReentrantLock 支持重入,而这里的不支持重入
为什么不支持重入?
在这里插入图片描述
调用t.start() 会回调worker的run方法,run方法调用runworker()
(线程的start 会 执行 run方法)

runworker() :不断的执行任务
在这里插入图片描述
getTask() 源码分析
做两件事:
1、从队列中拿出数据
2、回收线程
在这里插入图片描述
如何回收非核心线程?
当线程数 > 核心线程数的时候,根据超时时间去阻塞当前线程,如果超时时间过了,还没有任务的话,就销毁非核心线程;
核心线程可以被回收吗?如何回收?
可以的,通过设置allowCoreThreadTimeOut参数为true,也就可以被回收啦;

 public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor)executorService1;
        threadPoolExecutor.allowCoreThreadTimeOut(true);
    }

四、其他问题

4.1 阿里手册里不建议使用Executors创建线程,为哈?

线程会影响cpu和内存的占用,Executors创建时会带入很多默认参数,如果不了解去直接用,不安全,但是如果去重写自己构造的话,就会很清楚每一个参数的意义,比较可控一点。

4.2 线程池大小应该如何设置?

线程池的大小取决于硬件环境和软件环境;
硬件环境:CPU的核心数
软件环境:线程的执行情况 -> IO密集型(CPU时间片的切换) 和 CPU密集型
IO密集型,可以设置更多线程,如果被IO阻塞住了,CPU就可以执行其他任务,而任务越多,就可以不断切换,从而达到最大性能,CPU核心数的两倍;
或者依赖这个公式:最大线程数 = (线程等待的时间 + 线程CPU时间) / 线程CPU时间 * CPU核心数。
CPU密集型,计算任务多,cpu利用率会非常高,应该设置少一点的线程(多的线程会导致要CPU一直切换,降低性能),以CPU核心数为准,设置最大线程数为CPU核心数+1;

4.3 线程池的初始化
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor)executorService1;
        threadPoolExecutor.prestartAllCoreThreads(); // 线程池的初始化 -- 预热
    }

可以先预热,这样有任务来临时,就可以直接执行了;

4.4 线程池的关闭

shutdown():等到任务执行完毕,才会终止线程;
shutdownNow():当前立马终止,不管任务是不是在执行过程中;

4.4 线程池容量的调整

通过set去调整
在这里插入图片描述

4.5 线程池的excute和submit的区别

通过Callable/Future 实现带返回值的线程;
所以,submit可以实现带返回值的任务(阻塞获取返回值),excute不支持返回值;
在这里插入图片描述
excute发生错误会抛出异常,submit不会抛出异常;

五、线程池常见面试题

5.1 如果在线程池中使用无界阻塞队列会发生什么问题?(在远程服务异常的情况下,使用无界阻塞队列,是否会导致内存异常飙升?)

如果线程在访问服务的时候出现异常,如超时等,那么,队列里就会一直有任务堆积等待,随着时间的推移,任务处理越来越慢,队列里的数量就会变得越来越多,导致内存异常飙升,进而导致内存溢出,oom。

5.2 线程池的队列满了之后,会发生什么事情?

如果是无界队列,可能会导致oom;
如果是有界队列,设置最大线程数 = integer.max,那么会导致不停的创建额外的线程出来,导致一台机器上有成千上万的线程,而线程都有自己的栈内存,占有一定的内存资源,会导致内存资源耗尽,系统崩溃。
所以要结合具体的业务情况来设置参数,最好控制一下线程数,自行设置拒绝策略,如将处理不过来的任务持久化到磁盘上去,等线程池的负载降下来了,再去处理。

5.3 如果线上机器突然宕机,线程池的阻塞队列中的请求怎么办?

机器宕机,那么队列里积压的任务肯定会丢失。

解决方案:数据落库,即提交任务的时候,在数据库里插入这个任务信息,更新状态未提交,提交完成之后,更新状态已提交,执行完成之后,更新状态已完成,这样宕机后,机器重启可以根据状态从数据库里捞任务。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值