1. 程式人生 > 其它 >【Java多執行緒】synchronized、ReentrantLock基礎原理

【Java多執行緒】synchronized、ReentrantLock基礎原理

什麼是多執行緒?

在執行程式碼的過程中,我們很多時候需要同時執行一些操作,這些同時進行操作可以儘可能的提升程式碼執行效率,充分發揮CPU運算能力。

public class Test implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + "_" + i);
        }
    }

    public static void main(String[] args) {
        Test t1 = new Test();
        Thread ta = new Thread(t1, "tbA");
        Thread tb = new Thread(t1, "tbB");
        ta.start();
        tb.start();
    }
}

輸出:

tbA_0
tbB_0
tbA_1
tbB_1
tbB_2
tbA_2

為什麼使用多執行緒?

  • 分離單一邏輯
  • 提高程式碼效率
  • 充分發揮硬體能力

多執行緒的代價?

在多個執行緒同時需要訪問同一物件時,會出現意料之外的結果。

public class Test implements Runnable {
    private static Integer i = 0;
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "的賽道:" + (++i));
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Test t1 = new Test();
        System.out.println("----4位選手進入賽道----");
        Thread thread1 = new Thread(t1, "tbA");
        Thread thread2 = new Thread(t1, "tbB");
        Thread thread3 = new Thread(t1, "tbC");
        Thread thread4 = new Thread(t1, "tbD");

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

輸出:

----4位選手進入賽道----
tbA的賽道:1
tbD的賽道:3
tbB的賽道:1
tbC的賽道:2

選手A和B進入了同一賽道,這不是我們期望的。

怎麼保證執行緒安全?

為物件加鎖,可以最大可能保證執行緒安全。
執行緒鎖有哪些?
最常用的是synchronized,除此之外還有ReentrantLock

synchronized鎖原理

兩個概念

CAS

是一個CPU指令,三個引數:地址,原始值,新值,返回一個bool型。

function cas(p , old , new ) returns bool {
    if *p ≠ old { // *p 表示指標p所指向的記憶體地址
        return false
    }
    *p ← new
    return true
}

Mark Word

Java物件頭的Mark Word中儲存了HashCode、分代年齡、鎖狀態等資訊。

三種鎖

三種鎖依次完成了三種設想下的執行緒安全保障。

起初我們悲觀的認為,幾乎所有的多執行緒訪問物件都可能存在併發競爭,需要阻塞競爭的執行緒以已達到在同一時間只有一個執行緒訪問物件。這就是synchronized最初設計的重量級鎖。

重量級鎖

使用作業系統的互斥量實現。在使用者態與核心態切換,需要消耗較大效能。

核心態:cpu可以訪問記憶體的所有資料,包括外圍裝置,例如硬碟,網絡卡,cpu也可以將自己從一個程式切換到另一個程式。
使用者態:只能受限的訪問記憶體,且不允許訪問外圍裝置,佔用cpu的能力被剝奪,cpu資源可以被其他程式獲取。

輕量級鎖

輕量級鎖的誕生源於我們的比較樂觀的設想——部分多執行緒在訪問物件時可能是序列的競爭關係。
序列競爭是指,雖然多執行緒依然存在競爭,但不是同時訪問,而是依次的。我們可以讓執行緒按照先來後到的順序有序訪問。

流程:
1.物件(OBJ)被建立,它目前沒有被任何執行緒佔用
2.執行緒1嘗試訪問OBJ,通過OBJ的頭標記(Mark Word)內記錄的資訊,發現他沒有被任何執行緒佔用
3.執行緒1將OBJ的Mark Word拷貝至自己棧幀的鎖空間(Lock Record)內,我們稱它為置換標記(Displaced Mark Word),並通過CAS將原OBJ的Mark Word內所指標指向執行緒1的Lock Record
4.執行緒1開始佔用OBJ,並執行自己內部操作
5.執行緒2嘗試訪問OBJ,通過OBJ的Mark Word發現已被執行緒1佔用
6.執行緒2開始執行自旋鎖,進入迴圈等待,每隔一段時間嘗試通過CAS判斷OBJ是否被佔用,如果是則繼續迴圈等待,如果否則佔用OBJ,自旋嘗試有次數限制(預設10,可以通過JVM調優修改)
7.執行緒1執行完畢,通過CAS將自己Displaced Mark Word拷貝至 OBJ的Mark Wrod,進行復原,釋放OBJ
8.執行緒2在自旋鎖執行過程中發現OBJ已經被執行緒1釋放,執行第3步操作佔用OBJ
9.執行緒3嘗試訪問OBJ...

偏向鎖

輕量級鎖已經極大優化了重量級鎖阻塞帶來的負擔。但很快,我們又想到另一種更樂觀的多執行緒情況——只有一個執行緒多次訪問某個物件。
在這種情況下,甚至沒有第二個執行緒,只有唯一的一個執行緒不斷的訪問物件。不存在其他執行緒競爭,不存在等待。

流程:
1.執行緒1嘗試訪問OBJ,通過其頭標記(Mark Word)發現其沒有執行緒佔用且可以設定偏向鎖(Mark Wor中有一個標識代表是否可以設定偏向鎖)
2.執行緒1通過CAS佔用OBJ,並執行自己的操作
3.執行緒1再次嘗試訪問OBJ,發現Mark Word中記錄的是自己的資訊,則直接訪問OBJ執行操作

鎖之間的關係

輕量級鎖及偏向鎖是較樂觀的情況,但如果出現了不那麼樂觀的特殊情況怎麼辦?

一般synchronized鎖會預設執行偏向鎖,但在執行過程中發現有其他執行緒競爭,自動膨脹至輕量級鎖。但當執行多次自旋鎖都沒法爭取物件時,將自動膨脹至重量級鎖。

ReentrantLock鎖原理

ReentrantLock的原理是通過CAS及AQS佇列搭配實現。

AQS
AQS使用一個FIFO的佇列(也叫CLH佇列,是CLH鎖的一種變形),表示排隊等待鎖的執行緒。佇列頭節點稱作“哨兵節點”或者“啞節點”,它不與任何執行緒關聯。其他的節點與等待執行緒關聯,每個節點維護一個等待狀態waitStatus。

流程:
1.執行緒1嘗試訪問OBJ,通過AQS佇列的state屬性發現其沒有被佔用(state=0)
2.執行緒1佔用OBJ,設定AQS的state=1,並設定其AQS的thread為當前執行緒(執行緒1)
3.執行緒2嘗試訪問OBJ。發現其已被佔用(狀態為1),加入AQS的等待佇列
4.執行緒1執行完畢,釋放OBJ
5.位於AQS等待佇列最前的執行緒2開始嘗試訪問OBJ...

程式碼

瞭解了鎖的原理,讓我們用實際程式碼解決剛開始遇到的多執行緒問題。

synchronized方式:

public class Test implements Runnable {
    private static Integer i = 0;
    @Override
    public void run() {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + "的賽道:" + (++i));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Test t1 = new Test();
        System.out.println("----4位選手進入賽道----");
        Thread thread1 = new Thread(t1, "tbA");
        Thread thread2 = new Thread(t1, "tbB");
        Thread thread3 = new Thread(t1, "tbC");
        Thread thread4 = new Thread(t1, "tbD");

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

輸出:

----4位選手進入賽道----
tbA的賽道:1
tbD的賽道:2
tbB的賽道:3
tbC的賽道:4

ReentrantLock方式:

public class Test implements Runnable {
    private static Integer i = 0;
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        lock.lock();
        System.out.println(Thread.currentThread().getName() + "的賽道:" + (++i));
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock.unlock();
    }

    public static void main(String[] args) {
        Test t1 = new Test();
        System.out.println("----4位選手進入賽道----");
        Thread thread1 = new Thread(t1, "tbA");
        Thread thread2 = new Thread(t1, "tbB");
        Thread thread3 = new Thread(t1, "tbC");
        Thread thread4 = new Thread(t1, "tbD");

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

輸出與上一個一致。

參考文獻: https://www.cnblogs.com/maxigang/p/9041080.html
https://blog.csdn.net/lengxiao1993/article/details/81568130
https://zhuanlan.zhihu.com/p/249147493