你所不知道的五件事情--java.util.concurrent(第二部分)
這是Ted Neward在IBM developerWorks中5 things系列文章中的一篇,仍然講述了關于Java并發集合API的一些應用竅門,值得大家學習。(2010.06.17最后更新)
摘要:除了便于編寫并發應用的集合API外,java.util.concurrent還引入了其它的預置程序組件,這些組件能輔助你在多線程應用中控制和執行線程。Ted Neward再介紹了五個來自于java.util.concurrent的Java編程必備竅門。
通過提供線程安全,性能良好的數據結構,并發集合框架使并發編程變得更容易。然而在有些情況下,開發者需要多走一步,并要考慮控制和/或調節線程的執行。提供java.util.concurrent包的全部原因就是為了簡化多線程編程--事實上正是如此。
接著第一部分,本文介紹了多個同步數據結構,這些數據結構比核心語言基本結構(監視器)的層次要高,但不會高到將它們圈囿在一個集合類中。一旦知道了這些鎖與栓的用途,就能徑直去用了。
1. 信號量
在有些企業級系統中,常需要開發者去控制針對特定資源的請求(線程或動作)的數量。雖然完全可能試著手工編寫這樣的調節程序,但使用Semaphore類會更容易些,該類會為你處理對線程的控制,如清單1所示:
清單1. 使用信號量調節線程
import java.util.*;import java.util.concurrent.*;
public class SemApp
{
public static void main(String[] args)
{
Runnable limitedCall = new Runnable() {
final Random rand = new Random();
final Semaphore available = new Semaphore(3);
int count = 0;
public void run()
{
int time = rand.nextInt(15);
int num = count++;
try
{
available.acquire();
System.out.println("Executing " +
"long-running action for " +
time + " seconds
#" + num);
Thread.sleep(time * 1000);
System.out.println("Done with #" +
num + "!");
available.release();
}
catch (InterruptedException intEx)
{
intEx.printStackTrace();
}
}
};
for (int i=0; i<10; i++)
new Thread(limitedCall).start();
}
}
雖然上例有10個線程在運行(針對運行SemApp的Java進程執行jstack程序可以驗證這一點),但只有3個是活動的。另外7個線程會被保存起來,直到其中一個信號量計數器被釋放出來。(準確地說,Semaphore類支持一次獲取和釋放一個以上的被許可線程,但在此處的場景中這么做沒有意義。)
2. CountDownLatch
如果并發類Semaphore是被設計為在同一時刻允許"其中"一個線程執行的話,那么CountDownLatch就是賽馬比賽中的起跑門。該類持有所有的線程,當遇到某個特定條件,那時CountDownLatch就會一次性釋放全部的線程。
清單2. CountDownLatch:讓我們比賽!
import java.util.*;
import java.util.concurrent.*;
class Race
{
private Random rand = new Random();
private int distance = rand.nextInt(250);
private CountDownLatch start;
private CountDownLatch finish;
private List<String> horses = new ArrayList<String>();
public Race(String
names)
{
this.horses.addAll(Arrays.asList(names));
}
public void run()
throws InterruptedException
{
System.out.println("And the horses are stepping up to the gate
");
final CountDownLatch start = new CountDownLatch(1);
final CountDownLatch finish = new CountDownLatch(horses.size());
final List<String> places =
Collections.synchronizedList(new ArrayList<String>());
for (final String h : horses)
{
new Thread(new Runnable() {
public void run() {
try
{
System.out.println(h +
" stepping up to the gate
");
start.await();
int traveled = 0;
while (traveled < distance)
{
// In a 0-2 second period of time
.
Thread.sleep(rand.nextInt(3) * 1000);
//
a horse travels 0-14 lengths
traveled += rand.nextInt(15);
System.out.println(h +
" advanced to " + traveled + "!");
}
finish.countDown();
System.out.println(h +
" crossed the finish!");
places.add(h);
}
catch (InterruptedException intEx)
{
System.out.println("ABORTING RACE!!!");
intEx.printStackTrace();
}
}
}).start();
}
System.out.println("And
they're off!");
start.countDown();
finish.await();
System.out.println("And we have our winners!");
System.out.println(places.get(0) + " took the gold
");
System.out.println(places.get(1) + " got the silver
");
System.out.println("and " + places.get(2) + " took home the bronze.");
}
}
public class CDLApp
{
public static void main(String[] args)
throws InterruptedException, java.io.IOException
{
System.out.println("Prepping
");
Race r = new Race(
"Beverly Takes a Bath",
"RockerHorse",
"Phineas",
"Ferb",
"Tin Cup",
"I'm Faster Than a Monkey",
"Glue Factory Reject"
);
System.out.println("It's a race of " + r.getDistance() + " lengths");
System.out.println("Press Enter to run the race
.");
System.in.read();
r.run();
}
}
注意在清單2中,CountDownLatch服務于兩個目的:首先,它同時釋放所有的線程,模擬比賽的開始;但之后,另一個CountDownLatch模擬了比賽的結束。一場比賽會有更多的評論,你可以在比賽的"轉彎"和"半程"點添加CountDownLatch,當馬匹跑過1/4程,半程和3/4程時。
3. Executor
清單1和清單2中的例子都遭遇了一個令人非常沮喪的錯誤,你被迫要直接地創建Thread對象。這是一個造成麻煩的方式,因為在有些JVM中,創建Thread對象是一件重量級的工作,所以重用而非創建新的線程要好得多。然而在另一些JVM中,情況就恰恰相反:Thread是非常輕量級的,若你需要一個線程,直接創建它則會好得多。當然,如果Murphy有他自己的方法(他經常就是這么做的),無論你使用哪種方法,對于你最終所依賴的某種Java平臺都會是錯誤的。
JSR-166專家組在一定程度上預見到了這種情況。與讓Java開發者直接創建Thread實例不同,他們推薦Executor接口,這是一個創建新線程的抽象。如果清單3所示,Executor允許你自己不必使用new操作符去創建Thread對象:
清單3. Executor
Executor exec = getAnExecutorFromSomeplace();
exec.execute(new Runnable() {
});
使用Excutor的主要缺點與我們使用所有對象工廠所遇到的缺點一樣:工廠必須來源于某處。不幸地是,不同于CLR,JVM并不帶有一個標準的VM范圍內的線程池。
Executor類只是作為獲取Executor實現實例的常用地方,但它只有new方法(例如,為了創建新的線程池);它沒有預創建的實例。所以,如果你想創建并使用一個能貫穿于整個程序的Executor實現,你就可以創建一個你自己的Executor實例。(或者,在有些情況下,你可以使用你所選容器/平臺所提供的Executor實例。)
ExecutorService,為你服務
ExecutorService的用處在于使你不必關心Thread來自于何處,Executor接口缺乏Java開發者可能期望的一些功能,比如啟動一個線程,該線程用于產生結果,它會以非阻塞方式一直等待,直到結果出現為止。(在桌面應用中這是很普通的需求,在這種應用中用戶會執行一個需要訪問數據庫的UI操作,如果它耗時太長的話,就可能想要在它完成之前就取消這一操作。)
為此,JSR-166的專家們創造一個更為有用的抽象,ExecutorService接口,該接口將啟動線程的工廠模型化為一個服務,這樣就能對該服務進行集合化控制了。例如,不對每個任務調用一次execute()方法,ExecutorService能創建一個任務的集合,并可返回代表這些任務未來結果的Future集合。
4. ScheduledExecutorServices
與ExecutorService接口同樣優秀,特定的任務需要以計劃的形式進行執行,例如在特定的時間間隔或在特定的時刻執行給定的任務。這就是繼承自ExecutorService的ScheduledExecutorService的職責范疇。
如果你的目的是創建一個"心跳"命令,該命令每5秒鐘就去"ping"一次。ScheduledExecutorService會幫你做到這一點,正如你在清單4中所見的那般簡單:
清單4. ScheduledExecutorService按計劃去"Ping"
import java.util.concurrent.*;
public class Ping
{
public static void main(String[] args)
{
ScheduledExecutorService ses =
Executors.newScheduledThreadPool(1);
Runnable pinger = new Runnable() {
public void run() {
System.out.println("PING!");
}
};
ses.scheduleAtFixedRate(pinger, 5, 5, TimeUnit.SECONDS);
}
}
怎么樣?沒有操作線程的煩惱,如果用戶想取消心跳,也不必操心如何去做,前臺或后臺都沒有顯示的標記線程;所有的調度細節都留給了ScheduledExecutorService。
順便提一下,如果用戶想要取消心跳,從scheduleAtFixedRate()方法返回的會是一個ScheduledFuture實例,它不僅含有執行結果(如果有的話),也有一個cancel()方法去停止該計劃任務。
5. 超時方法
擁有為阻塞操作置一個確定的超時控制的能力(這樣就可以避免死鎖)是java.util.concurrent類庫相比于舊有并發API,如針對鎖的監視器,的最大優點之一。
這些方法幾乎總是按int/TimeUnit對的方式進行重載,該int/TimeUnit對用于指示方法在跳出執行并將控制返回給平臺之前需要等待多長時間。這要求開發者對此做更多的工作--如果沒有獲得鎖,將如何進行恢復?--但結果卻幾乎總是正確的:更少的死鎖,以及更加生產安全的代碼。(更多關于生產就緒的代碼,請見Michael Nygard的Release It!)
結論
java.util.concurrent包含有許多更優雅的工具,它們出于集合框架,但更勝之,特別是.locks和.atomic包中的類。深入挖掘之,你將發現像CyclicBarrier這樣的十分有用的控制結構,甚至于更多。
下一次,我們將步入一個新的主題:你所不知道的五件關于Jar的事情。
請關注你所不知道的五件事情--java.util.concurrent(第一部分)