浅析线程与线程池
1.基本概念
1.1 程序、进程、线程
程序
:能完成预订功能和性能的静态指令序列
进程
:操作系统资源分配和处理器调度的基本单位
线程
:处理器调度的基本单位
1.2 线程的状态:
新建:
当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建 状态
就绪:
处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已 具备了运行的条件,只是没分配到CPU资源
运行:
当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线 程的操作和功能
阻塞:
在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中 止自己的执行,进入阻塞状态
死亡:
线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
线程池的5种状态:
RUNNING -1 线程池处于RUNNING时能够接受新任务,以及对已添加的任务进行处理
SHUTDOWN 0 线程池处于SHUTDOWN 时不接受新任务,但是能够处理已添加的任务
STOP 1 线程池处于STOP表示不接受新任务,也不处理已添加的任务
TIDYING 2 当前任务已终止,ctl的记录数位0。此时会执行钩子函数terminated()
TERMINATED 3 线程池彻底终止
1.3API:
void start():
启动线程,并执行对象的run()方法
run():
线程在被调度时执行的操作 String getName():返回线程的名称
void setName(String name):
设置该线程名称
static Thread currentThread():
返回当前线程。在Thread子类中就 是this,通常用于主线程和Runnable实现类 static
void yield():
线程让步 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程 若队列中没有同优先级的线程,忽略此方法
join() :
当某个程序执行流中调用其他线程的 join() 方法时,调用线程将 被阻塞,直到 join() 方法加入的 join线程执行完为止 低优先级的线程也可以获得执行
static void sleep(long millis):
(指定时间:毫秒) 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后 重排队。 抛出InterruptedException异常
stop():
强制线程生命期结束,不推荐使用 boolean isAlive():返回boolean,判断线程是否还活着
1.4 线程的通信
wait():
令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当
前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有 权后才能继续执行。
notify():
唤醒正在排队等待同步资源的线程中优先级最高者结束等待
notifyAll ():
唤醒正在排队等待资源的所有线程结束等待.
这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常。 因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁, 因此这三个方法只能在Object类中声明
作业:如何让AB两个线程交替打印12345abcde?
2 . 线程池的种类
2.1 spring自带线程池
// 固定大小线程池
ExecutorService threadPool1 = Executors.newFixedThreadPool(3);
//可以弹性伸缩的线程池,遇强则强
ExecutorService threadPool2 = Executors.newCachedThreadPool();
// 只有一个线程的线程池
ExecutorService threadPool3 = Executors.newSingleThreadExecutor();
//自定义线程池参数
ExecutorService threadPool4 = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit unit, workQueue, threadFactory, handler);
newCachedThreadPool :
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若 无可回收,则新建线程。
newFixedThreadPool :
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
newScheduledThreadPool :
创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor :
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
这种办法创建的线程池,存在弊端(阿里原话)
1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
2.2 手动创建线程池
底层这几种创建方式都是一样的:
Executors.newFixedThreadPool(3)底层为:
return new **ThreadPoolExecutor**(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
Executors.newCachedThreadPool()底层为:
return new **ThreadPoolExecutor**(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
Executors.newSingleThreadExecutor()底层为:
new **ThreadPoolExecutor**(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>())
LinkedBlockingQueue都没指定大小那默认就是最大整数。由此可见他们底层都是同一个方法。
3. 自定义参数线程池
public class ThreadPoolTec {
public static void main(String[] args) {
//推荐的线程池的创建方法
/**
* 最多可以存在的人,maximumPoolSize + LinkedBlockingDeque的容量
*触发非活跃线程变为活跃的方法是:当前任务数量大于最大线程池容量,每超一个则激活一个,直到都被激活
* // 拒绝策略说明:
* // 1. AbortPolicy (默认的:队列满了,就丢弃任务抛出异常!)
* // 2. CallerRunsPolicy(哪来的回哪去? 谁叫你来的,你就去哪里处理)
* // 3. DiscardOldestPolicy (尝试将最早进入对立与的人任务删除,尝试加入队列)
* // 4. DiscardPolicy (队列满了任务也会丢弃,不抛出异常)
*/
Integer processors = Runtime.getRuntime().availableProcessors());
ExecutorService threadPool = new ThreadPoolExecutor(
2,//核心活跃线程数,类比银行两个柜台一直保持营业
5*processors ,//线程池最大大小,类比银行共25个柜台可以营业
2L,//超时回收空闲的线程,类比有三个非活跃线程处于活跃状态,在一定时间还未接到任务就进入非活跃状态(就是不营业了)
TimeUnit.SECONDS,//时间单位
new ArrayBlockingQueue<>(3),//存放等待任务的队列,类比为银行的候客区,不指定大小的话就是最大整数
Executors.defaultThreadFactory(),// 线程工厂,不修改!用来创建
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略,如果线程满了,线程池就会使用拒绝策略
);
try {
for(int j=1;j<=8;j++){
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"is running………………");
});
}
} catch (Exception e) {
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
}
这里几点说明:
1.创建线程池最大线程数与运行机器的线程数一致是最大利用资源。 Runtime.getRuntime().availableProcessors());就是获取当前机器的最大线程数
2.任务排队的队列要指定大小,new LinkedBlockingDeque<>(3000),这样就不会太多等待的。
3.绝句策略依具体要求而定,我采用new ThreadPoolExecutor.CallerRunsPolicy()
4.注意多线程的执行,采用lamda方式写得:
for(int j=1;j<=8;j++){
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"is running………………");
});
}
4 . ThreadPoolExecutor 底层工作原理:
拒绝策略流程图:
①核心线程数是否满了
②如果核心线程满了,任务添加到队列
③ 队列满了线程判断线程池是否满了(即最大线程是否用完)
④最大线程也满了就出发拒绝策略
5 . 线上踩坑
5.1.部署中遇到docker部署时一直不能启动问题
Runtime.getRuntime().availableProcessors()
在kms的部署中遇到docker部署时一直不能启动问题。原因是创建线程池的核心线程数使用的是Runtime.getRuntime().availableProcessors(),最大线程数使用的 固定32。jdk是1.7,所以产生一下问题
①jdk版本不同Runtime.getRuntime().availableProcessors()在docker获取到的cpu来源不同
jdk 1.8.0_131 在docker内 获取的是宿主机上的内核数
jdk 1.8.0_202 在docker内 获取的是docker被限制的内核数,kubernetes不指定resource默认限制为1
②部署机核数很高大于了最大线程数
服务器核数为48,但是线程池的最大线程写的32导致核心线程数大于最大线程数,所以报错,创建线程池失败。服务起不来
看看创建线程池的底层代码
5.2.多线程操作共享变量,共享变量为引用类型,出现引用类型值出现改变的问题(重点)
背景:
ESG_M0NITOR遇到在统计保存链路trace信息时,我异步统计分钟和小时的调用量,保存至DB的数据正常,但是保存在redis的分钟小时数据实战不正常,会丢失数据
原因:
list被主线程统计存于cassandra,同时子线程统计了存于redis,但是主线程统计完后,反悔了list,在返回的地方又做了数据的改动,所以子线程有可能拿到的是改动后的数据,导致数据不准确
解决办法:
将list重新复制一份到一个新的对象(不光list,也包括list中的对象),新的对象供子线程操作,做到数据互不干扰
//复制集合:解决多线程操作共享变量,共享变量为引用类型,出现引用类型值出现改变的问题。minList是原集合
List<StatisData> minAndHourStaticsList = minList.stream().map(item->{
StatisData statisData = new StatisData();
BeanUtils.copyProperties(item,statisData);
return statisData;
}).collect(Collectors.toList());
//提交线程池
staticsThreadPool.execute(new MinAndHourStaticsThread(minAndHourStaticsList));
5.3.线程池参数的设置
网上流传,经我实践,并不生效:
CPU密集型:核心线程数 = CPU核数 + 1
IO密集型:核心线程数 = CPU核数 * 2 + 1
I/O密集型(一般系统都是IO密集):*
设置 方案 1
1、corePoolSize = 每秒需要多少个线程处理=tasks/(1/tasktime) =tasks*tasktime
2、queueCapacity = (coreSizePool/tasktime)*responsetime
3、maxPoolSize = (max(tasks)- queueCapacity)/(1/tasktime)
tasks :每秒的任务数,假设为500~1000
taskcost:每个任务花费时间,假设为0.1s
responsetime:系统允许容忍的最大响应时间,假设为1s
设置方案2
核心线程数:
CPU 核数 +1
最大线程数:
线程数 = CPU 核心数 / (1 - 阻塞系数)
其中计算密集型阻塞系数为 0,IO密集型阻塞系数接近 1,一般认为在 0.8 ~ 0.9 之间。比如 8 核 CPU,按照公式就是 8 / ( 1 - 0.9 ) = 80个线程数
阻塞系数=阻塞时间/(阻塞时间+计算时间)
线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间)
5.4.拒绝策略
ThreadPoolExecutor内部有实现4个拒绝策略:
(1)、CallerRunsPolicy
,由调用execute方法提交任务的线程来执行这个任务;
(2)、AbortPolicy
,抛出异常RejectedExecutionException拒绝提交任务;
(3)、DiscardPolicy
,直接抛弃任务,不做任何处理;
(4)、DiscardOldestPolicy
,去除任务队列中的第一个任务(最旧的),重新提交;
生产中我们常常自定义实现拒绝策略,而不是使用它自带的拒绝策略
6.线程池如何回收非核心线程的?
线程池销毁线程的机制:
1、任务数低于最大线程数,会回收多余非核心线程
2、线程运行报异常会销毁该线程
3、执行线程的shutdown方法会销毁线程
任务数低于最大线程数,会回收多余非核心线程 ------看源码
poll(long timeout, TimeUnitunit):
从BlockingQueue取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则到时间超时还没有数据可取,返回失败。
take():
取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
//①利用死循环一直盘循环判断直到
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
//线程池 SHUTDOWN 和 SHUTDOWN NOW的区别。。。这很重要: 如果线程池被关闭,即置为SHUTDOWN 此时队列还有任务,任务会执行完才会关闭
//如果是 shutdownnow 它会把线程池的状态改为STOP,那么它会立即中断返回null
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
//获取当前的线程数
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
//②利用CAS 比较并交换机制,把大于核心线程数的那部分线程置为null(即回收掉)
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
//这里如果 timed 是true则说明 现有线程数依然大于核心线程数 则 返回null
//这里如果 timed 是false则说明 现有线程数依然小于等于核心线程数 则 阻塞
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
核心利用:
1、CAS 比较并交换机制,把大于核心线程数的那部分线程置为null(即回收掉)
2、利用阻塞队列阻塞核心线程