线程的生命周期
线程在创建后,通过start执行了run方法后,会被自动回收。
复用线程的原因
- 线程频繁创建和关闭花费大量时间
- 线程本身占用内存,创建过多线程可能导致OOM
1. 线程池
线程池中存放空闲线程,每当有任务执行,不再创建线程而是从线程池中获取,任务执行完后,将线程归还给线程池.
1) JDK对线程池的支持
ThreadPoolExecutor 类的构造函数
public ThreadPoolExecutor(int corePoolSize,//核心线程数
int maximumPoo1Sizer//最大线程数
long keepAliveTime,//非核心线程数保活时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//被提交但未执行任务的队列
ThreadFactory threadFactory,//线程工厂
RejectedExecutionHandler handler)//拒绝策略
线程池执行流程
- 当前任务数< corePoolSize,将使用已有的线程执行任务。
- 当前任务数>= corePoolSize,新提交任务将被放入workQueue中,等待线程池中任务调度执行
- 当workQueue已满,且corePoolSize<MaxmumPoolSize,新提交任务会创建新线程执行任务
- 当工作线程= maximumPoolSize时,且workQueue已满,新提交任务由RejectedExecutionHandler处理
2) 任务队列
- 直接提交队列(SynchronousQueue)
SynchronousQueue没有容量
每一个插入需要等待对应的删除操作,反之亦然
提交的任务不会保存在队列,而是准备直接提交给线程
- 有界的任务队列(ArrayBlockingQueue)
public ArrayBlockingQueue(int capacity)
当任务队列已满,则准备将任务提交给线程
- 无界的任务队列(LinkedBlockingQueue )
任务会直接进入任务队列直到线程池中有空闲的线程
- 优先任务队列(PriorityBlockingQueue)
public PriorityBlockingQueue(int initialCapacity,Comparator<? super E> comparator
无界的任务队列,并会对入队的任务按照比较器排序
3) 拒绝策略
当任务队列已满且线程数=maximumPoolSize,再有新的任务到达,就要用到拒绝策略
JDK内置拒绝策略
- AbortPolicy 策略: 该策略策略会直接抛出异常, 阻止系统正常工作
- CallerRunsPolicy 策略: 只要线程池未关闭, 该策略直接在调用者线程中, 运行当前被丢弃的任务。
- DiscardOldestPolicy 策略: 读策略将丢弃最老的一个请求, 并尝试再次提交当前任务。
- DiscardPolicy 策略: 该策略默默地丢弃无法处理的任务, 不予任何处理。
自定义拒绝策略
拒绝策略都需要实现RejectedExecutionHandler接口,并实现**rejectedExecution()**方法
public class RejectThreadPoolDemo {
public static class MyTask implements Runnable {
@Override
public void run() {
System.out.println(System.currentTimeMillis() + ":Thread ID:"
+ Thread.currentThread().getId());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
MyTask task = new MyTask();
ExecutorService es = new ThreadPoolExecutor(5, 5,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(5),
Executors.defaultThreadFactory(),
new RejectedExecutionHandler(){
@Override
public void rejectedExecution(Runnable r,
ThreadPoolExecutor executor) {
System.out.println(r.toString()+" is discard");
}
});
for (int i = 0; i < Integer.MAX_VALUE; i++) {
es.submit(task);
Thread.sleep(10);
}
}
}
1571878291492:Thread ID:19
1571878291502:Thread ID:20
1571878291512:Thread ID:21
1571878291522:Thread ID:22
1571878291532:Thread ID:23
java.util.concurrent.FutureTask@33909752 is discard
java.util.concurrent.FutureTask@55f96302 is discard
代码分析
- 当任务队列已满,且线程数=maximumPoolSize,再有任务提交时,会执行拒绝策略
- LinkedBlockingQueue也可以指定任务队列大小,但一般使用ArrayBlockingQueue
4) 线程工厂
线程池中的线程由线程工厂创建.当线程池需要创建线程时,会执行ThreadFactory以下方法
Thread newThread(Runnable r);
自定义线程工厂:设置线程的名称,组,是否守护,优先级等信息
public class TFThreadPoolDemo {
public static class MyTask implements Runnable {
@Override
public void run() {
System.out.println(System.currentTimeMillis() + ":Thread Name:"
+ Thread.currentThread().getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
MyTask task = new MyTask();
ExecutorService es = new ThreadPoolExecutor(5, 5,
0L, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactory(){
@Override
public Thread newThread(Runnable r) {
Thread t= new Thread(r);
t.setDaemon(true);
t.setName(System.currentTimeMillis()+"");
return t;
}
}
);
for (int i = 0; i < 5; i++) {
es.submit(task);
}
Thread.sleep(2000);
}
}
1571879131576:Thread Name:1571879131575
1571879131577:Thread Name:1571879131575
1571879131577:Thread Name:1571879131576
1571879131576:Thread Name:1571879131576
1571879131576:Thread Name:1571879131575
代码分析
- 通过自定义线程工厂,将创建线程的时间作为线程名,并将线程设置为守护线程
- 在线程执行时,打印当前时间和线程名中的时间,可以分析线程调度消耗的时间
Executors工厂方法
5. 固定大小的线程池
public static ExecutorService newFixedThreadPool(int nThreads)
核心线程数和最大线程数都为nThreads,任务队列为LinkedBloddngQueue.
public static ExecutorService newSingleThreadExecutor()
核心线程数和最大线程数都为1,任务队列为LinkedBloddngQueue.
public static ExecutorService newCachedThreadPool()
核心线程数为0,最大线程数都为max,任务队列为SynchronousQueue
- 计划任务线程池
public static ScheduledExecutorService newSingleThreadScheduledExecutor()
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
计划任务调度方法
public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
- ScheduledExecutorService不会立即安排任务,而是按照计划时间安排任务
- schedule只对任务调度一次,其他2个方法对任务进行周期调度
- 周期地调度不会让任务堆叠执行.如果周期太短,任务会在上次任务结束后立即调度
2. 扩展线程池
ThreadPoolExecutor 是一个可以扩展的线程池
任务执行前运行
protected void beforeExecute(Thread t, Runnable r)
任务结束后运行
protected void afterExecute(Runnable r, Throwable t)
线程池退出时运行
protected void terminated()
源码分析
在ThreadPoolExecutor.Worker.runTask()中
try {
beforeExecute(wt,task); //运行前
Throwable thrown=null;
try {
task.run{); / /运行任务
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown); //运行结束后
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
- beforeExecute会在任务执行前运行,包含工作线程和工作任务两个参数
- afterExecute会在任务执行后运行,尽管任务抛出异常.包含工作任务和异常信息两个参数
3. 优化线程池数量
线程池的大小对系统的性能有一定的影响。 过大或者过小的线程数量都无法发挥最优的系统性能
N
c
p
u
=
C
P
U
的
数
量
Ncpu = CPU 的数量
Ncpu=CPU的数量
获取cpu数量
Runtime.getRuntime().availableProcessors()
U
c
p
u
=
目
标
C
P
U
的
使
用
率
,
0
≤
U
c
p
u
≤
1
Ucpu =目标 CPU 的使用率, 0≤Ucpu≤1
Ucpu=目标CPU的使用率,0≤Ucpu≤1
W
/
C
=
等
待
时
间
与
计
算
时
间
的
比
率
W/C= 等待时间与计算时间的比率
W/C=等待时间与计算时间的比率
最优线程池大小
N
t
h
r
e
a
d
s
=
N
c
p
u
×
U
c
p
u
×
(
1
+
W
/
C
)
Nthreads = Ncpu×Ucpu ×(1 + W/C)
Nthreads=Ncpu×Ucpu×(1+W/C)
4. 在线程池中寻找堆栈
线程池中的幽灵般错误
class DivTask implements Runnable {
int a,b;
public DivTask(int a,int b){
this.a=a;
this.b=b;
}
@Override
public void run() {
double re=a/b;
System.out.println(re);
}
}
public class TraceMain {
public static void main(String[] args) {
ThreadPoolExecutor pools=new ThreadPoolExecutor(0, Integer.MAX_VALUE,
0L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
for(int i=0;i<5;i++){
pools.submit(new DivTask(100,i));
}
}
}
100.0
25.0
33.0
50.0
代码分析
- 线程池执行了一个除法运算任务,但除以0并为抛出异常,而是直接跳过
获取异常信息
-
将submit()改为execute()
-
改造submit()方法
Future retools.submit(new DivTask(100,i)); re.get{);
异常信息
Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero
at geym.conc.ch3.trace.DivTask.run(DivTask.java:11)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
错误信息包含了出现异常的任务,但是没有提示任务提交的位置
保存任务提交的堆栈信息
public class TraceThreadPoolExecutor extends ThreadPoolExecutor {
public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void execute(Runnable task) {
super.execute(wrap(task, clientTrace(), Thread.currentThread()
.getName()));
}
@Override
public Future<?> submit(Runnable task) {
return super.submit(wrap(task, clientTrace(), Thread.currentThread()
.getName()));
}
private Exception clientTrace() {
return new Exception("Client stack trace");
}
private Runnable wrap(final Runnable task, final Exception clientStack,
String clientThreadName) {
return new Runnable() {
@Override
public void run() {
try {
task.run();
} catch (Exception e) {
clientStack.printStackTrace();
throw e;
}
}
};
}
}
代码解析
- 在TraceTraceThreadPoolExecutor中重写了execute和submit方法
- 提交的任务被wrap成新的Runnable,若抛出异常
通过clientStack先打印任务提交位置的堆栈信息,再将任务异常抛出
捕获任务异常,打印任务异常堆栈信息
打印任务提交的堆栈信息
public class TraceMain {
public static void main(String[] args) {
ThreadPoolExecutor pools=new TraceThreadPoolExecutor(0, Integer.MAX_VALUE,
0L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
for(int i=0;i<5;i++){
pools.execute(new DivTask(100,i));
}
}
}
异常信息
java.lang.Exception: Client stack trace
at geym.conc.ch3.trace.TraceThreadPoolExecutor.clientTrace(TraceThreadPoolExecutor.java:27)
at geym.conc.ch3.trace.TraceThreadPoolExecutor.execute(TraceThreadPoolExecutor.java:16)
at geym.conc.ch3.trace.TraceMain.main(TraceMain.java:14)
Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero
at geym.conc.ch3.trace.DivTask.run(DivTask.java:11)
at geym.conc.ch3.trace.TraceThreadPoolExecutor$1.run(TraceThreadPoolExecutor.java:36)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
堆栈信息包含了任务提交时的位置信息
分而治之:Fork/Join框架
ForkJoinPool原理一
ForkJoinPool原理二
任务窃取算法
ForkJoinTask
- ForkJoinTask实现了Future接口,可通过get()获取线程的状态.
- compute()方法在RecursiveAction子类没有返回值,RecursiveTack子类有返回值
任务分割
ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题
线程互助
每个物理线程实际上是需要处理多个逻辑任务的。 每个线程必然需要拥有一个任务队列
当一个线程试图“ 帮助” 其他线程时, 总是从任务队列的底部开始获取数据, 而线程试图执行自己的任务时, 则是从相反的顶部开始获取数据。
ForkJoinPool示例
public class CountTask extends RecursiveTask<Long>{
private static final int THRESHOLD = 10000;
private long start;
private long end;
public CountTask(long start,long end){
this.start=start;
this.end=end;
}
public Long compute(){
long sum=0;
boolean canCompute = (end-start)<THRESHOLD;
if(canCompute){//如果任务规模小,直接进行
for(long i=start;i<=end;i++){
sum +=i;
}
}else{//如果任务规模大,分解成多个子任务
//分成10个小任务
long step=(end-start)/10;
ArrayList<CountTask> subTasks=new ArrayList<CountTask>();
long pos=start;
for(int i=0;i<10;i++){
long lastOne=pos+step;
if(lastOne>end)lastOne=end;
CountTask subTask=new CountTask(pos,lastOne);//创建子任务
pos+=step+1;
subTasks.add(subTask);//记录子任务
subTask.fork();//提交子任务列表
}
for(CountTask t:subTasks){
sum+=t.join();//获取子任务返回值
}
}
return sum;//返回计算值
}
public static void main(String[]args){
ForkJoinPool forkJoinPool = new ForkJoinPool();
CountTask task = new CountTask(0,2000L);//创建任务
ForkJoinTask<Long> result = forkJoinPool.submit(task);//提交任务
try{
long res = result.get();
System.out.println("sum="+res);
}catch(InterruptedException e){
e.printStackTrace();
}catch(ExecutionException e){
e.printStackTrace();
}
}
}
fork()
public final ForkJoinTask<V> fork()
- 将任务提交到工程线程的工作队列
- 线程的工作队列为双端队列
自身的任务:LIFO(队尾添加,队尾取用)
偷窃的任务:FIFO(队头添加,队尾取用)
join()
public final V join()
-
检查调用 join() 的线程是否是 ForkJoinThread 线程。
如果不是,则阻塞当前线程,等待任务完成。
如果是,则不阻塞。 -
查看任务的完成状态,如果已经完成,直接返回结果。
-
如果任务尚未完成,查看每个工作线程的工作队列
若工作队列不为空,完成自身任务
若工作队列为空,实行任务窃取算法
直到所有工作都完成.
代码解析
- forkJoinPool.submit(task)会自动执行task中compute()
- compute()将大任务分解成小任务,大任务的返回值依赖于小任务的返回值
- compute()的返回值由task.join()获取
Guava 中对线程池的扩展
Guava并非JDK内置的,其中的DirectExecutor对线程池进行了扩展.
特殊的 DirectExecutor 线程池
DirectExecutor 没有真的创建或者使用额外线程, 而是将任务在当前线程中直接执行.
广义线程池
任何一个可以运行 Runnable 实例的模块都可以被视为线程池, 即便它没有真正创建线程
DirectExecutor示例
public class MoreExecutorsDemo {
public static void main(String[] args) {
Executor exceutor = MoreExecutors.directExecutor();
exceutor.execute(() -> System.out.println("I am running in " + Thread.currentThread().getName()));
}
}
输出:
I am running in main
代码解析
- DirectExecutor将Runnable示例在当前线程执行
- 可以使用狭义线程池代替DirectExecutor.实现异步和同步在代码上的统一
Daemon 线程池
将普通线程池转换为Daemon线程池
public static ExecutorService getExitingExecutorService(ThreadPoolExecutor executor)
代码演示
public class MoreExecutorsDemo2 {
public static void main(String[] args) {
ThreadPoolExecutor exceutor = (ThreadPoolExecutor)Executors.newFixedThreadPool(2);
MoreExecutors.getExitingExecutorService(exceutor);
exceutor.execute(() -> System.out.println("I am running in " + Thread.currentThread().getName()));
}
}
输出:
I am running in pool-1-thread-1(程序结束)
代码分析
- getExitingExecutorService()将线程池转换为Daemon线程池.不会阻止程序退出
- 普通的线程池必须调用shutdown才能正常退出.
问题
- future相关类图
- submit吞异常,而execute不会