淺談java中的併發控制

併發指在宏觀上的同一時間內同時執行多個任務。爲了知足這一需求,現代的操做系統都抽象出 線程 的概念,供上層應用使用。java

這篇博文不打算詳細展開分析,而是對java併發中的概念和工具作一個梳理。
沿着併發模型、併發要解決的問題、基本工具、衍生工具這一思路展開。程序員

<!-- more -->算法

線程

首先線程是什麼?線程是由OS抽象並實現的,咱們知道OS的職責是管理併合理分配硬件資源,那麼OS爲了更好的管理、分配CPU資源,同時也爲了知足同時執行任務這一需求,設計了線程這一律念。編程

雖然java程序運行在JVM虛擬機上,可是java的線程仍然是對操做系統原生線程的封裝,同時,jvm對線程實現時也將jvm的運行棧設計成線程私有內存,所以,java線程和原生線程在理解上實際上沒太大區別。緩存

線程的五種狀態:安全

graph LR
新建 --> 就緒;
就緒 --> 運行;
運行 --> 就緒;
運行 --> 阻塞;
阻塞 --> 就緒;
運行 --> 死亡;

先來看上面的就緒狀態和運行狀態。咱們知道線程雖然宏觀上是同時執行的,可是微觀上使用如時間片輪轉算法使得線程依次執行。那麼,同一時間只有一個線程執行,其它須要執行的線程處於 就緒隊列 中,等待本身被調度到。數據結構

而若是線程想要暫時放棄在CPU上運行的權利,就會阻塞本身。這時對應着阻塞狀態,同時線程會從就緒隊列中移除,進入等待隊列。
很顯然,阻塞線程被喚醒確定是進入就緒隊列等待調度,而不多是直接分配到CPU上運行。多線程

在線程同步時,線程可能因爲如下狀況被阻塞:併發

  1. 同步阻塞。就是被鎖阻塞。
  2. 等待阻塞。被條件變量阻塞。
  3. 其它。調用sleep(), join()或等待IO操做時的阻塞。

併發須要解決的問題

功能性問題

線程同步面臨兩個問題,想象下有兩個線程在協做工做完成某項任務。那麼須要解決如下問題:jvm

  1. 線程兩個線程之間交互數據,必然涉及到數據共享。而某些數據資源沒法被多個線程同時使用(臨界區),這時須要,即線程互斥問題。
  2. 假如一個線程進行的太快,另一個線程就須要等等它,即線程同步問題。

性能和可用性問題

在多線程程序的性能問題上,若是是對於一樣一段臨界區的多線程訪問,那麼則有如下幾個思路:

  1. 互斥鎖。互斥鎖即保證同一時間只有一個線程訪問臨界區並完整執行完,其它線程在臨界區外面等待。
  2. 無障礙或無鎖。線程們一開始直接進入臨界區執行,注意其中不能修改共享數據。執行完後再判斷剛纔這段時間是否有其它線程執行,沒有的話才修改共享數據,若是有的話就回滾重來。
  3. 下降鎖粒度。也即將這個大的臨界區拆分紅幾個小的臨界區,分別加互斥鎖控制,這樣提升了線程同時訪問的臨界區的機會變多,性能提升。顯然這要對代碼仔細推敲,考慮如何拆分鎖粒度而不影響總體的語義。

以上三種思路的性能優劣沒有一個普適的結果,和具體的場景相關。

併發中還會出現如下幾種狀況致使系統不可用:

  1. 死鎖。不解釋。
  2. 飢餓。線程調度算法若是不是平等分配的,那麼就可能出現優先級高的線程長時間佔用CPU,致使優先級低的線程沒法獲得執行機會。
  3. 活鎖。這個我解釋不來。。。

併發代碼的幾個性質

併發編程中須要考慮的幾個概念:

  1. 原子性:指某個操做一旦被某個線程執行,直到該操做執行完畢都不會有其它線程來干擾。
  2. 可見性:指某個變量或某塊內存若是被A線程修改,B線程可否立刻讀取到修改後的值。
  3. 有序性:A線程執行的代碼序列,在B線程看來是不是有序的。

從我我的的理解來看,原子性屬於由併發和線程這一理論概念天然而然推導衍生而來的概念,而可見性和有序性是具體的工程實踐中產生的。
實際中,jvm並不能實現的特別完美,總會有工程上的妥協。理論模型與實際模型沒法完美契合,總存在必定的誤差。
好比說,jvm爲了向性能妥協使用了緩存機制,犧牲了數據一致性,這就產生了可見性的概念,須要程序員編程時本身控制。
jvm爲了指令更高效率的執行進行了指令重排優化,則產生了有序性的問題。印象裏之前大學裏學過的CPU的流水線技術,爲了指令可以更好的被CPU流水線利用,減小流水線的空閒時間,編譯器編譯時也會在不影響 串行語義 的前提下,進行指令重排。
總而言之,這是在性能和理論模型完整性之間的一種妥協。

併發的工具

技術上的工具、概念繁多複雜,可是若是咱們能理解技術設計上無時無刻的不運用抽象和分層的手段,
那麼,咱們能夠把技術上的工具分爲兩種:

  1. 最基本的、原生的工具。
  2. 在原生提供的工具上,進行封裝獲得的更高層次的工具。

更高層次的工具對基礎工具進行了抽象和封裝,屏蔽了其中的實現細節。
這裏想強調的是,工具的接口實現是分開的,二者能夠沒有關係。
如java的監視器鎖從接口上來看,其語義和互斥鎖同樣。然而它並不必定使用互斥鎖實現,而是能夠爲了性能存在優化,只要最終的行爲與接口相同便可。

基本工具

鎖、條件變量、信號量

有三種用於線程同步的工具:

  1. 鎖。鎖可用於規定一個 臨界區,同一時間臨界區內僅能由一個線程訪問。其餘線程則在臨界區外等待(阻塞)。

    • 互斥鎖。使用信號量實現。臨界區外等待的線程會被阻塞。
    • 自旋鎖。臨界區外等待的線程會忙等。
  2. 條件變量(Condition)。線程在某種條件不知足時阻塞本身,等待其它的線程條件知足時再喚醒它們。很顯然全部等待的線程要放入一個數據結構中,這個數據結構就在條件變量內。
  3. 信號量。操做系統原生的機制。實際上,鎖 + 條件變量可完成全部信號量能夠完成的邏輯。

在java中,Object類有wait()、notify()和notifyAll()之類的方法。
這些方法能夠認爲每一個對象都內置了一個條件變量,而這些方法是對這些條件變量的操做,所以,可使用這些方法將對象看成條件變量使用,從而作到線程的同步。

無狀態編程

底層機制直接對應獲得的

底層機制的特色直接獲得的:

1. java中的volatile關鍵字。
2. CAS。

volatile關鍵字可以保證變量的可見性,或者說是讀或寫的原子性。

CAS即compareAndSwap,原子操做
CAS操做直接可以對應到單條CPU指令,所以自然具備原子性。java中是經過JNI調用C語言從而調用CPU底層指令實現。

CAS的行爲和如下代碼一致:

int cas(long *addr, long old, long new)
{
    if (*addr == old) {
        *addr = new;
        return 1;
    } else {
        return 0; //*
    }
}

那麼CAS能夠作什麼呢?不少樂觀併發控制能夠基於CAS實現。
好比說,經過一個標記變量來記錄臨界區被誰佔有,線程進入臨界區前不斷的使用CAS操做判斷標記變量是否爲空同時將其記錄爲本身,來實現鎖機制。這就是自旋鎖的思路。

除此以外,樂觀鎖也能用CAS實現,好比java的Atomic系列,就是這樣實現的。

由基本工具封裝、優化而成的衍生工具

synchronized關鍵字

前面說到能夠認爲每一個對象內置一個條件變量,一樣,每一個對象也內置一個鎖。這個內置鎖在Java中被抽象爲監視器鎖(monitor)。
synchronized關鍵字的使用實際上就至關於使用監視器鎖定義了一個臨界區。使用這種語法也特別直觀簡單,因此java常常用synchronizd來進行線程的同步。

JDK1.6以後,爲了提高監視器鎖的性能,java經過某些手段進行了優化。其中包含鎖優化機制,對應三種鎖:

1. 偏向鎖
2. 輕量級鎖
3. 重量級鎖

一開始只有一個線程使用線程時使用偏向鎖,當存在多個線程使用時膨脹爲輕量級鎖,而出現比較多的線程競爭時再膨脹爲重量級鎖。

併發的數據結構

  1. 線程安全的容器,如VectorConcurrentHashMap等。
  2. 讀寫鎖,即java中的ReentrantReadWriteLock
    讀寫鎖又能夠看作一個讀鎖和一個寫鎖組成的鎖系統,讀鎖和寫鎖又叫共享鎖和排它鎖。
  3. BlockedQueue,阻塞隊列。
  4. Atomic。 java提供的atomic包封裝了一組經常使用原子類,使用無鎖方式實現。
  5. ThreadLocal。每一個線程都擁有一份對象拷貝,互相不干擾。

其它:

  1. 雙重檢查鎖

    1. 其實是一種對於線程安全的懶漢單例模式的一種優化。

鎖的屬性

爲了表達某種鎖的特色,也會有着不少的概念。
可是這種概念對應的不是某一種鎖,而是一類擁有特定屬性的鎖。如:

  1. 悲觀鎖和樂觀鎖。
    悲觀鎖假設發生衝突,並使用各類方式保證一次只有一個線程使用臨界區。
    樂觀鎖聽任線程去使用資源,在執行完後判斷剛纔是否有其它線程用過(破壞了數據完整性),若是是則撤回重試。
  2. 公平鎖和非公平鎖。
    公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的前後順序來一次得到鎖。java中的ReentrantLock是公平鎖。
  3. 遞歸鎖(可重入鎖)/非遞歸鎖(不可重入鎖)同一個線程能夠屢次獲取同一個遞歸鎖,不會產生死鎖。若是一個線程屢次獲取同一個非遞歸鎖,則會產生死鎖。
相關文章
相關標籤/搜索