執行緒關鍵字、鎖、同步集合筆記

李納斯小盒發表於2018-05-21

Android開發筆記 onGithub

[TOC]

1.原子性、可見性、有序性

1.1 原子性

指一個操作是不可中斷的,即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。

例如 int a = 10這是一個原子操作,但是a++; a = a + 1就不是原子操作。

能夠保證原子性的有:Java記憶體模型的8種操作(參考《深入理解Java虛擬機器》或Java八種記憶體操作)、synchronized

1.2 可見性

可見性是指當一個執行緒修改了共享變數後,其他執行緒能夠立即得知這個修改。

能夠保證可見性的有:volatile、synchronized、final

1.3 有序性

即程式執行的順序按照程式碼的先後順序執行。

能夠保證有序性的有:volatile、synchronized、


2.volatile

輕量級

保證可見性、有序性,但不保證原子性

如果volatile保證原子性,必須滿足下面兩個條件

  • 運算結果並不依賴於變數的當前值,或者能夠確保只有一個執行緒修改變數的值;
  • 變數不需要與其他的狀態變數共同參與不變約束

volatile關鍵字禁止指令重排序有兩層意思:

1)當程式執行到volatile變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;

2)在進行指令優化時,不能將在對volatile變數訪問的語句放在其後面執行,也不能把volatile變數後面的語句放到其前面執行。

//x、y為非volatile變數
//flag為volatile變數
 
x = 2;        //語句1
y = 0;        //語句2
flag = true;  //語句3
x = 4;        //語句4
y = -1;       //語句5
複製程式碼

由於flag變數為volatile變數,那麼在進行指令重排序的過程的時候,不會將語句3放到語句1、語句2前面,也不會講語句3放到語句4、語句5後面。但是要注意語句1和語句2的順序、語句4和語句5的順序是不作任何保證的;並且volatile關鍵字能保證,執行到語句3時,語句1和語句2必定是執行完畢了的,且語句1和語句2的執行結果對語句3、語句4、語句5是可見的。

實現原理

4.volatile的原理和實現機制

前面講述了源於volatile關鍵字的一些使用,下面我們來探討一下volatile到底如何保證可見性和禁止指令重排序的。

下面這段話摘自《深入理解Java虛擬機器》:

“觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編程式碼發現,加入volatile關鍵字時,會多出一個lock字首指令”

lock字首指令實際上相當於一個記憶體屏障(也成記憶體柵欄),記憶體屏障會提供3個功能:

1)它確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;

2)它會強制將對快取的修改操作立即寫入主存;

3)如果是寫操作,它會導致其他CPU中對應的快取行無效。

可參考

3.鎖

3.1 synchronized

重量級

執行緒關鍵字、鎖、同步集合筆記

如圖,synchronized可以用在方法上也可以使用在程式碼塊中,其中方法是例項方法和靜態方法分別鎖的是該類的例項物件和該類的物件。而使用在程式碼塊中也可以分為三種,具體的可以看上面的表格。這裡的需要注意的是:如果鎖的是類物件的話,儘管new多個例項物件,但他們仍然是屬於同一個類依然會被鎖住,即執行緒之間保證同步關係。

實現原理

monitorenter、monitorexit指令

參考

3.2 顯示鎖Lock與ReentrantLock

Lock是一個介面提供了無條件的、可輪詢的、定時的、可中斷的鎖獲取操作,所有加鎖和解鎖的方法都是顯式的。包路徑是:java.util.concurrent.locks.Lock。核心方法是lock(),unlock(),tryLock(),實現類有ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock。

Condition包括await、signal、signalAll方法。

ReenTrantLock是可重入鎖。

Lock與synchronized 的比較:

  • 1,Lock使用起來比較靈活,但是必須有釋放鎖的動作;
  • 2,Lock必須手動釋放和開啟鎖,synchronized 不需要;
  • 3,Lock只適用與程式碼塊鎖,而synchronized 物件之間的互斥關係;

使用 ReentrantLock 的吞吐量通常要比 synchronized 好。換句話說,當許多執行緒試圖訪問 ReentrantLock 保護的共享資源時,JVM 將花費較少的時間來排程執行緒,而用更多時間執行執行緒。雖然 ReentrantLock 類有許多優點,但是與同步相比,它有一個主要缺點 — 它可能忘記釋放鎖定。ReentrantLock實在工作中對方法塊加鎖使用頻率最高的。

舉個例子

public class ReadAndWriteLock {  
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();  
    public void get(Thread thread) {  
        lock.readLock().lock();  
        try{  
            System.out.println("start time:"+System.currentTimeMillis());  
            for(int i=0; i<5; i++){  
                try {  
                    Thread.sleep(20);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                System.out.println(thread.getName() + ":正在進行讀操作……");  
            }  
            System.out.println(thread.getName() + ":讀操作完畢!");  
            System.out.println("end time:"+System.currentTimeMillis());  
        }finally{  
            lock.readLock().unlock();  
        }  
    }  
      
    public static void main(String[] args) {  
        final ReadAndWriteLock lock = new ReadAndWriteLock();  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                lock.get(Thread.currentThread());  
            }  
        }).start();  
          
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                lock.get(Thread.currentThread());  
            }  
        }).start();  
    }  
}  
複製程式碼

3.3 訊號量Semaphore

計數訊號量,本質是一個“共享鎖”。通過acquire獲取訊號量的許可,通過release釋放。

public class SemaphoreTest {
    static int time = 0;
    public static void main(String[] args) {
    final ExecutorService executorService = Executors.newFixedThreadPool(3);
    final Semaphore semaphore = new Semaphore(3);
    for (int i = 0; i < 5; i++) {
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                  try {
                      semaphore.acquire();
                      System.out.println(" 剩餘許可 : " + semaphore.availablePermits());
                      Thread.sleep(2000);
                      semaphore.release();
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
            }
          });
      }
    }
}
複製程式碼

輸出結果為:

 剩餘許可 : 2
 剩餘許可 : 1
 剩餘許可 : 0
 剩餘許可 : 2
 剩餘許可 : 1
複製程式碼

3.4 迴圈柵欄 CyclicBarrier

CyclicBarrier是一個同步輔助類,允許一組執行緒相互等待,直到某個公共屏障點,因為該barrier在釋放等待執行緒後可以重用,所以稱它為迴圈的barrier。

也就是若干執行緒等待到達同一個條件後再去執行。

執行緒關鍵字、鎖、同步集合筆記

結果:

Thread-0 等待 CyclicBarrier.
Thread-2 等待 CyclicBarrier.
Thread-1 等待 CyclicBarrier.
Thread-3 等待 CyclicBarrier.
Thread-4 等待 CyclicBarrier.
 ---> 滿足條件,執行特定操作。 參與者: 5
Thread-4 繼續執行.
Thread-1 繼續執行.
Thread-3 繼續執行.
Thread-0 繼續執行.
Thread-2 繼續執行.
複製程式碼

應用場景

CyclicBarrier可以用於多執行緒計算資料,最後合併計算結果的應用場景。

CyclicBarrier和CountDownLatch的區別

  • CountDownLatch的計數器只能使用一次。而CyclicBarrier的計數器可以使用reset() 方法重置。所以CyclicBarrier能處理更為複雜的業務場景,比如如果計算髮生錯誤,可以重置計數器,並讓執行緒們重新執行一次。
  • CyclicBarrier還提供其他有用的方法,比如getNumberWaiting方法可以獲得CyclicBarrier阻塞的執行緒數量。isBroken方法用來知道阻塞的執行緒是否被中斷。比如以下程式碼執行完之後會返回true。

參考:

3.5 閉鎖 CountDownLatch

CountDownLatch也是一個同步輔助類,在完成一組正在其他執行緒中執行的操作之前,它允許一個或多個執行緒一直等待,直到條件被滿足。

執行緒關鍵字、鎖、同步集合筆記
結果:

主執行緒等待.
Thread-0 執行操作.
Thread-3 執行操作.
Thread-4 執行操作.
Thread-1 執行操作.
Thread-2 執行操作.
主執行緒繼續執行
複製程式碼

易混淆的關鍵字 transient

transient是Java語言的關鍵字,用來表示一個域不是該物件序列化的一部分。當一個物件被序列化的時候,transient型變數的值不包括在序列化的表示中,然而非transient型的變數是被包括進去的。

靜態成員變數屬於類不屬於物件,所以不會參與序列化和反序列化過程; transient關鍵字標記的成員變數不參與序列化過程

5.相關同步類

5.1 AtomicInteger等

能夠保證原子性,內部使用了volatile關鍵字

參考

public class AtomicOperationDemo {  
       static AtomicInteger count=new AtomicInteger(0);  
       public static class AddThread implements Runnable{  
        @Override  
        public void run() {  
            for(int k=0;k<1000;k++){  
                count.incrementAndGet();  
            }  
         }   
       }  
       public static void AtomicIntShow(){  
         System.out.println("AtomicIntShow() enter");  
         ExecutorService threadpool =   Executors.newFixedThreadPool(10);  
           
         for(int k=0;k<100;k++){  
             threadpool.submit(new AddThread());  
         }  
           
         try {  
            Thread.sleep(2000);  
        } catch (InterruptedException e) {  
            // TODO Auto-generated catch block  
            e.printStackTrace();  
        }  
           
         /* output 
          * AtomicIntShow() enter 
          * result of acumulated sum=100000 
          * AtomicIntShow() exit 
          */  
           
         System.out.println("result of acumulated sum="+count);  
         threadpool.shutdown();  
         System.out.println("AtomicIntShow() exit");  
         return ;  
          
    }  
} 
複製程式碼

5.2 CopyOnWriteArrayList

適合讀多寫少的併發

Copy-On-Write是一種基於程式設計中的優化策略,基本思路是,從多個執行緒共享同一個列表,當某個執行緒想要修改這個列表的元素時,會把列表中的元素Copy一份,然後進行修改,修改後將新元素設定給這個列表,這是一種延時懶惰策略。

/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
複製程式碼

好處是可以併發的讀,而不需要加鎖。

執行緒關鍵字、鎖、同步集合筆記

可以看到CopyOnWriteArrayList的add方法是同步方法,而讀的時候並沒有加鎖。

public E get(int index) {    
      return (E) elements[index];
}
複製程式碼

通過寫時拷貝的原理可以將讀、寫分離,使併發場景下對列表的操作效率得到提高,但在新增、移除元素時佔用的記憶體空間翻了一倍。

CopyOnWriteArrayList的add方法在Android API25 中 和J.U.C中實現的不一樣,但是總體思路是一樣的,就是COW(Copy-On-Write)

5.3 ConcurrentHashMap

ConcurrentHashMap使用分段鎖技術,將資料分成一段一段的儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料時,其他段的資料也能被其他執行緒訪問。

在Java 1.5作為Hashtable的替代選擇新引入的。在Java 1.5之前,如果想要實現一個可以在多執行緒和併發的程式中安全使用的Map,只能在Hashtable和synchronized Map(Collections.synchronizedMap())中選擇,因為HashMap並不是執行緒安全的。CHM不但是執行緒安全的,而且比HashTable和synchronizedMap的效能要好。相對於Hashtable和synchronizedMap鎖住了整個Map,CHM只鎖住部分Map。

ConcurrentHashMap中的HashEntry相對於HashMap中的Entry有一定的差異性:HashEntry中的value以及next都被volatile修飾,這樣在多執行緒讀寫過程中能夠保持它們的可見性,程式碼如下:

static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;
複製程式碼

根據預設的併發級別(concurrency level),Map被分割成16個部分,並且由不同的鎖控制。這意味著,同時最多可以有16個寫執行緒操作Map。

什麼時候使用ConcurrentHashMap

CHM適用於讀者數量超過寫者時 ,當寫者數量大於等於讀者時,CHM的效能是低於Hashtable和synchronized Map的。這是因為當鎖住了整個Map時,讀操作要等待對同一部分執行寫操作的執行緒結束。CHM適用於做cache,在程式啟動時初始化,之後可以被多個請求執行緒訪問。正如Javadoc說明的那樣,CHM是Hashtable一個很好的替代,但要記住,CHM的比Hashtable的同步性稍弱。

5.4 BlockingQueue

5.4.1 ArrayBlockingQueue

ArrayBlockingQueue內部的阻塞佇列是通過重入鎖ReentrantLock和Condition條件佇列實現的,所以ArrayBlockingQueue中的元素存在公平訪問與非公平訪問的區別,對於公平訪問佇列,被阻塞的執行緒可以按照阻塞的先後順序訪問佇列,即先阻塞的執行緒先訪問佇列。而非公平佇列,當佇列可用時,阻塞的執行緒將進入爭奪訪問資源的競爭中,也就是說誰先搶到誰就執行,沒有固定的先後順序。

部分實現程式碼:

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

    /** 儲存資料的陣列 */
    final Object[] items;

    /**獲取資料的索引,主要用於take,poll,peek,remove方法 */
    int takeIndex;

    /**新增資料的索引,主要用於 put, offer, or add 方法*/
    int putIndex;

    /** 佇列元素的個數 */
    int count;


    /** 控制並非訪問的鎖 */
    final ReentrantLock lock;

    /**notEmpty條件物件,用於通知take方法佇列已有元素,可執行獲取操作 */
    private final Condition notEmpty;

    /**notFull條件物件,用於通知put方法佇列未滿,可執行新增操作 */
    private final Condition notFull;

    /**
       迭代器
     */
    transient Itrs itrs = null;
    
    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }
}
複製程式碼

//offer方法
public boolean offer(E e) {
     checkNotNull(e);//檢查元素是否為null
     final ReentrantLock lock = this.lock;
     lock.lock();//加鎖
     try {
         if (count == items.length)//判斷佇列是否滿
             return false;
         else {
             enqueue(e);//新增元素到佇列
             return true;
         }
     } finally {
         lock.unlock();
     }
 }

//入隊操作
private void enqueue(E x) {
    //獲取當前陣列
    final Object[] items = this.items;
    //通過putIndex索引對陣列進行賦值
    items[putIndex] = x;
    //索引自增,如果已是最後一個位置,重新設定 putIndex = 0;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;//佇列中元素數量加1
    //喚醒呼叫take()方法的執行緒,執行元素獲取操作。
    notEmpty.signal();
}

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

複製程式碼

5.4.2 LinkedBlockingQueue

和ArrayBlockingQueue相似,也是通過重入鎖ReentrantLock和Condition實現,但是使用了兩個ReentrantLock。

5.5 Deque and BlockingDeque

5.5.1 LinkedBlockingDeque

理解了LinkedBlockingQueue就不難理解LinkedBlockingDeque了。

5.6 ConcurrentSkipListMap and ConcurrentSkipListSet

6.鎖的種類

其實也算是鎖的優化

6.1 自旋鎖

自旋鎖原理非常簡單,如果持有鎖的執行緒能在很短時間內釋放鎖資源,那麼那些等待競爭鎖的執行緒就不需要做核心態和使用者態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等持有鎖的執行緒釋放鎖後即可立即獲取鎖,這樣就避免使用者執行緒和核心的切換的消耗。由於自旋鎖只是將當前執行緒不停地執行迴圈體,不進行執行緒狀態的改變,所以響應速度更快。但當執行緒數不停增加時,效能下降明顯,因為每個執行緒都需要執行,佔用CPU時間。如果執行緒競爭不激烈,並且保持鎖的時間段。適合使用自旋鎖。

6.2 阻塞鎖

阻塞鎖,可以說是讓執行緒進入阻塞狀態進行等待,當獲得相應的訊號(喚醒,時間) 時,才可以進入執行緒的準備就緒狀態,準備就緒狀態的所有執行緒,通過競爭,進入執行狀態。 JAVA中,能夠進入/退出、阻塞狀態或包含阻塞鎖的方法有 ,synchronized 關鍵字(其中的重量鎖),ReentrantLock,Object.wait()\notify(),LockSupport.park()/unpart()(j.u.c經常使用)

阻塞鎖的優勢在於,阻塞的執行緒不會佔用cpu時間, 不會導致 cpu佔用率過高,但進入時間以及恢復時間都要比自旋鎖略慢。

在競爭激烈的情況下 阻塞鎖的效能要明顯高於自旋鎖。

理想的情況則是,線上程競爭不激烈的情況下,使用自旋鎖,競爭激烈的情況下使用,阻塞鎖。

6.3 可重入鎖

可重入鎖,也叫做遞迴鎖,指的是同一執行緒 外層函式獲得鎖之後 ,內層遞迴函式仍然有獲取該鎖的程式碼,但不受影響。 在JAVA環境下 ReentrantLock 和synchronized 都是 可重入鎖

可重入鎖最大的作用是避免死鎖

6.4 偏向鎖

HotSpot的作者經過研究發現,大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖。

當一個執行緒訪問同步塊並獲取鎖時,會在物件頭和棧幀中的鎖記錄裡儲存鎖偏向的執行緒ID,以後該執行緒在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下物件頭的Mark Word裡是否儲存著指向當前執行緒的偏向鎖。如果測試成功,表示執行緒已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設定成1(表示當前是偏向鎖):如果沒有設定,則使用CAS競爭鎖;如果設定了,則嘗試使用CAS將物件頭的偏向鎖指向當前執行緒

偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖。

6.5 公平鎖和非公平鎖

公平鎖:每個執行緒搶佔鎖的順序為先後呼叫lock方法的順序依次獲取鎖,類似於排隊吃飯。

非公平鎖:每個執行緒搶佔鎖的順序不定,誰運氣好,誰就獲取到鎖,和呼叫lock方法的先後順序無關。

7.CAS的介紹

Compare and Swap,即比較並替換,實現併發演算法時常用到的一種技術.

CAS的實現需要硬體指令集的支撐,CAS比較交換的過程可以通俗的理解為CAS(V,O,N),包含三個值分別為:V 記憶體地址存放的實際值;O 預期的值(舊值);N 更新的新值。當V和O相同時,也就是說舊值和記憶體中實際的值相同表明該值沒有被其他執行緒更改過,即該舊值O就是目前來說最新的值了,自然而然可以將新值N賦值給V。反之,V和O不相同,表明該值已經被其他執行緒改過了則該舊值O不是最新版本的值了,所以不能將新值N賦給V,返回V即可。當多個執行緒使用CAS操作一個變數是,只有一個執行緒會成功,併成功更新,其餘會失敗。失敗的執行緒會重新嘗試,當然也可以選擇掛起執行緒

存在的問題

ABA、自旋時間過長、只能保證一個共享變數的原子操作

8.happens-before規則

從JDK 5 開始,JMM就使用happens-before的概念來闡述多執行緒之間的記憶體可見性。

在JMM中,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須存在happens-before關係。

happens-before原則定義如下:

  • 1,如果一個操作happens-before另一個操作,那麼第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。

  • 2,兩個操作之間存在happens-before關係,並不意味著一定要按照happens-before原則制定的順序來執行。如果重排序之後的執行結果與按照happens-before關係來執行的結果一致,那麼這種重排序並不非法。

  • 【死磕Java併發】-----Java記憶體模型之happens-before

相關文章