一、Thread
Java创建线程Thread的三种方式
1、通过继承Thread类创建线程
- 单继承:编写简单,缺点是只能继承一个类,要是想同时继承其他业务类,不能实现;要想实现多继承,只能使用implements
2、通过实现Runnable接口来创建线程
- 数据共享:Runnable是可以实现数据共享的,多个Thread可以同时加载一个runnable
- 线程不安全:当各自Thread获得CPU时间片的时候开始运行Runnable,Runnable里面的资源是被共享的,所以使用Runnable更加的灵活,但是也容易造成线程不安全
3、通过实现Callable接口来创建线程
- 有返回值:Runnable是执行工作的独立任务,但是它不返回任何值。如果你希望任务在完成的能返回一个值,那么可以实现Callable接口而不是Runnable接口
二、ThreadPoolExecutor
2.1、线程池的概念
- 线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,如MySQL。
- 使用线程池可以带来一系列好处:
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
2.2、ThreadPoolExecutor的使用
* corePoolSize: 线程池维护线程的最少数量
* maximumPoolSize:线程池维护线程的最大数量
* keepAliveTime:非核心线程最大存活时长, 线程池维护线程所允许的空闲时间
* unit: 线程池维护线程所允许的空闲时间的单位
* workQueue: 线程池所使用的缓冲队列
* handler: 线程池对拒绝任务的处理策略
// 五个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,//核心线程数
int maximumPoolSize,//最大线程数
long keepAliveTime,//非核心线程最大存活时长
TimeUnit unit,//时长单位
BlockingQueue<Runnable> workQueue)//阻塞队列,存放着等待执行的线程任务
// 六个参数的构造函数-1
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)//创建线程的工厂
// 六个参数的构造函数-2
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)//拒绝策略,当线程池无法再接受任务时调用
// 七个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
-
必传入参
-
corePoolSize:指定了线程池中的核心线程数,即不会因线程空闲而被销毁的线程。线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务。它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去。
核心线程:线程池中有两类线程,核心线程和非核心线程。核心线程默认情况下会一直存在于线程池中,即使这个核心线程什么都不干(铁饭碗),而非核心线程如果长时间的闲置,就会被销毁(临时工)。
-
maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量。
该值等于核心线程数量 + 非核心线程数量。
最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)。 -
keepAliveTime:非核心线程最大存活时长。
非核心线程如果处于闲置状态超过该值,就会被销毁。如果设置allowCoreThreadTimeOut(true),则会也作用于核心线程。
非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);非核心线程:当线程池中空闲线程数量超过corePoolSize时,多余的线程就叫非核心线程。 -
unit:keepAliveTime的单位。
TimeUnit是一个枚举类型 ,包括以下属性:
NANOSECONDS : 1微毫秒 = 1微秒 / 1000 MICROSECONDS : 1微秒 = 1毫秒 / 1000 MILLISECONDS : 1毫秒 = 1秒 /1000 SECONDS : 秒 MINUTES : 分 HOURS : 小时 DAYS : 天 -
workQueue:阻塞队列,存放着等待执行的Runnable任务对象。它一般分为直接提交队列、有界任务队列、*任务队列、优先任务队列、延迟队列几种。
SynchronousQueue:同步队列,内部容量为0,每个put操作必须等待一个take操作,反之亦然。
LinkedBlockingQueue:链式阻塞队列,底层数据结构是链表,默认大小是Integer.MAX_VALUE,也可以指定大小。
ArrayBlockingQueue:数组阻塞队列,底层数据结构是数组,需要指定队列的大小。
PriorityBlockingQueue:基于优先级的*阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),内部控制线程同步的锁采用的是非公平锁。
DelayQueue:延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素
-
-
选传入参
- threadFactory:创建线程的工厂,一般用默认即可。可以自定义线程的名字,设置一些参数等等,如果不想自定义,可以使用默认的Executors.defaultThreadFactory()创建工厂。
-
handle:拒绝策略,当线程池数量大于maximumPoolSize,则执行拒绝策略。
ThreadPoolExecutor.AbortPolicy:默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常.
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)。
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
2.3、使用规范
-
1、线程池数量设置
io密集型程序:线程数=cpu核数+1;(计算密集型,但是任务越多,任务切换时间越多,则执行效率低,配置线程数尽可能小,则同时进行的任务数量等于cpu核数,线程数=cpu核数+1,处理任务中尽量少打日志,较少IO操作)
cpu密集型程序:2*CPU核数;(涉及网络和磁盘IO操作,IO密集型任务线程并不是一直在执行任务,配置线程数尽可能多,如2*CPU核数) -
2、线程池的处理流程
一个任务通过 execute(Runnable)方法被添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是 Runnable类型对象的run()方法
当一个任务通过execute(Runnable)方法欲添加到线程池时:
1、如果此时线程池中的数量<corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
2、如果此时线程池中的数量= corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
3、如果此时线程池中的数量>corePoolSize,缓冲队列workQueue满,并且线程池中的数量<maximumPoolSize,建新的线程来处理被添加的任务
4、如果此时线程池中的数量>corePoolSize,缓冲队列workQueue满,并且线程池中的数量=maximumPoolSize,那么通过 handler所指定的策略来处理此任务。也就是处理任务的优先级为:
核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。
2.4、四种常见线程池设置
1、Executors类中提供了几个静态方法实现了创建线程池
-
newCachedThreadPool
核心线程数为0,非核心线程数为Integer.MAX_VALUE,所以这是一个线程只要空闲60秒就会被回收的线程池,适用于短时间高并发的处理业务,而在峰值过后并不会占用系统资源。public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory); }
-
newFixedThreadPool
核心线程数量和总线程数量相等,都是传入的参数nThreads,这是一个池中都是核心线程的线程池,所有线程都不会销毁。执行任务的全是核心线程,当没有空闲的核心线程时,任务会进入到阻塞队列,直到有空闲的核心线程才会去从阻塞队列中取出任务并执行,也导致该线程池基本不会发生使用拒绝策略拒绝任务。还有因为LinkedBlockingQueue阻塞队列的大小默认是Integer.MAX_VALUE,如果使用不当,很可能导致内存溢出public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
-
newSingleThreadExecutor
有且仅有一个核心线程的线程池( corePoolSize == maximumPoolSize=1),使用了LinkedBlockingQueue(容量很大),所以,不会创建非核心线程。所有任务按照先来先执行的顺序执行。如果这个唯一的线程不空闲,那么新来的任务会存储在任务队列里等待执行。public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory)); }
-
newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行,这是一个支持延时任务执行的线程池。public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, //注意这里使用延迟队列 new DelayedWorkQueue()); }
使用案例
ScheduledExecutorService pool = Executors.newScheduledThreadPool(10); // 入参Runnable实例,延时时间,时间单位 pool.schedule(() -> System.out.println("执行1"), 5, TimeUnit.SECONDS); pool.schedule(() -> System.out.println("执行2"), 4, TimeUnit.SECONDS); pool.schedule(() -> System.out.println("执行3"), 6, TimeUnit.SECONDS); 打印结果: 执行2 执行1 执行3
2、newCachedThreadPool与newFixedThreadPool区别
- 因为
corePoolSize == maximumPoolSize
,所以FixedThreadPool
只会创建核心线程。 而CachedThreadPool
因为corePoolSize=0
,所以只会创建非核心线程。 -
FixedThreadPool
在getTask()
方法,如果队列里没有任务可取,线程会一直阻塞在LinkedBlockingQueue.take()
,线程不会被回收。CachedThreadPool
会在60s后收回。 - 由于线程不会被回收,会一直卡在阻塞,所以没有任务的情况下,
FixedThreadPool
占用资源更多。 - 都几乎不会触发拒绝策略,但是原理不同。
FixedThreadPool
是因为阻塞队列可以很大(最大为Integer.MAX_VALUE
),故几乎不会触发拒绝策略;CachedThreadPool
是因为线程池很大(最大为Integer.MAX_VALUE
),几乎不会导致线程数量大于最大线程数,故几乎不会触发拒绝策略。
参考文档:
https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
https://blog.csdn.net/qq_41135605/article/details/11445310