講到Java多線程,大多數人腦海中跳出來的是Thread、Runnable、synchronized……這些是最基本的東西,雖然已經足夠強大,但想要用好還真不容易。從JDK1.5開始,增加了java.util.concurrent包,它的引入大大簡化了多線程程式的開發(要感謝一下大牛Doug Lee)。
java.util.concurrent包分成了三個部分,分別是java.util.concurrent、java.util.concurrent.atomic和java.util.concurrent.lock。內容涵蓋了並發集合類、線程池機制、同步互斥機制、安全執行緒的變數更新工具類、鎖等等常用工具。
為了便於理解,本文使用一個例子來做說明,交代一下它的情境:
假設要對一套10個節點群組成的環境進行檢查,這個環境有兩個進入點,通過節點間的依賴關係可以遍曆到整個環境。依賴關係可以構成一張有向圖,可能存在環。為了提高檢查的效率,考慮使用多線程。
1、Executors
通過這個類能夠獲得多種線程池的執行個體,例如可以調用newSingleThreadExecutor()獲得單線程的ExecutorService,調用newFixedThreadPool()獲得固定大小線程池的ExecutorService。拿到ExecutorService可以做的事情就比較多了,最簡單的是用它來執行Runnable對象,也可以執行一些實現了Callable<T>的對象。用Thread的start()方法沒有傳回值,如果該線程執行的方法有傳回值那用ExecutorService就再好不過了,可以選擇submit()、invokeAll()或者invokeAny(),根據具體情況選擇合適的方法即可。
import java.util.ArrayList;import java.util.List;import java.util.concurrent.ExecutionException;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;import java.util.concurrent.TimeUnit;/** * 線程池服務類 * * @author DigitalSonic */public class ThreadPoolService { /** * 預設線程池大小 */ public static final int DEFAULT_POOL_SIZE = 5; /** * 預設一個任務的逾時時間,單位為毫秒 */ public static final long DEFAULT_TASK_TIMEOUT = 1000; private int poolSize = DEFAULT_POOL_SIZE; private ExecutorService executorService; /** * 根據給定大小建立線程池 */ public ThreadPoolService(int poolSize) { setPoolSize(poolSize); } /** * 使用線程池中的線程來執行任務 */ public void execute(Runnable task) { executorService.execute(task); } /** * 線上程池中執行所有給定的任務並取回運行結果,使用預設逾時時間 * * @see #invokeAll(List, long) */ public List<Node> invokeAll(List<ValidationTask> tasks) { return invokeAll(tasks, DEFAULT_TASK_TIMEOUT * tasks.size()); } /** * 線上程池中執行所有給定的任務並取回運行結果 * * @param timeout 以毫秒為單位的逾時時間,小於0表示不設定逾時 * @see java.util.concurrent.ExecutorService#invokeAll(java.util.Collection) */ public List<Node> invokeAll(List<ValidationTask> tasks, long timeout) { List<Node> nodes = new ArrayList<Node>(tasks.size()); try { List<Future<Node>> futures = null; if (timeout < 0) { futures = executorService.invokeAll(tasks); } else { futures = executorService.invokeAll(tasks, timeout, TimeUnit.MILLISECONDS); } for (Future<Node> future : futures) { try { nodes.add(future.get()); } catch (ExecutionException e) { e.printStackTrace(); } } } catch (InterruptedException e) { e.printStackTrace(); } return nodes; } /** * 關閉當前ExecutorService * * @param timeout 以毫秒為單位的逾時時間 */ public void destoryExecutorService(long timeout) { if (executorService != null && !executorService.isShutdown()) { try { executorService.awaitTermination(timeout, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { e.printStackTrace(); } executorService.shutdown(); } } /** * 關閉當前ExecutorService,隨後根據poolSize建立新的ExecutorService */ public void createExecutorService() { destoryExecutorService(1000); executorService = Executors.newFixedThreadPool(poolSize); } /** * 調整線程池大小 * @see #createExecutorService() */ public void setPoolSize(int poolSize) { this.poolSize = poolSize; createExecutorService(); }}
invokeAll()和invokeAny()方法。前者會執行給定的所有Callable<T>對象,等所有任務完成後返回一個包含了執行結果的List<Future<T>>,每個Future.isDone()都是true,可以用Future.get()拿到結果;後者只要完成了列表中的任意一個任務就立刻返回,傳回值就是執行結果。
和其他資源一樣,線程池在使用完畢後也需要釋放,用shutdown()方法可以關閉線程池,如果當時池裡還有沒有被執行的任務,它會等待任務執行完畢,在等待期間試圖進入線程池的任務將被拒絕。也可以用shutdownNow()來關閉線程池,它會立刻關閉線程池,沒有執行的任務作為傳回值返回。
2、Lock
多線程編程中常常要鎖定某個對象,之前會用synchronized來實現,現在又多了另一種選擇,那就是java.util.concurrent.locks。通過Lock能夠實現更靈活的鎖定機制,它還提供了很多synchronized所沒有的功能,例如嘗試獲得鎖(tryLock())。
使用Lock時需要自己獲得鎖並在使用後手動釋放,這一點與synchronized有所不同,所以通常Lock的使用方式是這樣的
Lock l = ...; l.lock();try {// 執行操作} finally {l.unlock();}
java.util.concurrent.locks中提供了幾個Lock介面的實作類別,比較常用的應該是ReentrantLock。
講到Lock,就不能不講Conditon,前者代替了synchronized,而後者則代替了Object對象上的wait()、notify()和notifyAll()方法(Condition中提供了await()、signal()和signalAll()方法),當滿足運行條件前掛起線程。Condition是與Lock結合使用的,通過Lock.newCondition()方法能夠建立與Lock綁定的Condition執行個體。JDK的JavaDoc中有一個例子能夠很好地說明Condition的用途及用法:
class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100]; int putptr, takeptr, count; public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; notEmpty.signal(); } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } } }
3、並發集合類
java.util包中的集合類有的是安全執行緒的,有的則不是,在編寫多線程的程式時使用安全執行緒的類能省去很多麻煩,但這些類的效能如何呢?java.util.concurrent包中提供了幾個並髮結合類,例如ConcurrentHashMap、ConcurrentLinkedQueue和CopyOnWriteArrayList等等,根據不同的使用情境,開發人員可以用它們替換java.util包中的相應集合類。
CopyOnWriteArrayList是ArrayList的一個變體,比較適合用在讀取比較頻繁、修改較少的情況下,因為每次修改都要複製整個底層數組。ConcurrentHashMap中為Map介面增加了一些方法(例如putIfAbsenct()),同時做了些最佳化,下面的代碼中使用ConcurrentHashMap來作為全域節點表,完全無需考慮並發問題。
import java.util.ArrayList;import java.util.List;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;/** * 執行驗證的服務類 * * @author DigitalSonic */public class ValidationService {/** * 全域節點表 */public static final Map<String, Node> NODE_MAP = new ConcurrentHashMap<String, Node>();private ThreadPoolService threadPoolService;public ValidationService(ThreadPoolService threadPoolService) {this.threadPoolService = threadPoolService;}/** * 給出一個入口節點的WSDL,通過廣度遍曆的方式驗證與其相關的各個節點 * * @param wsdl 入口節點WSDL */public void validate(List<String> wsdl) {List<String> visitedNodes = new ArrayList<String>();List<String> nextRoundNodes = new ArrayList<String>();nextRoundNodes.addAll(wsdl);while (nextRoundNodes.size() > 0) {List<ValidationTask> tasks = getTasks(nextRoundNodes);List<Node> nodes = threadPoolService.invokeAll(tasks);visitedNodes.addAll(nextRoundNodes);nextRoundNodes.clear();getNextRoundNodes(nodes, visitedNodes, nextRoundNodes);}}private List<String> getNextRoundNodes(List<Node> nodes,List<String> visitedNodes, List<String> nextRoundNodes) {for (Node node : nodes) {for (String wsdl : node.getDependencies()) {if (!visitedNodes.contains(wsdl)) {nextRoundNodes.add(wsdl);}}}return nextRoundNodes;}private List<ValidationTask> getTasks(List<String> nodes) {List<ValidationTask> tasks = new ArrayList<ValidationTask>(nodes.size());for (String wsdl : nodes) {tasks.add(new ValidationTask(wsdl));}return tasks;}}
4、AtomicInteger
對變數的讀寫操作都是原子操作(除了long或者double的變數),但像數實值型別的++--操作不是原子操作,像i++中包含了獲得i的原始值、加1、寫回i、返回原始值,在進行類似i++這樣的操作時如果不進行同步問題就大了。好在java.util.concurrent.atomic為我們提供了很多工具類,可以以原子方式更新變數。
以AtomicInteger為例,提供了代替++--的getAndIncrement()、incrementAndGet()、getAndDecrement()和decrementAndGet()方法,還有加減給定值的方法、當前值等於預期值時更新的compareAndSet()方法。