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发布于博客园

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章