Java對併發程式設計的支援

範大腳腳發表於2017-11-22

1.同步

如何同步多個執行緒對共享資源的訪問是多執行緒程式設計中最基本的問題之一。

當多個執行緒併發訪問共享資料時會出現資料處於計算中間狀態或者不一致的問題,從而影響到程式的正確執行。我們通常把這種情況叫做競爭條件(race condition),把併發訪問共享資料的程式碼叫做關鍵區域(critical section)。

同步就是使得多個執行緒順序進入關鍵區域從而避免競爭條件的發生。

2.執行緒安全性

編寫執行緒安全的程式碼的核心是要對狀態訪問操作進行管理,尤其是對共享的和可變的狀態訪問。 

執行緒安全性的定義:當多個執行緒訪問某個類時,這個類始終能表現出正確的行為,那麼就稱這個類是執行緒安全的。 

無狀態物件一定是執行緒安全的。

3.原子性和競爭條件

(1)具有原子性的操作被稱為原子操作

在Java中,對除了long和double之外的基本型別的簡單操作都具有原子性。簡單操作就是賦值或者return。比如”a = 1;“和 “return a;”這樣的操作都具有原子性。

在某些JVM中”a += b”可能要經過這樣三個步驟:

1) 讀取:取出a和b 

2) 修改:計算a+b 

3) 寫入:將計算結果寫入記憶體

非原子操作都會存線上程安全問題,需要我們使用同步技術(sychronized)來讓它變成一個原子操作。concurrent包下提供了一些原子類,比如:AtomicInteger、AtomicLong、AtomicReference等。

(2)競爭條件,指的是在併發程式設計中,由於不恰當的執行時序而出現不正確的結果的情況

當某個計算結果的正確性取決於多個執行緒的交替執行時序時就會發生競態條件。 

常見的兩種競爭條件是:“先檢查後執行”和 “讀取-修改-寫入”。

(3)內建鎖和可重入鎖

Java提供了一種內建鎖機制來支援原子性:同步程式碼塊。同步程式碼塊包括兩個部分:一個是作為鎖的物件引用,一個是作為由這個鎖保護的程式碼塊。每個Java物件都可以用作一個實現同步的鎖,這些鎖被稱為內建鎖。

當某個執行緒請求一個由其他執行緒持有的鎖時,發出請求的執行緒就會被阻塞,然而內建鎖是可以重入的。因此如果某個執行緒試圖獲得一個已經有它自己持有的鎖,那麼這個請求就會成功。 

注意,“鎖”的持有者是例項物件,而不是類!

4.執行緒和集合類

(1)執行緒安全的集合類

java.util.Vector

java.util.Stack

java.util.HashTable

java.util.concurrent.ConcurrentHashMap

java.util.concurrent.CopyOnWriteArrayList

java.util.concurrent.CopyOnWriteArraySet

java.util.concurrent.ConcurrentLinkedQueue

(2)非執行緒安全集合類

java.util.BitSet

java.util.HashSet (LinkedHashSet)

java.util.TreeSet

java.util.HashMap (WeekHashMap, TreeMap, LinkedHashMap, IdentityHashMap)

java.util.ArrayList (LinkedList)

java.util.PriorityQueue

這些非執行緒安全的集合可以通過java.util.Collections.SynchronizedList、SynchronizedMap、SynchronizedSet等方法包裝成執行緒安全的集合。包裝器類簡單地給被包裝集合的各項操作加上了synchronized保護。值得注意的是在使用遊標遍歷這些包裝器集合的時候必須加上額外的synchronized保護,否則會出現問題。

1
2
3
4
5
6
7
List list = Collections.synchronizedList(new ArrayList()); 
    ... 
synchronized(list) { 
    Iterator i = list.iterator(); // Must be in synchronized block 
    while (i.hasNext()) 
        foo(i.next()); 

  

(3)執行緒通知集合類

java.util.concurrent.ArrayBlockingQueue

java.util.concurrent.LinkedBlockingQueue

java.util.concurrent.SynchronousQueue

java.util.concurrent.PriorityBlockingQueue

java.util.concurrent.DelayQueue

這些集合類都實現了BlockingQueue介面。阻塞佇列的特點是當從佇列中取出元素時如果佇列為空,執行緒會被阻塞直到佇列中有元素被插入。當從佇列中插入元素時如果佇列已滿,執行緒會被阻塞直到佇列中有元素被取出出現空閒空間。阻塞佇列可以用來實現生產者消費者模式(Producer/Consumer Pattern) 。

5.執行緒池

頻繁地建立和銷燬執行緒會降低程式的效能。

應用程式可以建立執行緒的數量是受機器物理條件制約的,過多的執行緒會耗盡機器的資源,在設計程式的時候需要限制併發執行緒的數量。

執行緒池在啟動的時候一次性初始化若干個執行緒(也可以根據負載按需啟動,也有閒置一定時間的執行緒會被銷燬的策略),然後程式把任務交給執行緒池去執行而不是直接交給某個執行緒執行,由執行緒池給這些任務分配執行緒。

當某個執行緒執行完一個任務後,執行緒池會把它設成空閒狀態以備下一個任務重用而不是銷燬它。

執行緒池在初始化的時候需要指定執行緒數量上限,當併發任務數量超過執行緒數量的時候,

執行緒池不會再建立新的執行緒而是讓新任務等待,這樣我們就不在需要擔心執行緒數量過多耗盡系統資源了。JDK1.5開始為我們提供了標準的執行緒池。

(1)Executor介面

Java的執行緒池實現了以下Executor介面:

1
2
3
4
Java的執行緒池實現了以下Executor介面:        
public interface Executor { 
    void execute(Runnable command); 
}

在多執行緒程式設計中,執行器是一種常用的設計模式,它的好處在於提供了一種簡單有效的程式設計模型,我們只需把需要併發處理的工作拆分成獨立的任務,然後交給執行器去執行即可而不必關心執行緒的建立,分配和排程。

JDK主要提供了兩種功能的執行器:ThreadPoolExecutor和ScheduledThreadPoolExecutor。ThreadPoolExecutor是基本的執行緒池實現,ScheduledThreadPoolExecutor在前者基礎上增加了任務排程的功能,在把任務交給它時我們可以指定任務的執行時間,而不是立刻執行。

(2)Executors建立執行緒池

java.util.concurrent.Executors是用來建立執行緒池的工廠類,

通過它提供的工廠方法,我們可以方便地建立不同特性的執行緒池,包括快取執行緒池、各種優先順序執行緒池等。

(3)Future介面

Executor介面並沒有看起來那麼理想,有時候我們執行一個任務是要得到計算的結果,有時候我們需要對任務有更多控制,例如知道它是否完成,或者中途終止它。返回void的execute方法並不能滿足我們這些需求。當然我們可以在傳入的Runnable類上下功夫來提供類似的功能,但是這樣做繁瑣且容易出錯。實際上執行緒池實現了一個更為豐富的ExecutorService介面,它定義了執行任務並返回代表該任務的Future物件的submit方法。

通過Future介面,我們可以檢視已經被提交給執行緒池執行的任務是否完成,獲取執行的結果或者終止任務。

(4) Runnable 和Callable 介面

實現了Runnable或Callable介面的類都可以作為任務提交給執行緒池執行,這兩個介面的主要區別在於Callable的call方法有結果返回並且可以丟擲異常而Runnable的run方法返回void且不允許有可檢查的異常丟擲(只能拋runtime exception)。因此如果我們的任務執行後有結果返回,應該使用Callable介面。


6.顯式鎖

協調共享物件訪問的機制:JDK5之前是synchronized和volatile(),JDK5增加了ReentrantLock,現在可以用Lock顯式的lock()和unlock(),並且有定時鎖,讀寫鎖等。

(1)ReentrantLock

ReentrantLock實現了Lock介面,並提供了與synchronized相同的互斥性和記憶體可見性。

1
2
3
4
5
6
7
8
9
10
public interface Lock {
void lock();
//如果當前執行緒未被中斷,則獲取鎖定。
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long timeout, TimeUnit unit)
    throws InterruptedException;
    void unlock();
    Condition newCondition();
}

  



使用ReentrantLock來保護物件狀態。

1
2
3
4
5
6
7
Lock lock = new ReentrantLock();
lock.lock();
try {
    //相關操作
finally {
    lock.unlock();  //一定要釋放鎖
}

(2)輪詢鎖和定時鎖

可定時的與可輪詢的鎖獲取模式是由tryLock方法實現的,與無條件的鎖獲取模式相比,它具有更完善的錯誤恢復機制。

(3)可中斷的鎖

(4)ReadWriteLock 讀-寫鎖

ReadWriteLock 維護了一對相關的鎖定,一個用於只讀操作,另一個用於寫入操作。

//ReadWriteLock 介面

1
2
3
4
5
//ReadWriteLock 介面
public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

  

7.計數器CountDownLatch和柵欄CyclicBarrier

(1)CountDownLatch,計數器或者閉鎖

CountDownLatch是一個同步輔助類,java.util.concurrent.CountDownLatch,在完成一組正在其他執行緒中執行的操作之前,它允許一個或多個執行緒一直等待。用給定的計數 初始化 CountDownLatch。由於呼叫了 countDown() 方法,所以在當前計數到達零之前,await 方法會一直受阻塞。之後,會釋放所有等待的執行緒,await 的所有後續呼叫都將立即返回。這種現象只出現一次——計數無法被重置。

CountDownLatch即一個執行緒(或者多個), 等待另外N個執行緒完成某個事情之後才能執行

(2)CyclicBarrier,柵欄

通過閉鎖(CountDownLatch)來同時啟動一組相關執行緒,或等待一組相關執行緒的結束。可是閉鎖是一次性物件,一旦進入終止狀態,就不能被重置。柵欄類似於閉鎖,它能夠阻塞一組執行緒直到某個事件發生。

CyclicBarrier在並行迭代演算法中是非常有用。


8.訊號量Semaphore

計數訊號量(Counting Semaphore)用來控制同時訪問某個特定資源的運算元量,或者同時執行某個指定操作的數量。

有時候我們有多個相同的共享資源可以同時被多個執行緒使用。我們希望在鎖的基礎上加上一個計數器,根據資源的個數來初始化這個計數器,每次成功的lock操作都會使計數器的值減去1,只要計數器的值不為零就表示還有資源可以使用,lock操作就能成功。每次unlock操作都會給這個計數器加1。只有當計數器的值為0的時候lock操作才會阻塞當前執行緒。這就是Java中的訊號量Semaphore。

Semaphore類提供的方法和Lock介面非常類似,當把訊號量的資源個數設定成1時,訊號量就退化為普通的鎖。

9.ThreadLocal 執行緒私有變數

(1)是變數不是執行緒

如果每個執行緒都有自己私有的成員變數,那麼我們也不需要同步。ThreadLocal就是執行緒的私有變數,每個使用ThreadLocal變數的執行緒都會有自己獨立的ThreadLocal物件,因此就不存在多個執行緒訪問同一個變數的問題。

它並不是一個Thread,而是threadlocalvariable(執行緒區域性變數)。

(2)ThreadLocal 的實現原理

每個Thread物件有自己用來儲存私有ThreadLocal物件的容器ThreadLocalMap,當某個執行緒呼叫ThreadLocal物件的get()方法來 取值的時候,

get方法首先會取得當前執行緒物件,然後取出該執行緒的ThreadLocalMap,然後檢查自己是否已經在map中,如果自己已經存在,直接返回map中的value。

如果不存在,把自己作key並初始化一個value加入到當前執行緒的map中。

1
2
3
4
5
6
7
8
9
10
public T get() { 
        Thread t = Thread.currentThread(); 
        ThreadLocalMap map = getMap(t); 
        if (map != null) { 
            ThreadLocalMap.Entry e = map.getEntry(this); 
            if (e != null
                return (T)e.value; 
        
        return setInitialValue(); 

  

本文轉自邴越部落格園部落格,原文連結:http://www.cnblogs.com/binyue/p/5073952.html,如需轉載請自行聯絡原作者


相關文章