全面解析多執行緒與同步技術:SYNC、CAS、ThreadLocal | 公平鎖、可重入鎖、樂觀鎖

全面解析多執行緒與同步技術:SYNC、CAS、ThreadLocal | 公平鎖、可重入鎖、樂觀鎖

Overview of Content

在這篇全面解析多執行緒與同步技術的文章中,我們將深入探討多執行緒的安全性,從基本的執行緒安全考量點到 Java synchronized 的詳細用法,包括方法、同步區塊、靜態方法和靜態物件的同步

我們還會探討多執行緒之間的協作機制,如 wait 方法與 notify / notifyAll,以及鎖的釋放時機。此外,我們將詳細介紹 ReentrantLock 機制,包括可重入鎖、顯式鎖與隱式鎖、公平鎖與非公平鎖,並解析死鎖與活鎖的概念,樂觀鎖與悲觀鎖的對比

文章還涵蓋了 CAS 原子操作及其在 JDK 中的應用,並通過簡單範例、手動實現及源碼分析,讓讀者全面了解 ThreadLocal 如何實現執行緒隔離。這篇文章將為讀者提供深入的多執行緒與同步技術知識,幫助解決實際開發中的各種挑戰

以下可能會混用 “線程”、“執行序”,兩者是相同意思

寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀

個人程式分享時比較注重「縮排」,所以可能不適合手機的排版閱讀,建議切換至「電腦版」、「平板版」視窗看


多執行序:安全概述

多執行序是常見可以加強 CPU 吞吐量的手段,可以有效提高效率(正確使用的話),但由於 多執行緒是程式設計提供的概念(並且每種語言的實現並不同),它不具備保證資源存儲的安全性!

多執行序的不安全是在 對同個物件進行寫入 (改變) 資源時(讀取則不會有安全問題)

● 這裡談及的是「執行序, Thread」而不是程序

程序(也就是「應用」)是核心系統提供的功能,核心系統會將其稱之為 Process,它就具備資源訪問安全性,它讓每個應用都活在虛擬記憶體中不會相互干擾

執行緒安全:考量點

多執行序安全往往是以「性能」作為代價(鎖),因此我們使用鎖 🔒 需考慮到以下幾點

● 只對可能產生資源搶奪的區塊進行代碼同步,同步的代碼越長效能越差,因為要做更多的等待

如果沒有安排的隨意使用執行緒,那代碼的執行效率反而會不升反降

● 我們也可以在創建類時區分 「單執行緒執行環境」、「多執行緒執行環境」,避免統一用多執行環境的物件(這是一種開發上的約束)


Java synchronized 同步

synchronized 是 Java 語言在使用同步時的一個「修飾符」,一次只讓一個執行序訪問,一個物件讓多個執行序訪問;以下是幾個 Java synchronized 修飾符使用時要注意的事情…

可以 Synchronized Null 物件 ? 不行 !

Synchronized 一定是針對某一物件做出同步動作,所以一定要有物件(要有物件可以搶,不然就無法同步了)

synchronized 效果可以繼承 ? 不行 !

Override 不可繼承 synchronized 效果,繼承時 要自己加入 synchronized 關鍵字,但是可以透過 super(呼叫父類的同步方法)

以下案例中來證明 synchronized 不會作用在繼承

TestSynch 子類覆寫 addData 方法但並沒有加上同步,就沒有同步功能

synchronized 方法:自動鎖定

● 這裡的自動鎖動,是指使用 synchronized 時不去指定物件,而使用預設的 this 物件

認識 this:每一個 物件都有內含 this 關鍵字它代表的是這個物件的 instance 實例,它是個隱藏物件

graph LR subgraph Hello_instance t(this 物件) end c(class Hello) -.-> |實例化| Hello_instance

● 接下來我們 使用 synchronized 關鍵字來同步方法,就是手法鎖定 this 物件鎖的物件是目前的實例(也就是 this);接下來凡事使用到該物件的地方都需要做等待

當然,請特別注意它鎖定的是「實例」,也就是 多個執行序都要訪問同一個實例,那才有所定的功能;如果多個執行序訪問不同的實例,那就沒有同步的效果


// 同步方法
synchronized void addData() {
    // TODO:
}

// 區塊同步,鎖的物件同上,下一小節會說明…
void addData() {
    synchronized(this) {
        // TODO:
    }
}

--實作--

上面的範例 15 個太多 (不方便觀察),改為 5 個;可以看到使用兩個 Thread 訪問同一個物件的方法,會依照順序訪問 !

synchronized 同步區塊:指定同步物件

synchronized 除了上述的 this 物件可以鎖定之外,也允許我們指定一個物件來鎖定(也可以是 this,因為自身就是一個物件),持有此物件的 Thread 才可執行同步的函數

● 透過指定物件而不使用 this 的好處在於,可以更精細的去調整同步,可以有效的提高效率

也就是 可以在一個物件中使用多個「物件(也就是鎖)」,而不是全部執行序都搶 this 這把鎖

synchronized 指定 this 物件來鎖定,效果跟「synchronized 方法」是一樣的


public void addData() {
    synchronized(this) {
        //TODO:
        }
    }
}

synchronized 物件指定自己創建物件來鎖定


private Object o = new Object();    // 一定是實例化,不實例化無法作為 key
// or 
// private byte[] b = new byte[0];    // byte[] array 比 Object 需要更小的範圍

public void addData() {
    synchronized(this) {
        //TODO:
        }
    }
}

● 但這個 物件(鎖)一定要實例化,不可以為 null,否則會報錯,因為 synchronized 鎖的是一個物件

--實作--

接下來的程式,我們再次強調「鎖定區塊」的特性

看看區塊同步鎖,是否只同步區塊內部(鎖定的區域),而非區塊內的程式不同不


public class HelloWorld {

    public static void main(String []args){

        HelloWorld h = new HelloWorld();

        new Thread(
                h::testPrint
        ).start();

        new Thread(
                h::testPrint
        ).start();
    }

    private void testPrint() {
        synchronized(this) {    // 同步區塊開始
            for(int i = 0; i <5; i++) {
                System.out.println(Thread.currentThread().getName() + ", test: " + i);
            }
        } // 同步區塊結束
        
        
        // 故意不同步一行程式
        System.out.println("\n" + Thread.currentThread().getName() + ", Finish");
    }
}

從下圖可見,可以看到 for 回圈內的行為會同步,但是 最後的 println 資訊則是 Thread 互搶!因為這行程式沒有被同步!!

synchronized 同步 - 靜態方法 / 靜態物件 / 類

● 由於 靜態物件在 JVM 虛擬機中只存在一個物件,所以是指同一把鎖,同步 Class 也是相同的意思,因為 Class 在虛擬機中也只存在一個物件

想解解 Class 最好認識一下 ClassLoader,ClassLoder 知識請點連結

JVM 內存模型中,靜態方法、物件物件、類 (Class) 都是存在不同地方

目標內存模型儲存位置JVM 中物件數量是否單一
靜態方法方法區Y
靜態物件靜態變量區Y
物件(instance堆區N

// 同步靜態方法
public synchronized static void addData() {
    for(int i = 0; i < 15; i++) {
        x++;
        System.out.println(Thread.currentThread().getName() + ", x: " + x);
    }
}

// 同步靜態物件
private static int x = 0;
private static Object o = new Object();

public void addData() {
    synchronized(o) {
        for(int i = 0; i < 15; i++) {
            x++;
            System.out.println(Thread.currentThread().getName() + ", x: " + x);
        }
    }
}

// 同步 class類物件
public void addData() {
    synchronized(TestSynch.class) {
        for(int i = 0; i < 15; i++) {
            x++;
            System.out.println(Thread.currentThread().getName() + ", x: " + x);
        }
    }
}

● 範例:創建了兩個物件 s1 & s2,用不同線程 t1 & t2 訪問不同物件

同步 靜態方法:由於是靜態方法,所有是所有物件共用的方法,所以不同物件也仍會做等待的行為

同步 靜態物件:其實仍相同,由於使用 static 物件同步,所以可以達到相同的效果

同步 Class:同上,由於在 JVM 內一個物件只會有一個 Class 物件,所以可以等同於同步靜態物件


多執行序之間協做

一個任務可以交給多個 Thread 運行,如果操作得當可以加快運行速度

wait 方法與 notify / notifyAll

● 如同開關訊號,可用來等待或通知,等待或通知方是使用 同一把鎖;這三個方法必須 使用在 synchronized 之下 (同把鎖)

方法說明
wait()釋放物件鎖,並讓 Thread 進入等待狀態
wait(int)預設單位為 ms,時間過後喚醒
wait(long, int)會釋放物件鎖,等待附加時間條件
notify()通知隨機一個持有鎖的物件,從 wait 到 work 狀態
notifyAll()通知所有持有鎖的物件,從 wait 到 work 狀態

● 注意:喚醒是透過物件(鎖)來喚醒,並且喚醒時(notify/notifyAll)是要同一個鎖的物件來喚醒,不同物件(鎖)不能被通知


public class startThread1 {
    public static void main(String[] args) {
        TestClass t = new TestClass("Alien");
        //t.run();		"1. "
        t.start();

        for(int i = 0; i < 17; i++) {
            t.setTime(i);

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

class TestClass extends Thread {

    private String name;
    private int time = 0;

    TestClass(String name) {
        this.name = name;
    }

    void setTime(int time) {
        synchronized(this) {	// "2: "
            this.time = time;
            System.out.println("Now time is " + time);
            this.notifyAll();
        }
    }

    @Override
    public void run() {
        synchronized(this) {        // "3: "
            while(time < 8) {	
                try {
                    System.out.println("Before wait");
                    this.wait(500);    // "4:"
                    System.out.println("After wait");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            /*
            if(time < 8) {		// "5: "
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            */
            System.out.println("Till work time, "+ name + " go to work");	
        }
    }
}

A. 使用 run() 會變成排隊執行main Thread wait work Thread),導致程式無法正常運作

B. 必須持有「同一把物件(鎖)」,如果持有不同鎖則無法正常喚醒目標物件;this 是指 TestClass 物件鎖(並非類鎖)

C. Java 的每一個物件都可以作為鎖 (Object)

D. wait() 內如果設置時間 (單位預設為ms),就是限制等待時間,等待時間一過就自動 喚醒;如下圖可以看到,1s 喚醒一次,但 wait 只休息 500ms,所以在 while 內喚醒了 2 次,所以判斷了兩次 (time 相同)

使用 wait() 要搭配 while() 判斷式

使用 if,可以看出 notify 後會從 wait 休眠的行數 繼續執行

鎖的釋放時機

● 鎖的釋放時機:一般來說有以下幾個時機會釋放鎖

● 執行完同步程式碼區塊

出現例外狀況(拋出異常)導致執行序被終止,鎖就會自動釋放(這很重要,避免程序造成死鎖無法正常運行)

Object#wait 方法:這個方法很特別,它會將當前執行緒掛起(Blocking),並且釋放鎖,讓其他執行序執行該方法!

● Thread 的 sleep 方法 & Object 的 wait 方法兩者個區別

比較點Thread#sleepObject#wait
釋放 CPU 資源YY
重入時,是否釋放鎖(重入鎖下個小節說明)不釋放釋放鎖
是否需要喚醒不需要,只會睡眠規定時間可以持續等待直到被喚醒
區域性必須在 synchronized 區塊內使用

ReentrantLock 機制:認識更多不同的鎖

在 Java 中,ReentrantLock 是一種提供 可重入鎖reentrant lock)功能的鎖實現,屬於 java.util.concurrent.locks

ReentrantLock 提供了比 synchronized 關鍵字更靈活的鎖機制,允許更細粒度的鎖控制,並提供了一些額外的功能

ReentrantLock 的主要特性

A. 可重入性:如果一個執行序已經獲得了鎖,可以再次獲得鎖而不會被阻塞(synchronized 也同樣是可重入)

B. 公平鎖:ReentrantLock 可以配置為公平鎖,確保等待時間最長的執行序最先獲得鎖

默認情況下是非公平鎖,因為非公平鎖的效能較加

C. 可中斷鎖:等待鎖的執行序可以被中斷

D. 嘗試獲取鎖:可以嘗試在獲取鎖時設置超時,防止長時間等待

E. 提供條件變量:ReentrantLock 可以產生多個 Condition 對象,實現更複雜的執行序間同步

認識可重入鎖

● 鎖是否可重入的行為在「遞歸」的程式設計中相當重要;在「不可重入鎖」中使用遞歸會造成程式卡死… 而在「可重入鎖」遞歸調用時,可以重新獲得鎖,再次進入執行程式

遞歸的程式設計概念如下


// 概念程式

synchronized int showData(int x) {
    if(x <= 0) {
        return 0;
    }
    return 1 + showData(x - 1);        // 遞歸呼叫,重入同步方法
}

synchronized 默認可支持重入鎖,如果非重入鎖,就會發生死鎖,因為一直在等待開鎖,使用 遞歸就可證明 synchronized 是重入鎖


synchronized int showData(int x) {
    System.out.println("x: " + x);
    x--;
    if(x == 0) {
        System.out.println("Reach zero");
        return x;
    }
    return showData(x);    // 遞歸定調用
}

認識顯式鎖 & 隱式鎖

隱式鎖synchronized 是隱式鎖又稱為內置鎖,因為它的加鎖、解鎖功能都不會顯示在程式中,可掌控度較低,但使用起來較為簡單

使用 synchronized 關鍵字時,鎖的獲取和釋放是由 Java 虛擬機自動處理的


public synchronized void someMethod() {
    // 此方法由隱式鎖保護
}

public void someMethod() {
    synchronized (this) {
        // 這段代碼由隱式鎖保護
    }
}

當執行序進入 synchronized 區塊或方法時,自動獲取鎖;當執行序離開 synchronized 區塊或方法時,自動釋放鎖

隱式鎖ReentrantLock 是顯式鎖,因為它的加鎖、解鎖都要開發者自己來操作,所以 把解鎖放在 finally 中很重要

明確的加鎖和解鎖:使用 ReentrantLock 時,需要明確地調用 lock() 方法來獲取鎖,並在不需要鎖時調用 unlock() 方法來釋放鎖…


private final ReentrantLock lock = new ReentrantLock();

public void someMethod() {
    lock.lock();
    try {
        // 這段代碼由顯式鎖保護
    } finally {
        lock.unlock();
    }
}

在 try 區塊內進行需要同步的操作,並在 finally 區塊中確保鎖被釋放,即使在出現異常的情況下也能保證鎖被釋放,避免死鎖

高控制度:ReentrantLock 提供了更多功能和更高的控制度,比如可中斷鎖、嘗試獲取鎖以及公平鎖的支持,使得其在需要更複雜同步機制的情況下更具優勢

條件變量:ReentrantLock 提供了 Condition 物件,用於實現更複雜的執行序間協調


private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private boolean ready = false;

public void awaitMethod() throws InterruptedException {
    lock.lock();
    try {
        while (!ready) {
            condition.await();
        }
        // 處理 ready 為 true 時的情況
    } finally {
        lock.unlock();
    }
}

public void signalMethod() {
    lock.lock();
    try {
        ready = true;
        condition.signalAll();
    } finally {
        lock.unlock();
    }
}

ReentrantLock 常用方法

● synchronized & ReentrantLock 鎖的差異如下表

synchronizedReentrantLock解釋
synchronized()lock()獲取鎖
沒辦法tryLock()用非阻塞的方法,嘗試獲取鎖,並返回
沒辦法lockInterruptibly()跟 lock 的差別在,獲取鎖的過程中可以響應中斷
自動釋放unlock()釋放鎖
wait()await()等待並釋放鎖
notify()signal()隨機喚醒持有鎖的物件
notifyAll()signalAll()喚醒全部持有鎖的物件

ReentrantLock 使用、Condition 條件控制

● ReentrantLock 也可以達到跟 synchronized 相同的同步效果,而要保障的安全同步操作區需要在獲取鎖(lock)、釋放鎖(unlock)之間執行

● 另外說到喚醒,ReentrantLock 自身就可以喚醒鎖… 而且 ReentrantLock 也提供條件控制類 Condition 來達到更細節的控制

ReentrantLock 可以取得多個 Condition 物件,未來可以用在 不同的條件喚醒,使用 一個 condition 對應一個鎖的條件可達到更精準的控制(對於效能也有一定的改善)

Conditoin 的操作必須在 lock & unlock 之間


class TestRETL implements Runnable {

    private ReentrantLock lock = new ReentrantLock();
    private Condition c = lock.newCondition();
    private int a = 0;

    public void compareData() {
        lock.lock();
        try {
            while(a < 8) {
                try {
                    System.out.println("before await");
                    c.await();
                    System.out.println("after await");;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            lock.unlock();
        }
        System.out.println("a is 10");
    }

    @Override
    public void run() {
        compareData();
    }

    public void addValue() {
        lock.lock();
        try {
            a++;
            c.signal();
        } finally {
            lock.unlock();
        }
    }
}

--實做--

認識公平鎖 & 非公平鎖

● 公平鎖就是依照時間去排序,去執行 (公平鎖的獲取是順序的)

● 非公平鎖的的獲取是「搶順序

● ReentrantLock 有一個構造函數,可以控制是否是公平鎖 (ReentrantLock 預設是非公平鎖,synchronized 預設就是非公平鎖)

在一般開發中較少使用公平鎖,如果有需要,也可以使用 ReentrantLock 獲得公平鎖


Lock lock = new ReentrantLock(true);    // 設置為公平鎖

非公平鎖的效率較高,為甚麽?

假設喚醒執行序要 1000us 休眠需要 1000us,現在有 A、B、C 執行序,A 搶到鎖(假設鎖的流程 A-B-C),B 休眠 3s,C 休眠 1s

A. 依照公平鎖,執行順序就是 A-B-C,C 醒後 1000us 會在休眠 1000us,這樣 C 就要喚醒在睡眠花 2000us (因為要等 B 結束)

B. 如果是非公平鎖,執行順序就是先醒 C 的可先搶到資源,並在B 醒之前釋放鎖,這樣效率更高

認識死鎖 & 活鎖

● 死鎖指的是,兩個以上的執行序(m >= 2), 搶奪兩個以上的資源(n >= 2,並且死鎖在學術上也有 4 個定義,如下所述

A. 請求 & 持有 : 當一個執行序持有一個資源後將其保持,並開始請求下一個資源

B. 互斥 : 當一個資源只能有一個執行序持有,當持有後就會鎖住不讓其他執行序獲取該資源

C. 不剝奪 : 當一個執行序獲得資源後,在為完成任務之前是不能被剝奪權力的,只能透過該資源自己釋放

D. 環狀等待 : A 等待 B 釋放資源,但 B 又在等 A 釋放資源

而這些死鎖上的定義也有相對的處理方式

狀態處理方式
請求 & 持有執行序運行前先獲取所有資源,沒有獲得所需全部資源則不運行
互斥修改獨佔資源改為虛擬資源,類似於指標物件,但大部分已無法修改
不剝奪當一個執行序持有一個資源後,在去獲取另一個物件失敗時,就連之前的資源一起釋放
環狀等待所有進程只能按照編號申請資源

解決死鎖的關鍵在,拿鎖的順序一致,以下我們來試試看死鎖、解除死鎖的方式

從下面的範例可以看它們 取鎖的順序是相反不同的(兩個執行序對 Lock_1Lock_2 兩個鎖的順序不同),所以會導致死鎖,如果順序相同就不會產生死鎖 (如果改為相同順序就可以正常取鎖)


// 死鎖範例

public class DeadLock {
    private static Object Lock_1 = new Object();
    private static Object Lock_2 = new Object();


    public static void main(String[] args) {
        new AlienTask().start();
        new KyleTask().start();
        System.out.println("Both Finish Task");
    }

    private static class AlienTask extends Thread {
        @Override
        public void run() {
            synchronized(Lock_1) {
                System.out.println("Alien get Lock_1");
                try {
                    Thread.sleep(100);	// 睡眠 100ms 讓其他執行序搶
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized(Lock_2) {
                    System.out.println("Alien get Lock_2");
                }
            }
        }
    }

    private static class KyleTask extends Thread {
        @Override
        public void run() {
            synchronized(Lock_2) {
                System.out.println("Kyle get Lock_2");
                try {
                    Thread.sleep(100);	// 睡眠 100ms 讓其他執行序搶
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized(Lock_1) {
                    System.out.println("Kyle get Lock_1");
                }
            }
        }
    }

}

● 如果我們把鎖的順序對調,也就是將兩個執行序取鎖的順序調整為一樣順序,那就可以解開這個死鎖

活鎖:代表 不斷的嘗試拿取鎖,並且當無法獲得鎖時就釋放鎖,範例如下…


// 活鎖範例

public class AliveLock {
    public static Lock lock_1 = new ReentrantLock();
    public static Lock lock_2 = new ReentrantLock();

    public static void main(String[] args) {
        new AlienTask_1().start();
        new KyleTask_1().start();
    }

    public static class AlienTask_1 extends Thread {
        @Override
        public void run() {
            while(true) {
                if(lock_1.tryLock()) {
                    System.out.println("Alien get One Lock");
                    try {
                        if(lock_2.tryLock()) {
                            try {
                                System.out.println("Alien get Two Lock");
                                break;
                            } finally {
                                lock_2.unlock();
                            }
                        }
                    } finally {
                        lock_1.unlock();	// 獲取 lock_2 不成功連 lock_1 都釋放
                    }
                } else {
                    System.out.println("Alien get Key failed");
                }
            }
        }
    }

    public static class KyleTask_1 extends Thread {
        @Override
        public void run() {
            while(true) {

                if(lock_2.tryLock()) {
                    System.out.println("Kyle get One Lock");
                    try {
                        if(lock_1.tryLock()) {
                            try {
                                System.out.println("Kyle get Two Key");
                                break;
                            } finally {
                                lock_1.unlock();
                            }
                        }
                    } finally {
                        lock_2.unlock();
                    }
                } else {
                    System.out.println("Kyle get Key failed");
                }	
            }
        }
    }
}

採用嘗試拿鎖的機制,Lock's tryLock 方法等等

--範例--

樂觀鎖 & 悲觀鎖

悲觀鎖synchronized 即為悲觀鎖,不管有沒有搶資源都依率鎖住,這樣會導致效能下降 (好像總有人要跟它搶鎖,不管 3721 先鎖住資源就對了)

樂觀鎖:像是 CAS 就是樂觀鎖 (下面會在介紹),複製一份副本,先進行作業,如果比對後發現不同於原來副本,則再次複製,一直循環到成功為止

以效率來說是 樂觀鎖的效率會比較高

因為當執行序休眠需要 10000 ~ 20000 個指令時間,而 cpu 假設執行一個指令需要 0.6u 時間,那休眠 + 喚醒一次所花的時間,也就是 20000 * 0.6u * 2 = 24ms當執行一次作業不需要這麼長時間時就是樂觀鎖效率較高


CAS 原子操作

全名是 Compare And Swap(比較和交換),這是 使用 CPU 的特殊指令集,它會確保該操作是連貫動作「不可再切割」(有就是動作的最小單位)

使用 CAS 操作時,每個執行序都會取得目標值的「副本」,在進行操作後會先比較原來的值是否為當初複製的值,如果不是則重新複製,重新操作(樂觀鎖)

效率由低至高:Synchronized < ReentrantLock < CAS

注意 CAS 的問題

● CAS 主要要注意三個問題,在考量這三個問題後再決定是否要用 CAS、或是直接改為同步

A. ABA 問題 : CAS 會在放入數值後比對,如果數值如同剛複製的值就設定,否則就再次取值,要考慮到中途可能已經被修改過,然後再次改過來

簡單來說就是中間被修改過也不會知道

複製值 A,執行操作到 C,放回去比對,也比對為 A 可設定 (但是必須考量到,比對的 A 可能中間已經被修改過了 A -> B -> A)

B. 循環時間開銷大 : 如果一直設定失敗會造成 CPU 負擔,並且花費時間也長,需要衡量

C. 只能保證原子操作 : 如果操作 2 個以上的元素則不保證這 2 個元素都是原子操做,但是可以將 2 個以上的元素包裝成為一個類,對該類進行操作就可以保證原子性

JDK 中的原子操作

● JDK 提供的 CAS 操作主要分為三種,從它們的 Function name 就可以看出要它是如何操作的,如下表所示…

功能Function 舉例
AtomicInteger對 int 進行原子操做、當然也有 AtomicBoolean、AtomicLong...等等addAndGet(int <輸入變數相加後返回>)getAndSet(設置新值再返回舊值)compareAndSet(int<期望值>, int<新值>)getAndIncrement(內部變數加一並返回)
AtomicIntegerArray以原子方式更新數組addAndGet(int<引索>, int<數值相加並返回>)compareAndSet(int<引索>, int<期望值>, int<新值>)
AtomicReference原子數據操作類compareAndSet 比較&交換
AtomicStampedReference上面有提到 ABA 的問題就可以用這個解決,該方法是使用 int 來計數原子的操作次數-
AtomiMarkableReference同上方法功能,不過較為簡單,使用 boolean mark,只關心是否有被動過-

A. AtomicInteger 的使用範例


public class AtomicNormal {

    public static void main(String[] args) {
        Thread[] ts = new Thread[3];
        for(int i = 0; i < 3; i++) {
            ts[i] = new Atomic_Task_1();
        }
        for(int i = 0; i < 3; i++) {
            ts[i].start();
        }
    }

    static class Atomic_Task_1 extends Thread {

        static AtomicInteger ai = new AtomicInteger(10);

        @Override
        public void run() {
            int i = ai.getAndAdd(2);		// Like a++,返回舊值
            System.out.println("Name: " + Thread.currentThread().getName() +
                    ", Old value: " + i + ", now value: " + ai.addAndGet(3)); // ++a,返回新值
        }
    }

}

--實作結果--

B. AtomicReference 的使用範例


public class AtomicObject {

    public static void main(String[] args) {
        Thread[] ts = new Thread[3];
        for(int i = 0; i < 3; i++) {
            ts[i] = new Atomic_Task_2(new InfoTable(i, i + 10));
        }
        for(int i = 0; i < 3; i++) {
            ts[i].start();
        }
    }

    static class Atomic_Task_2 extends Thread {
        static AtomicReference<InfoTable> af = new AtomicReference<>(new InfoTable(9527, 24));
        InfoTable temp;

        Atomic_Task_2(InfoTable i) {
            temp = i;
        }

        @Override
        public void run() {
            // 執行到過為止
            while(!af.compareAndSet(af.get(), temp));	// <比較值>  <新值>

            InfoTable now = af.get();
            System.out.println(now.toString());
        }
    }

    private static class InfoTable {
        private long id;
        private int age;

        InfoTable(long id, int age) {
            this.id = id;
            this.age = age;
        }

        @Override
        public String toString() {
            return "id: " + id + ", age: " + age;
        }
    }

}

--實作結果--

C. AtomicStampedReference 的使用範例


public class AtomicStamp {
    static AtomicStampedReference<String> asr = new AtomicStampedReference<>("Pan", 0);

    public static void main(String[] args) throws InterruptedException {

        final int oldStamp = asr.getStamp();
        final String oldRef = asr.getReference();
        System.out.println("old refernece: " + oldRef + ", stamp " + oldStamp + "\n");

        Thread task_1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Task_1" 
                        + ", ref: " + oldRef 
                        + ", Stamp: " + oldStamp + " - "
                        + asr.compareAndSet(oldRef,
                                oldRef + " Hello",
                                oldStamp,
                                oldStamp + 1));
            }
        });

        Thread task_2 = new Thread(new Runnable() {
            @Override
            public void run() {
                String reference = asr.getReference();
                System.out.println("Task_2"
                        + ", ref: " + reference 
                        + ", Stamp:" + asr.getStamp() + " - "
                        + asr.compareAndSet(reference,
                                reference + " World", 
                                oldStamp,
                                oldStamp + 1));
            }
        });

        task_1.start();
        task_2.start();
        Thread.sleep(10);

        System.out.println("\nnow refernece: " + asr.getReference() + ", stamp " + asr.getStamp());
    }
}

--實作結果--


ThreadLocal 執行序隔離

ThreadLocal 是使用執行序來隔離,內部使用了 Map<Key, Value>Key 為執行序,Value 為自己設定的數值可以用來隔離共享變數的操作,讓每個執行序都擁有一個自身變量的操作,不會相互影響

ThreadLocal 本身並不是用來保障同步行為的

它的主要目的是為每個執行序提供獨立的變量副本,從而避免多執行序環境下的變量共享問題… 這種設計可以避免執行序之間的數據競爭,從而簡化多執行序編程中的數據隔離問題

ThreadLocal 簡單範例

● 以下我們就來在多執行序的狀況下,透過 ThreadLocal 隔離變量,讓每個執行序自身安全的操作一個共享變量(正確點來說是共享變量的副本)


// 使用 ThreadLocal

public class ThreadLocalExample {
    // 創建 ThreadLocal,並初始化共享變量
    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(new Task()).start();
        }
    }

    static class Task implements Runnable {
        @Override
        public void run() {
            int value = threadLocal.get();
            value++;
            threadLocal.set(value);
            System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
        }
    }
}

在這個例子中,ThreadLocal 保證每個執行序有自己獨立的 Integer 變量,並且每個執行序修改自己的 Integer 變量而不影響其他執行序

● ThreadLocal 簡單來說就是把共享的變數,拷貝原型後後制到目前所在的執行序中,讓每一個 Thread 都擁有此變數,而該變數互不相干

ThreadLocal 的內部實做是 將靜態變數儲存到 ThreadLocalMap 普通變數,並存於每個 Thread 中每一個 Thread 都訪問自己的 ThreadLocalMap 來操作這個變數(這樣就是安全的操作)

--實做--

image

手做 ThreadLocal 機制

● 自己 實踐一個與 ThreadLocal 相同效果的類,它針對每一個執行序給予執行序自身的變量,來達到相同的安全操作(請注意,這邊不是指安全的同步,而是指安全的隔離操作變量)

其中的重點有兩個

A. 透過 Map 把執行序作為 Key 來保存,並且在設置、取值的時候都透過 Thread.currentThread() 來取出當前的執行序

B. 需要一把鎖,來鎖定對於 setget 的操作,來保證多執行序對於 Map 的同步操作(以下用自己創建鎖的方式)


// 手做 ThreadLocal

import java.util.HashMap;
import java.util.Map;


class MyThreadLocal<T> {
    private final Object lock = new Object();

    private T initVal;
    private Map<Thread, T> maps = new HashMap<>();

    public MyThreadLocal(T initVal) {
        this.initVal = initVal;
    }

    public void set(T t) {
        synchronized (lock) {
            Thread thread = Thread.currentThread();

            maps.put(thread, t);
        }
    }

    public T get() {
        synchronized (lock) {
            Thread thread = Thread.currentThread();

            T value = maps.get(thread);

            if (value == null) {
                return initVal;
            }
            return value;
        }
    }
}

public class HandlerThreadLocal {
    private static MyThreadLocal<Integer> threadLocal = new MyThreadLocal<>(0);

    public static void main(String[] args) {
        for(int i = 0; i < 3; i++) {
            new Thread(new MyHandlerTask()).start();
        }
    }

    static class MyHandlerTask implements Runnable {

        @Override
        public void run() {
            int value = threadLocal.get();
            value++;
            threadLocal.set(value);

            System.out.println(Thread.currentThread().getName() +
                    ", hThreadLocal : " + threadLocal.get());
        }
    }
}

--實做--

image

ThreadLocal 源碼分析

● 從 ThreadLocal 開始分析,會發現 ThreadLocal & Thread 有關係,每一個 Thread 中都會儲存一個 ThreadLocalMap 物件

以下我們來看看 ThreadLocal#get 操作


// ThreadLocal 源碼的 get() 方法

// Thread 元素
ThreadLocal.ThreadLocalMap threadLocals = null;    // "2. "

public T get() {
    //"1. "
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}


// ThreadLocal 內部類 ThreadLocalMap 的內部類 Entry
static class Entry extends WeakReference<ThreadLocal> {
    Object value;

    Entry(ThreadLocal k, Object v) {
        super(k);
        value = v;
    }
}


// ThreadLocal getEntry(ThreadLocal) 方法
private Entry getEntry(ThreadLocal key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    // "3. "
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

A. 從這裡可以看得出來它是使用 currentThread 當作 key,但是 ThreadLocal 並不是使用 Map 來儲存,而是 TheadLocalMap 這個物件

B. 每一個 Thread 物件中都有 ThreadLocalMap 屬性,該物件也不是靜態物件,當該物件為空時就在內部創建 ThreadLocalMap,而 ThreadLocalMap 內部有一個屬性 Entry[] 數組

private Entry[] table;

C. 使用 ThreadLocal 物件去取,代表 一個 ThreadLocal 物件可以存多個數值,而該數值與其它物件無關


public class DemoTest implements Runnable {
    ThreadLocal<Thread> tl = new ThreadLocal<>();

    @Override 
    public void run() {
        tl.set(1);        // Key : tl, Value : 1
        tl.set(2);        // Key : tl, Value : 2
        tl.set(3);        // Key : tl, Value : 3
    }
}


更多的 Java 語言相關文章

Java 語言深入

● 在這個系列中,我們全方位地探討了 Java 語言的各個核心主題,旨在幫助你徹底掌握這門強大的編程語言。無論你是想深入理解 Java 的基礎類型與變數作用域,還是探索異常處理與運算子的細節,這些文章都將為您提供寶貴的知識

深入 Java 物件導向

● 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!幫助你針對物件導向設計有更深入的了解!

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響