CAS、原子操作類的應用與淺析及Java8對其的優化

来源:https://www.cnblogs.com/CodeBear/archive/2019/03/04/10468848.html
-Advertisement-
Play Games

前幾天刷朋友圈的時候,看到一段話: 如果現在我是傻逼,那麼我現在不管怎麼努力,也還是傻逼,因為我現在的傻逼是由以前決定的,現在努力,是為了讓以後的自己不再傻逼 。話糙理不糙,如果妄想現在努力一下,馬上就不再傻逼,那是不可能的,需要積累,需要沉澱,才能慢慢的不再傻逼。 好了,雞湯喝完。 今天我們的內容 ...


前幾天刷朋友圈的時候,看到一段話:如果現在我是傻逼,那麼我現在不管怎麼努力,也還是傻逼,因為我現在的傻逼是由以前決定的,現在努力,是為了讓以後的自己不再傻逼。話糙理不糙,如果妄想現在努力一下,馬上就不再傻逼,那是不可能的,需要積累,需要沉澱,才能慢慢的不再傻逼。

好了,雞湯喝完。

今天我們的內容是CAS以及原子操作類應用與源碼淺析,還會利用CAS來完成一個單例模式,還涉及到偽共用等。因為CAS是併發框架的基石,所以相當重要,這篇博客是一個長文,請做好準備。

說到CAS,不得不提到兩個專業詞語:悲觀鎖,樂觀鎖。我們先來看看什麼是悲觀鎖,什麼是樂觀鎖。

悲觀鎖,樂觀鎖

第一次看到悲觀鎖,樂觀鎖的時候,應該是在應付面試,看面試題的時候。有這麼一個例子:如何避免多線程對資料庫中的同一條記錄進行修改。

悲觀鎖

如果是mysql資料庫,利用for update關鍵字+事務。這樣的效果就是當A線程走到for update的時候,會把指定的記錄上鎖,然後B線程過來,就只能等待,A線程修改完數據之後,提交事務,鎖就被釋放了,這個時候B線程終於可以繼續做他的事情了。悲觀鎖往往是互斥的:只有我一個人可以進來,其他人都給我等著。這麼做是相當影響性能的。

樂觀鎖

在數據表中加一個版本號的欄位:version,這個欄位不需要程式員手動維護,是資料庫主動維護的,每次修改數據,version都會發生更改。

當version現在是1:

  1. A線程進來,讀到version是1。
  2. B線程進來,讀到version是1。
  3. A線程執行了更新的操作:update stu set name='codebear' where id=1 and version=1。成功。資料庫主動把version改成了2。
  4. B線程執行了更新的操作:update stu set name='hello' where id=1 and version=1。失敗。因為這個時候version欄位已經不是1了。

樂觀鎖其實不能叫鎖,它沒有鎖的概念。

在Java中,也有悲觀鎖,樂觀鎖的概念,悲觀鎖的典型代表就是Synchronized,而樂觀鎖的典型代表就是今天要說的CAS。而說CAS之前,先要說下原子操作類,因為CAS是原子操作類的基石,我們先要看看原子操作類的強大之處,從而產生探究CAS的興趣。

原子操作類的應用

我們先來看看原子操作類的應用。在Java中提供了很多原子操作類,比如AtomicInteger,其中有一個自增方法。

public class Main {
    public static void main(String[] args) {
        Thread[] threads = new Thread[20];
        AtomicInteger atomicInteger = new AtomicInteger();
        for (int i = 0; i < 20; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    atomicInteger.incrementAndGet();
                }
            });
            threads[i].start();
        }
        join(threads);
        System.out.println("x=" + atomicInteger.get());
    }

    private static void join(Thread[] threads) {
        for (int i = 0; i < 20; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

運行結果:
image.png

這就是原子操作類的神奇之處了,在高併發的情況下,這種方法會比Synchronized更有優勢,畢竟Synchronized關鍵字會讓代碼串列化,失去了多線程優勢。

我們再來看個案例:

如果有一個需求,一個欄位的初始值為0,開三個線程:

  1. 一個線程執行:當x=0,x修改為100
  2. 一個線程執行:當x=100,x修改為50
  3. 一個線程執行:當x=50,x修改為60
    public static void main(String[] args) {
        AtomicInteger atomicInteger=new AtomicInteger();
        new Thread(() -> {
            if(!atomicInteger.compareAndSet(0,100)){
                System.out.println("0-100:失敗");
            }
        }).start();

        new Thread(() -> {
            try {
                Thread.sleep(500);////註意這裡睡了一會兒,目的是讓第三個線程先執行判斷的操作,從而讓第三個線程修改失敗
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(!atomicInteger.compareAndSet(100,50)){
                System.out.println("100-50:失敗");
            }
        }).start();

        new Thread(() -> {
            if(!atomicInteger.compareAndSet(50,60)){
                System.out.println("50-60:失敗");
            }
        }).start();

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

運行結果也是一樣的:
image.png

這個例子好像沒有什麼意思啊,甚至有點無聊,為什麼要舉這個例子呢,因為在這裡,我所調用的方法compareAndSet,首字母就是CAS,而且傳遞了兩個參數,這兩個參數是在原生CAS操作中必須要傳遞的,離原生的CAS操作更近一些。

既然原子操作類那麼牛逼,我們很有必要探究下原子操作類的基石:CAS。

CAS

CAS的全稱是Compare And Swap,即比較交換,當然還有一種說法:Compare And Set,調用原生CAS操作需要確定三個值:

  • 要更新的欄位
  • 預期值
  • 新值

其中,要更新的欄位(變數)有時候會被拆分成兩個參數:1.實例 2.偏移地址。

也許你看到這裡,會覺得雲里霧裡,不知道我在說什麼,沒關係,繼續硬著頭皮看下去。

我們先來看看compareAndSet的源碼。

compareAndSet源碼淺析

首先,調用這個方法需要傳遞兩個參數,一個是預期值,一個是新值,這個預期值就相當於資料庫樂觀鎖版本號的概念,新值就是我們希望修改的值(是值,不是欄位)。我們來看看這個方法的內部實現:

 public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

調用了unsafe下的compareAndSwapInt方法,除了傳遞了我們傳到此方法的兩個參數之外,又傳遞了兩個參數,這兩個參數就是我上面說的實例和偏移地址,this代表是當前類的實例,即AtomicInteger類的實例,這個偏移地址又是什麼鬼呢,說的簡單點,就是確定我們需要修改的欄位在實例的哪個位置。知道了實例,知道了我們的需要修改的欄位是在實例的哪個位置,就可以確定這個欄位了。不過,這個確定的過程不是在Java中做的,而是在更底層做的。

偏移地址是在本類的靜態代碼塊中獲得的:

    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

unsafe.objectFieldOffset接收的是Field類型的參數,得到的就是對應欄位的偏移地址了,這裡就是獲得value欄位在本類,即AtomicInteger中的偏移地址。

我們在來看看value欄位的定義:

 private volatile int value;

volatile是為了保證記憶體的可見性。

大家肯定想一探究竟compareAndSwapInt和objectFieldOffset這兩個方法中做了什麼事情,很遺憾,個人水平有限,目前還沒有能力去探究,只知道這種寫法是JNI,會調用到C或者C++,最終會把對應的指令發送給CPU,這是可以保證原子性的。

我們可以看下這兩個方法的定義:

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public native long objectFieldOffset(Field var1);

這兩個方法被native標記了。

我們來為compareAndSwapInt方法做一個比較形象的解釋:

當我們執行compareAndSwapInt方法,傳入10和100,Java會和更底層進行通信:老鐵,我給你了欄位的所屬實例和偏移地址,你幫我看下這個欄位的值是不是10,如果是10的話,你就改成100,並且返回true,如果不是的話,不用修改,返回false把。

其中比較的過程就是compare,修改的值的過程就是swap,因為是把舊值替換成新值,所以我們把這樣的操作稱為CAS。

我們再來看看incrementAndGet的源碼。

incrementAndGet源碼淺析

    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

incrementAndGet方法會調到用getAndAddInt方法,這裡有三個參數:

  • var1:實例。
  • var2:偏移地址。
  • var4:需要自增的值,這裡是1。

getAndAddInt方法內部有一個while迴圈,迴圈體內部根據實例和偏移地址獲得對應的值,這裡先稱為A,再來看看while裡面的判斷內容,JDK和更底層進行通訊:嘿,我把實例和偏移地址給你,你幫我看下這個值是不是A,如果是的話,幫我修改成A+1,返回true,如果不是的話,返回false吧。

這裡要思考一個問題:為什麼需要while迴圈?

比如同時有兩個線程執行到了getIntVolatile方法,拿到的值都是10,其中線程A執行native方法,修改成功,但是線程B就修改失敗了啊,因為CAS操作是可以保證原子性的,所以線程B只能苦逼的再一次迴圈,這一次拿到的值是11,又去執行native方法,修改成功。

像這樣的while迴圈,有一個高大上的稱呼:CAS自旋

讓我們試想一下,如果現在併發真的很高很高,會出現什麼事情?大量的線程在進行CAS自旋,這太浪費CPU了吧。所以在Java8之後,對原子操作類進行了一定的優化,這個我們後面再說。

可能大家對於原子操作類的底層實現,還是比較迷茫,還是不知道unsafe下麵的方法到底是什麼意思,畢竟剛纔只是簡單的讀了下代碼,俗話說“紙上得來終覺淺,絕知此事要躬行”,所以我們需要自己調用下unsafe下麵的方法,來加深理解。

Unsafe

Unsafe:不安全的,既然有這樣的命名,說明這個類是比較危險的,Java官方也不推薦我們直接操作Unsafe類,但是畢竟現在是學習階段,寫寫demo而已,只要不是發佈到生產環境,又有什麼關係呢?

Unsafe下麵的方法還是比較多的,我們選擇幾個方法來看下,最終我們會利用這幾個方法來完成一個demo。

objectFieldOffset:接收一個Field類型的數據,返回偏移地址。
compareAndSwapInt:比較交換,接收四個參數:實例,偏移地址,預期值,新值。
getIntVolatile:獲得值,支持Volatile,接收兩個參數:實例,偏移地址。

這三個方法在上面的源碼淺析中,已經出現過了,也進行了一定的解釋,這裡再解釋一下,就是為了加深印象,我在學習CAS的時候,也是反覆的看博客,看源碼,突然恍然大悟。我們需要用這三個方法來完成一個demo:寫一個原子操作自增的方法,自增的值可以自定義,沒錯,這個方法上面我已經分析過了。下麵直接放出代碼:

public class MyAtomicInteger {

    private volatile int value;

    private static long offset;//偏移地址

    private static Unsafe unsafe;

    static {
        try {
            Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafeField.setAccessible(true);
            unsafe = (Unsafe) theUnsafeField.get(null);
            Field field = MyAtomicInteger.class.getDeclaredField("value");
            offset = unsafe.objectFieldOffset(field);//獲得偏移地址
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void increment(int num) {
        int tempValue;
        do {
            tempValue = unsafe.getIntVolatile(this, offset);//拿到值
        } while (!unsafe.compareAndSwapInt(this, offset, tempValue, value + num));//CAS自旋
    }

    public int get() {
        return value;
    }
}
public class Main {
    public static void main(String[] args) {
        Thread[] threads = new Thread[20];
        MyAtomicInteger atomicInteger = new MyAtomicInteger();
        for (int i = 0; i < 20; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    atomicInteger.increment(1);
                }
            });
            threads[i].start();
        }
        for (int i = 0; i < threads.length; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("x=" + atomicInteger.get());
    }
}

運行結果:
image.png

你可能會有疑問,為什麼需要用反射來獲取theUnsafe,其實這是JDK為了保護我們,讓我們無法方便的獲得unsafe,如果我們和JDK一樣來獲得unsafe會報錯:

    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");//如果我們也以getUnsafe來獲得theUnsafe,會拋出異常
        } else {
            return theUnsafe;
        }
    }

CAS與單例模式

對的,你沒看錯,我也沒寫錯,用CAS也可以完成單例模式,雖然在正常開發中,不會有人用CAS來完成單例模式,但是是檢驗是否學會CAS的一個很好的題目。

public class Singleton {
    private Singleton() {
    }

    private static AtomicReference<Singleton> singletonAtomicReference = new AtomicReference<>();

    public static Singleton getInstance() {
        while (true) {
            Singleton singleton = singletonAtomicReference.get();// 獲得singleton
            if (singleton != null) {// 如果singleton不為空,就返回singleton
                return singleton;
            }
            // 如果singleton為空,創建一個singleton
            singleton = new Singleton();
            // CAS操作,預期值是NULL,新值是singleton
            // 如果成功,返回singleton
            // 如果失敗,進入第二次迴圈,singletonAtomicReference.get()就不會為空了
            if (singletonAtomicReference.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}

註釋寫的已經比較清楚了,可以對著註釋,再好好理解一下。

ABA

compareAndSet方法,上面已經寫過一個demo,大家可以也試著分析下源碼,我就不再分析了,我之所以要再次提到compareAndSet方法,是為了引出一個問題。

假設有三個步驟:

  1. 修改150為50
  2. 修改50為150
  3. 修改150為90

請仔細看,這三個步驟做的事情,一個變數剛開始是150,修改成了50,後來又被修改成了150!(又改回去了),最後如果這個變數是150,再改成90。這就是CAS中ABA的問題。

第三步,判斷這個值是否是150,有兩種不同的需求:

  • 沒錯啊,雖然這個值被修改了,但是現在被改回去了啊,所以第三步的判斷是成立的。
  • 不對,這個值雖然是150,但是這個值曾經被修改過,所以第三步的判斷是不成立的。

針對於第二個需求,我們可以用AtomicStampedReference來解決這個問題,AtomicStampedReference支持泛型,其中有一個stamp的概念。下麵直接貼出代碼:

    public static void main(String[] args) {
        try {
            AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(150, 0);
            Thread thread1 = new Thread(() -> {
                Integer oldValue = atomicStampedReference.getReference();
                int stamp = atomicStampedReference.getStamp();
                if (atomicStampedReference.compareAndSet(oldValue, 50, 0, stamp + 1)) {
                    System.out.println("150->50 成功:" + (stamp + 1));
                }
            });
            thread1.start();

            Thread thread2 = new Thread(() -> {
                try {
                    Thread.sleep(1000);//睡一會兒,是為了保證線程1 執行完畢
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Integer oldValue = atomicStampedReference.getReference();
                int stamp = atomicStampedReference.getStamp();
                if (atomicStampedReference.compareAndSet(oldValue, 150, stamp, stamp + 1)) {
                    System.out.println("50->150 成功:" + (stamp + 1));
                }
            });
            thread2.start();

            Thread thread3 = new Thread(() -> {
                try {
                    Thread.sleep(2000);//睡一會兒,是為了保證線程1,線程2 執行完畢
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Integer oldValue = atomicStampedReference.getReference();
                int stamp = atomicStampedReference.getStamp();
                if (atomicStampedReference.compareAndSet(oldValue, 90, 0, stamp + 1)) {
                    System.out.println("150->90 成功:" + (stamp + 1));
                }
            });
            thread3.start();

            thread1.join();
            thread2.join();
            thread3.join();
            System.out.println("現在的值是" + atomicStampedReference.getReference() + ";stamp是" + atomicStampedReference.getStamp());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Java8對於原子操作類的優化

在進行incrementAndGet源碼解析的時候,說到一個問題:在高併發之下,N多線程進行自旋競爭同一個欄位,這無疑會給CPU造成一定的壓力,所以在Java8中,提供了更完善的原子操作類:LongAdder。

我們簡單的說下它做了下什麼優化,它內部維護了一個數組Cell[]和base,Cell裡面維護了value,在出現競爭的時候,JDK會根據演算法,選擇一個Cell,對其中的value進行操作,如果還是出現競爭,會換一個Cell再次嘗試,最終把Cell[]裡面的value和base相加,得到最終的結果。

因為其中的代碼比較複雜,我就選擇幾個比較重要的問題,帶著問題去看源碼:

  1. Cell[]是何時被初始化的。
  2. 如果沒有競爭,只會對base進行操作,這是從哪裡看出來的。
  3. 初始化Cell[]的規則是什麼。
  4. Cell[]擴容的時機是什麼。
  5. 初始化Cell[]和擴容Cell[]是如何保證線程安全性的。

這是LongAdder類的UML圖:
image.png

add方法:

 public void add(long x) {
        Cell[] cs; long b, v; int m; Cell c;
        if ((cs = cells) != null || !casBase(b = base, b + x)) {//第一行
            boolean uncontended = true;
            if (cs == null || (m = cs.length - 1) < 0 ||//第二行
                (c = cs[getProbe() & m]) == null ||//第三行
                !(uncontended = c.cas(v = c.value, v + x)))//第四行
                longAccumulate(x, null, uncontended);//第五行
        }
    }

第一行:
||判斷,前者是判斷cs=cells是否【不為空】,後者是判斷CAS是否【不成功】 。
casBase做什麼了?

final boolean casBase(long cmp, long val) {
        return BASE.compareAndSet(this, cmp, val);
}

這個比較簡單,就是調用compareAndSet方法,判斷是否成功:

  • 如果當前沒有競爭,返回true。
  • 如果當前有競爭,有線程會返回false。

再回到第一行,整體解釋下這個判斷:如果cell[]已經被初始化了,或者有競爭,才會進入到第二行代碼。如果沒有競爭,也沒有初始化,就不會進入到第二行代碼。

這就回答了第二個問題:如果沒有競爭,只會對base進行操作,是從這裡看出來的。

第二行代碼:
||判斷,前者判斷cs是否【為NULL】,後者判斷(cs的長度-1)是否【大於0】。這兩個判斷,應該都是判斷Cell[]是否初始化的。如果沒有初始化,會進入第五行代碼。

第三行代碼:
如果cell進行了初始化,通過【getProbe() & m】演算法得到一個數字,判斷cs[數字]是否【為NULL】,並且把cs[數字]賦值給了c,如果【為NULL】,會進入第五行代碼。
我們需要簡單的看下getProbe() 中做了什麼:

    static final int getProbe() {
        return (int) THREAD_PROBE.get(Thread.currentThread());
    }

    private static final VarHandle THREAD_PROBE;

我們只要知道這個演算法是根據THREAD_PROBE算出來的即可。

第四行代碼:
對c進行了CAS操作,看是否成功,並且把返回值賦值給uncontended,如果當前沒有競爭,就會成功,如果當前有競爭,就會失敗,在外面有一個!(),所以CAS失敗了,會進入第五行代碼。需要註意的是,這裡已經是對Cell元素進行操作了。

第五行代碼:
這方法內部非常複雜,我們先看下方法的整體:
image.png

有三個if:
1.判斷cells是否被初始化了,如果被初始化了,進入這個if。

這裡面又包含了6個if,真可怕,但是在這裡,我們不用全部關註,因為我們的目標是解決上面提出來的問題。

我們還是先整體看下:

image.png

第一個判斷:根據演算法,拿出cs[]中的一個元素,並且賦值給c,然後判斷是否【為NULL】,如果【為NULL】,進入這個if。

                    if (cellsBusy == 0) {       // 如果cellsBusy==0,代表現在“不忙”,進入這個if
                        Cell r = new Cell(x);   //創建一個Cell
                        if (cellsBusy == 0 && casCellsBusy()) {//再次判斷cellsBusy ==0,加鎖,這樣只有一個線程可以進入這個if
                            //把創建出來Cell元素加入到Cell[]
                            try {       
                                Cell[] rs; int m, j;
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    break done;
                                }
                            } finally {
                                cellsBusy = 0;//代表現在“不忙”
                            }
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;

這就對第一個問題進行了補充,初始化Cell[]的時候,其中一個元素是NULL,這裡對這個為NULL的元素進行了初始化,也就是只有用到了這個元素,才去初始化。

第六個判斷:判斷cellsBusy是否為0,並且加鎖,如果成功,進入這個if,對Cell[]進行擴容。

                     
                    try {
                        if (cells == cs)        // Expand table unless stale
                            cells = Arrays.copyOf(cs, n << 1);
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;  

這就回答了第五個問題的一半:擴容Cell[]的時候,利用CAS加了鎖,所以保證線程的安全性。

那麼第四個問題呢?首先你要註意,最外面是一個for (;;)死迴圈,只有break了,才終止迴圈。

一開始collide為false,在第三個if中,對cell進行CAS操作,如果成功,就break了,所以我們需要假設它是失敗的,進入第四個if,第四個if中會判斷Cell[]的長度是否大於CPU核心數, 如果小於核心數,會進入第五個判斷,這個時候collide為false,會進入這個if,把collide改為true,代表有衝突,然後跑到advanceProbe方法,生成一個新的THREAD_PROBE,再次迴圈。如果在第三個if中,CAS還是失敗,再次判斷Cell[]的長度是否大於核心數,如果小於核心數,會進入第五個判斷,這個時候collide為true,所以不會進入第五個if中去了,這樣就進入了第六個判斷,進行擴容。是不是很複雜。

簡單的來說,Cell[]擴容的時機是:當Cell[]的長度小於CPU核心數,並且已經兩次Cell CAS失敗了。

2.前面兩個判斷很好理解,主要看第三個判斷:

    final boolean casCellsBusy() {
        return CELLSBUSY.compareAndSet(this, 0, 1);
    }

cas設置CELLSBUSY為1,可以理解為加了個鎖,因為馬上就要進行初始化了。

                try {                           // Initialize table
                    if (cells == cs) {
                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        break done;
                    }
                } finally {
                    cellsBusy = 0;
                }

初始化Cell[],可以看到長度為2,根據演算法,對其中的一個元素進行初始化,也就是此時Cell[]的長度為2,但是裡面有一個元素還是NULL,現在只是對其中一個元素進行了初始化,最終把cellsBusy修改成了0,代表現在“不忙了”。

這就回答了
第一個問題:當出現競爭,且Cell[]還沒有被初始化的時候,會初始化Cell[]。
第四個問題:初始化的規則是創建長度為2的數組,但是只會初始化其中一個元素,另外一個元素為NULL。
第五個問題的一半:在對Cell[]進行初始化的時候,是利用CAS加了鎖,所以可以保證線程安全。

3.如果上面的都失敗了,對base進行CAS操作。

如果大家跟著我一起在看源碼,會發現一個可能以前從來也沒有見過的註解:
image.png

這個註解是乾什麼的?Contended是用來解決偽共用的

好了,又引出來一個知識盲區,偽共用為何物。

偽共用

我們知道CPU和記憶體之間的關係:當CPU需要一個數據,會先去緩存中找,如果緩存中沒有,會去記憶體找,找到了,就把數據複製到緩存中,下次直接去緩存中取出即可。

但是這種說法,並不完善,在緩存中的數據,是以緩存行的形式存儲的,什麼意思呢?就是一個緩存行可能不止一個數據。假如一個緩存行的大小是64位元組,CPU去記憶體中取數據,會把臨近的64位元組的數據都取出來,然後複製到緩存。

這對於單線程,是一種優化。試想一下,如果CPU需要A數據,把臨近的BCDE數據都從記憶體中取出來,並且放入緩存了,CPU如果再需要BCDE數據,就可以直接去緩存中取了。

但在多線程下就有劣勢了,因為同一緩存行的數據,同時只能被一個線程讀取,這就叫偽共用了。

有沒有辦法可以解決這問題呢?聰明的開發者想到了一個辦法:如果緩存行的大小是64位元組,我可以加上一些冗餘欄位來填充到64位元組。

比如我只需要一個long類型的欄位,現在我再加上6個long類型的欄位作為填充,一個long占8位元組,現在是7個long類型的欄位,也就是56位元組,另外對象頭也占8個位元組,正好64位元組,正好夠一個緩存行。

但是這種辦法不夠優雅,所以在Java8中推出了@jdk.internal.vm.annotation.Contended註解,來解決偽共用的問題。但是如果開發者想用這個註解, 需要添加 JVM 參數,具體參數我在這裡就不說了,因為我沒有親測過。

這一章的篇幅相當長,幾乎涵蓋了CAS中大部分常見的問題。

併發框架,是非常難學的,因為在開發中,很少會真正用到併發方面的知識,但是併發對於提高程式的性能,吞吐量是非常有效的手段,所以併發是值得花時間去學習,去研究的。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 最近實習生招聘已經開始了,昨天晚上也終於迎來了第一場筆試,筆試的公司是cvte,筆試題總共27題,25道不定項還有2道編程題,雖然出的都是前端題,但是因為之前沒有好好準備,還是很多做的不是很好o(╥﹏╥)o考完試後也總結了一下,考的以node,原型,promise比較多,之前以為自己原型掌握的還可以 ...
  • 大致步驟: 1、寫一個p標簽,指定一個id選擇器,輸入數字! 2、寫一個input標簽,指定type屬性的屬性值為button,創建一個按鈕,加入onclick事件! 3、為p標簽和input標簽指定相關的CSS樣式(可以省略) 4、用js創建一個自加的函數,在函數中用document對象的getE ...
  • 回調函數作為參數傳給另一個函數,所以我們在調用回調函數時,預先並不知道所調用的函數具體是哪個函數,因為我們調用的是一個參數,形如: 普通函數是通過具體的函數名來調用,所有我們在調用普通函數時,預先知道要調用的是哪個函數,形如: ...
  • 彈性容器單行:主軸居中,交叉軸居中。 彈性容器單行;主軸兩端對齊;交叉軸一個為起點邊緣;一個為終點邊緣; 單選;主軸兩端對齊;交叉軸一個為起點邊緣;一個為居中對齊;一個為終點邊緣; 1:彈性視窗設置為多行,交叉軸為兩端對齊 2:每行基礎大小設置為100%,然後每個子行主軸為兩端對齊 在4的基礎上增加 ...
  • align-content 和 align-items : 1:共同點:它們對齊方向為交叉軸 2:不同點:align-content 應用於為 多行 而 align-items:應用於單行。 單行對齊例子: 多行對齊例子 ...
  • js中有三個改變this指針的方法,分別是 apply,call,bind。很多人只知道能改變的this,但是具體的適用場景不是太清楚。我也是遇到坑後不斷的實踐發現了區別。 call ,apply方法: 在Food類中,因為使用了call改變類Product的類的this執向。所以這個時候在Prod ...
  • 高可用 負載均衡(負載均衡演算法) 反向代理 服務隔離 服務限流 服務降級(自動優雅降級) 失效轉移 超時重試(代理超時、容器超時、前端超時、中間件超時、資料庫超時、NoSql超時) 回滾機制(上線回滾、資料庫版本回滾、事務回滾) 高併發 應用緩存 HTTP緩存 多級緩存 分散式緩存 連接池 非同步併發 ...
  • 在上一篇的-負載均衡Robbin中,我們簡單講解到負債均衡的演算法和策略。負載均衡就是分發請求流量到不同的伺服器,以減小伺服器的壓力和訪問效率,但是當負載均衡的某個伺服器或是服務掛掉之後,那麼程式會出現問題麽?接下來Hystrix將會講解 1.1.簡介 Hystix,即熔斷器。 主頁:https:// ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...