Core Java Concurrency 多線程小手冊(基本涵蓋多線程所有要點)

關於 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() 方法和釋放鎖。

 

public   class  Counter  {

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 包提供了一個讀寫鎖的接口,這個接口定義了讀和寫的一對鎖,

一般允許並行的讀和排他的寫。下面的代碼展示了上述功能。

 

public   class  Statistic  {

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 關鍵字使其屬性對於後續的線程的可見性。

 

 

public   class  Processor  implements  Runnable  {

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 的不足。

 

public   class  Counter  {

private  AtomicInteger value  =   new  AtomicInteger();

public   int  next()  {

return  value.incrementAndGet();

}


}

 

ThreadLocal

     ThreadLocal 存貯了該 線程所需要的數據,不需要鎖的機制。一般而言, ThreadLocal 存放當前的事務和其他資源等。如下代碼, TransactionManager 中, ThreadLocal   類型的 currentTransaction 存貯了當前事務。


public   class  TransactionManager  {

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, 如果該線程被非安全終止時會收到通知。 如下代碼:

 

Thread t  =   new  Thread(runnable);

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 來檢測條件的方式。比如,一個線程可能等待一個需要處理的隊列,當需要處理的內容添加到隊裏中時,而另外一個線程會通知等待的線程。

 

規範的用法如下:

 

public   class  Latch  {

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 通過鎖的實例獲得。

 

public   class  LatchCondition  {

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 運行時被修改。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章