JDK默认提供了四种线程池:SingleThreadExecutor、FiexdThreadPool、CachedThreadPool、ScheduledThreadPoolExecutor。

本文会先从前三个线程池的使用开始讲解,然后过度到线程池参数、拒绝策略等方面进行全面讲解,最后自己根据参数构造一个

线程池。

SingleThreadExecutor

    public static void singleThreadExecutorTest() {
ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.execute(()->{
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executorService.execute(()->{
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executorService.execute(()->{
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}); try {
Thread.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
executorService.shutdown();
}

该代码是使用Exectors工具类创建的一个大小为1的线程池,并且创建三个任务提交到该线程执行,每个任务执行需要一秒。因此从运行结果中我们可以很清楚的看到三个任务一次只能执行一个线程。

FiexdThreadPool

public static void fixedThreadPoolTest() {
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(()->{
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
service.execute(()->{
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
service.execute(()->{ System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}); try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
service.shutdown();
}

该代码是使用Exectors工具类创建的一个大小为2的线程池,并且创建三个任务提交到该线程执行,每个任务执行需要一秒。从结果可以看到第一秒两个线程一起执行了,第二秒第三个线程才执行。

CachedThreadPool

public static void cachedThreadPoolTest() {
ExecutorService service = Executors.newCachedThreadPool();
service.execute(()->{
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
service.execute(()->{
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
service.execute(()->{ System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}); try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
service.shutdown();
}

该代码是使用Exectors工具类创建的一个大小为Integer.MAX_VALUE的线程池,并且创建三个任务提交到该线程执行,每个任务执行需要一秒。从结果可以看到三个线程一起执行了。

原理分析

构造SingThreadExecutor需要的参数

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

构造FiexdThreadPool需要的参数

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

构造CachedThreadPool需要的参数

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

通过上面三个线程池构造的源码我们可以看到,创建线程池的时候都是通过创建ThreadPoolExecutor对象,创建ThreadPoolExecutor需要5个参数,那么这5个参数是什么呢,我们继续跟踪源码

构造ThreadPoolExecutor对象需要的参数

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

这里可以看到生成ThreadPoolExecutor对象需要为其构造参数传递5个参数,但是下面的this里却是7个参数。原来有两个参数是已经固定好的。我们跟踪进去看看这七个参数

ThreadPoolExecutor构造器源码

到这里重头戏来了,下面这个代码才是真真正在创建ThreadPoolExecutor对象的构造器源码。我们可以看到一共有7个参数,我们来谈谈这七个参数

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

创建线程池需要的七个参数

int corePoolSize : 常驻核心线程数

int maximumPoolSize:最大线程数

long keepAliveTime:除了常驻线程的额外线程的存活时间

TimeUnit unit:存活时间的单位

BlockingQueue<Runnable> workQueue:任务队列

ThreadFactory threadFactory:创建线程的工厂

RejectedExecutionHandler handler:拒绝策略

我们先眼熟一下这七个参数,接下来我们从线程池的工作流程来讲解这七个参数

线程池工作流程

线程池刚创建的时候里面是空的,这时候还没有线程。当main线程往线程池里面加任务的时候才会开始创线程

1 主线程往线程池里添加任务,发现核心线程为空,创建核心线程执行任务

2 随着主线程往线程池里加任务,核心线程都被占用了,这时任务会被放入任务队列

3 随着主线程继续往线程池里加任务,任务队列也满了,线程池会创建额外线程执行任务。

4 主线程继续添加任务,任务队列满了,核心线程满了。额外线程+核心线程=总线程数,即额外线程也满了,这时候就会执行拒绝策略。

5 如果没有到4那么极端,随着任务的执行,任务队列越来越少,直至没有,那么额外线程在等待keepaliveTime时间后被销毁,线程池里只剩下核心线程存在。

相信知道了流程以后,上面的7个参数我们也就知道都是什么了,这里需要注意的一点是核心线程数是包含在总线程数里面。

谈谈JDK自带的线程池弊端

从上面我们可以看到,JDK自带的SingleThreadExecutor、FixedThreadPool都是采用LinkedBlockingQueue阻塞队列作为任务队列,由于LinkedBlockingQueue是一个

大小为Integer.MAX_VALUE的阻塞队列,因此main线程在添加任务的时候阻塞队列不会满,也就是不会触发拒绝策略,可能会导致任务持续添加引发OOM。而CachedThreadPool

的最大线程数为Integer.MAX_VALUE,会无线创建线程,来一个任务创一个线程,可能会导致线程创太多导致OOM所有我们一般都是自己构造参数创线程池。

四大拒绝策略

AbortPolicy:当任务队列和线程达到最大线程数后还添加任务的话会直接抛异常,阻止程序运行

CallerRunsPolicy:当任务队列和线程达到最大线程数后还添加任务不会抛异常和抛弃任务,而是会将任务回退给调用者。

DiscardOldestPolicy:当任务队列和线程达到最大线程数后还添加任务的话会将任务队列中等待最久的一个任务抛弃掉然后添加新任务

DiscardPolicy:直接抛弃任务。不做任何处理也不抛异常

自己构造线程池

该线程池核心线程数为2,最大线程数为5,额外线程存活时间为60秒,阻塞队列大小为3,采用默认的线程工厂,采用默认的拒绝策略,即抛异常

因此该线程池最多可以连续提供8个任务,超过8个很可能促发拒绝策略,抛异常

public static void myThreadPool() {

        ExecutorService threadPool = new ThreadPoolExecutor(2,
5,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()); for(int i=0;i<8;i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadPool.shutdown();
}

线程池参数如何选择

CPU密集型:CPU密集型CPU使用率高,一直在工作,空闲时间少,因此最大线程数最好设为CPU核数+1

IO密集型:IO密集型大部分时间都在做IO操作,CPU空闲时间多,因此最大线程数最好设为CPU核数*2.

JUC线程池深入刨析的更多相关文章

  1. 死磕 java线程系列之线程池深入解析——普通任务执行流程

    (手机横屏看源码更方便) 注:java源码分析部分如无特殊说明均基于 java8 版本. 注:线程池源码部分如无特殊说明均指ThreadPoolExecutor类. 简介 前面我们一起学习了Java中 ...

  2. Java并发编程与技术内幕:线程池深入理解

    摘要: 本文主要讲了Java当中的线程池的使用方法.注意事项及其实现源码实现原理,并辅以实例加以说明,对加深Java线程池的理解有很大的帮助. 首先,讲讲什么是线程池?照笔者的简单理解,其实就是一组线 ...

  3. Java线程池深入理解

    之前面试baba系时遇到一个相对简单的多线程编程题,即"3个线程循环输出ADC",自己答的并不是很好,深感内疚,决定更加仔细的学习<并发编程的艺术>一书,到达掌握的强度 ...

  4. 转:Java并发编程与技术内幕:线程池深入理解

    版权声明:本文为博主林炳文Evankaka原创文章,转载请注明出处http://blog.csdn.net/evankaka 目录(?)[+] ); } catch (InterruptedExcep ...

  5. 原创:ThreadPoolExecutor线程池深入解读(一)----原理+应用

    本文档,适合于对多线程有一定基础的开发人员.对多线程的一些基础性的解读,请参考<java并发编程>的前5章. 对于源代码的解读,本人认为可读可不读.如果你想成为一位顶级的程序员,那就培养自 ...

  6. 死磕 java线程系列之线程池深入解析——体系结构

    (手机横屏看源码更方便) 注:java源码分析部分如无特殊说明均基于 java8 版本. 简介 Java的线程池是块硬骨头,对线程池的源码做深入研究不仅能提高对Java整个并发编程的理解,也能提高自己 ...

  7. 死磕 java线程系列之线程池深入解析——生命周期

    (手机横屏看源码更方便) 注:java源码分析部分如无特殊说明均基于 java8 版本. 注:线程池源码部分如无特殊说明均指ThreadPoolExecutor类. 简介 上一章我们一起重温了下线程的 ...

  8. 死磕 java线程系列之线程池深入解析——未来任务执行流程

    (手机横屏看源码更方便) 注:java源码分析部分如无特殊说明均基于 java8 版本. 注:线程池源码部分如无特殊说明均指ThreadPoolExecutor类. 简介 前面我们一起学习了线程池中普 ...

  9. 死磕 java线程系列之线程池深入解析——定时任务执行流程

    (手机横屏看源码更方便) 注:java源码分析部分如无特殊说明均基于 java8 版本. 注:本文基于ScheduledThreadPoolExecutor定时线程池类. 简介 前面我们一起学习了普通 ...

随机推荐

  1. 怕忘记-windows 2003服务器安装Node.js NPM

    现在高版本的Nodejs安装已经自带了NPM模块,本次我需要安装的是:supervisor 执行命令: npm install -g supervisor 等待安装完成. 查看版本可以cmd里面运行: ...

  2. 【Trapping Rain Water】cpp

    题目: Given n non-negative integers representing an elevation map where the width of each bar is 1, co ...

  3. ogre3D学习基础19 --- 材质的继承,纹理的滚动与旋转

    以上一节为基础,废话不多说. 首先新增一个节点,用于比较显示 //新增一个节点 ent = mSceneMgr->createEntity("Quad"); ent-> ...

  4. flask url_for()和redirect的区别

    一. 两者用来重定向的时候,被操作的对象不同. redirect直接是url,就是app.route的路径参数. url_for()是对函数进行操作. from flask import Flask, ...

  5. [oldboy-django][2深入django]mysql查询语句--原生sql

    # 增(一共有三种方式) # 插入单条记录 insert into t1(name,...) values('lzp',..); 注意一点:t1(name,...)必须包含所有非空列(除去自增列) # ...

  6. 初学Linux 命令

    查看ip:ifconfig 切换用户:us root(root为用户名) 显示当前目录:pwd 列出当前目录下所有文件:ls 进入某个目录 :cd 创建一个文件夹:mkdir 创建多个目录(当没有该父 ...

  7. Android EditText默认不弹出输入法,以及获取光标,修改输入法Enter键的方法

    一.Android EditText默认不弹出输入法的办法:1. 在AndroidManifest.xml中将需要默认隐藏键盘的Activity中添加属性即可(常用此方法) android:windo ...

  8. [LOJ#2329]「清华集训 2017」我的生命已如风中残烛

    [LOJ#2329]「清华集训 2017」我的生命已如风中残烛 试题描述 九条可怜是一个贪玩的女孩子. 这天她在一堵墙钉了 \(n\) 个钉子,第 \(i\) 个钉子的坐标是 \((x_i,y_i)\ ...

  9. php中的对象赋值

    如果现在问你一个问题: <?php class A{ public $a = 1; } $a = new A(); $b = $a; $b->a = 3; print_r($a->a ...

  10. webpack 样式模块打包深入学习

    1. style-loader css-loader sass-loader 分别的作用 style-loader: 将所有的样式嵌入到dom的style属性当中. css-loader: 将css当 ...