在學習完Java多線程之基礎篇(一)和Java多線程之基礎篇(二)後接下來開始學習Java多線程之進階篇的內容。
Java 5 添加了一個新的包到Java平臺,這個包是java.util.concurrent包(簡稱JUC)。這個包包含了有一系列能夠讓Java的併發編程更加輕鬆的類。本文使用的Java 7 版本的JUC,下面讓我們繼續來學習吧!
一、線程池
提到線程線程池我們先來說一下線程池的好處,線程池的有點大概可以概括三點:
(1)重用線程池中的線程,避免因爲線程的創建和銷燬所帶來的性能開銷。
(2)能有效控制線程池的最大併發數,避免大量線程之間因互相搶奪系統資源而導致的阻塞現象。
(3)能夠對線程進行簡單的管理,並提供定時執行以及指向間隔循環執行等功能。
1.1 線程池的創建
Java SE 5的java.util.concurrent包中的執行器(Executor)將爲你管理Thread對象,從而簡化了併發編程。Executor在客戶端和任務執行之間提供了一個間接層;與客戶端直接執行任務不同,這個中介對象將執行任務。Executor允許你管理異步任務的執行,而無須顯式的管理線程的生命週期。Executor在Java中啓動任務的優選方法。
public class CachedThreadPool {
/**
* @param args
*/
public static void main(String[] args) {
class MyRunnable implements Runnable{
private int a = 5;
@Override
public void run() {
synchronized(this){
for(int i=0;i<10;i++){
if(this.a>0){
System.out.println(Thread.currentThread().getName()+" a的值:"+this.a--);
}
}
}
}
}
ExecutorService exec = Executors.newCachedThreadPool();
for(int i=0;i<5;i++)
exec.execute(new MyRunnable());
exec.shutdown();
}
}
運行結果:
pool-1-thread-2 a的值:5
pool-1-thread-1 a的值:5
pool-1-thread-1 a的值:4
pool-1-thread-1 a的值:3
pool-1-thread-3 a的值:5
pool-1-thread-2 a的值:4
pool-1-thread-1 a的值:2
pool-1-thread-1 a的值:1
pool-1-thread-2 a的值:3
pool-1-thread-2 a的值:2
pool-1-thread-2 a的值:1
pool-1-thread-3 a的值:4
pool-1-thread-3 a的值:3
pool-1-thread-3 a的值:2
pool-1-thread-3 a的值:1
pool-1-thread-5 a的值:5
pool-1-thread-5 a的值:4
pool-1-thread-5 a的值:3
pool-1-thread-5 a的值:2
pool-1-thread-5 a的值:1
pool-1-thread-4 a的值:5
pool-1-thread-4 a的值:4
pool-1-thread-4 a的值:3
pool-1-thread-4 a的值:2
pool-1-thread-4 a的值:1
說明:
這個結果可以和Java多線程之基礎篇(一)的 3.2.1定義任務(Runnable)的例子和結果做對比。發現用Executor來管理時,Runnable中的“資源不在共享”
,這個疑問我還沒有解決?知道的可以告訴我一聲。
ExecutorService是一個接口,並繼承了接口Executor。而Executors是一個工具類,下面來看看它們之間的UML圖:
其中最爲主要的是ThreadPoolExecutor類和Executors中的四類方法,下面我們來逐個分析。
1.1.1 ThreadPoolExecutor
(1)ThreadPoolExecutor簡介
ThreadPoolExecutor是線程池類。對於線程池,可以通俗的將它理解爲“存放一定數量的一個線程集合。線程池允許若個線程同時運行,運行同時運行的線程數量就是線程池的容量。當添加到線程池中的線程超過它的容量時,會有一部分線程阻塞等待,線程池會通過相應的調度策略和拒絕策略,對添加到線程池中的線程進行管理。”
(2)ThreadPoolExecutor的數據結構
下面是ThreadPoolExecutor類中比較典型的部分代碼:
public class ThreadPoolExecutor extends AbstractExecutorService {
// 阻塞隊列。
private final BlockingQueue<Runnable> workQueue;
// 互斥鎖
private final ReentrantLock mainLock = new ReentrantLock();
// 線程集合。一個Worker對應一個線程。
private final HashSet<Worker> workers = new HashSet<Worker>();
// “終止條件”,與“mainLock”綁定。
private final Condition termination = mainLock.newCondition();
// 線程池中線程數量曾經達到過的最大值。
private int largestPoolSize;
// 已完成任務數量
private long completedTaskCount;
// ThreadFactory對象,用於創建線程。
private volatile ThreadFactory threadFactory;
// 拒絕策略的處理句柄。
private volatile RejectedExecutionHandler handler;
// 保持線程存活時間。
private volatile long keepAliveTime;
private volatile boolean allowCoreThreadTimeOut;
// 核心池大小
private volatile int corePoolSize;
// 最大池大小
private volatile int maximumPoolSize;
//構造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
}
對一些關鍵的變量進行介紹:
- workers
workers是HashSet類型,它是一個Worker集合。而一個Worker對應一個線程,也就是說線程池通過workers包含了“一個線程集合”。當Worker對應的線程池啓動時,它會執行線程池中的任務;當執行完一個任務後,它會從線程池的阻塞隊列中取出一個阻塞的任務來繼續運行。workers的作用是:線程池通過它來實現了“允許多個線程同時運行”。 - workQueue
workQueue是BlockingQueue類型,它是一個阻塞隊列。當線程池中的線程超過它的容量的時候,線程會進入阻塞隊列進行阻塞等待。workQueue的作用是:讓線程池實現 了阻塞功能。 - mainLock
mainLock是互斥鎖,通過mainLock實現了對線程池的互斥訪問。 - corePoolSize和maximumPoolSize
corePoolSize是“核心池大小”,maximumPoolSize是“最大池大小”。它們的作用是:調整“線程池中實際運行的線程的數量”。
例如,當新任務提交給線程池時(通過execute方法)。
——如果此時,線程池中運行的線程數量 小於 corePoolSize;則僅當阻塞隊列滿時才創建新線程。
——如果此時,線程池中運行的線程數量 大於 corePoolSize,但卻是 小於 maximumPoolSize;則僅當阻塞隊列慢時才創建新線程。
——如果此時,corePoolSize和maximumPoolSize相同,則創建了固定大小的線程池。如果maximumPoolSize設置爲基本的無界值(如,Integer.MAX_VALUE),則允許線程池適應任意數量的併發任務。在大多數情況下,核心池大小和最大池大小的值在創建線程池設置的。但是,也可以使用setCorePoolSize(int)和setMaximumPoolSize(int)進行動態更改。 - poolSize
poolSize是當前線程池的實際大小,即線程池中任務的數量。 - allowCoreThreadTimeOut和keepAliveTime
allowCoreThreadTimeOut表示是否允許“線程在空閒狀態時,仍然能夠存活”。
keepAliveTime表示線程池處於空閒狀態的時候,超過keepAliveTime時間之後,空閒的線程會被終止。 - threadFactory
threadFactory是ThreadFactory對象,它是一個線程工廠類,即“線程池通ThreadFactory創建線程” - handler
handler是RejectedExecutionHandler類型。它是“線程池拒絕策略”的句柄,也就是說“當某任務添加到線程池中,而線程池拒絕任務是,線程池會通過handler進行相應的處理”
綜上所述,線程池通過workers來管理“線程集合”,每個線程在啓動後,會執行線程池中的任務;當一個任務執行完後,它會從線程池的阻塞隊列中取出任務來繼續運行。阻塞隊列時管理線程池任務的隊列,當添加到線程池中的任務超過線程池的容量時,該任務就會進入阻塞隊列進行等候。
1.1.2 線程池的分類
ExecutorService是Executor直接的擴展接口,也是最常用的線程池接口,我們通常見到的線程池定時任務線程池都是它的實現類。上面的Executors.newCachedThreadPool();中的Executors還有其他靜態方法可以調用,每個方法都有不同特性,它們都是直接或間接的通過配置ThreadPoolExecutor來實現自己的功能特性,這四類線程池分別是FixedThreadPool、CachedThreadPool、ScheduledThreadPool以及SingleThreadExecutor。
(1)FixedThreadPool
通過Executor的newFixedThreadPool方法來創建。它是一種線程數量固定的線程池,當線程池處於空閒狀態時,它們並不會被回收,除非線程池被關閉了。當所有的線程都處於活動狀態時,新任務都會處於等待狀態,直到有線程空閒出來。由於FixedThreadPool只有核心線程線程並且這些核心線程不會被回收,這意味着它能過更加快速的相應外界的請求。newFixedThreadPool方法的實現如下,可以發現FixedThreadPool中只有核心線程並且這些核心線程沒有超時機制,另外任務隊列也是沒有大小限制的。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newFixedThreadPool()在調用ThreadPoolExecutor()時,它傳遞一個LinkedBlockingQueue()對象,而LinkedBlockingQueue是單向鏈表實現的阻塞隊列。在線程池中,就是通過該阻塞隊列來實現“當線程池中任務數量超過允許的任務數量時,部分任務會阻塞等待”。關於LinkedBlockingQueue的實現細節,在後續的文章會繼續介紹。
有了FixedThreadPool,你可以一次性預先執行代價高昂的線程分配,因而也就可以限制線程的數量了。這可以節省時間,因爲你不用爲每個任務都固定的付出創建線程的開銷。在事件驅動的系統中,這種方式較好。
(2)SingleThreadExecutor
通過Executor的newSingleThreadExecutor方法來創建。這類線程池內部只有一個核心線程,它確保所有的任務都在同一個線程中按順序執行。SingleThreadExecutor的意義在於統一所有的外界任務到一個線程中,這使得這些任務之間不需要處理線程同步的問題。SingleThreadExecutor方法的實現如下所示:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
這對於你希望在另一個線程中連續運行的任何事物(長期存活的任務)來說,這是很有用的,例如監聽進入的套接字連接的任務。它對於希望在線程中運行的短任務也是同樣方便,例如,更新本地或遠程日誌的小任務,或者是事件分發線程。
(3)ScheduledThreadPool
通過Executors的newScheduledPool方法來創建。它的核心線程數量時固定的,而非核心線程數是沒有限制的,並且當非核心線程閒置是會被立即回收。ScheduledThreadPool這類線程主要用於執行定時任務和具有固定週期的重複任務,newScheduledThreadPool方法的實現如下:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS,
new DelayedWorkQueue());
}
ScheduledThreadPoolExecutor繼承ThreadPoolExecutor,並實現ScheduledExecutorService。
(4)CachedThreadPool
通過Executors的newCachedThreadPool方法來創建。它是一種線程數量不定的線程池,它只有非核心線程,並且其最大線程數爲Integer.MAX_VALUE。由於Integer.MAX_VALUE是一個很大的數,實際上就相當於最大線程數可以任意大。當線程池中的線程都是處於活動狀態時,線程池會創建新的線程來處理新任務,否則就會利用空閒的線程來處理新任務。線程池中的空閒線程都有超時機制,這個超時長爲60秒,超過60秒閒置線程就會被回收。和FixedThreadPool不同的是,CachedThreadPool的任務隊列其實相當於一個空集合,這將導致任何任務都會立即被執行,因爲在這種場景下SynchronousQueue是無法插入任務的。SynchronousQueue是一個非常特殊的隊列,在很多情況下可以把它簡單理解爲一個無法存儲元素的隊列,由於它在實際中較少使用,這裏就不探討了。從CachedThreadPool的特性來看,這類線程池比較適合執行大量的耗時較少的任務。當整個線程池都處於閒置狀態時,線程池中的線程都會超時而被終止,這個時候CachedThreadPool之中實際上是沒有任何線程的,它幾乎是不佔用任何系統資源的,newCachedThreadPool的實現方法如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
1.2 線程池中任務的添加
1.2.1 execute()
execute()定義在ThreadPoolExecutor.java中,源碼如下:
public void execute(Runnable command) {
// 如果任務爲null,則拋出異常。
if (command == null)
throw new NullPointerException();
// 獲取ctl對應的int值。該int值保存了"線程池中任務的數量"和"線程池狀態"信息
int c = ctl.get();
// 當線程池中的任務數量 < "核心池大小"時,即線程池中少於corePoolSize個任務。
// 則通過addWorker(command, true)新建一個線程,並將任務(command)添加到該線程中;然後,啓動該線程從而執行任務。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 當線程池中的任務數量 >= "核心池大小"時,
// 而且,"線程池處於允許狀態"時,則嘗試將任務添加到阻塞隊列中。
if (isRunning(c) && workQueue.offer(command)) {
// 再次確認“線程池狀態”,若線程池異常終止了,則刪除任務;然後通過reject()執行相應的拒絕策略的內容。
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
// 否則,如果"線程池中任務數量"爲0,則通過addWorker(null, false)嘗試新建一個線程,新建線程對應的任務爲null。
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 通過addWorker(command, false)新建一個線程,並將任務(command)添加到該線程中;然後,啓動該線程從而執行任務。
// 如果addWorker(command, false)執行失敗,則通過reject()執行相應的拒絕策略的內容。
else if (!addWorker(command, false))
reject(command);
}
說明:execute()的作用是將任務添加到線程池中執行。它分爲三種情況:
(1)如果“線程池中任務數量” < “核心池大小” 時,即線程池中少於corePoolSize個任務;此時就新建一個線程,並將該任務添加到線程中進行執行。
(2)如果“線程池中任務數量” >= “核心池大小” ,並且“線程池是允許狀態”;此時,則將任務添加到阻塞隊列中阻塞等待。在該情況下,會再次確認“線程狀態”,如果“第2次讀到的線程池狀態”和“第1次讀到的線程次狀態”不同,則從阻塞隊列中刪除該任務。
(3)如果非上述的兩種情況,就會嘗試新建一個線程,並將該任務添加到線程中進行執行。如果執行失敗,則通過reject()拒絕該任務。
1.2.2 addWorker()
addWorker()的源碼如下:
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
// 更新"線程池狀態和計數"標記,即更新ctl。
for (;;) {
// 獲取ctl對應的int值。該int值保存了"線程池中任務的數量"和"線程池狀態"信息
int c = ctl.get();
// 獲取線程池狀態。
int rs = runStateOf(c);
// 有效性檢查
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
// 獲取線程池中任務的數量。
int wc = workerCountOf(c);
// 如果"線程池中任務的數量"超過限制,則返回false。
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 通過CAS函數將c的值+1。操作失敗的話,則退出循環。
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
// 檢查"線程池狀態",如果與之前的狀態不同,則從retry重新開始。
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
// 添加任務到線程池,並啓動任務所在的線程。
try {
final ReentrantLock mainLock = this.mainLock;
// 新建Worker,並且指定firstTask爲Worker的第一個任務。
w = new Worker(firstTask);
// 獲取Worker對應的線程。
final Thread t = w.thread;
if (t != null) {
// 獲取鎖
mainLock.lock();
try {
int c = ctl.get();
int rs = runStateOf(c);
// 再次確認"線程池狀態"
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// 將Worker對象(w)添加到"線程池的Worker集合(workers)"中
workers.add(w);
// 更新largestPoolSize
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
// 釋放鎖
mainLock.unlock();
}
// 如果"成功將任務添加到線程池"中,則啓動任務所在的線程。
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
// 返回任務是否啓動。
return workerStarted;
}
addWorker()的作用是將firstTask添加到線程池中,並啓動該任務。當core爲true是,則以corePoolSize爲界限,若“線程池中已有任務數量” >= corePoolSize ,那麼返回false;當core爲false時,則以maximumPoolSize爲界限,若“線程池中已有任務數量” >= maximumPoolSize ,則返回false。addWorker()方法會先通過for循環不斷嘗試更新 ctl狀態,ctl 記錄了“線程池中任務數量和線程池狀態”。更新成功後,在通過try模塊來將任務添加到線程池中,並啓動任務所在的線程。
從addWorker()方法中,我們可以發現:線程池在添加任務時,會創建任務對應的Worker對象,而一個Worker對象包含了一個Thread對象。通過將Worker對象添加到“線程的workers集合中”,從而實現將任務添加到線程池中。通過啓動Worker對應的Thread線程,則執行該任務。
1.2.3 submit()
submit()實際上也是通過調用execute()實現的,源碼如下:
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
1.3 線程池的關閉
在ThreadPoolExecutor類中的shutdown()方法源碼爲:
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
// 獲取鎖
mainLock.lock();
try {
// 檢查終止線程池的“線程”是否有權限。
checkShutdownAccess();
// 設置線程池的狀態爲關閉狀態。
advanceRunState(SHUTDOWN);
// 中斷線程池中空閒的線程。
interruptIdleWorkers();
// 鉤子函數,在ThreadPoolExecutor中沒有任何動作。
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
// 釋放鎖
mainLock.unlock();
}
// 嘗試終止線程池
tryTerminate();
}
1.4 使用Callable
Runnable是執行工作的獨立任務,但是它不返回任何值。如果你希望任務在完成時能夠返回一個值,那麼可以實現Callable接口而不是Runnable接口。在Java SE 5 中引入的Callable是一種具有類型參數的泛型,它的類型參數表示的是從方法call()中返回的值,並且必須使用ExecutorService.submit()方法調用它,下面是簡單示例:
public class CallableDemo {
/**
* @param args
*/
public static void main(String[] args) {
class TaskWithResult implements Callable<String>{
private int id;
public TaskWithResult(int id){
this.id = id;
}
@Override
public String call() throws Exception {
return "result of Callable "+id;
}
}
ExecutorService exec = Executors.newCachedThreadPool();
ArrayList<Future<String>> results = new ArrayList<Future<String>>();
for(int i=0;i<5;i++){
results.add(exec.submit(new TaskWithResult(i)));
}
for(Future<String> fs:results){
try {
System.out.println(fs.get());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
exec.shutdown();
}
}
}
}
輸出的結果:
result of Callable 0
result of Callable 1
result of Callable 2
result of Callable 3
result of Callable 4
submit()方法會產生Future對象,它用Callable返回結果的特定類型進行了參數化。
二、解決共享資源競爭
在Java SE5 的java.util.concurrent類庫中還包含有定義在java.util.concurrent.locks中的顯式的互斥機制。Lcok對象必須被顯示的創建、鎖定、和釋放。因此,它與內間的鎖形式相比,代碼缺乏優雅性。但是,對於解決某些類型的問題,它更加靈活。下面是用Lock寫以解決共享資源的示例:
public class LockAndUnLock {
static Lock lock = new ReentrantLock();//新建鎖
public static void main(String[] args) {
new Thread("A"){
public void run() {
Thread.yield();//當前線程的讓步,加快線程切換
numPrint();
};
}.start();
new Thread("B"){
public void run() {
Thread.yield();//當前線程的讓步,加快線程切換
numPrint();
};
}.start();
}
private static void numPrint(){
lock.lock();
try{
for(int i=0;i<10;i++){
Thread.sleep(100);
System.out.println("當前線程"+Thread.currentThread().getName()+":"+i);
}
}catch(Exception e){
}finally{
lock.unlock();
}
}
}
輸出結果:
當前線程A:0
當前線程A:1
當前線程A:2
當前線程A:3
當前線程A:4
當前線程A:5
當前線程A:6
當前線程A:7
當前線程A:8
當前線程A:9
當前線程B:0
當前線程B:1
當前線程B:2
當前線程B:3
當前線程B:4
當前線程B:5
當前線程B:6
當前線程B:7
當前線程B:8
當前線程B:9
可以看出一個被互斥調用的鎖,並使用lock()和unlock()方法在numPrint()內創建了臨界資源。當你在使用Lock對象時,將這裏的所示的慣用法內部化是很重要的:緊接着的對lock()的調用,你必須放再finally子句中帶有unlock()的try-finally語句中。儘管try-finally所需的代碼比synchronized關鍵字要多,但是這也代表了顯示的Lock對象的優點之一。如果在使用synchronized關鍵字,某些事務失敗了,那麼就會拋出一個異常。但是你沒有機會去做任何清理工作,以維護系統使其處於良好狀態。有了顯示的Lock對象,你就可以使用finally子句將系統維護在正確的狀態了。
大體上,當你使用synchronized關鍵字時,需要寫的代碼量更少,並且用戶錯誤出現的可能性也會降低,因此通常只有在解決特殊問題時,才使用顯示的Lock對象。
以上,先對線程池做了大體的介紹,然後我會逐步介紹JUC中的原子類、線程安全的集合、鎖以及深層次剖析線程池原理。