JAVA中關於併發的一些理解

一,JAVA線程是如何實現的?html

同步,涉及到多線程操做,那在JAVA中線程是如何實現的呢?java

操做系統中講到,線程的實現(線程模型)主要有三種方式:node

①使用內核線程實現緩存

②使用用戶線程實現安全

③使用用戶線程加輕量級線程實現多線程

 

二,JAVA語言定義了哪幾種線程狀態?併發

JAVA語言定義了五種線程狀態:①新建(New),當你 new 了一個Thread,可是並無調用它的 start()方法時,就處於這種狀態。app

②運行(Run),這裏包含了兩種狀態:一種是可運行狀態,就是你調用了Thread的start()方法以後,可是該線程還未得到CPU(至關於操做系統中講的就緒狀態);另外一種是運行狀態,就是該線程被調度器分配了CPU,正在執行。函數

③等待(Waiting),處於這種狀態的線程不會被分配CPU執行時間,它須要等待其餘線程喚醒。等待又分紅兩種:無限等待和超時等待(限期等待)。高併發

無限等待通常是執行等待的方法沒有指定超時參數

好比Object類的wait()方法,會使線程進入無限等待狀態,它還有一個帶有 timeout(超時) 參數 的重載方法 Object.wait(long timeout),使線程超時等待。

調用Thread.sleep() 、 Object.wait()、Thread.join()方法都會使線程進入等待狀態。

④阻塞(Blocked),我的感受阻塞是與鎖有關,而等待並不必定與鎖有關。

好比,兩個線程爭奪對象鎖(synchronized),未得到鎖的那個線程將進入阻塞狀態。而線程進入等待狀態則有多是由於③中提到的調用了Thread.sleep()方法,或者是線程讀某個Socket端口上的數據,可是此時數據還未到達,則線程進入等待狀態。

 ⑤結束(Terminated),線程的run方法運行完畢,進入結束狀態。

 

三,同步(加鎖)爲何會有代價?

《深刻理解JVM》中講到,在目前的JDK版本中,操做系統支持怎樣的線程模型,在很大程序上決定了JAVA虛擬機的線程是怎樣映射的。JAVA的線程是映射到操做系統的原生線程之上的,若是要阻塞或喚醒一個線程,都須要操做系統幫忙完成,這就須要從用戶態切換到核心態中,所以狀態轉換須要耗費不少的處理器時間

而在JAVA裏面要實現同步,一種是使用對象鎖,即synchronized關鍵字;另外一種則是使用 java.util.ReentrantLock。得到鎖的線程進入臨界區執行、未得到鎖的線程會阻塞,而後在某種條件下被喚醒。所以,同步(使用鎖)是有代價的。

 

四,對象鎖同步(synchronized同步) 與 ReentrantLock同步的區別

它們都是可重入鎖,可用來互斥同步,但主要有三個方面的區別

①使用synchronized進行同步的線程在阻塞等待時,是不可中斷的。而使用ReentrantLock進行同步的線程在阻塞等待時可中斷。

若是臨界區須要執行很長的時間,synchronized就只能一直阻塞等待了,而ReentrantLock 能夠在阻塞等待一段時間以後,若還未得到鎖,就能夠被其餘線程中斷,從而去幹其餘事情。一個很好的示例可參考:ReentrantLock鎖實現中斷線程阻塞

經過調用 lock.lockInterruptibly()方法來實現線程在阻塞等待時 可被其餘線程 中斷。

 

②ReentrantLock能夠實現公平鎖

 公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次得到鎖。而非公平鎖則按照搶佔的方式來得到鎖,synchronized中的鎖是不公平的,而ReentrantLock能夠在構造函數中指定建立的鎖是否爲公平鎖。

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

 

③使用ReentrantLock 鎖能夠同時綁定多個條件變量(Condition對象)

所謂 條件對象 就是說:線程好不容易得到了鎖 進入臨界區,卻發現須要在知足某一條件以後,它才能執行。所以,使用一個條件對象來管理那些已經得到了鎖可是卻不能作有用工做的線程。

好比說:生產者--消費者模型,消費者得到了隊列的鎖,去隊列中取產品,可是結果發現隊列爲空,它沒有產品可消費。換句話說:它須要在隊列不爲空的條件下,才能消費產品。(沒有產品,確定沒法消費產品啊!)

好比,消費者的代碼通常是下面這樣:進入consume()方法須要得到鎖,可是卻發現隊列爲空,故只能調用wait()方法 進入等待狀態。(不是阻塞狀態)

複製代碼

public synchronized void consume(){
        while(queue.isEmpty())
            wait();//沒有產品可消費,只能放棄鎖,並等待生產者線程生產了產品以後,喚醒它
        //consume product
        notifyAll();
        //....
    }

複製代碼

 

那ReentrantLock呢?

複製代碼

Condition emptyCondition = lock.newCondition();
    Condition fullCondition = lock.newCondition();
    .......
    public synchronized void consume(){
        try{
            lock.lock();//ReentrantLock lock
            while(queue.isEmpty())
                emptyCondition.await();
            //consume product
            emptyCondition.singalAll();
            //....
        }finally{
            lock.unlock();
        }
    }

複製代碼

對於同一把ReentrantLock,它能夠 new多個Condition,即同一把鎖能夠關聯多個條件變量。

Condition.awit()方法的JDK源碼解釋很是值得一讀。部分摘錄以下:

Causes the current thread to wait until it is signalled or
     * {@linkplain Thread#interrupt interrupted}.

* <p>The lock associated with this {@code Condition} is atomically
     * released and the current thread becomes disabled for thread scheduling
     * purposes and lies dormant until <em>one</em> of four things happens

調用Condition.await()方法使線程放棄鎖,並進入等待狀態(不是阻塞狀態),直到有下列四種狀況發生 才從等待狀態退出.....

 .....

 

五,互斥同步 與 非阻塞同步 是什麼?

所謂互斥同步,就是多個線程爭奪一把鎖時,未得到鎖的那些線程將會阻塞。所以,互斥同步最主要的是進行線程阻塞和喚醒帶來的性能問題,所以 互斥同步稱爲阻塞同步。從處理問題的方式上看,它是一種悲觀的併發策略:它認爲若是不採起正確的同步措施,那執行就可能出問題。也就是說:它老是對共享數據先進行加鎖,而後再去訪問,儘管在訪問過程當中 也許 並無 其餘線程 訪問該共享數據。

而正如前面(三)中 提到,加鎖是有代價的,若是對共享數據加了鎖,可是在訪問共享數據過程當中,並無其餘線程來訪問該共享數據(即並無出現競爭),那此次加鎖就感受有點浪費了。(就至關於:花了大量的人力、物力應對某次可能發生的地震,可是最終地震沒有發生)

因而,爲了進一步的」優化「,就出現了基於衝突檢測的樂觀併發策略

樂觀併發策略就是:先進行操做,若是沒有其餘線程爭用共享數據,那麼操做就成功了。若是數據有爭用,產生了衝突,那再採用補救措施。

所以,樂觀併發策略的不少實現都不須要把線程掛起(由於它是 先執行了 操做再說),於是稱爲:非阻塞同步。

樂觀併發策略須要硬件指令集的支持。由於,它在操做的過程當中須要檢測是否發生了衝突,須要「操做」和「衝突檢測」這兩個步驟具有原子性。原子性如何保證?若是用互斥同步保證 那就又變回 悲觀併發策略 了,所以只能靠硬件來保證原子性。

好比java.util.concurrent.atomic.AtomicInteger.java 中的自增長1方法getAndIncrement(),就是經過硬件原子指令:CAS指令(Compare and swap)來實現

複製代碼

public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

複製代碼

 

六,關於鎖優化的一些知識

1)自旋鎖 的 實現思想

在多核CPU下,一個線程得到鎖進入臨界區佔用CPU執行時,咱們有理由相信這個臨界區代碼很快就會執行完成,即: 共享數據的鎖定狀態只會持續很短的一段時間,而當另一個線程恰好在這段時間去搶佔鎖,掛起這個搶佔鎖的線程有點 不值得。(畢竟佔用鎖的線程很快就會把鎖釋放了呀)

因而,就讓請求搶佔鎖的那個線程「稍微等待一下」,但不放棄處理器的執行時間(一旦放棄,就意味着須要掛起和恢復線程了),看持有鎖的線程是否很快就會釋放鎖。爲了讓線程等待一下,咱們只需讓線程執行一個忙循環(自旋),這就是:自旋鎖。

 

2)自旋鎖的「改進」---自適應自旋

 自旋等待避免了線程切換的開銷,可是它是要佔用處理器時間的。所以,若是鎖被佔用的時間很短,那自旋等待的效果就會很是好;如何鎖被佔用的時間很長,執行忙循環(自旋)的線程就白白消耗處理器資源,形成性能上的浪費。默認狀況下,自旋的次數是10次,但能夠經過JVM參數進行修改。

爲了應對鎖被佔用很長時間 而致使的長時間無效的自旋,自旋的時間必須有必定的限度。

那如何肯定一個合適的限定呢?這就是自適應自旋的目標了。

自適應自旋對自旋的次數沒有固定,好比說:在同一個鎖對象,自旋等待剛剛成功得到過鎖,那麼此次也頗有可能成功,進而容許自旋等待持續相對更長的時間。

再好比說,對於某個鎖,自旋不多成功得到過鎖,那在之後獲取這個鎖時可省略自旋過程,以免處理器資源浪費。

 

3)輕量級鎖和偏向鎖

 我的感受輕量級鎖和偏向鎖 的功能 與 緩存 的思想有點像。直接同步互斥加鎖的代價是很大的(重量級鎖),那咱們能夠先來一個輕量級鎖或偏向鎖。

若是在加了 輕量級鎖或偏向鎖的過程當中 沒有發生其餘線程來爭搶鎖(相似於緩存命中!)這意味着整個過程「幾乎」不須要同步。

若是有其餘線程爭搶鎖,那輕量級鎖將再也不有效(偏向鎖的偏向模式失效),輕量級鎖要膨脹爲「重量級鎖」,後面等待鎖的線程要進入阻塞狀態。這就是相似於緩存未命中!

關於輕量級鎖和偏向鎖的具體解釋:可參考《深刻理解JVM》

 

上面關於鎖的優化是JVM的一些鎖優化策略,在應用層進行鎖優化方式有以下:

①儘可能減小鎖的持有時間。只對必要的須要同步的代碼進行同步。

synchronized{this}
{
    methodA();// 把不須要同步的方法放到 sync 外面去
    mutex();
  //other method...// 把不須要同步的方法放到 sync 外面去
}

 

②減小鎖的粒度

ConcurrentHashMap就很好的應用了這種思想。它將整個HashMap分紅了若干個段(Segment),每一個段都有本身的鎖,每一個段負責管理HashMap中的一部分HashEntry,段與段之間的HashEntry互不干擾,多線程能夠並行地操做不一樣的Segment管理下的HashEntry。

這樣鎖的粒度就減小了。若是整個HashMap只有一把鎖管理,鎖的粒度就很大。操做HashMap不一樣的區域都須要互斥同步。而ConcurrentHashMap將HashMap分解成段,每一個段有一把鎖,鎖的粒度就少了。可是與此同時,鎖的數量增多了。當須要訪問ConcurrentHashMap的全局屬性時(好比ConcurrentHashMap的size()方法),須要 得到 全部的段的鎖。

1         try {
2             for (;;) {
3                 if (retries++ == RETRIES_BEFORE_LOCK) {
4                     for (int j = 0; j < segments.length; ++j)
5                         ensureSegment(j).lock(); // force creation

以上是size()方法的部分代碼,size()的具體實現確定也有相應的優化。

 

③鎖分離

鎖分離與鎖分段有點類似,鎖分離就是對不一樣的操做使用不一樣的鎖。好比,java.util.concurrent.LinkedBlockingQueue 是一個線程安全的阻塞隊列,take()方法從隊列中取元素,put()方法向隊列中添加元素。

這個隊列是用鏈式存儲結構實現,它的結點類以下:

複製代碼

1     /**
 2      * Linked list node class
 3      */
 4     static class Node<E> {
 5         E item;
 6 
 7         /**
 8          * One of:
 9          * - the real successor Node
10          * - this Node, meaning the successor is head.next
11          * - null, meaning there is no successor (this is the last node)
12          */
13         Node<E> next;
14 
15         Node(E x) { item = x; }
16     }

複製代碼

它的 put 操做 和 take 操做分別做用於隊列的尾部和頭部,並無相互衝突。若是 take 和 put 都共享同一把鎖,那麼從隊列中取走元素的同時,就不能向隊列中添加元素,儘管它們互不「干擾」。

所以,爲了提升併發效率,就使用了兩把鎖:一把 put 鎖,一把 take 鎖。這就是鎖分離機制。

複製代碼

1     /** Lock held by take, poll, etc */
 2     private final ReentrantLock takeLock = new ReentrantLock();
 3 
 4     /** Wait queue for waiting takes */
 5     private final Condition notEmpty = takeLock.newCondition();
 6 
 7     /** Lock held by put, offer, etc */
 8     private final ReentrantLock putLock = new ReentrantLock();
 9 
10     /** Wait queue for waiting puts */
11     private final Condition notFull = putLock.newCondition();

複製代碼

 

take鎖對應着 notEmtpy條件變量,putLock對應着一個 notFull條件變量。

在大部分狀況下,多線程能夠同時並行地向阻塞隊列中添加元素和取出元素。好比線程A向隊列添加元素的時候,並不阻塞線程B從隊列中取出元素。

經過使用 take 鎖 和 put 鎖,將LinkedBlockingQueue的讀寫分離。優化了併發效率。

 

④鎖粗化

這個與①中的儘可能減小 鎖的時間 思想相反。但它們適用的狀況是不一樣的。

好比,當程序須要在 for 循環內部加鎖時,每執行一次 for 循環中的操做就須要加鎖一次,加鎖以後執行臨界區代碼後又釋放鎖,這樣加鎖、釋放鎖的頻率很是大,效率反而低了。

複製代碼

1     public void syncMethod(){
 2         for(int i = 0; i < COUTN; i++)
 3         {
 4             synchronized(this){
 5                 mutex();
 6             }
 7             //do something, do not need lock
 8             synchronized(this){
 9                 mutex();
10             }
11             //do another thing which needs no lock
12         }
13     }

複製代碼

 

這樣,直接用synchronized修飾方法反而要更好一點。

 

七,參考資料

《深刻理解JVM》周志明

http://thrillerzw.iteye.com/blog/2055486

 

原文:http://www.cnblogs.com/hapjin/p/5765573.html

相關文章
相關標籤/搜索