多線程java.util.concurrent.RejectedExecutionException

項目運行一段時間後現場突然報了一個異常,多線程讀取本地文件時失敗導致文件大量積壓,查看日誌發現以下異常:
java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@12bb4df8 rejected from java.util.concurrent.ThreadPoolExecutor@4cc77c2e[Shutting down, pool size = 5, active threads = 0, queued tasks = 0, completed tasks = 50]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134)
由於本地測試只放了一兩個文件所以忽略了此問題,放多個文件的時候該問題重現。
從異常名稱裏很容易分析出是提交的任務被線程池拒絕了。查看源碼發現是在Activity裏,AsyncTask是在自定義的線程池的運行的,但是onDestory函數裏卻是先顯示調用了線程池的shutdown方法,然後纔是AsyncTask的cancel操作,因此可能導致任務被拒絕。
ThreadPoolExecutor
一個ExecutorService,它使用可能的幾個線程池之一執行每個提交的任務,通常使用Executors工廠方法配置,但是查看源碼,發現工廠方法也是統一調用了ThreadPoolExecutor類,以爲例,源碼如下:
public class Executors {
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}

}
雖然我一直認同程序員應使用較爲方便的Executors工廠方法Executors.newCachedThreadPool() (無界線程池,可以進行自動線程回收)、Executors.newFixedThreadPool(int)(固定大小線程池)和Executors.newSingleThreadExecutor()(單個後臺線程),但是通過源碼我們可以發現最後他們均調用了ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) 方法,因此我們在分析java.util.concurrent.RejectedExecutionException之前,需要深入學習一下ThreadPoolExecutor的使用。
核心池和最大池的大小
TreadPoolExecutor將根據corePoolSize和maximumPoolSize設置的邊界自動調整池大小。當新任務在方法execute(java.lang.Runnable)中提交時,如果運行的線程少於corePoolSize,則創建新線程來處理請求,即使其他輔助線程是空閒的。如果運行的線程多於corePoolSize而少於maximumPoolSize,則僅當隊列滿時才創建新的線程。如果設置的corePoolSize和maximumPoolSize相同,則創建了固定大小的線程池。如果將maximumPoolSize設置爲基本的無界值(如Integer.MAX_VALUE),則允許線程池適應任意數量的併發任務。
保持活動時間
如果池中當前有多於corePoolSize的線程,則這些多出的線程在空閒時間超過keepAliveTime時將會終止。
排隊
所有BlockingQueue都可用於傳輸和保持提交的任務。可以使用此隊列與池大小進行交互:
如果運行的線程少於corePoolSize,則Executor始終首選添加新的線程,而不進行排隊。
如果運行的線程等於或多於corePoolSize,則Executor始終首選將請求加入隊列,而不添加新的線程。
如果無法將請求加入隊列,則創建新的線程,除非創建此線程超出maximumPoolSize,在這種情況下,任務將被拒絕(拋出RejectedExecutionException)。
排隊有三種通用策略:
直接提交。工作隊列的默認選項是synchronousQueue,它將任務直接提交給線程而不保持它們。在此,如果不存在可用於立即運行任務的線程,則試圖把任務加入隊列將失敗,因此會構造一個新的線程。此策略可以避免在處理可能具有內部依賴性的請求集時出現鎖。直接提交通常要求無界maximumPoolSizes以避免拒絕新提交的任務。當命令以超過隊列所能處理的平均數連續到達時,此策略允許無界線程具有增加的可能性。
無界隊列。使用無界隊列(例如,不具有預定義容量的LinkedBlockingQueue)將導致在所有corePoolSize線程都忙時新任務在隊列中等待。這樣,創建的線程就不會超過corePoolSize(因此,maximumPoolSize的值也就無效了)。
有界隊列。當使用有限的maximumPoolSizes時,有界隊列(如ArrayBlockingQueue)有助於防止資源耗盡,但是可能較難調整和控制。隊列大小和最大池大小可能需要相互折衷:使用大型隊列和小型池可以最大限度的降低CPU使用率、操作系統資源和上下文切換開銷,但是可能導致人工降低吞吐量。如果任務頻繁阻塞,則系統可能爲超過您許可的更多線程安排時間,使用小型隊列通常要求較大的池大小,CPU使用率較高,但是可能遇到不可接受的調度開銷,這樣可會降低吞吐量。
終止
程序不再引用的池沒有剩餘線程會自動shutdown。如果希望確保回收取消引用的池(即使用戶忘記調用shutdown()),則必須安排未使用的線程最終終止。
分析
通過對ThreadPoolExecutor類分析,引發java.util.concurrent.RejectedExecutionException主要有兩種原因:

  1. 線程池顯示的調用了shutdown()之後,再向線程池提交任務的時候,如果你配置的拒絕策略是ThreadPoolExecutor.AbortPolicy的話,這個異常就被會拋出來。
  2. 當你的排隊策略爲有界隊列,並且配置的拒絕策略是ThreadPoolExecutor.AbortPolicy,當線程池的線程數量已經達到了maximumPoolSize的時候,你再向它提交任務,就會拋出ThreadPoolExecutor.AbortPolicy異常。

顯示關閉掉線程池
這一點很好理解。比如說,你向一個倉庫去存放貨物,一開始,倉庫管理員把門給你打開了,你放了第一件商品到倉庫裏,但是當你放好出去後,有人把倉庫門關了,那你下次再來存放物品時,你就會被拒絕。示例代碼如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TextExecutor {
public ExecutorService fixedExecutorService = Executors.newFixedThreadPool(5);
public ExecutorService cachedExecutorService = Executors.newCachedThreadPool();
public ExecutorService singleExecutorService = Executors.newSingleThreadExecutor();

public void testExecutorException() {
	for (int i = 0; i < 10; i ++) {
		fixedExecutorService.execute(new SayHelloRunnable());
		fixedExecutorService.shutdown();
	}
}

private class SayHelloRunnable implements Runnable {

	@Override
	public void run() {
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			System.out.println("hello world!");
		}
		
	}
}

public static void main(String[] args) {
	TextExecutor testExecutor = new TextExecutor();
	testExecutor.testExecutorException();
}

}
解決方案

  1. 不要顯示的調用shutdown方法,例如Android裏,只有你在Destory方法裏cancel掉AsyncTask,則線程池裏沒有活躍線程會自己回收自己。
  2. 調用線程池時,判斷是否已經shutdown,通過API方法isShutDown方法判斷,示例代碼:
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;

public class TextExecutor {
public ExecutorService fixedExecutorService = Executors.newFixedThreadPool(5);
public ExecutorService cachedExecutorService = Executors.newCachedThreadPool();
public ExecutorService singleExecutorService = Executors.newSingleThreadExecutor();

public void testExecutorException() {
	for (int i = 0; i < 10; i ++) {
		// 增加isShutdown()判斷
		if (!fixedExecutorService.isShutdown()) {
			fixedExecutorService.execute(new SayHelloRunnable());
		}
		fixedExecutorService.shutdown();
	}
}

private class SayHelloRunnable implements Runnable {

	@Override
	public void run() {
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			System.out.println("hello world!");
		}
		
	}
}

public static void main(String[] args) {
	TextExecutor testExecutor = new TextExecutor();
	testExecutor.testExecutorException();
}

}
線程數量超過maximumPoolSize
示例代碼裏使用了自定義的ExecutorService,可以復現這種問題:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class TextExecutor {
public ExecutorService fixedExecutorService = Executors.newFixedThreadPool(5);
public ExecutorService cachedExecutorService = Executors.newCachedThreadPool();
public ExecutorService singleExecutorService = Executors.newSingleThreadExecutor();
public ExecutorService customerExecutorService = new ThreadPoolExecutor(3, 5, 0, TimeUnit.MILLISECONDS, new SynchronousQueue());

public void testExecutorException() {
	for (int i = 0; i < 10; i ++) {
		// 增加isShutdown()判斷
		if (!fixedExecutorService.isShutdown()) {
			fixedExecutorService.execute(new SayHelloRunnable());
		}
		fixedExecutorService.shutdown();
	}
}

public void testCustomerExecutorException() {
	for (int i = 0; i < 100; i ++) {
		customerExecutorService.execute(new SayHelloRunnable());
	}
}

private class SayHelloRunnable implements Runnable {

	@Override
	public void run() {
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} finally {
			System.out.println("hello world!");
		}
		
	}
}

public static void main(String[] args) {
	TextExecutor testExecutor = new TextExecutor();
	testExecutor.testCustomerExecutorException();;
}

}
解決方案

  1. 儘量調大maximumPoolSize,例如設置爲Integer.MAX_VALUE
    public ExecutorService customerExecutorService = new ThreadPoolExecutor(3, Integer.MAX_VALUE, 0, TimeUnit.MILLISECONDS, new SynchronousQueue());
  2. 使用其他排隊策略,例如LinkedBlockingQueue
    public ExecutorService customerExecutorService = new ThreadPoolExecutor(3, 5, 0, TimeUnit.MILL
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章