Java线程-02
Java 8
--
0、前言
一年前写了一篇“Java线程-01”,只是没学透彻。现在继续。ben发布于博客园
比如,怎么配置 线程池的线程名称、设置拒绝策略、使用ScheduledThreadPoolExecutor 等内容。
1、ThreadPoolExecutor 概述
java.util.concurrent.ThreadPoolExecutor
四个构造函数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue);
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);
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
其中,corePoolSize、maximumPoolSize、keepAliveTime+unit 很好理解,自己之前也主要用这些参数建立线程池(Executors工具类)。
workQueue 表示 等待执行的任务,BlockingQueue 对象;ben发布于博客园
threadFactory 表示 线程工厂,之前未使用过,也就没有自动配置过线程池中线程的名称;
handler 表示 拒绝策略,排队满了,线程池无法接收更多任务时根据配置的拒绝策略来执行任务——比如,直接丢弃(拒绝)。
BlockingQueue 类型层次结构(Eclipse查看):
RejectedExecutionHandler 类型层次结构:常用的是 4个标记为 s 的静态类
2、ThreadPoolExecutor 中的线程名称配置
需要实现 ThreadFactory接口 才可以定制线程名称,ThreadFactory接口 只有1个函数:ben发布于博客园
Thread newThread(Runnable r);
需要使用Thread类的构造函数基于 入参r 构造Thread对象:其中有的参数就可以 配置线程名称
建立线程池:使用 Thread(Runnable target) 函数
public static ExecutorService es = new ThreadPoolExecutor(5, 10, 30L, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(10), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r);
}
});
Thread(Runnable target) 函数:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
/* For autonumbering anonymous threads. */
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
测试程序:使用线程池建立5个线程执行
public static void main(String[] args) {
IntStream.range(0, 5).forEach(i->{
es.execute(()->{
String tname = Thread.currentThread().getName();
int secs = ThreadLocalRandom.current().nextInt(10);
System.out.println("i=" + i + ", name=" + tname + ", secs=" + secs);
try {
TimeUnit.SECONDS.sleep(secs);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i + " end");
});
});
System.out.println("main end");
}
测试结果:
main end
i=3, name=Thread-3, secs=7
i=4, name=Thread-4, secs=7
i=2, name=Thread-2, secs=9
i=1, name=Thread-1, secs=9
i=0, name=Thread-0, secs=0
0 end
4 end
3 end
2 end
1 end
线程名为默认的,“Thread-” 开头,再加上数字。
根据 参考资料#1 建立 AppThreadFactory 类进行测试:ben发布于博客园
public class AppThreadFactory implements ThreadFactory {
private String prefix = "app-";
private Integer count = 0;
public AppThreadFactory() {
}
public AppThreadFactory(String prefix) {
if (StringUtils.hasText(prefix)) {
prefix.trim();
this.prefix = prefix;
}
}
@Override
public Thread newThread(Runnable r) {
return new Thread(r, prefix + (count++));
}
}
使用了 下面的Thead构造函数:
public Thread(Runnable target, String name);
使用 AppThreadFactory 建立线程池进行测试:
public static ExecutorService es = new ThreadPoolExecutor(5, 10, 30L, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(10),
new AppThreadFactory()); // 默认前缀
public static ExecutorService es = new ThreadPoolExecutor(5, 10, 30L, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(10),
new AppThreadFactory("Tom-")); // 自定义线程名前缀
继续使用上面的 main 函数测试,可以得到不同的线程名前缀。ben发布于博客园
3、ThreadPoolExecutor 中的拒绝策略测试
使用前面没有配置 拒绝策略 的线程池,修改main函数,提交 超过线程池可以支持的最大数量的线程:
public static void main(String[] args) {
// 5个线程
// IntStream.range(0, 5).forEach(i->{
// 25个线程:超过线程池的 队列长度+最大线程数量(20)
IntStream.range(0, 25).forEach(i->{
es.execute(()->{
String tname = Thread.currentThread().getName();
int secs = ThreadLocalRandom.current().nextInt(10);
System.out.println("i=" + i + ", name=" + tname + ", secs=" + secs);
try {
TimeUnit.SECONDS.sleep(secs);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i + " end");
});
});
System.out.println("main end");
}
执行后发生了异常:java.util.concurrent.RejectedExecutionException
从上图可以看到,默认使用了 AbortPolicy。
可以配置为其它的:使用 handler 参数
比如,配置为 new ThreadPoolExecutor.CallerRunsPolicy():ben发布于博客园
此时,超过线程池承载量 的线程由调用方线程main线程执行:可以看到,i=20时 由main线程执行了。
当然,不同情况,输出结果不同。不过,调用方执行会导致调用方卡住。
CallerRunsPolicy测试结果
i=1, name=Tom-1, secs=9
i=19, name=Tom-9, secs=2
i=0, name=Tom-0, secs=8
i=20, name=main, secs=9
i=2, name=Tom-2, secs=0
i=3, name=Tom-3, secs=4
i=18, name=Tom-8, secs=0
18 end
i=17, name=Tom-7, secs=8
i=16, name=Tom-6, secs=6
i=4, name=Tom-4, secs=6
i=15, name=Tom-5, secs=6
i=5, name=Tom-8, secs=9
2 end
i=6, name=Tom-2, secs=6
19 end
i=7, name=Tom-9, secs=8
3 end
i=8, name=Tom-3, secs=2
4 end
16 end
i=10, name=Tom-6, secs=8
15 end
i=11, name=Tom-5, secs=3
6 end
i=9, name=Tom-4, secs=9
i=12, name=Tom-2, secs=6
8 end
i=13, name=Tom-3, secs=7
17 end
i=14, name=Tom-7, secs=0
14 end
0 end
1 end
20 end
5 end
11 end
i=22, name=Tom-5, secs=3
i=23, name=Tom-7, secs=7
i=21, name=Tom-8, secs=3
main end
i=24, name=Tom-0, secs=3
7 end
21 end
24 end
22 end
12 end
13 end
10 end
9 end
23 end
其它的 DiscardOldestPolicy、DiscardPolicy 会导致任务不被执行,被Discard——丢弃,前者丢弃已经提交的,后者丢弃正在提交的。
可以检查各个拒绝策略类的源码。
当然,也可以自己实现 RejectedExecutionHandler接口 ,开发自己的拒绝策略类。
怎么开发?有必要吗?什么时候需要?没经历过,不太明白。ben发布于博客园
4、使用 ScheduledThreadPoolExecutor
java.util.concurrent.ScheduledThreadPoolExecutor
这是一个 定时调度线程池,提交到这个线程池的任务都会 按照配置的时间策略执行,可以执行一次,可以反复执行。
有4个构造函数:
public ScheduledThreadPoolExecutor(int corePoolSize);
public ScheduledThreadPoolExecutor(int corePoolSize,
RejectedExecutionHandler handler);
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory);
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
可以看到,只设置了核心线程池,,没有最大线程池、排队的阻塞队列设置。
默认情况下,阻塞队列的最大长度可以认为是无限的——Integer.MAX_VALUE,这也是存在风险的地方,可能导致资源耗尽。
调度功能的实现,需要调用以下函数:
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit);
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
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);
前面2个schedule函数都只会把任务执行1次,后面2个则是周期执行。
用法也很简单:
package com.lib.webdemo.config;
import java.util.Date;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
public class AppScheduledThreadPoolExecutor {
public static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(5, new AppThreadFactory("stpe-"));
public static void main(String[] args) throws InterruptedException {
System.out.println("main 1 " + new Date());
IntStream.range(0, 10).forEach(i->{
executor.schedule(()->{
System.out.println(i + ", schedule, tname=" + Thread.currentThread().getName());
}, 10, TimeUnit.SECONDS);
});
System.out.println("main 2 " + new Date());
int slp = 0;
BlockingQueue<Runnable> bq = executor.getQueue();
// while (executor.getActiveCount() > 1) {
while (! bq.isEmpty()) {
System.out.println("main bq.size=" + bq.size() + ", remainingCapacity=" + bq.remainingCapacity());
TimeUnit.SECONDS.sleep(1L);
slp++;
}
System.out.println("main END bq.size=" + bq.size() + ", remainingCapacity=" + bq.remainingCapacity());
System.out.println("main 3 slp=" + slp + ", " + new Date());
executor.shutdown();
System.out.println("main END " + new Date());
}
}
测试结果:ben发布于博客园
测试ScheduledThreadPoolExecutor
main 1 Fri Sep 23 12:43:59 CST 2022
main 2 Fri Sep 23 12:43:59 CST 2022
main bq.size=10, remainingCapacity=2147483647
main bq.size=10, remainingCapacity=2147483647
main bq.size=10, remainingCapacity=2147483647
main bq.size=10, remainingCapacity=2147483647
main bq.size=10, remainingCapacity=2147483647
main bq.size=10, remainingCapacity=2147483647
main bq.size=10, remainingCapacity=2147483647
main bq.size=10, remainingCapacity=2147483647
main bq.size=10, remainingCapacity=2147483647
main bq.size=10, remainingCapacity=2147483647
0, schedule, tname=stpe-0
1, schedule, tname=stpe-1
3, schedule, tname=stpe-3
2, schedule, tname=stpe-2
4, schedule, tname=stpe-4
5, schedule, tname=stpe-3
6, schedule, tname=stpe-1
9, schedule, tname=stpe-0
7, schedule, tname=stpe-2
8, schedule, tname=stpe-3
main END bq.size=0, remainingCapacity=2147483647
main 3 slp=10, Fri Sep 23 12:44:10 CST 2022
main END Fri Sep 23 12:44:10 CST 2022
从线程池的队列的 remainingCapacity 属性可以看到,其值几乎是无限的。ben发布于博客园
是否可以修改 队列的排队长度呢?
没有找到。
缘起:
之前使用 spring 的 @Schedule 注解 执行调度时出现了一些问题(修改服务器时间后,调度失效),看博文后才知道是属于 此类。
不过,这个类 存在一些问题。
还有更好的方式执行调度任务吗?不会导致OOM那种。ben发布于博客园
线程池的队列中存在任务时,此时停止程序(断电、kill等),队列中的任务是不是就不能被执行了?
参考资料
1、使用ThreadFactory
https://www.jianshu.com/p/7040054b069b
2、JUC之ScheduledThreadPoolExecutor
https://blog.csdn.net/weixin_50518271/article/details/119116334
ben发布于博客园