首頁>Club>
4
回覆列表
  • 1 # Java從入門到架構師

    synchronized

    假設我們要增加一個整數,該整數可以從多個執行緒同時訪問。

    我們count使用一種increment()將計數增加一的方法定義一個欄位:

    int count = 0;

    void increment() {

    count = count + 1;

    }

    從多個執行緒同時呼叫此方法時,我們遇到了嚴重的麻煩:

    ExecutorService executor = Executors.newFixedThreadPool(2);

    IntStream.range(0, 10000)

    .forEach(i -> executor.submit(this::increment));

    stop(executor);

    System.out.println(count); // 9965

    而不是看到恆定的結果計數10000,實際結果隨上述程式碼的每次執行而變化。原因是我們在不同執行緒上共享一個可變變數,而沒有synchronized對此變數的訪問,從而導致競爭狀態。

    幸運的是,Java從早期開始就透過synchronized關鍵字支援執行緒synchronized。我們可以利用上述條件來解決:

    synchronized void incrementSync() {

    count = count + 1;

    }

    當incrementSync()同時使用時,我們得到的期望結果計數為10000。不再發生競爭條件,並且每次執行程式碼後結果都穩定:

    ExecutorService

    executor=Executors.newFixedThreadPool(2);

    IntStream.range(0, 10000)

    .forEach(i -> executor.submit(this::incrementSync));

    stop(executor);

    System.out.println(count); // 10000

    該synchronized關鍵字也可作為一個塊語句。

    void incrementSync() {

    synchronized (this) {

    count = count + 1;

    }

    }

    Lock

    synchronized併發API 並非透過關鍵字使用隱式Lock定,而是支援Lock介面指定的各種顯式Lock定。

    標準JDK中提供了多種Lock實現,下面幾節將對其進行演示。

    ReentrantLock

    該類ReentrantLock是互斥Lock,具有與透過synchronized關鍵字訪問的隱式監視器相同的基本行為,但具有擴充套件功能。顧名思義,此Lock實現了隱式監視器一樣的可重入特徵。

    讓我們來看一下上面的示例如何使用ReentrantLock:

    ReentrantLock lock = new ReentrantLock();

    int count = 0;

    void increment() {

    lock.lock();

    try {

    count++;

    } finally {

    lock.unlock();

    }

    }

    可透過獲取Lock,並透過lock()釋放Lockunlock()。將程式碼包裝到一個try/finally塊中以確保在發生異常情況下解Lock是很重要的。與synchronized物件一樣,此方法也是執行緒安全的。如果另一個執行緒已經獲取了Lock,則隨後的呼叫將lock()暫停當前執行緒,直到Lock被解Lock。在任何給定時間,只有一個執行緒可以持有該Lock。

    Lock支援各種用於細粒度控制的方法,如下一個示例所示:

    ExecutorService executor = Executors.newFixedThreadPool(2);

    ReentrantLock lock = new ReentrantLock();

    executor.submit(() -> {

    lock.lock();

    try {

    sleep(1);

    } finally {

    lock.unlock();

    }

    });

    executor.submit(() -> {

    System.out.println("Locked: " + lock.isLocked());

    System.out.println("Held by me: " + lock.isHeldByCurrentThread());

    boolean locked = lock.tryLock();

    System.out.println("Lock acquired: " + locked);

    });

    stop(executor);

    當第一個任務將Lock保持一秒鐘時,第二個任務獲得有關Lock的當前狀態的不同資訊:

    Locked: true

    Held by me: false

    Lock acquired: false

    ReadWriteLock

    該介面ReadWriteLock指定另一種型別的Lock,其中維護一對用於讀取和寫入訪問的Lock。讀寫Lock背後的想法是,只要沒有人正在寫入該變數,通常可以安全地同時讀取可變變數。因此,只要沒有執行緒持有寫Lock,即可同時由多個執行緒持有讀Lock。如果讀取的頻率比寫入的頻率高,則可以提高效能和吞吐量。

    ExecutorService executor = Executors.newFixedThreadPool(2);

    Map<String, String> map = new HashMap<>();

    ReadWriteLock lock = new ReentrantReadWriteLock();

    executor.submit(() -> {

    lock.writeLock().lock();

    try {

    sleep(1);

    map.put("foo", "bar");

    } finally {

    lock.writeLock().unlock();

    }

    });

    上面的示例首先獲取一個寫Lock,以便在休眠一秒鐘後將新值放入對映中。在此任務完成之前,將提交其他兩項任務,以嘗試從地圖中讀取條目並休眠一秒鐘:

    Runnable readTask = () -> {

    lock.readLock().lock();

    try {

    System.out.println(map.get("foo"));

    sleep(1);

    } finally {

    lock.readLock().unlock();

    }

    };

    executor.submit(readTask);

    executor.submit(readTask);

    stop(executor);

    當您執行此程式碼示例時,您會注意到兩個讀取任務都必須等待一秒鐘,直到寫入任務完成為止。釋放寫Lock定後,將並行執行兩個讀取任務,並將結果同時列印到控制檯。它們不必彼此等待,因為只要另一個執行緒不持有寫Lock,就可以安全地併發獲取讀Lock。

    StampedLock

    Java 8附帶了一種稱為的新型LockStampedLock,它也支援讀和寫Lock,就像上面的示例一樣。與返回ReadWriteLock的Lock定方法相反StampedLock,以long值表示的圖章。您可以使用這些標記來釋放Lock或檢查Lock是否仍然有效。另外,加蓋Lock支援另一種稱為樂觀Lock的Lock模式。

    讓我們重寫最後一個StampedLock代替使用的示例程式碼ReadWriteLock:

    ExecutorService executor = Executors.newFixedThreadPool(2);

    Map<String, String> map = new HashMap<>();

    StampedLock lock = new StampedLock();

    executor.submit(() -> {

    long stamp = lock.writeLock();

    try {

    sleep(1);

    map.put("foo", "bar");

    } finally {

    lock.unlockWrite(stamp);

    }

    });

    Runnable readTask = () -> {

    long stamp = lock.readLock();

    try {

    System.out.println(map.get("foo"));

    sleep(1);

    } finally {

    lock.unlockRead(stamp);

    }

    };

    executor.submit(readTask);

    executor.submit(readTask);

    stop(executor);

    透過readLock()或writeLock()返回標記獲得讀取或寫入Lock定,該標記隨後用於在finally塊內進行解Lock。請記住,加蓋的Lock不會實現可重入的特徵。每次對Lock的呼叫都會返回一個新的標記,如果沒有可用的Lock,即使同一執行緒已持有Lock,也將阻止該Lock。因此,您必須特別注意不要陷入僵局。

    就像在前面的ReadWriteLock示例中一樣,兩個讀取任務都必須等待,直到釋放了寫Lock定。然後,這兩個讀取任務會同時列印到控制檯,因為只要沒有寫入Lock定,多次讀取就不會相互阻塞。

  • 2 # Java識堂

    synchronized和lock比較淺析

    synchronized是基於jvm底層實現的資料同步,lock是基於Java編寫,主要透過硬體依賴CPU指令實現資料同步。下面一一介紹

    一、synchronized的實現方案

      1.synchronized能夠把任何一個非null物件當成鎖,實現由兩種方式:

      a.當synchronized作用於非靜態方法時,鎖住的是當前物件的事例,當synchronized作用於靜態方法時,鎖住的是class例項,又因為Class的相關資料儲存在永久帶,因此靜態方法鎖相當於類的一個全域性鎖。

      b.當synchronized作用於一個物件例項時,鎖住的是對應的程式碼塊。

      2.synchronized鎖又稱為物件監視器(object)。 3.當多個執行緒一起訪問某個物件監視器的時候,物件監視器會將這些請求儲存在不同的容器中。

      >Contention List:競爭佇列,所有請求鎖的執行緒首先被放在這個競爭佇列中

      >Entry List:Contention List中那些有資格成為候選資源的執行緒被移動到Entry List中

      >Wait Set:哪些呼叫wait方法被阻塞的執行緒被放置在這裡

      >OnDeck:任意時刻,最多隻有一個執行緒正在競爭鎖資源,該執行緒被成為OnDeck

      >Owner:當前已經獲取到所資源的執行緒被稱為Owner

      > !Owner:當前釋放鎖的執行緒

      下圖展示了他們之前的關係

      4.synchronized在jdk1.6之後提供了多種最佳化方案:

      >自旋鎖

        jdk1.6之後預設開啟,可以使用引數-XX:+UseSpinning控制,自旋等待不能代替阻塞,且先不說對處理器數量的要求,自旋等待本身雖然避免了執行緒切換的開銷,但它是要佔用處理器時間的,因此,如果鎖被佔用的時間很短,自旋等待的效果就會非常好,反之,如果鎖被佔用的時候很長,那麼自旋的執行緒只會白白消耗處理器資源,而不會做任何有用的工作,反而會帶來效能上的浪費。自旋次數的預設值是 10 次,使用者可以使用引數 -XX:PreBlockSpin 來更改。

        自旋鎖的本質:執行幾個空方法,稍微等一等,也許是一段時間的迴圈,也許是幾行空的彙編指令。

      >鎖消除

        即時編譯器在執行時,對一些程式碼上要求同步,但是被檢測到不可能存在共享資料競爭的鎖進行消除,依據來源於逃逸分析的資料支援,那麼是什麼是逃逸分析?對於虛擬機器來說需要使用資料流分析來確定是否消除變數底層框架的同步程式碼,因為有許多同步的程式碼不是自己寫的。

    例1.1

    public static String concatString(String s1, String s2, String s3) { return s1 + s2 + s3; }

      由於 String 是一個不可變的類,對字串的連線操作總是透過生成新的 String 物件來進行的,因此 Javac 編譯器會對 String 連線做自動最佳化。在 JDK 1.5 之前,會轉化為 StringBuffer 物件的連續 append() 操作,在 JDK 1.5 及以後的版本中,會轉化為 StringBuilder 物件的連續 append() 操作,這裡的stringBuilder.append是執行緒不同步的(假設是同步)。

      Javac 轉化後的字串連線程式碼為:

    public static String concatString(String s1, String s2, String s3) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); sb.append(s3); return sb.toString(); }

      此時的鎖物件就是sb,虛擬機器觀察變數 sb,很快就會發現它的動態作用域被限制在 concatString() 方法內部。也就是說,sb 的所有引用永遠不會 “逃逸” 到concatString() 方法之外,其他執行緒無法訪問到它,雖然這裡有鎖,但是可以被安全地消除掉,在即時編譯之後,這段程式碼就會忽略掉所有的同步而直接執行了。

      >鎖粗化

      將同步塊的作用範圍限制得儘量小——只在共享資料的實際作用域中才進行同步,這樣是為了使得需要同步的運算元量儘可能變小,如果存在鎖競爭,那等待鎖的執行緒也能儘快拿到鎖。

      >輕量級鎖

      加鎖過程:在程式碼進入同步塊的時候,如果此同步物件沒有被鎖定(鎖標誌位為 “01” 狀態)虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的 Mark Word 的複製,這時候執行緒堆疊與物件頭的狀態如圖 13-3 所示

       

      然後,虛擬機器將使用 CAS 操作嘗試將物件的 Mark Word 更新為指向 Lock Record 的指標。如果這個更新動作成功了,那麼這個執行緒就擁有了該物件的鎖,並且物件 Mark Word 的鎖標誌位 (Mark Word 的最後 2bit)將轉變為 “00”,即表示此物件處於輕量級鎖定狀態,這時執行緒堆疊與物件頭的狀態如圖13-4  

      如果上述更新操作失敗,則說明這個鎖物件被其他鎖佔用,此時輕量級變為重量級鎖,標誌位為“10”,後面等待的執行緒進入阻塞狀態。

      解鎖過程:也是由CAS進行操作的,如果物件的 Mark Word 仍然指向著執行緒的鎖記錄,那就用 CAS 操作把物件當前的 Mark Word 和執行緒中複製的 Displaced Mark Word 替換回來,如果替換成功,整個同步過程就完成了。如果替換失敗,說明有其他執行緒嘗試過獲取該鎖,那就要釋放鎖的同時,喚醒被掛起的執行緒。

      輕量級鎖能提升程式同步效能的依據是 “對於絕大部分的鎖,在整個同步週期內都是不存在競爭的”,這是一個經驗資料。如果沒有競爭,輕量級鎖使用 CAS 操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了 CAS 操作,因此在有競爭的情況下,輕量級鎖會比傳統的重量級鎖更慢。

      >偏向鎖

      偏向鎖也是 JDK 1.6 中引入的一項鎖最佳化,它的目的是消除資料在無競爭情況下的同步原語,進一步提高程式的執行效能。如果說輕量級鎖是在無競爭的情況下使用 CAS 操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除掉,連 CAS 操作都不做了。

      實質就是設定一個變數,判斷這個變數是否是當前執行緒,是就避免再次加鎖解鎖操作,從而避免了多次的CAS操作。壞處是如果一個執行緒持有偏向鎖,另外一個執行緒想爭用偏向物件,擁有者想釋放這個偏向鎖,釋放會帶來額外的效能開銷,但是總體來說偏向鎖帶來的好處還是大於CAS的代價的。在具體問題具體分析的前提下,有時候使用引數 -XX:-UseBiasedLocking 來禁止偏向鎖最佳化反而可以提升效能。

    二、lock的實現方案

      與synchronized不同的是lock是純java手寫的,與底層的JVM無關。在java.util.concurrent.locks包中有很多Lock的實現類,常用的有ReenTrantLock、ReadWriteLock(實現類有ReenTrantReadWriteLock)

    ,其實現都依賴java.util.concurrent.AbstractQueuedSynchronizer類(簡稱AQS),實現思路都大同小異,因此我們以ReentrantLock作為講解切入點。

    分析之前我們先來花點時間看下AQS。AQS是我們後面將要提到的CountDownLatch/FutureTask/ReentrantLock/RenntrantReadWriteLock/Semaphore的基礎,因此AQS也是Lock和Excutor實現的基礎。它的基本思想就是一個同步器,支援獲取鎖和釋放鎖兩個操作。

      

      要支援上面鎖獲取、釋放鎖就必須滿足下面的條件:

      1、 狀態位必須是原子操作的

      2、 阻塞和喚醒執行緒

      3、 一個有序的佇列,用於支援鎖的公平性

      場景:可定時的、可輪詢的與可中斷的鎖獲取操作,公平佇列,或者非塊結構的鎖。

      主要從以下幾個特點介紹:

      1.可重入鎖

        如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上表明瞭鎖的分配機制:基於執行緒的分配,而不是基於方法呼叫的分配。

      2.可中斷鎖

        可中斷鎖:顧名思義,就是可以相應中斷的鎖。

        在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。

        如果某一執行緒A正在執行鎖中的程式碼,另一執行緒B正在等待獲取該鎖,可能由於等待時間過長,執行緒B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的執行緒中中斷它,這種就是可中斷鎖。

      3.公平鎖和非公平鎖

        公平鎖以請求鎖的順序來獲取鎖,非公平鎖則是無法保證按照請求的順序執行。synchronized就是非公平鎖,它無法保證等待的執行緒獲取鎖的順序。而對於ReentrantLock和ReentrantReadWriteLock,它預設情況下是非公平鎖,但是可以設定為公平鎖。

        引數為true時表示公平鎖,不傳或者false都是為非公平鎖。

    ReentrantLock lock = new ReentrantLock(true);

      4.讀寫鎖

      讀寫鎖將對一個資源(比如檔案)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。

      正因為有了讀寫鎖,才使得多個執行緒之間的讀操作不會發生衝突。

      ReadWriteLock就是讀寫鎖,它是一個介面,ReentrantReadWriteLock實現了這個介面。

      可以透過readLock()獲取讀鎖,透過writeLock()獲取寫鎖。

    三、總結

      1.synchronized

      優點:實現簡單,語義清晰,便於JVM堆疊跟蹤,加鎖解鎖過程由JVM自動控制,提供了多種最佳化方案,使用更廣泛

      缺點:悲觀的排他鎖,不能進行高階功能

      2.lock

      優點:可定時的、可輪詢的與可中斷的鎖獲取操作,提供了讀寫鎖、公平鎖和非公平鎖  

      缺點:需手動釋放鎖unlock,不適合JVM進行堆疊跟蹤

      3.相同點 

      都是可重入鎖

  • 中秋節和大豐收的關聯?
  • 風景園林好學嗎?