Synchronized鎖升級原理與過程深入剖析

2022-08-12 06:00:11

Synchronized鎖升級原理與過程深入剖析

前言

在上篇文章深入學習Synchronized各種使用方法當中我們仔細介紹了在各種情況下該如何使用synchronized關鍵字。因為在我們寫的程式當中可能會經常使用到synchronized關鍵字,因此JVM對synchronized做出了很多優化,而在本篇文章當中我們將仔細介紹JVM對synchronized的各種優化的細節。

工具準備

在正式談synchronized的原理之前我們先談一下自旋鎖,因為在synchronized的優化當中自旋鎖發揮了很大的作用。而需要了解自旋鎖,我們首先需要了解什麼是原子性

所謂原子性簡單說來就是一個一個操作要麼不做要麼全做,全做的意思就是在操作的過程當中不能夠被中斷,比如說對變數data進行加一操作,有以下三個步驟:

  • data從記憶體載入到暫存器。
  • data這個值加一。
  • 將得到的結果寫回記憶體。

原子性就表示一個執行緒在進行加一操作的時候,不能夠被其他執行緒中斷,只有這個執行緒執行完這三個過程的時候其他執行緒才能夠運算元據data

我們現在用程式碼體驗一下,在Java當中我們可以使用AtomicInteger進行對整型資料的原子操作:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {

  public static void main(String[] args) throws InterruptedException {
    AtomicInteger data = new AtomicInteger();
    data.set(0); // 將資料初始化位0
    Thread t1 = new Thread(() -> {
      for (int i = 0; i < 100000; i++) {
        data.addAndGet(1); // 對資料 data 進行原子加1操作
      }
    });
    Thread t2 = new Thread(() -> {
      for (int i = 0; i < 100000; i++) {
        data.addAndGet(1);// 對資料 data 進行原子加1操作
      }
    });
    // 啟動兩個執行緒
    t1.start();
    t2.start();
    // 等待兩個執行緒執行完成
    t1.join();
    t2.join();
    // 列印最終的結果
    System.out.println(data); // 200000
  }
}

從上面的程式碼分析可以知道,如果是一般的整型變數如果兩個執行緒同時進行操作的時候,最終的結果是會小於200000。

我們現在來模擬一下一般的整型變數出現問題的過程:

  • 主記憶體data的初始值等於0,兩個執行緒得到的data初始值都等於0。

  • 現線上程一將data加一,然後執行緒一將data的值同步回主記憶體,整個記憶體的資料變化如下:

  • 現線上程二data加一,然後將data的值同步回主記憶體(將原來主記憶體的值覆蓋掉了):

我們本來希望data的值在經過上面的變化之後變成2,但是執行緒二覆蓋了我們的值,因此在多執行緒情況下,會使得我們最終的結果變小。

但是在上面的程式當中我們最終的輸出結果是等於20000的,這是因為給data進行+1的操作是原子的不可分的,在操作的過程當中其他執行緒是不能對data進行操作的。這就是原子性帶來的優勢。

事實上上面的+1原子操作就是通過自旋鎖實現的,我們可以看一下AtomicInteger的原始碼:

public final int addAndGet(int delta) {
  // 在 AtomicInteger 內部有一個整型資料 value 用於儲存具體的數值的
  // 這個 valueOffset 表示這個資料 value 在物件 this (也就是 AtomicInteger一個具體的物件)
  // 當中的記憶體偏移地址
  // delta 就是我們需要往 value 上加的值 在這裡我們加上的是 1
  return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

上面的程式碼最終是呼叫UnSafe類的方法進行實現的,我們再看一下他的原始碼:

public final int getAndAddInt(Object o, long offset, int delta) {
  int v;
  do {
    v = getIntVolatile(o, offset); // 從物件 o 偏移地址為 offset 的位置取出資料 value ,也就是前面提到的儲存整型資料的變數
  } while (!compareAndSwapInt(o, offset, v, v + delta));
  return v;
}

上面的程式碼主要流程是不斷的從記憶體當中取物件內偏移地址為offset的資料,然後執行語句!compareAndSwapInt(o, offset, v, v + delta)

這條語句的主要作用是:比較物件o記憶體偏移地址為offset的資料是否等於v,如果等於v則將偏移地址為offset的資料設定為v + delta,如果這條語句執行成功返回 true否則返回false,這就是我們常說的Java當中的CAS

看到這裡你應該就發現了當上面的那條語句執行不成功的話就會一直進行while迴圈操作,直到操作成功之後才退出while迴圈,假如沒有操作成功就會一直「旋」在這裡,像這種操作就是自旋,通過這種自旋方式所構成的鎖