主頁 >  其他 > 【進擊面試_03】Java 并發

【進擊面試_03】Java 并發

2021-03-04 10:21:16 其他

1.1 volatile

1.1.1 JMM

? JMM 是什么
??JMM(Java 記憶體模型:Java Memory Model,簡稱 JMM)本身是一種抽象的概念并不真實存在,它描述的是一組規則或規范,定義了程式中各個共享變數的訪問規則,即在虛擬機中將變數存盤到記憶體和從記憶體讀取變數這樣的底層細節,
??根據 JMM 的設計,系統存在一個主記憶體(Main Memory),Java 中所有實體變數都儲存在主存中,對于所有執行緒都是共享的,每個執行緒都有自己的作業記憶體(Working Memory)是私有資料區域,執行緒對變數的操作(讀取賦值等)必須在作業記憶體中進行,首先要將變數從主記憶體拷貝的自己的作業記憶體空間,然后對變數進行操作,操作完成后再將變數寫回主記憶體,不能直接操作主記憶體中的變數,各個執行緒中的作業記憶體中存盤著主記憶體中的變數副本拷貝,不同的執行緒間無法訪問對方的作業記憶體,執行緒間的通信必須通過主記憶體來完成,

在這里插入圖片描述

? JMM 特性

特性說明
可見性一個執行緒對共享變數做了修改之后,其他的執行緒立即能夠感知到該變數的修改,
原子性一個操作不能被打斷,要么全部執行完畢,要么不執行,
有序性JMM 允許指令重排,但不管怎么重排,重排后的指令絕對不能改變原有的串行語意,

1.1.2 volatile 是什么

??volatile 是 Java 提供的一種輕量級的同步機制,volatile 基本滿足了 JMM 要求,保證了可見性、禁止指令重排(有序性)但是不保證原子性,


1.1.3 可見性

/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/23
 * @desc 資源類
 */
public class MyData{
	// 沒有 volatile 修飾時,沒有可見性
	int i = 0;

    public void change() {
        this.i = 100;
    }
}
/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/23
 * @desc Volatile 可見性
 */
public class VolatileDemo {
    public static void main(String[] args) {
        // 資源類
        MyData myData = new MyData();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " 開始操作 ===== MyData");

            try {
                // 模擬操作耗時
                Thread.sleep(300);
                myData.change();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + " 操作完成 ===== MyData");

        }, "volatile").start();


        while (0 == myData.i) {}

        System.out.println(Thread.currentThread().getName() + " 我來康康 i = " + myData.i);
    }
}

在這里插入圖片描述
??運行上述代碼可以發現,程式一致未結束,很明顯是卡在了 while 回圈,mian 執行緒一致認為 MyData 中 的變數 i 的值是 0,所以不會退出回圈,現在我們在變數 i 上加 volatile 關鍵字后再此執行,發現 mian 發現了變數 i 的修改,退出了 while 回圈,
在這里插入圖片描述

1.1.4 原子性

? 不保證原子性

/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/23
 * @desc 資源類
 */
public class MyData {
    volatile int i = 0;

    public void add() {
        i++;
    }
}
/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/23
 * @desc Volatile 不保證原子性
 */
public class VolatileDemo {
    public static void main(String[] args) {
        // 資源類
        MyData myData = new MyData();

        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    myData.add();
                }
            }, "add-" + i).start();
        }

        // mian 與 GC 執行緒
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + " 我來康康 i = " + myData.i);
    }
}

在這里插入圖片描述
??運行上述代碼,輸出結果并非我們所想為 20000,這是因為 volatile 不保證原子性,會導致在執行緒執行程序中有執行緒加塞,例如,add-0 執行緒讀到 i = 1 執行 ++ 操作,add-1 執行緒也讀到 i = 1 執行 ++ 操作,正當二者要寫回主記憶體時,add-0 執行緒阻塞,add-1 執行緒將 i = 2 寫回了主記憶體,正準備通知其他執行緒 i 修改了,add-0 執行緒先一步將 i = 2 也寫回了主記憶體,就造成了丟失,

? 解決方案

/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/23
 * @desc 資源類
 */
public class MyData {
    volatile int i = 0;
	
	// 不推薦使用 synchronized,高射炮打蚊子
    public synchronized void add() {
        i++;
    }
}
/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/23
 * @desc 資源類
 */
public class MyData {
	// 推薦使用 atomic 原子操作介面
    AtomicInteger atomicInteger = new AtomicInteger();

    public void addAtomic() {
        atomicInteger.getAndIncrement();
    }
}

1.3.5 指令重排

? 什么是指令重排
??在虛擬機層面,為了盡可能減少記憶體操作速度遠慢于 CPU 運行速度所帶來的 CPU 空置的影響,虛擬機會按照自己的一些規則將指令重排——即寫在后面的代碼在時間順序上可能會先執行,而寫在前面的代碼會后執行——以盡可能充分地利用 CPU,在硬體層面,與虛擬機層面原因類似,CPU 會將接收到的一批指令按照其規則重排序,只是硬體處理的話,每次只能在接收到的有限指令范圍內重排序,而虛擬機可以在更大層面、更多指令范圍內重排序,

在這里插入圖片描述

??上圖便是匯編指令的執行程序,在某些指令上存在 X 的標志,X 代表中斷的含義,也就是只要有 X 的地方就會導致指令流水線技術停頓,同時也會影響后續指令的執行,可能需要經過 1 個或幾個指令周期才可能恢復正常,那為什么停頓呢?這是因為部分資料還沒準備好,如執行 ADD 指令時,需要使用到前面指令的資料 R1,R2,而此時 R2 的 MEM 操作沒有完成,即未拷貝到存盤器中,這樣加法計算就無法進行,必須等到 MEM 操作完成后才能執行,也就因此而停頓了,其他指令也是類似的情況,

在這里插入圖片描述

??停頓會造成 CPU 性能下降,因此我們應該想辦法消除這些停頓,這時就需要使用到指令重排了,既然 ADD 指令需要等待,那我們就利用等待的時間做些別的事情,如把 LW R4,eLW R5,f 移動到前面執行,畢竟 LW R4,eLW R5,f 執行并沒有資料依賴關系,對他們有資料依賴關系的 SUB R6,R5,R4 指令在 R4,R5 加載完成后才執行的,所以沒有影響,重排后,所有的停頓都完美消除了,指令流水線也無需中斷了,這樣 CPU 的性能也能帶來很好的提升,這就是處理器指令重排的作用,

? 記憶體屏障
??記憶體屏障是 CPU 指令,如果你的欄位是 volatile,Java 記憶體模型將在寫操作后插入一個寫屏障指令,在讀操作前插入一個讀屏障指令,下面是基于保守策略的 JMM 記憶體屏障插入策略:
?? 在每個 volatile 寫操作的前面插入一個 StoreStore 屏障,
?? 在每個 volatile 寫操作的后面插入一個 StoreLoad 屏障,
?? 在每個 volatile 讀操作的前面插入一個 LoadLoad 屏障,
?? 在每個 volatile 讀操作的后面插入一個 LoadStore 屏障,
記憶體屏障,又稱記憶體柵欄,是一組處理器指令,用于實作對記憶體操作的順序限制,
在這里插入圖片描述





1.2 CAS

1.2.1 什么是 CAS

??CAS(Compare and swap)比較和替換是設計并發演算法時用到的一種技術,簡單來說,比較和替換是使用一個期望值和一個變數的當前值進行比較,如果當前變數的值與我們期望的值相等,就使用一個新值替換當前變數的值,AtomicInteger 類中的 compareAndSet 方法就是這種思想,

/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/24
 * @desc CAS
 */
public class CASDemo {

    public static void main(String[] args) {
    	// 創建初始值是 5 的原子型整數
        AtomicInteger atomicInteger = new AtomicInteger(5);
		// 期望當前變數值沒有人動過,依舊是 5 時,將 5 替換為 2021
        System.out.println(atomicInteger.compareAndSet(5, 2021) + " == " + atomicInteger);
		// 期望當前變數值沒有人動過,依舊是 5 時,將 5 替換為 2022
        System.out.println(atomicInteger.compareAndSet(5, 2022) + " == " + atomicInteger);
    }
}

在這里插入圖片描述

1.2.2 CAS 原理

??在前文的 volatile 的原子性中我們使用了 AtomicInteger 類中的 getAndIncrement 方法,為什么他就能保證原子性,我們來看一下原始碼,發現最終在 Unsafe 類中使用了一個 while 回圈,首先呼叫 getIntVolatile 方法根據物件和偏移值獲取到記憶體中的資料,相當于將主存資料復制到本地作業空間,然后呼叫 compareAndSwapInt 方法來判斷期望的值與主存的值是否一致,一致則更新主存值,回傳 true 取反退出回圈,否則繼續回圈,
在這里插入圖片描述

1.2.3 CAS 缺點

① 回圈開銷大:底層使用的是 while 回圈,極限情況可能導致回圈 N 次,性能開銷大
② 只能保證一個共享變數原子操作
③ ABA 問題:一個執行緒速度較快,將 A 改為 B 后又改為 A,其他執行緒一看還是 A 認為沒有人動過,

在這里插入圖片描述

1.2.4 解決 ABA 問題

??ABA 問題的產生是因為有人改過而我不知道,那么改過之后記錄以下不就好了,我們都用過 Git 當我們拉取最新版本的代碼,修改了某個地方提交并推送到遠端后,覺得修改的有問題,還是原來的好,就把修改的地方恢復了再次提交并推送到遠端,這個時候另外一個人肯定知道你改過代碼,因為有版本號,同樣的 Java 也提供了攜帶版本號的原子參考型別 AtomicStampedReference<V>

/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/24
 * @desc CAS
 */
public class CASDemo {

    public static void main(String[] args) {
    	// 攜帶版本號的原子參考型別
        AtomicStampedReference<Integer> integerAtomicStampedReference = new AtomicStampedReference<>(5, 1);


        new Thread(() -> {
            int stamp = integerAtomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + " ? 我初次獲取的版本號:" + stamp);

            // 等待 B 執行緒獲取版本號
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // ABA 操作
            boolean b = integerAtomicStampedReference.compareAndSet(5, 100, stamp, stamp + 1);
            System.out.print(Thread.currentThread().getName() + " ? 5 -> 100 為 " + b);
            System.out.println(", 版本號:" + integerAtomicStampedReference.getStamp());
            
            boolean b1 = integerAtomicStampedReference.compareAndSet(100, 5, stamp + 1, stamp + 2);
            System.out.print(Thread.currentThread().getName() + " ? 100 -> 5 為 " + b1);
            System.out.println(", 版本號:" + integerAtomicStampedReference.getStamp());
        }, "A").start();

        new Thread(() -> {
            int stamp = integerAtomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + " ? 我初次獲取的版本號:" + stamp);

            // 等待 A 執行緒完成 ABA 操作
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            boolean b = integerAtomicStampedReference.compareAndSet(5, 200, stamp, stamp + 1);
            System.out.print(Thread.currentThread().getName() + " ? 5 -> 200 為 " + b);
            System.out.println(", 版本號:" + integerAtomicStampedReference.getStamp());
        }, "B").start();
    }
}

在這里插入圖片描述

1.3 Java 中的鎖

一般我們認為鎖大體分為兩種,樂觀鎖和悲觀鎖,樂觀鎖是一種樂觀思想,即認為讀多寫少,遇到并發寫的可能性低,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,采取在寫時先讀出當前版本號,然后加鎖操作,如果失敗則要重復讀-比較-寫的操作,Java 中的樂觀鎖基本都是通過 CAS 實作的,悲觀鎖是就是悲觀思想,即認為寫多,遇到并發寫的可能性高,每次去拿資料的時候都認為別人會修改,所以每次在讀寫資料的時候都會上鎖,這樣別人想讀寫這個資料就會 block 直到拿到鎖,

1.3.1 公平鎖/非公平鎖

??公平鎖(Fair):多個執行緒按照申請鎖的順序去獲得鎖,執行緒會直接進入佇列去排隊,永遠都是佇列的第一位才能得到鎖,優點是所有的執行緒都能得到資源,不會餓死在佇列中,缺點是吞吐量會下降很多,佇列里面除了第一個執行緒,其他的執行緒都會阻塞,CPU 喚醒阻塞執行緒的開銷會很大,
??非公平鎖(Nonfair):多個執行緒去獲取鎖的時候,會直接去嘗試獲取,獲取不到,再去進入等待佇列,如果能獲取到,就直接獲取到鎖,優點是可以減少 CPU 喚醒執行緒的開銷,整體的吞吐效率會高點,CPU 也不必取喚醒所有執行緒,會減少喚起執行緒的數量,缺點是可能導致佇列中間的執行緒一直獲取不到鎖或者長時間獲取不到鎖,導致餓死,synchronized 是非公平鎖,ReentrantLock 空參構造回傳的是非公平鎖,若是要創建公平鎖則使用 ReentrantLock 有參構造傳入 true,

在這里插入圖片描述


1.3.2 可重入鎖(遞回鎖)

??可重入鎖,也叫做遞回鎖,它的可重入性表現在同一個執行緒可以多次獲得鎖,指的是同一執行緒有內外兩層被同一把鎖鎖住的函式,外層函式獲得鎖之后,進入內層函式時自動獲得鎖,并且不發生死鎖,ReentrantLock 和 synchronized 都是可重入鎖,

/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/24
 * @desc 可重入鎖
 */
public class MyText {

    Lock lock = new ReentrantLock();

    public void method1() {

        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 執行外層函式");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

        method2();
    }

    public void method2() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 執行內層函式");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }
}
/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/24
 * @desc //TODO
 */
public class ReentrantDemo {

    public static void main(String[] args) {
        MyText myText = new MyText();

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                myText.method1();
            },  ((char)(65 + i) + "").start();
        }

    }
}

1.3.3 自旋鎖

??自旋鎖原理非常簡單,如果持有鎖的執行緒能在很短時間內釋放鎖資源,那么那些等待競爭鎖的執行緒就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋,回圈獲取鎖),等持有鎖的執行緒釋放鎖后即可立即獲取鎖,這樣就避免用戶執行緒和內核的切換的消耗, 執行緒自旋是需要消耗 cup 的,說白了就是讓 cup 在做無用功,如果一直獲取不到鎖,那執行緒也不能一直占用 cup 自旋做無用功,所以需要設定一個自旋等待的最大時間, 如果持有鎖的執行緒執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的執行緒在最大等待時間內還是獲取不到鎖,這時爭用執行緒會停止自旋進入阻塞狀態,

/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/24
 * @desc 自旋鎖
 */
public class MyLock {
    AtomicReference<Thread> threadAtomicReference = new AtomicReference<>();

    public void myLock() {
        Thread thread = Thread.currentThread();
        while (!threadAtomicReference.compareAndSet(null, thread)){}
        System.out.println(thread.getName() + " 鎖住了");
    }

    public void myUnLock() {
        Thread thread = Thread.currentThread();
        threadAtomicReference.compareAndSet(thread, null);
        System.out.println(thread.getName() + " 開鎖了");
    }
}
/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/24
 * @desc //TODO
 */
public class SpinLockDemo {

    public static void main(String[] args) {

        MyLock lock = new MyLock();

        new Thread(() -> {
            lock.myLock();
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.myUnLock();
        }, "A").start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            lock.myLock();
            lock.myUnLock();
        }, "B").start();

    }
}

1.3.4 獨占鎖(寫鎖)/共享鎖(讀鎖)

??獨占鎖:獨占鎖也叫排他鎖,是指該鎖一次只能被一個執行緒所持有,如果執行緒 T 對資料 A 加上排他鎖后,則其他執行緒不能再對 A 加任何型別的鎖,獲得排它鎖的執行緒即能讀資料又能修改資料,ReentrantLock 和 synchronized 都是獨占鎖
??共享鎖:共享鎖是指該鎖可被多個執行緒所持有,如果執行緒 T 對資料 A 加上共享鎖后,則其他執行緒只能對 A 再加共享鎖,不能加排它鎖,獲得共享鎖的執行緒只能讀資料,不能修改資料,ReentrantReadWriteLock 讀鎖是共享鎖,寫鎖是獨占鎖,讀鎖的共享可以保證并發讀是高效的,讀寫,寫讀,寫寫是互斥的

/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/24
 * @desc 讀寫鎖
 */
public class MyCache {
    private volatile Map<String, String> map = new HashMap<>();
    private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

    public void put(String key, String value) {
        reentrantReadWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在寫入:" + key);
            Thread.sleep(300);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + " 寫入完成");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantReadWriteLock.writeLock().unlock();
        }
    }

    public void get(String key) {
        reentrantReadWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在讀取:" + key);
            Thread.sleep(300);
            map.get(key);
            System.out.println(Thread.currentThread().getName() + " 讀取完成");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantReadWriteLock.readLock().unlock();
        }
    }
}
/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/24
 * @desc //TODO
 */
public class ReadWriteLockDemo {

    public static void main(String[] args) {

        MyCache myCache = new MyCache();

        for (int i = 0; i < 3; i++) {
            int finalI = i;
            new Thread(() -> {
                myCache.put(finalI + "", "");
            },  ((char)(65 + i) + "")).start();
        }

        for (int i = 0; i < 3; i++) {
            int finalI = i;
            new Thread(() -> {
                myCache.get(finalI + "");
            },  ((char)(65 + i) + "")).start();
        }

    }
}

為什么要加讀鎖?
??讀鎖自然也是為了避免原子性問題,比如一個 long 型引數的寫操作并不是原子性的,如果允許同時讀和寫,那讀到的數很可能是就是寫操作的中間狀態,比如剛寫完前 32位,就被讀到了,





1.4 JUC 并發工具類

1.4.1 CountDownLatch

??CountDownLatch 這個類使一個執行緒等待其他執行緒各自執行完畢后再執行,內部是通過一個計數器來實作的,計數器的初始值是執行緒的數量,每當一個執行緒執行完畢后,計數器的值就 -1,當計數器的值為 0 時,表示所有執行緒都執行完畢,然后等待的執行緒就可以恢復作業了,

/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/24
 * @desc CountDownLatch
 */
public class CountDownLatchDemo {

    public static void main(String[] args) {

        CountDownLatch countDownLatch = new CountDownLatch(3);

        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " 吃完了");
                countDownLatch.countDown();
            }, ((char)(65 + i) + "")).start();
        }

        try {
            // 等待執行緒執行完畢
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + " 收桌子了");
    }
}

在這里插入圖片描述


1.4.2 CyclicBarrier

??CyclicBarrier 的字面意思是可回圈使用(Cyclic)的屏障(Barrier),它要做的事情是,讓一組執行緒到達一個屏障(也可以叫同步點)時被阻塞,直到最后一個執行緒到達屏障時,屏障才會開門,所有被屏障攔截的執行緒才會繼續干活,

/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/24
 * @desc CAS
 */
public class CyclicBarrierDemo {

    public static void main(String[] args) {

        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> System.out.println("召喚神龍"));

        for (int i = 0; i < 7; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " 收集到了龍珠");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, ((char)(65 + i) + "")).start();
        }
    }
}

在這里插入圖片描述


1.4.3 Semaphore

??Semaphore 是計數信號量,Semaphore 管理一系列許可,每個 acquire 方法阻塞,直到有一個許可證可以獲得然后拿走一個許可證;每個 release 方法增加一個許可,這可能會釋放一個阻塞的 acquire 方法,然而,其實并沒有實際的許可這個物件,Semaphore 只是維持了一個可獲得許可證的數量,

/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/24
 * @desc Semaphore
 */
public class SemaphoreDemo {

    public static void main(String[] args) {

        Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < 7; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + " 搶到了車位");
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName() + " 離開了車位");
                }
            }, ((char)(65 + i) + "")).start();
        }
    }
}

在這里插入圖片描述

1.4.4 Exchanger

??Exchanger 是 JDK 1.5 開始提供的一個用于兩個作業執行緒之間交換資料的封裝工具類,簡單說就是一個執行緒在完成一定的事務后想與另一個執行緒交換資料,則第一個先拿出資料的執行緒會一直等待第二個執行緒,直到第二個執行緒拿著資料到來時才能彼此交換對應資料,其定義為 Exchanger<V> 泛型型別,其中 V 表示可交換的資料型別,

/**
 * @author Demo_Null
 * @version 1.0
 * @date 2021/2/24
 * @desc Exchanger
 */
public class ExchangerDemo {

    public static void main(String[] args) {

        Exchanger<String> stringExchanger = new Exchanger<>();

        new Thread(() -> {
            try {
                String data = Thread.currentThread().getName();
                System.out.println(Thread.currentThread().getName() + " 交換前資料:" + data);
                String exchange = stringExchanger.exchange(data);
                System.out.println(Thread.currentThread().getName() + " 交換后資料:" + exchange);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "A").start();

        new Thread(() -> {
            try {
                String data = Thread.currentThread().getName();
                System.out.println(Thread.currentThread().getName() + " 交換前資料:" + data);
                String exchange = stringExchanger.exchange(data);
                System.out.println(Thread.currentThread().getName() + " 交換后資料:" + exchange);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "B").start();
    }
}

在這里插入圖片描述


1.4.5 工具類匯總

在這里插入圖片描述





1.5 AQS

1.5.1 什么是 AQS

??AbstractQueuedSynchronizer 類如其名,抽象的佇列式的同步器,AQS 提供了原子式管理同步狀態、阻塞和喚醒執行緒功能以及佇列模型的簡單框架,Java 中的大部分同步類(Lock、Semaphore、ReentrantLock 等)都是基于 AQS 實作的,AQS 是用來構建鎖或者其它同步器組件的重量級基礎框架及整個 JUC 體系的基石,使用一個 volatile 的 int 型別的成員變數來表示同步狀態,通過內置的 FIFO 佇列來完成資源獲取的排隊作業,將每條要去搶占資源的執行緒封裝成 一個 Node 節點來實作鎖的分配,通過 CAS 完成對 State 值的修改,


1.5.2 AQS 能干嘛

??搶到資源的執行緒可以直接執行業務邏輯,搶占不到資源的執行緒的必然要去排隊等候,AQS 的作業就是將排隊等候安排的明明白白的,AQS 內部維護了一個 CLH 佇列來管理鎖,執行緒會首先嘗試獲取鎖,如果失敗就將當前執行緒及等待狀態等資訊包裝成一個 node 節點加入到同步佇列 sync queue里,當前節點為 head 的直接后繼節點時就會嘗試獲取鎖,如果失敗就會阻塞自己直到自己被喚醒,而當持有鎖的執行緒釋放鎖的時候,會喚醒佇列中的后繼執行緒,

在這里插入圖片描述

CLH(Craig,Landin,and Hagersten)佇列是一個虛擬的雙向佇列,虛擬的雙向佇列即不存在佇列實體,僅存在結點之間的關聯關系,


1.5.3 AQS 原始碼決議(ReentrantLock 為例)

在這里插入圖片描述


??最開始,一切準備就緒,但是沒有任何執行緒進來,好比銀行剛開門沒有任何人開辦理業務,
在這里插入圖片描述

??第一個執行緒 ThreadA 開始呼叫 lock 方法搶占鎖,首先判斷當前 state 是否是 0 空閑狀態,若是則將其設定為 1,然后將當前執行執行緒設定為 ThreadA,就好比第一個顧客進入銀行后獨占唯一的一個視窗開始辦理業務,
在這里插入圖片描述
在這里插入圖片描述

??第二個執行緒 ThreadB 也開始呼叫 lock 方法搶占鎖,發現 state 為 1,有人占了,然后就執行 acquire 方法,acquire 方法呼叫 tryAcquire 方法,tryAcquire 方法又呼叫 nonfairTryAcquire 方法,在 nonfairTryAcquire 方法中判斷當前 state 是否為 0,不為零在判斷當前執行緒是否是正在執行的執行緒,此處由于 ThreadA 仍在執行,所以回傳 false,
在這里插入圖片描述
??然后呼叫 addWaiter 方法,在 addWaiter 方法呼叫 enq 方法,很明顯這個方法里面是一個自旋,第一次由于尾指標 tail 是指向 null 的,所以添加一個空的節點,該節點被稱為哨兵節點,并將 tail 指向哨兵節點;第二次 tail 非空,則將 tail 指向真正的 ThreadB node,并將哨兵節點的 next 也指向該 node,緊接著執行 acquireQueued 方法,該方法里面又是一個自旋,自旋時當前為哨兵節點后第一個節點時會再次嘗試搶占鎖,未搶到會呼叫 shouldParkAfterFailedAcquire 方法,將哨兵節點的 waitStatus 設為 -1 后進行第二次自旋,第二次自旋 shouldParkAfterFailedAcquire 回傳 true,開始執行 parkAndCheckInterrupt 方法,該方法讓 ThreadB 阻塞,
在這里插入圖片描述
在這里插入圖片描述
??第三個執行緒 ThreadC 也開始呼叫 lock 方法搶占鎖,同理將其加入到佇列等待,ThreadB、ThreadC 的這種操作類似于,第二個顧客進入銀行,看到第一個顧客在辦理業務,就走到了候客區,然后再看了一眼第一個顧客,發現還沒有辦理完,然后坐下了,
在這里插入圖片描述

??ThreadA 執行完畢后,呼叫 unlock 釋放鎖,在 unlock 方法中呼叫 release 方法,在 release 方法中呼叫 tryRelease 方法,tryRelease 方法將 state 設為 0 并將當前執行執行緒設定為 null,然后呼叫 unparkSuccessor 方法,在該方法中將 ThreadB 的 waitStatus 設為 0,然后喚醒 ThreadB,

在這里插入圖片描述

??ThreadB 被喚醒后,回傳 false,繼續自旋,這個時候由于沒有執行緒占用,ThreadB 直接獲取到了鎖,然后將當前 ThreadB 節點修改為哨兵節點,原有哨兵節點等待 GC 回收,

在這里插入圖片描述
在這里插入圖片描述



轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/265858.html

標籤:其他

上一篇:【Java自頂向下】ConcurrentHashMap面試題(2021最新版)

下一篇:一文帶你識別移動端主流加固的方案

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more