线程及线程池
本篇文章主要简述线程及线程池的类型、功能以及线程优先级等几个重要部分以文码结合的方式进行说明
编写不易,如果可以,还希望来波点赞、关注,有想吐槽的或内容纠错的,还请辛苦辛苦,评论区说明。
2021 编码暴富~!
线程
线程实现方式
线程的实现方式总共有三种,追根溯源其实只有两种。
- 有返回值的线程 -> Callable(接口)
package com.base.spring.test;
import java.util.concurrent.Callable;
/**
* Description:
* 测试Callable
* <p>
* ClassName: TestCallable
* date: 2021/1/4 10:02
*
* @author jo.li
* @version 1.0
* @since JDK 1.8
*/
public class TestCallable implements Callable {
@Override
public Object call() throws Exception {
// 输出线程信息
System.out.println(Thread.currentThread()+"CallAble");
return "我是Callable返回值,我执行完了。";
}
}
- 无返回值的 -> Runnable(接口)
package com.base.spring.test;
/**
* Description:
* Runnable测试类
* <p>
* ClassName: TestRunnable
* date: 2021/1/4 9:32
*
* @author jo.li
* @version 1.0
* @since JDK 1.8
*/
public class TestRunnable implements Runnable{
@Override
public void run() {
// 输出线程信息
System.out.println(Thread.currentThread()+"Runnable");
}
}
- 以及同样无返回值的 -> Thread (类)
package com.base.spring.test;
/**
* Description:
* Thread测试类
* <p>
* ClassName: TestThread
* date: 2021/1/4 9:34
*
* @author jo.li
* @version 1.0
* @since JDK 1.8
*/
public class TestThread extends Thread{
@Override
public void run(){
// 输出线程信息
System.out.println(Thread.currentThread()+"Thread");
}
}
实现的三种方式中只有Thread是继承的方式实现的。通过源码可以看出,Thread实现了Runnable接口,并封装了一些方法。内部方法包含sleep、clone、init、yield就不赘述了。
线程的优先级
- 线程优先级说明
Java中线程优先级用1~10来表示,分为三个级别:
低优先级:1~4,其中类变量Thread.MIN_PRORITY最低,数值为1;
默认优先级:如果一个线程没有指定优先级,默认优先级为5,由类变量Thread.NORM_PRORITY表示;
高优先级:6~10,类变量Thread.MAX_PRORITY最高,数值为10。
注意:具有相同优先级的多个线程,若它们都为高优先级Thread.MAX_PRORITY,则每个线程都是独占式的,也就是这些线程将被顺序执行;
若它们优先级不是高优先级,则这些线程将被同时执行,可以说是无序执行。 - 优先级设置及获取
通过Thread类的Priority来控制优先级 get/set
线程池
手动创建线程池
- 该线程池创建方式也是阿里开发手册推荐的方法,也是各类线程池实现的基础,知一可通其四。
- 使用方式
public static void main(String[] args) {
// 测试手动创建的线程池
TestSourceThreadPoll();
}
/**
* Description: <br/>
* 手动创建线程池
* TestSourceThreadPoll <br/>
* Date 2021/1/4 11:58
* @return: void
* @author jo.li
**/
static void TestSourceThreadPoll(){
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,1,1,TimeUnit.SECONDS,
new LinkedBlockingQueue(),
new ThreadPoolExecutor.CallerRunsPolicy());
threadPoolExecutor.execute(new TestThread());
threadPoolExecutor.execute(new TestRunnable());
Future<String> future = threadPoolExecutor.submit(new TestCallable());
readResult(future);
}
/**
* Description: <br/>
* 读取返回结果 <br/>
* readResult <br/>
* Date 2021/1/4 10:39
* @param future: 返回结果集
* @return: void
* @author jo.li
**/
static void readResult(Future<String> future){
try {
String result = future.get();
System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
-
参数说明
corePollSize:核心线程数maximumPoolSize:最大线程数。
需要注意的是当核心线程满且阻塞队列也满时才会判断当前线程数是否小于最大线程数,并决定是否创建新线程。keepAliveTime:线程保活时间。当线程数大于核心时,多于的空闲线程最多存活时间
unit:保活时间单位
workQueue:线程池阻塞队列
handler:拒绝策略
-
线程池阻塞队列
线程池阻塞队列有三种类型的BlockingQueue可以选择 有界、无界、同步移交- 有界队列(ArrayBlockingQueue)
常用的有两类,一类是遵循FIFO原则的队列如ArrayBlockingQueue,另一类是优先级队列如PriorityBlockingQueue。PriorityBlockingQueue中的优先级由任务的Comparator决定。
使用有界队列时队列大小需和线程池大小互相配合,线程池较小有界队列较大时可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量。 - 无界队列(LinkedBlockingQueue)
队列大小无限制,常用的为无界的LinkedBlockingQueue,使用该队列做为阻塞队列时要尤其当心,当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM。阅读代码发现,Executors.newFixedThreadPool 采用就是 LinkedBlockingQueue,当QPS很高,发送数据很大,大量的任务被添加到这个无界LinkedBlockingQueue 中,会导致cpu和内存飙升服务器挂掉。 - 同步移交(SynchronousQueue)
如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。
- 有界队列(ArrayBlockingQueue)
-
线程池拒绝策略
线程池拒绝策略一共有四种,分别是抛出异常(默认AbortPolicy) 、无处理直接抛弃(DiscardPolicy)、抛弃阻塞队列头元素(DiscardOldestPolicy)、提交者执行(CallerRunsPolicy)- AbortPolicy
使用该策略时在饱和时会抛出RejectedExecutionException(继承自RuntimeException),调用者可捕获该异常自行处理。 - DiscardPolicy
不做任何处理直接抛弃任务 - DiscardOldestPolicy
先将阻塞队列中的头元素出队抛弃,再尝试提交任务。如果此时阻塞队列使用PriorityBlockingQueue优先级队列,将会导致优先级最高的任务被抛弃,因此不建议将该种策略配合优先级队列使用。 - CallerRunsPolicy
既不抛弃任务也不抛出异常,直接运行任务的run方法,换言之将任务回退给调用者来直接运行。使用该策略时线程池饱和后将由调用线程池的主线程自己来执行任务,因此在执行任务的这段时间里主线程无法再提交新任务,从而使线程池中工作线程有时间将正在处理的任务处理完成。
- AbortPolicy
可缓存线程池
- 使用方式
- 源码分析
从源码和源码注释上可以看出,可缓存线程池实际上就是一个核心线程数设置为0,最大线程数设置为Int最大值,60秒空闲线程保活,阻塞队列采用同步移交的线程池。
需要注意的是,该线程池无限添加,60秒内线程数过多可能导致OOM
定长线程池
- 使用方式
- 源码分析
通过源码可以看出,定长线程池是通过最大线程数=核心线程数的设置来保证线程池内线程数量定长,该阻塞队列采用的是无界队列,虽然线程保活时间设置为0,但同样存在OOM风险。
定长周期线程池
- 使用方式
1.定长使用
- 延时使用
通过delay以及单位实现延时X秒后执行,但该方法不会周期执行
- 周期循环 -> 固定比率循环
为了更加直观,在TestRunnable中新增一段代码,使它睡眠10秒后再执行
initialDelay:延时x秒后执行
period:执行完成后间隔x秒后再次执行
从图中可以看到Runnable是1秒1执行,Thread是三秒一执行,正常情况应该是输出三个Runnable一个Thread,但在Runnable加入睡眠10秒的代码以后,运行时发现Runnable睡眠时,会阻塞Thread的运行(sleep不会释放资源)。
其实从实现的名字scheduleAtFixedRate上也能看出,该种方法以固定的频率来执行某项计划(任务)。
举个栗子:高铁定时发车,过时不候;period为一天,以天为周期,优先保证任务执行的频率。 - 固定周期循环
同样在Runnable添加睡眠10秒代码,我们来看下使用情况
虽然同样会阻塞,但不会受间隔时间比率影响了。
同样,从名字scheduleWithFixedDelay上也能看出,相对固定的延迟后,执行某项计划。
举个栗子:这个例子相当的不好找。。就拿炉石传说来讲吧,假设他每玩一局休息10秒钟,然后再开始玩。每开新的一局,打完的时间不是固定的,但是间隔是固定的,就是delay=10秒,表现在时间轴上就是【第一局260秒】【休息10秒】【第二局150秒】 【休息10秒】…【第N局180秒】【休息10秒】。这个是优先保证任务执行的间隔。
- 延时使用
- 源码分析
可以看出,不一样的是该类继承了ThreadPoolExecutor,super调用父类构造方法,传输了这些参数。
至于如何实现周期及延时的,以后再丰富。
单线程化 线程池
- 使用方式
该部分结合线程优先级进行测试,测试中可以发现,最低优先级与最高优先级执行存在一个数秒的时间差,在单线程化线程池中,执行顺序会因优先级不同而稍有阻塞。- 源码分析
- 源码分析