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發佈於博客園

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