關於 Java Concurrency
自從
Java
誕生之時,
Java
就支持並行的概念,比如線程和鎖機制。這個教程幫助開發多線程
Java
程序員能夠理解核心的
Java
並行理念以及如何使用他們。
內容涉及到
Java
語言中的線程,
重練級以及輕量級同步機制
以及
JavaSE 5
中的鎖,原子量
並行容器,線程調度
以及線程執行者。
開發人員使用這些知識能夠開發好併發線程安全的
Java
應用程序。
Java 並行的概念( Java Concurrency Concepts )
概念 |
描述 |
Java 內存模型 |
在 JavaSE5 JSR133 規範中詳細定義了 Java 內存模型 Java Memory Model ( JMM ),該模型定義了相關的操作 比如讀 , 寫操作,以及在監視器上的同步。 這些操作按 Happens-before 的順序。 這個定義保證了一個線程可以看到另一個線程操作的結果,同時保證了同步的程序, 以及如何定義一個不變的屬性 等等。 |
監視器 |
在 Java 中,任何一個對象都有一個監視器,來排斥共享訪問臨界區域的代碼。這些臨界區可以是一個方法 或者是一段代碼塊,這些臨界區域作爲同步塊。線程只有獲取該監視器才能執行同步塊的代碼。當一個線程到達這塊代碼是,首先等待來確定是否其他線程已經釋放這個監控器。監控器除了排斥共享訪問,還能通過 Wait 和 Notify 來協調線程之間的交互。 |
原子屬性 |
除了 Double 和 long 類型,其他的簡單類型都是原子類型。 Double 和 long 類型的修改在 JVM 分爲兩個不封。爲了保證更新共享的 Double 和 Long 類型,你應該將 Double 和 long 的屬性作爲 Volatile 或者將修改代碼放入同步塊中。 |
競爭情況 |
當許多線程在一系列的訪問共享資源操作中,並且結果跟操作順便有關係的時候,就發生了競爭情況。 |
數據競爭 |
數據競爭涉及到當許多線程訪問不是 non-final 或者 non-volatile 並沒有合適的同步機制的屬性時, JMM 不能保證不同步的訪問共享的熟悉。數據競爭導致比個預知的行爲。
|
自公佈
|
還沒有通過構造方法實例化對象之前,把這個對象的引用公佈時不安全的。 一種是通過註冊一個監聽器,當初始化的時候回調來發布引用。 另一種是在構造方法裏面啓動線程。這兩種都會導致其他線程引用部分初始化的對象。 |
Final 屬性 |
Final 屬性必須顯示的賦值,否則就會有編譯錯誤。一旦賦值,不能被修改。將一個對象引用標記爲 Final 只能保證該引用不會被修改,但該對象可以被修改。比如一個 Final ArrayIist 不能改變爲另一個 ArrayList 但你可以添加或者修改這個 List 的對象。在構造方法之後,一個對象的 Final 屬性是凍結的,保證了對象被安全的發佈。其他線程可以在構造方法時看到該變量,甚至在缺乏同步的機制下。
|
不變對象 |
Final 屬性從語義上能夠保證創建不變對象。而不變對象可以再沒有同步機制下多線程共享和讀取。爲保證該對象是不變的,必須保證如下: 這個對象被安全的發佈, this 引用不能在構造方法的時候被髮布 所有的屬性都是 Final 的 應用的對象必須保證在構造方法之後不能被修改。 這個對象需要聲明爲 Final 保證子類違法這些原則。 |
Protecting shared data
保護共享的數據
線程安全的程序需要開發人員在需要修改共享的數據時使用合適的鎖機制。鎖機制建立的
適合 JMM 的順序,保證對於其他程序的可視性。
當在同步機制外修改共享的 data 時,JMM不能保證其一致性。 JVM 提供了一些方法來保證其可視性。
Synchronized
每一個對象實體都有一個監視器(來之於 Object 對象),這個監視器能被再某一線程中鎖定。 Synchronized 關鍵字來指定在方法或者代碼塊上持有該對象監視器上的鎖定。 當某一線程同步修改一屬性,後續線程將能看到被該線程修改的數據。
Lock
java.util.concurrent.locks 包提供了 Lock 的接口, ReentrantLock 實現了類似 Synchronized 關鍵字的功能。同時還提供了額外的功能,比如不是阻塞的 tryLock() 方法和釋放鎖。
private final Lock lock = new ReentrantLock();
private int value = 0 ;
public int increment() {
lock.lock();
try {
return ++ value;
} finally {
lock.unlock();
}
}
}
同時,在多線程高衝突的情況下,
ReentrantLock
要比
Synchronized
效率好。
ReadWriteLock
java.util.concurrent.locks 包提供了一個讀寫鎖的接口,這個接口定義了讀和寫的一對鎖,
一般允許並行的讀和排他的寫。下面的代碼展示了上述功能。
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int value;
public void increment() {
lock.writeLock().lock();
try {
value ++ ;
} finally {
lock.writeLock().unlock();
}
}
public int current() {
lock.readLock().lock();
try {
return value;
} finally {
lock.readLock().unlock();
}
}
}
Volatile
Volatile 關鍵字使其屬性對於後續的線程的可見性。
private volatile boolean stop;
public void stopProcessing() {
stop = true ;
}
public void run() {
while ( ! stop) {
// .. do processing
}
}
}
注意:將 array 標記爲 Volatile 不能保證數組裏面元素的 Volatile ,只能保證數組的引用時
可見的。使用 AtomicIntegerArray 來保證整個數組都是可見的。
原子類
Volatile 的缺點是隻能保證可見性。不能保證修改結果的可見性。而 java.util.concurrent.atomic
包包含了一組支持原子操作的類來彌補 Volatile 的不足。
private AtomicInteger value = new AtomicInteger();
public int next() {
return value.incrementAndGet();
}
}
ThreadLocal
ThreadLocal 存貯了該 線程所需要的數據,不需要鎖的機制。一般而言, ThreadLocal 存放當前的事務和其他資源等。如下代碼, TransactionManager 中, ThreadLocal 類型的 currentTransaction 存貯了當前事務。
private static final ThreadLocal < Transaction > currentTransaction =
new ThreadLocal < Transaction > () {
@Override
protected Transaction initialValue() {
return new NullTransaction();
}
} ;
public Transaction currentTransaction() {
Transaction current = currentTransaction.get();
if (current.isNull()) {
current = new TransactionImpl();
currentTransaction.put(current);
}
return current;
}
}
並行容器
合理維護共享數據一致性的核心技術是在訪問數據時採取同步機制。這種技術使得所有訪問共享數據的方式保證了同步的原則。 java.util.concurrent 提供了可以並行使用的數據結構。通常而言,使用這些數據結構優於通過 Synchronized 包裝的非同步集合。
同步的 lists and sets
類 |
描述 |
CopyOnWriteArraySet
|
CopyOnWriteArraySet 提供 Copy-On-Write 的語義 即:每當修改某一數據時在整個容器內容拷貝上修改,然後將該備份同步入容器。
|
CopyOnWriteArrayList
|
類似 CopyOnWriteArraySet
|
ConcurrentSkipListSet
|
JSE6 提供的並行訪問可以排序的 Set 。 |
並行 maps
java.util.concurrent 擴展 map 接口,提供了名叫 ConcurrentMap 的並行 Map 。
如下面所有的操作都是原子性的。
方法 |
描述 |
putIfAbsent(K key, V value) : V
|
如果 Key 沒有在該 Map 中,將 Key Value 存入。 否則不做任何處理。 如果沒有該 Key 返回 Null 如果有 返回以前的值。 |
remove(Object key, Object value) : boolean
|
如果 Map 中包含該 key 則移出該 Value 否則不做任何操作。 |
replace(K key, V value) : V
|
如果 Map 中有該 Key 則用該 Value 值替換久值。 |
replace(K key, V oldValue, V newValue) : boolean
|
如果 Map 中有該 Key 且值爲 oldValue 時,用 newValue 替換該久值。 |
下面是具體實現類:
類 |
描述 |
ConcurrentHashMap
|
內部的 segment 實現了並行的讀取。 |
ConcurrentSkipListMap
|
JSE6 提供的並行訪問可排序的 Map 。 |
Queues
作爲生產者於消費者管道的 Queues ,生產的條目從一頭放入,然後從另一頭取出,典型的先進先出的順序。 Queues 接口在 JSE5 加入 java.util 包裏,應用於單線程的環境。最主要用於多生產者消費者的情況下。所有的讀寫操作都在同一 Queue 上。 在 Java.util.concurrent 包的 blockingQueues 接口擴張了 Queue 並處理了 Queue 可能已經被生產者添加慢的情況,或者消費者已經讀取或者取出完, Queue 爲空的情況。 在這些情況下, BlockingQueue 提供了阻塞的機制。可以設定阻塞的時間或者阻塞的條件。
下表反應了 Queue 於 BlockingQueue 對處理特殊條件下的不同策略。
類 |
策略 |
插入 |
移除 |
檢查 |
Queue |
扔出異常 |
Add |
remove |
element |
返回特定的值 |
Offer |
poll |
peek |
|
Blocking Queue |
永遠的阻塞 |
Put |
take |
n/a |
在設定的時間阻塞 |
Offer |
poll |
n/a |
下面是具體的實現類。
PriorityQueue |
唯一非並行的 Queue 。用於單線程 處理排序的集合。 |
ConcurrintlinkedQueue |
沒有容量限制的的並行實現,不支持阻塞。 |
ArrayBlockingQueue |
基於數組 有容量限制的 阻塞 Queue 。 |
LinkedBlockingQueue |
最通用的實現阻塞容量限制的 Queue 。 |
PriorityBlockingQueue |
相對於先進先出,該 Queue 的順序基於 Comparator 的優先級別,沒有容量限制。 |
DelayQueue |
沒有容量限制的 Queue ,有一個延遲值。 只有延遲時間超過時才能被移除。 |
SynchronousQueue |
容量爲 0 的隊列,只到下一個到來之前,生產者和消費者被阻塞。適合在線程中交換數據。 |
Deques
Deques 在 JSE6 加入,爲雙頭 Queue 。它不僅支持在對頭添加,在隊尾移除的功能,還提供在雙頭添加和移除。類似於 BlockingQueue ,也有一個 BlockingDeques 提供阻塞和超時的 Deque 。
下表爲 Deque 和 BlockingDeque 對於具體方法的策略。
接口 |
First 或者 Last |
策略 |
插入 |
移除 |
檢測 |
Deque |
Head |
扔出異常 |
addFirst |
removeFirst |
getFirst |
返回特定值 |
offerFisrt |
pollFirst |
peekFirst |
||
Tail |
扔出異常 |
addLast |
RemoveLast |
getLast |
|
返回特定值 |
offerLast |
PollLast |
PeekLast |
||
BlockingDeque |
Head |
永遠阻塞 |
putFirst |
takeFirst |
N/A |
在一定時間段內阻塞 |
offerFirst |
pollFirst |
N/A |
||
Tail |
永遠阻塞 |
PutLast |
takeLast |
N/A |
|
在一定時間段內阻塞 |
offerLast |
pollLast |
N/A |
對於 Deque 特殊的用法就是添加移除和檢測發生在隊列的末端。這種用法類似於棧 ( 先進後出順序 ) 。事實上, Deque 也提供了類似的方法, push() pop() 和 peek(). 這些方法被映射到 addFirst()
removeFirst() 和 PeekFirst().
下表爲 JDK 提供的實現類。
類 |
描述 |
LinkedList |
在 JSE6 中重新設計實現了 Deque 接口。 實現了非同步了堆棧。 |
ArrayDeque |
不支持並行 容量不限的 Deque 。 |
LinkedBlockingDeque |
唯一支持並行的 Deque , |
線程
在 Java 中, java.lang.Thread 被用來表述一個應用或者 JVM 線程。在代碼中,常用 Thread.currentThread() 來獲得當前線程。
下表爲線程的相關方法
線程方法 |
表述 |
Start |
啓動一個線程 執行 Run 方法。 |
Join |
阻塞當前線程直到其他線程退出。 |
interrupt |
中斷其他線程。如果一個線程正在被阻塞,如果試圖去 Interrupt 這個線程,這個線程會泡出 InterruptedException ,否則,置爲 interrupt 狀態。 |
Stop suspend resume destroy |
這些方法已經不贊成使用。這些危險的操作依賴於線程的狀態。可以使用 interrut 和 volatile 標記來實現。 |
Uncaught exception handlers
如果一個線程添加一個 UncaughtExceptionHandlers, 如果該線程被非安全終止時會收到通知。 如下代碼:
t.setUncaughtExceptionHandler( new Thread.
UncaughtExceptionHandler() {
void uncaughtException(Thread t, Throwable e) {
// get Logger and log uncaught exception
}
} );
t.start();
死鎖
當線程相互等待資源,而這些資源又被相互持有是,死鎖就發生了。最明顯的資源就是對象的監控器。但是可以引起阻塞(比如 Wait 和 notify )的任何資源也可以引起死鎖。
最新的 JVM 能夠檢測死鎖,並在線程的 Dump 中打印死鎖信息。
另外的死鎖情況是,飢餓線程和活鎖。 飢餓線程是指一些線程長時間持有鎖而是某些線程處於飢餓狀態而不處理真的業務。活鎖是指線程花費大量時間檢測資源 避免死鎖而不是真正處理業務邏輯。
線程交互
Wait/notify
Wait/nofify 是最合理的方式來處理一個線程子在一定條件下通過一個信號來通知另一線程,特別是代替在循環裏通過 Sleep 來檢測條件的方式。比如,一個線程可能等待一個需要處理的隊列,當需要處理的內容添加到隊裏中時,而另外一個線程會通知等待的線程。
規範的用法如下:
private final Object lock = new Object();
private volatile boolean flag = false ;
public void waitTillChange() {
synchronized (lock) {
while ( ! flag) {
try {
lock.wait();
} catch (InterruptedException e) {
}
}
}
}
public void change() {
synchronized (lock) {
flag = true ;
lock.notifyAll();
}
關於上面的代碼,需要着重說明的如下:
一定要在同步鎖中調用 call notify notifyall.
Wait 一定要在循環中檢測等待條件。
一定要在調用 notify 或者 notifyAll 之前改變條件,否則即使通知了其他線程也無法退出循環。
Condition
在 JSE5 中,新添加了 java.util.concurrent.locks.Condition 類。該類在語義上實現了 wait 和 notify 的功能,同時添加了額外的功能,比如每個鎖可以有多個條件,中斷的等待,訪問統計等。 Conditon 通過鎖的實例獲得。
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private volatile boolean flag = false ;
public void waitTillChange() {
lock.lock();
try {
while ( ! flag) {
condition.await();
}
} finally {
lock.unlock();
}
}
public void change() {
lock.lock();
try {
flag = true ;
condition.signalAll();
} finally {
lock.unlock();
}
}
}
Coordination 類
在 java.util.concurrent 包包含幾個常用的多線程交互類,這些類基本覆蓋了常用的情況。通常使用這些類比使用 wait/notice 更安全。
CyclicBarrier
CyclicBarrier 以特定的值對計數器初始化。參與的線程調用 await() 方法時,如果沒有沒有達到計數器的初始化的值時,該線程被阻塞。直到計數器達到特定的值時,所有阻塞的線程被釋放。 CyclicBarrier 可以重複設置,來調整一組線程的啓動與停止。
CountDownLatch
CountDownLatch 以特定的值初始化,線程調用 await() 時,如果計數器沒有減少到 0 時,該線程被阻塞。其他線程可以調用 countDown() 方法減少計數器。當計數器減少爲 0 時,該 CountDownLatch 不能重新設置計數器而重用。
Semaphore
Semaphore 管理一組許可證,線程通過調用 acquire() 方法來檢測是否有許可,如果沒有被阻塞,線程可以調用 release() 方法來釋放許可證。 Semaphore 等價於互斥排他鎖。
Exchanger
Exchanger 等待線程調用 exchange() 方法來交互數據,類似使用 SynchronousQueue ,通過它交互數據是雙向的。
Task Execution
許多 java 多線程程序需要一組工作線程從隊列中取出任務來執行。 Java.util.concurrent 包對這種工作線程提供了可靠的基礎。
ExecutorService
Exccutor 和更易擴展的 ExecutorService 接口定義了相關的方法來執行任務。通過使用這些接口可以得到衆多各式各樣的實現。
最基本的 Executor 接口只接受 Runnable 的任務。
void execute(Runnable command)
ExecutorService 繼承了 Executor 並添加了方法支持 Runnable 和 Callable 的任務。
Future<?> submit(Runnable task)
• Future<T> submit(Callable<T> task)
• Future<T> submit(Runnable task, T result)
• List<Future<T> invokeAll(Collection<? extends
Callable<T>> tasks)
• List<Future<T> invokeAll(Collection<? extends
Callable<T>> tasks, long timeout, TimeUnit unit)
T invokeAny(Collection<? extends Callable<T>> tasks)
• T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
Callable and Future
一個 Callalbe 類似於 Runnable ,但它可以有返回值或者扔出異常。
• V call() throws Exception;
通用的執行框架提交一個 Callalbe 並受到一個 Future , 一個 Future 被標記爲返回結果值。 Future 有方法來輪詢或者阻塞直到結果已經返回。你可以在執行之前或者執行時取消一個任務。
如果你想讓 Runalbe 來支持 Future ,你可以使用 FutureTask 作爲橋樑。 FutureTask 實現了 Future 和 Runnable ,所以你可以提交一個 Runnalbe 的任務,並作爲 Future 來取得結果。
ExecutorService 的實現
對於 ExecutorService 接口最主要的實現是 ThreadPoolExecutor 。這個實現類實現了各種可配置的如下功能。
-
線程池 配置核心線程數量,預先啓動,最大線程數。
-
線程工程 生成特殊定製的線程 , 比如線程名等。
-
工作隊列 制定隊列的實現,這個隊列是阻塞的,但可以是有界性或者無界限的。
-
拒絕的任務 可以指定策略來拒絕任務,比如 Queue 已經沒有空餘,或者沒有有效的工作線程。
-
生命週期的鉤子 類似攔截器可以在 Task 的生命週期添加功能,比如在工作開始於完成之間插入現有的功能。
-
關閉 停止接受提交的任務,直到所有的任務被完成。
ScheduledThreadPoolExecutor 擴展了 ThreadPoolExecutor, 提供了對任務調度的功能而不是先進先出。在這點上 Java.util.Timer 不能足夠的,而 ScheduledThreadPoolExecutor 經常提供了足夠的彈性。
Executors 類提供了許多靜態方法來創建包裝好的 ExecutoService 和 ScheduledExccutorService 實例。
方法 |
描述 |
newSingleThreadExecutor |
返回一個線程的 ExecutorService |
newFixedTreadPoll |
固定數量的線程池 |
newCachedThreadPoll |
大小變化的線程池 |
newSingleThreadScheduledExecutor |
單線程的 ScheduledExecutorService |
newScheduledThreadPool |
一組線程的 ScheduledExecutorService |
下面是對固定大小線程池的使用,提交長時間運行的任務。
int processors = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.
newFixedThreadPool(processors);
Future<Integer> futureResult = executor.submit(
new Callable<Integer>() {
public Integer call() {
// long running computation that returns an integer
}
});
Integer result = futureResult.get(); // block for result
在上述列子中,調用者提交了向執行者提交了長時間運行的任務,並立即返回。在結果還沒有返回之間,調用 get() 方法會被阻塞。
ExecutorService 基本覆蓋了所有的情況。
CompletionService
除了通常我們將任務提交到線程池的 Queue 之外,我們還需要每一個任務生產結果,併爲日後處理。
CompletionService 接口允許使用者提交 Callalbe 和 Runnable 的任務,同時在結果隊列中取出或者輪詢結果
Future<V> take() – take if available
• Future<V> poll() – block until available
• Future<V> poll(long timeout, TimeUnit unit) – block
until timeout ends
ExecutorCompletionService 是 CompletionService 接口的標準實現。構造方式類似於 Executor
提供輸入隊列和工作線程池。
重要提示:對於線程池大小的設定,一般採用邏輯上處理器數量。在 Java 中,通過 Runtime.getRuntime().avaiableProcessors() 來獲取有效的處理器數量,這個數量可能在 JVM 運行時被修改。