写点什么

霸气!这份清华学霸整理的 Java 线程池笔记,2 小时从入门到入坟

用户头像
飞飞JAva
关注
发布于: 2021 年 05 月 09 日
霸气!这份清华学霸整理的Java线程池笔记,2小时从入门到入坟

Hello,今天给各位童鞋们分享 Java 线程池,赶紧拿出小本子记下来吧!


​1.为什么使用线程池 1.频繁创建和销毁单个线程,浪费资源,并且还会出现频繁 GC

2.缺乏统一管理,各线程相互竞争

2.ThreadPoolExecutorThreadPoolExecutor 有四个重载的构造方法,我们这里来说说参数最多的那一个重载的构造方法,这样大家就知道其他方法参数的含义了,如下:

public ThreadPoolExecutor(int corePoolSize,


                          int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
复制代码


各参数详细说明:

这里是 7 个参数(我们在开发中用的更多的是 5 个参数的构造方法),OK,那我们来看看这里七个参数的含义:

corePoolSize 线程池中核心线程的数量

maximumPoolSize 线程池中最大线程数量

keepAliveTime 非核心线程的超时时长,当系统中非核心线程闲置时间超过 keepAliveTime 之后,则会被回收。如果 ThreadPoolExecutor 的 allowCoreThreadTimeOut 属性设置为 true,则该参数也表示核心线程的超时时长

unit 第三个参数的单位,有纳秒、微秒、毫秒、秒、分、时、天等

workQueue 线程池中的任务队列,该队列主要用来存储已经被提交但是尚未执行的任务。存储在这里的任务是由 ThreadPoolExecutor 的 execute 方法提交来的。

threadFactory 为线程池提供创建新线程的功能,这个我们一般使用默认即可

handler 拒绝策略,当线程无法执行新任务时(一般是由于线程池中的线程数量已经达到最大数或者线程池关闭导致的),默认情况下,当线程池无法处理新线程时,会抛出一个 RejectedExecutionException。

workQueue 介绍

1.ArrayBlockingQueue:这个表示一个规定了大小的 BlockingQueue,ArrayBlockingQueue 的构造函数接受一个 int 类型的数据,该数据表示 BlockingQueue 的大小,存储在 ArrayBlockingQueue 中的元素按照 FIFO(先进先出)的方式来进行存取

2.LinkedBlockingQueue:这个表示一个大小不确定的 BlockingQueue,在 LinkedBlockingQueue 的构造方法中可以传一个 int 类型的数据,这样创建出来的 LinkedBlockingQueue 是有大小的,也可以不传,不传的话,LinkedBlockingQueue 的大小就为 Integer.MAX_VALUE,源码如下:

3.PriorityBlockingQueue:这个队列和 LinkedBlockingQueue 类似,不同的是 PriorityBlockingQueue 中的元素不是按照 FIFO 来排序的,而是按照元素的 Comparator 来决定存取顺序的(这个功能也反映了存入 PriorityBlockingQueue 中的数据必须实现了 Comparator 接口)。

4.SynchronousQueue:这个是同步 Queue,属于线程安全的 BlockingQueue 的一种,在 SynchronousQueue 中,生产者线程的插入操作必须要等待消费者线程的移除操作,Synchronous 内部没有数据缓存空间,因此我们无法对 SynchronousQueue 进行读取或者遍历其中的数据,元素只有在你试图取走的时候才有可能存在。我们可以理解为生产者和消费者互相等待,等到对方之后然后再一起离开。

拒绝策略 AbortPolicy:直接拒绝,并抛出异常,这也是默认的策略。

CallerRunsPolicy:直接让调用 execute 方法的线程去执行此任务。

DiscardOldestPolicy:丢弃最老的未处理的任务,然后重新尝试执行当前的新任务。

DiscardPolicy:直接丢弃当前任务,但是不抛异常

3.执行过程

当线程数量未达到 corePoolSize 的时候,就会创建新的线程来执行任务。

当核心线程数已满,就会把任务放到阻塞队列。

当队列已满,并且未达到最大线程数,就会新建非核心线程来执行任务(重要)。

当队列已满,并且达到了最大线程数,则选择一种拒绝策略来执行。

4.其他线程池

1.FixedThreadPool

固定大小的线程池,可以指定线程池的大小,该线程池 corePoolSize 和 maximumPoolSize 相等,阻塞队列使用的是 LinkedBlockingQueue,大小为整数最大值。

该线程池中的线程数量始终不变,当有新任务提交时,线程池中有空闲线程则会立即执行,如果没有,则会暂存到阻塞队列。对于固定大小的线程池,不存在线程数量的变化。

同时使用无界的 LinkedBlockingQueue 来存放执行的任务。当任务提交十分频繁的时候,LinkedBlockingQueue 迅速增大,存在着耗尽系统资源的问题。

而且在线程池空闲时,即线程池中没有可运行任务时,它也不会释放工作线程,还会占用一定的系统资源,需要 shutdown

2.SingleThreadExecutor



可以看到阻塞队例 使用的是 LinkedBolckingQueue,且默认大小为 Integer.MAX_VALUE,这样的话,如果有大量请求到来,会放入到这个任务队列里,可能会导致 OOM;

3.Executors.newCachedThreadPool()



可缓存线程池,先查看线程池中有没有以前建立的线程,如果有就直接使用,如果没有新建一个线程加入线程池中,可缓存线程池

通常用于执行一些生存期很短的异步型任务;线程池为无限大,当执行当前任务时上一个任务已经完成,会复用执行上一个任务的线程,而不用每次新建线程

缓存的线程默认存活 60 秒。线程的核心池 corePoolSize 大小为 0,核心池最大为 Integer.MAX_VALUE,阻塞队列使用的是 SynchronousQueue。

是一个直接提交的阻塞队列,他总会迫使线程池增加新的线程去执行新的任务。

在没有任务执行时,当线程的空闲时间超过 keepAliveTime(60 秒),则工作线程将会终止被回收,当提交新任务时,

如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销。

如果同时又大量任务被提交,而且任务执行的时间不是特别快,那么线程池便会新增出等量的线程池处理任务,这很可能会很快耗尽系统的资源。

4.ScheduledThreadPool



创建一个定长线程池,支持定时及周期性任务执行

定时线程池,该线程池可用于周期性地去执行任务,通常用于周期性的同步数据。

scheduleAtFixedRate:是以固定的频率去执行任务,周期是指每次执行任务成功执行之间的间隔。

schedultWithFixedDelay:是以固定的延时去执行任务,延时是指上一次执行成功之后和下一次开始执行的之前的时间。

5.为什么阿里推荐自定义线程池

通过上述源码分析,我们发现 newFixedThreadPool 和 newSingleThreadExecutor 方法他们都使用了 LinkedBlockingQueue 的任务队列,LinkedBlockingQueue 的默认大小为 Integer.MAX_VALUE。而 newCachedThreadPool 中定义的线程池大小为 Integer.MAX_VALUE。

所以阿里禁止使用 Executors 创建线程池的原因就是 FixedThreadPool 和 SingleThreadPool 的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

CachedThreadPool 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

6.其他

1.shutDown() 关闭线程池,不影响已经提交的任务

2.shutDownNow() 关闭线程池,并尝试去终止正在执行的线程

3.allowCoreThreadTimeOut(boolean value) 允许核心线程闲置超时时被回收

4.单例模式创建线程池

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.util.concurrent.*;

/**

  • 异步任务处理器

*/

public class AsyncTaskExecutor {

/** 线程池保持ALIVE状态线程数 */
public static final int CORE_POOL_SIZE = 10;
/** 线程池最大线程数 */
public static final int MAX_POOL_SIZE = 40;
/** 空闲线程回收时间 */
public static final int KEEP_ALIVE_TIME = 1000;
/** 线程池等待队列 */
public static final int BLOCKING_QUEUE_SIZE = 1000;
/** 业务请求异步处理线程池 */
private static final ThreadPoolExecutor processExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.MICROSECONDS,
new LinkedBlockingQueue<Runnable>(BLOCKING_QUEUE_SIZE),
复制代码


new TreadFactoryBuilder.setNameFormat("boomoom-thread-pool-%d").build(),

new TreadPoolExecutor.DiscardPolicy());


private AsyncTaskExecutor() {}; 
/**
* 异步任务处理
*
* @param task 任务
*/
public void execute(Runnable task) {
processExecutor.submit(task);
}
复制代码


}


懒汉式和饥汉式区别


1.饿汉式是线程安全的,在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变。


2.懒汉式如果想要线程安全必须用双重检验锁并且对象还必须是 volatile,防止对象指令重排


好啦,今天的文章就到这里,希望能帮助到屏幕前迷茫的你们​


用户头像

飞飞JAva

关注

还未添加个人签名 2021.04.28 加入

分享、普及java相关知识

评论

发布
暂无评论
霸气!这份清华学霸整理的Java线程池笔记,2小时从入门到入坟