天天看點

Volatile、Synchronized、ReentrantLock鎖機制使用說明

作者:Doker多克

一、Volatile底層原理

volatile是輕量級的同步機制,volatile保證變量對所有線程的可見性,不保證原子性。

  1. 當對volatile變量進行寫操作的時候,JVM會向處理器發送一條LOCK字首的指令,将該變量所在緩存行的資料寫回系統記憶體。
  2. 由于緩存一緻性協定,每個處理器通過嗅探在總線上傳播的資料來檢查自己的緩存是不是過期了,當處理器發現自己緩存行對應的記憶體位址被修改,就會将目前處理器的緩存行置為無效狀态,當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中把資料讀到處理器緩存中。
來看看緩存一緻性協定是什麼。 緩存一緻性協定:當CPU寫資料時,如果發現操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會發出信号通知其他CPU将該變量的緩存行置為無效狀态,是以當其他CPU需要讀取這個變量時,就會從記憶體重新讀取。

volatile關鍵字的兩個作用:

  1. 保證了不同線程對共享變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
  2. 禁止進行指令重排序。
指令重排序是JVM為了優化指令,提高程式運作效率,在不影響單線程程式執行結果的前提下,盡可能地提高并行度。Java編譯器會在生成指令系列時在适當的位置會插入記憶體屏障指令來禁止處理器重排序。插入一個記憶體屏障,相當于告訴CPU和編譯器先于這個指令的必須先執行,後于這個指令的必須後執行。對一個volatile字段進行寫操作,Java記憶體模型将在寫操作後插入一個寫屏障指令,這個指令會把之前的寫入值都重新整理到記憶體。

volatile變量的使用:

public class Main extends Thread {
  private volatile boolean keepRunning = true;
  public void run() {
    System.out.println("Thread started");
    while (keepRunning) {
      try {
        System.out.println("Going to sleep");
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    System.out.println("Thread stopped");
  }
  public void stopThread() {
    this.keepRunning = false;
  }
  public static void main(String[] args) throws Exception{
    Main v = new Main();
    v.start();
    Thread.sleep(3000);
    System.out.println("Going to set the stop flag to true");
    v.stopThread();
  }
}           

二、synchronized的用法有哪些?

  1. 修飾普通方法:作用于目前對象執行個體,進入同步代碼前要獲得目前對象執行個體的鎖
  2. 修飾靜态方法:作用于目前類,進入同步代碼前要獲得目前類對象的鎖,synchronized關鍵字加到static 靜态方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖
  3. 修飾代碼塊:指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖

synchronized的作用有哪些?

  • 原子性:確定線程互斥的通路同步代碼;
  • 可見性:保證共享變量的修改能夠及時可見,其實是通過Java記憶體模型中的 “對一個變量unlock操作之前,必須要同步到主記憶體中;如果對一個變量進行lock操作,則将會清空工作記憶體中此變量的值,在執行引擎使用此變量前,需要重新從主記憶體中load操作或assign操作初始化變量值” 來保證的;
  • 有序性:有效解決重排序問題,即 “一個unlock操作先行發生(happen-before)于後面對同一個鎖的lock操作”;

synchronized 底層實作原理?

synchronized 同步代碼塊的實作是通過 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置。當執行 monitorenter 指令時,線程試圖擷取鎖也就是擷取 monitor的持有權。

monitor對象存在于每個Java對象的對象頭中, synchronized 鎖便是通過這種方式擷取鎖的,也是為什麼Java中任意對象可以作為鎖的原因

其内部包含一個計數器,當計數器為0則可以成功擷取,擷取後将鎖計數器設為1也就是加1。相應的在執行 monitorexit 指令後,将鎖計數器設為0 ,表明鎖被釋放。如果擷取對象鎖失敗,那目前線程就要阻塞等待,直到鎖被另外一個線程釋放為止

synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的确實是ACC_SYNCHRONIZED 辨別,該辨別指明了該方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED 通路标志來辨識一個方法是否聲明為同步方法,進而執行相應的同步調用。

關鍵字synchronized示例:

public class Main {
  private static int myValue = 1;

  public static void main(String[] args) {
    Thread t = new Thread(() -> {
      while (true) {
        updateBalance();
      }
    });
    t.start();
    t = new Thread(() -> {
      while (true) {
        monitorBalance();
      }
    });
    t.start();
  }

  public static synchronized void updateBalance() {
    System.out.println("start:" + myValue);
    myValue = myValue + 1;
    myValue = myValue - 1;
    System.out.println("end:" + myValue);
  }

  public static synchronized void monitorBalance() {
    int b = myValue;
    if (b != 1) {
      System.out.println("Balance  changed: " + b);
      System.exit(1); 
    }
  }
}           

上面的代碼生成以下結果:

Volatile、Synchronized、ReentrantLock鎖機制使用說明

volatile和synchronized的差別?

  1. volatile隻能使用在變量上;而synchronized可以在類,變量,方法和代碼塊上。
  2. volatile至保證可見性;synchronized保證原子性與可見性。
  3. volatile禁用指令重排序;synchronized不會。
  4. volatile不會造成阻塞;synchronized會。

Synchronized總共有三種用法:

  1. 當synchronized作用在執行個體方法時,螢幕鎖(monitor)便是對象執行個體(this);
  2. 當synchronized作用在靜态方法時,螢幕鎖(monitor)便是對象的Class執行個體,因為Class資料存在于永久代,是以靜态方法鎖相當于該類的一個全局鎖;
  3. 當synchronized作用在某一個對象執行個體時,螢幕鎖(monitor)便是括号括起來的對象執行個體;

ReentrantLock和synchronized差別?

  1. 使用synchronized關鍵字實作同步,線程執行完同步代碼塊會自動釋放鎖,而ReentrantLock需要手動釋放鎖。
  2. synchronized是非公平鎖,ReentrantLock可以設定為公平鎖。
  3. ReentrantLock上等待擷取鎖的線程是可中斷的,線程可以放棄等待鎖。而synchonized會無限期等待下去。
  4. ReentrantLock 可以設定逾時擷取鎖。在指定的截止時間之前擷取鎖,如果截止時間到了還沒有擷取到鎖,則傳回。
  5. ReentrantLock 的 tryLock() 方法可以嘗試非阻塞的擷取鎖,調用該方法後立刻傳回,如果能夠擷取則傳回true,否則傳回false。

wait()和sleep()的異同點?

相同點:

  1. 使目前線程暫停運作,把機會交給其他線程
  2. 任何線程在等待期間被中斷都會抛出InterruptedException

不同點:

  1. wait()是Object超類中的方法;而sleep()是線程Thread類中的方法
  2. 對鎖的持有不同,wait()會釋放鎖,而sleep()并不釋放鎖
  3. 喚醒方法不完全相同,wait()依靠notify或者notifyAll、中斷、達到指定時間來喚醒;而sleep()到達指定時間被喚醒
  4. 調用wait()需要先擷取對象的鎖,而Thread.sleep()不用

Runnable和Callable有什麼差別?

  • Callable接口方法是call(),Runnable的方法是run();
  • Callable接口call方法有傳回值,支援泛型,Runnable接口run方法無傳回值。
  • Callable接口call()方法允許抛出異常;而Runnable接口run()方法不能繼續上抛異常。

守護線程是什麼?

守護線程是運作在背景的一種特殊程序。它獨立于控制終端并且周期性地執行某種任務或等待處理某些發生的事件。在 Java 中垃圾回收線程就是特殊的守護線程。

線程間通信方式

volatile

volatile 使用共享記憶體實作線程間互相通信。多個線程同時監聽一個變量,當這個變量被某一個線程修改的時候,其他線程可以感覺到這個變化。

wait和 notify

wait/notify為Object對象的方法,調用wait/notify需要先獲得對象的鎖。對象調用wait()之後線程釋放鎖,将線程放到對象的等待隊列,當通知線程調用此對象的notify()方法後,等待線程并不會立即從wait()傳回,需要等待通知線程釋放鎖(通知線程執行完同步代碼塊),等待隊列裡的線程擷取鎖,擷取鎖成功才能從wait()方法傳回,即從wait()方法傳回前提是線程獲得鎖。

join

當在一個線程調用另一個線程的join()方法時,目前線程阻塞等待被調用join方法的線程執行完畢才能繼續執行。join()是基于等待通知機制實作的。

三、AQS原理

AQS,AbstractQueuedSynchronizer,抽象隊列同步器,定義了一套多線程通路共享資源的同步器架構,許多并發工具的實作都依賴于它,如常用的ReentrantLock/Semaphore/CountDownLatch。

AQS使用一個volatile的int類型的成員變量state來表示同步狀态,通過CAS修改同步狀态的值。當線程調用 lock 方法時 ,如果 state=0,說明沒有任何線程占有共享資源的鎖,可以獲得鎖并将 state加1。如果 state不為0,則說明有線程目前正在使用共享變量,其他線程必須加入同步隊列進行等待。

private volatile int state;//共享變量,使用volatile修飾保證線程可見性
           

同步器依賴内部的同步隊列(一個FIFO雙向隊列)來完成同步狀态的管理,目前線程擷取同步狀态失敗時,同步器會将目前線程以及等待狀态(獨占或共享)構造成為一個節點(Node)并将其加入同步隊列并進行自旋,當同步狀态釋放時,會把首節點中的後繼節點對應的線程喚醒,使其再次嘗試擷取同步狀态。

四、ReentrantLock 是如何實作可重入性的?

ReentrantLock内部自定義了同步器sync,在加鎖的時候通過CAS算法,将線程對象放到一個雙向連結清單中,每次擷取鎖的時候,會檢查目前占有鎖的線程和目前請求鎖的線程是否一緻,如果一緻,同步狀态加1,表示鎖被目前線程擷取了多次。

源碼如下:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
           

ReentrantLock 示例:

public class ReentrantLockDemo01 implements Runnable {

    private Lock lock = new ReentrantLock();

    private int tickets = 200;

    @Override
    public void run() {
        while (true) {
            lock.lock(); // 擷取鎖
            try {
                if (tickets > 0) {
                    TimeUnit.MILLISECONDS.sleep(100);
                    System.out.println(Thread.currentThread().getName() + " " + tickets--);
                } else {
                    break;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock(); // 釋放所
            }
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo01 reentrantLockDemo = new ReentrantLockDemo01();
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(reentrantLockDemo, "thread" + i);
            thread.start();
        }
    }
}           

reentrantlock用于替代synchronized

  • 需要注意的是,必須要必須要必須要手動釋放鎖(重要的事情說三遍)
  • 使用syn鎖定的話如果遇到異常,jvm會自動釋放鎖,但是lock必須手動釋放鎖,是以經常在finally中進行鎖的釋放
  • 使用reentrantlock可以進行“嘗試鎖定”tryLock,這樣無法鎖定,或者在指定時間内無法鎖定,線程可以決定是否繼續等待
  • 使用ReentrantLock還可以調用lockInterruptibly方法,可以對線程interrupt方法做出響應
  • 在一個線程等待鎖的過程中,可以被打斷

五、鎖的分類

公平鎖與非公平鎖

按照線程通路順序擷取對象鎖。synchronized是非公平鎖,Lock預設是非公平鎖,可以設定為公平鎖,公平鎖會影響性能。

public ReentrantLock() {
    sync = new NonfairSync();
}

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

共享式與獨占式鎖

共享式與獨占式的最主要差別在于:同一時刻獨占式隻能有一個線程擷取同步狀态,而共享式在同一時刻可以有多個線程擷取同步狀态。例如讀操作可以有多個線程同時進行,而寫操作同一時刻隻能有一個線程進行寫操作,其他操作都會被阻塞。

悲觀鎖與樂觀鎖

悲觀鎖,每次通路資源都會加鎖,執行完同步代碼釋放鎖,synchronized和ReentrantLock屬于悲觀鎖。

樂觀鎖,不會鎖定資源,所有的線程都能通路并修改同一個資源,如果沒有沖突就修改成功并退出,否則就會繼續循環嘗試。樂觀鎖最常見的實作就是CAS。

适用場景:

  • 悲觀鎖适合寫操作多的場景。
  • 樂觀鎖适合讀操作多的場景,不加鎖可以提升讀操作的性能。

六、樂觀鎖有什麼問題?

樂觀鎖避免了悲觀鎖獨占對象的問題,提高了并發性能,但它也有缺點:

  • 樂觀鎖隻能保證一個共享變量的原子操作。
  • 長時間自旋可能導緻開銷大。假如CAS長時間不成功而一直自旋,會給CPU帶來很大的開銷。
  • ABA問題。CAS的原理是通過比對記憶體值與預期值是否一樣而判斷記憶體值是否被改過,但是會有以下問題:假如記憶體值原來是A, 後來被一條線程改為B,最後又被改成了A,則CAS認為此記憶體值并沒有發生改變。可以引入版本号解決這個問題,每次變量更新都把版本号加一。

七、什麼是CAS?

CAS全稱Compare And Swap,比較與交換,是樂觀鎖的主要實作方式。CAS在不使用鎖的情況下實作多線程之間的變量同步。ReentrantLock内部的AQS和原子類内部都使用了CAS。

CAS 操作包含三個操作數 —— 記憶體位置(V)、預期原值(A)和新值(B)。 如果記憶體位置的值與預期原值相比對,那麼處理器會自動将該位置值更新為新值 。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前傳回該位置的值。(在 CAS 的一些特殊情況下将僅傳回 CAS 是否成功,而不提取目前 值。)CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則将 B 放到這個位置;否則,不要更改該位置,隻告訴我這個位置現在的值即可。” 通常将 CAS 用于同步的方式是從位址 V 讀取值 A,執行多步計算來獲得新 值 B,然後使用 CAS 将 V 的值從 A 改為 B。如果 V 處的值尚未同時更改,則 CAS 操作成功。 類似于 CAS 的指令允許算法執行讀-修改-寫操作,而無需害怕其他線程同時 修改變量,因為如果其他線程修改變量,那麼 CAS 會檢測它(并失敗),算法可以對該操作重新計算。

CAS的目的

原子操作是利用類似的特性完成的。整個JUC都是建立在CAS之上的,是以對于synchronized阻塞算法JUC在性能上有了很大的提升。

繼續閱讀