JUC中的原子操作類及其原理

来源:https://www.cnblogs.com/wyq1995/archive/2020/01/30/12240777.html
-Advertisement-
Play Games

昨天簡單的看了看Unsafe的使用,今天我們看看JUC中的原子類是怎麼使用Unsafe的,以及分析一下其中的原理! 一.簡單使用AtomicLong 還記的上一篇博客中我們使用了volatile關鍵字修飾了一個int類型的變數,然後兩個線程,分別對這個變數進行10000次+1操作,最後結果不是200 ...


  昨天簡單的看了看Unsafe的使用,今天我們看看JUC中的原子類是怎麼使用Unsafe的,以及分析一下其中的原理!

 

一.簡單使用AtomicLong

  還記的上一篇博客中我們使用了volatile關鍵字修飾了一個int類型的變數,然後兩個線程,分別對這個變數進行10000次+1操作,最後結果不是20000,現在我們改成AtomicLong之後,你會發現結果始終都是20000了!有興趣的可以試試,代碼如下

package com.example.demo.study;

import java.util.concurrent.atomic.AtomicLong;

public class Study0127 {

    //這是一個全局變數,註意,這裡使用了一個原子類AtomicLong
    public AtomicLong num = new AtomicLong();

    //每次調用這個方法,都會對全局變數加一操作,執行10000次
    public void sum() {
        for (int i = 0; i < 10000; i++) {
            //使用了原子類的incrementAndGet方法,其實就是把num++封裝成原子操作
            num.incrementAndGet();
            System.out.println("當前num的值為num= "+ num);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Study0127 demo = new Study0127();
        //下麵就是新建兩個線程,分別調用一次sum方法
        new Thread(new Runnable() {
            @Override
            public void run() {
                demo.sum();
            }
        }).start();

        new Thread(new Runnable() {

            @Override
            public void run() {
                demo.sum();
            }
        }).start();    
    }
}

 

二.走近AtomicLong類

  在java中JDK 1.5之後,就出現了一個包,簡稱JUC併發包,全稱就是java.util .concurrent,其中我們應該聽說過一個類ConcurrentHashMap,這個map挺有意思的,有興趣可以看看源碼!還有很多併發時候需要使用的類比如AtomicInteger,AtomicLong,AtomicBoolean等等,其實都差不多,這次我們就簡單看看AtomicLong,其他的幾個類也差不多

public class AtomicLong extends Number implements java.io.Serializable {
    
    //獲取Unsafe對象,上篇博客說了我們自己的類中不能使用這種方式的原因,但是官方的這個類為什麼可以這樣獲取呢?因為本類AtomicLong
    //就是在rt.jar包下麵,本類就是用Bootstrap類載入的,所以就可以用這種方式
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //value這個欄位的偏移量
    private static final long valueOffset;

    //判斷jvm是否支持long類型的CAS操作
    static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();
    private static native boolean VMSupportsCS8();

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicLong.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    //這裡用了volatile使的多線程下可見性,一定要分清楚原子性和可見性啊
    private volatile long value;

    //兩個構造器不多說
    public AtomicLong() {
    }
    public AtomicLong(long initialValue) {
        value = initialValue;
    }

 

  然後我們看看AtomicLong的+1操作,可以看到使用的還是unsafe這個類,只需要看看getAndAddLong方法就可以了

 

  方法getAndAddLong裡面就是進行了CAS操作,可以看成如果同時有多個線程都調用incrementAndGet方法進行+1,那麼同一時間只有一個線程會去進行操作,而其他的會不斷的使用CAS去嘗試+1,每次嘗試的時候都會去主記憶體中獲取最新的值;

 public final long getAndAddLong(Object o, long offset, long delta) {
        long v;
        do {
    //這個方法就是重新獲取主記憶體的值,因為使用了volatile修飾了那個變數,所以緩存就沒用了 v
= getLongVolatile(o, offset);     //這裡就是一個dowhile無限迴圈,多個線程不斷的調用compareAndSwapLong方法去設置值,其實就是CAS,沒什麼特別好說的吧,
    //當某個線程CAS成功就跳出這個迴圈,否則就一直在迴圈不斷的嘗試,這也是CAS和線程阻塞的區別
} while (!compareAndSwapLong(o, offset, v, v + delta)); return v; }
//這個CAS方法看不到,c實現的 public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

   有興趣的可以看看AtomicLong的其他方法,很多都一樣,CAS是核心

 

三.CAS的不足以及認識LongAdder

  從上面的例子中,我們可以知道在多線程下使用AtomicLong類的時候,同一個時刻使用那個共用變數的只能是一個線程,其他的線程都是在無限迴圈,這種迴圈也是需要消耗性能的,如果線程比較多,很多的線程都在各自的無限迴圈中,或者叫做多個線程都在自旋;每個線程都在自旋無數次真的是比較坑,比較消耗性能,我們可以想辦法自旋一定的次數,線程就結束運行了,有興趣的可以瞭解一下自旋鎖,其實就是這麼一個原理,很容易,哈哈哈!

  在JDK8之後,提供了一個更好的類取代AtomicLong,那就是LongAdder,上面說過同一時間只有一個線程在使用那個共用變數,其他的線程都在自旋,那麼如果可以把這個共用變數拆開成多個部分,那麼是不是可以多個線程同時可以去操作呢?然後操作完之後再綜合起來,有點分治法的思想,分而治之,最後綜合起來。

  那麼我們怎麼把那個共用變數拆成多個部分呢?

  在LongAdder中是這樣處理的,把那個變數拆成一個base(這個是long類型的,初始值為0)和一個Cell(這個裡面封裝了一個long類型的值,初始值為0),每個線程只會去競爭很多Cell就行了,最後把多個Cell中的值和base累加起來就是最終結果;而且一個線程如果沒有競爭到Cell之後不會傻傻的自旋,直接想辦法去競爭下一個Cell;

  下圖所示

 

 

四.簡單使用LongAdder

  用法其實和AtomicLong差不多,有興趣的可以試試,最後的結果始終都是20000

package com.example.demo.study;

import java.util.concurrent.atomic.LongAdder;

public class Study0127 {

    //這裡使用LongAdder類
    public LongAdder num = new LongAdder();

    //每次調用這個方法,都會對全局變數加一操作,執行10000次
    public void sum() {
        for (int i = 0; i < 10000; i++) {
            //LongAdder類的自增操作,相當於i++
            num.increment();
            System.out.println("當前num的值為num= "+ num);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Study0127 demo = new Study0127();
        //下麵就是新建兩個線程,分別調用一次sum方法
        new Thread(new Runnable() {
            @Override
            public void run() {
                demo.sum();
            }
        }).start();

        new Thread(new Runnable() {

            @Override
            public void run() {
                demo.sum();
            }
        }).start();    
    }
}

 

五.走進LongAdder

  從上面可以看到base只能是一個,而Cell可能有多個,而且Cell太多了也是很占記憶體的,所以一開始的時候不會創建Cell,只有在需要時才創建,也叫做惰性載入。

  我們可以知道LongAdder是繼承自Striped64這個類的

 

  而Striped64類中有三個欄位,cells數組用於存放多個Cell,一個是base不多說,還有一個cellsBusy用來實現自旋鎖,狀態只能是0或1(0表示Cell數組沒有被初始化和擴容,也沒有正在創建Cell元素,反之則為1),在創建Cell,初始化Cell數組或者擴容Cell數組的時候,就會用到這個欄位,保證同一時刻只有一個線程可以進行其中之一的操作。

 

  1.我們簡單看看Cell的結構

    從下麵代碼中可以很清楚的看到所謂的Cell就是對一個long類型變數的CAS操作

@sun.misc.Contended //這個註解的作用是為了避免偽共用,至於什麼偽共用,後面有機會再說說
static final class Cell {
    //每個Cell類中就是這個聲明的變數後期要進行累加的
    volatile long value;
    //構造函數
    Cell(long x) { value = x; }
    //Unsafe對象
    private static final sun.misc.Unsafe UNSAFE;
    //value的偏移量
    private static final long valueOffset;
    //這個靜態代碼塊中就是獲取Unsafe對象和偏移量的
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> ak = Cell.class;
            valueOffset = UNSAFE.objectFieldOffset
                (ak.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
     //CAS操作,沒什麼好說的
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }
}

 

 

  2.LongAdder類自增方法increment()

  我們可以看到increment()方法其實就是調用了add方法,我們需要關註add方法幹了一些什麼;

 

 

 

 

 

 public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    //這裡的cells是父類Striped64中的,不為空的話就保存到as中,然後調用casBase方法,就是CAS給base更新為base+x,也就是每次都新增x,
    //在這裡由於add(1L)傳入的參數是1,也就是每次就是加一
    //如果CAS成功之後就不說了,就完成操作了,如果CAS失敗,則進入到裡面去
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        //這個if判斷條件賊長,我們把這幾個條件分為1,2,3,4部分,前三部分都是用於決定線程應該訪問Cell數組中哪一個Cell元素,最後一個部分用於更新Cell的值
        //如果第1,2,3部分都不滿足,也就是說Cell數組存在而且已經找到了確定的Cell元素,那就到第四部分,更新對應的Cell中的值(在Cell類中的cas方法已經看過了)
        //如果第1,2,3部分滿足其中一個,那也就是說Cell數組根本就不存在或者線程找不到對應的Cell,就執行longAccumulate方法
        if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x)))
            //後面仔細看看這個方法,這是對Cell數組的初始化和擴容,很有意思
            longAccumulate(x, null, uncontended);
    }
}

//一個簡單的CAS操作
final boolean casBase(long cmp, long val) {
    return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}

  

  對於上面的,有興趣的可以看看是怎麼找到指定的Cell的,在上面的a = as[getProbe() & m]中,其中m=數組的長度-1,其實這裡也是一個取餘的運算,而getProbe()這個方法是用於獲取當前線程的threadLocalRandomProb(當前本地線程探測值,初始值為0),其實也就是一個隨機數啊,然後對數組的長度取餘得到的就是對應的數組的索引,首次調用這個方法是數組的第一個元素,如果數組的第一個元素為null,那麼就說明沒有找到對應的Cell;

  對於取餘運算,舉個簡單的例子吧,我也有點忘記了,比如隨機數9要對4進行取餘,我們可以9&(4-1)=9&3=1001&0011=1,利用位運算取餘瞭解一下;

  現在我們重點看看longAccumulate方法,代碼比較長,單獨提取出來看看

  3.longAccumulate方法

//此方法是對Cell數組的初始化和擴容,註意有個形參LongBinaryOperator,這是JDK8新增的函數式編程的介面,函數簽名為(T,T)->T,這裡傳進來的是null
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
    int h;
    //初始化當前線程的threadLocalRandomProbd的值,也就是生成一個隨機數
    if ((h = getProbe()) == 0) {
        ThreadLocalRandom.current(); // force initialization
        h = getProbe();
        wasUncontended = true;
    }
    boolean collide = false;                // True if last slot nonempty
    for (;;) {
        Cell[] as; Cell a; int n; long v;
        //這裡表示初始化完畢了
        if ((as = cells) != null && (n = as.length) > 0) {
            //這裡表示隨機數和數組大小取餘,得到的結果就是當前線程要匹配到的Cell元素的索引,如果索引對應在Cell數組中的元素為null,就新增一個Cell對象扔進去
            if ((a = as[(n - 1) & h]) == null) {
                //cellsBusy為0,表示當前Cell沒有進行擴容、初始化操作或者正在創建Cell等操作,那麼當前線程可以對這個Cell數組為所欲為
                if (cellsBusy == 0) {       // Try to attach new Cell
                    Cell r = new Cell(x);   // Optimistically create
                    //看下麵的Cell數組初始化,說的很清楚,主要是設置cellsBusy為1,然後將當前線程匹配到的Cell設置為新創建的Cell對象
                    if (cellsBusy == 0 && casCellsBusy()) {
                        boolean created = false;
                        try {               // Recheck under lock
                            Cell[] rs; int m, j;
                            if ((rs = cells) != null &&
                                (m = rs.length) > 0 &&
                                rs[j = (m - 1) & h] == null) {
                                rs[j] = r;
                                created = true;
                            }
                        } finally {
                            //將cellsBusy重置為0,表示此時其他線程又可以對Cell數組為所欲為了
                            cellsBusy = 0;
                        }
                        if (created)
                            break;
                        continue;           // Slot is now non-empty
                    }
                }
                collide = false;
            }


            else if (!wasUncontended)       // CAS already known to fail
                wasUncontended = true;      // Continue after rehash

            //Cell元素存在就執行CAS更新Cell中的值,這裡fn是形參為null
            else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                            fn.applyAsLong(v, x))))
                break;



            //當Cell數組元素個數大於CPU的個數
            else if (n >= NCPU || cells != as)
                collide = false;            // At max size or stale
            //是否有衝突
            else if (!collide)
                collide = true;
            //擴容Cell數組,和上面兩個else if一起看
            //如果當前Cell數組元素沒有達到CPU個數而且有衝突就新型擴容,擴容的數量是原來的兩倍Cell[] rs = new Cell[n << 1];,為什麼要和CPU個數比較呢?
            //因為當Cell數組元素和CPU個數相同的時候,效率是最高的,因為每一個線程都是一個CPU來執行,再來修改其中其中一個Cell中的值
            //這裡還是利用cellsBusy這個欄位,在下麵初始化Cell數組中的用法一樣,就不多說了
            else if (cellsBusy == 0 && casCellsBusy()) {
                try {
                    //這裡就是新建一個數組是原來的兩倍,然後將原來數組的元素複製到新的數組,再改變原來的cells的引用指向新的數組
                    if (cells == as) {      // Expand table unless stale
                        Cell[] rs = new Cell[n << 1];
                        for (int i = 0; i < n; ++i)
                            rs[i] = as[i];
                        cells = rs;
                    }
                } finally {
                    //使用完就重置為0
                    cellsBusy = 0;
                }
                collide = false;
                continue;                   // Retry with expanded table
            }
            //這裡的作用是當線程找了好久,發現所有Cell個數已經和CPU個數相同了,然後匹配到的Cell正在被其他線程使用
            //於是為了找到一個空閑的Cell,於是要重新計算hash值
            h = advanceProbe(h);
        }



        //初始化Cell數組
        //記得上面好像說過cellsBusy這個欄位是能是0或者是1,當時0的時候,說明Cell數組沒有初始化和擴容,也沒有正在創建Cell元素,
        //反之則為1,而casCellsBusy()方法就是用CAS將cellsBusy的值從0修改為1,表示當前線程正在初始化Cell數組,其他線程就不能進行擴容操作了
        //如果一個線程在初始化這個Cell數組,其他線程在擴容的時候,看上面擴容,也會執行casCellsBusy()方法進行CAS操作,會失敗,因為期望的值是1,而不是0
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            boolean init = false;
            try {                           // Initialize table
                if (cells == as) {
                    //這裡首先新建一個容量為2的數組,然後用隨機數h&1,也就是隨機數對數組的容量取餘的方式得到索引,然後初始化數組中每個Cell元素
                    Cell[] rs = new Cell[2];
                    rs[h & 1] = new Cell(x);
                    cells = rs;
                    init = true;
                }
            } finally {
                //初始化完成之後要把這個欄位重置為0,表示此時其他線程就又可以對這個Cell進行擴容了
                cellsBusy = 0;
            }
            if (init)
                break;
        }
     //將base更新為base+x,表示base會逐漸累加Cell數組中每一個Cell中的值
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; // Fall back on using base } }

 

  其實longAccumulate方法就是表示多線程的時候對Cell數組的初始化,添加Cell元素還有擴容操作,還有就是當一個線程匹配到了Cell元素,發現其他線程正在使用就會重新計算隨機數,然後繼續匹配其他的Cell元素去了,沒什麼特別難的吧!別看這個方法很長,就是做這幾個操作

 

六.總結

  這一篇核心就是CAS,我們簡單的說了一下原子操作類AtomicLong的自增,但是當線程很多的情況下,使用CAS有很大的缺點,就是同一時間是會有一個線程在執行,其他所有線程都在自旋,自旋會消耗性能,於是可以使用JDK提供的一個LongAdder類代替,這個類的作用就是將AtomicLong中的值優化為了一個base和一個Cell數組,多線程去競爭的時候,假設線程個數個CPU個數相同,那麼此時每一個線程都有單獨的一個CPU去運行,然後單獨的匹配到Cell數組中的某個元素,如果沒有匹配到那麼會對這個Cell數組進行初始化操作;如果匹配到的Cell數組中的元素正在使用,那麼久判斷是否可以新建一個Cell丟數組裡面去,如果數組已經滿了,而且數組數量小於CPU個數,那麼久進行擴容;擴容結束後,還是匹配到的Cell數組中的位置正在使用,那麼就是衝突,就會重新計算,通過一個新的隨機數和數組的取餘,得到一個新的索引,再去訪問該對應的Cell數組的位置。。。。

  仔細看看還是挺有意思的啊!


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

-Advertisement-
Play Games
更多相關文章
  • 一、故事背景: 1. 今天公司有個項目需求 2. 在前端頁面實現一個倒計時功能 3. 初步設想:後端根據需求規定一個未來的時間,前端根據當前時間進行計算 4. 然後將時間格式化,時分秒的格式 5. 時間倒計時完成,刷新頁面獲取最新的頁面 6. 最後在前端展示;大致是這樣 二、上代碼 <!DOCTYP ...
  • node 的`fs`文檔密密麻麻的 api 非常多,畢竟全面支持對文件系統的操作。文檔組織的很好,操作基本分為文件操作、目錄操作、文件信息、流這個大方面,編程方式也支持同步、非同步和 Promise。 本文記錄了幾個文檔中沒詳細描寫的問題,可以更好地串聯`fs`文檔思路: - 文件描述符 - ... ...
  • 其他設計模式 JavaScript 中不常用 對應不到經典場景 原型模式 行為型 clone 自己,生成一個新對象 java 預設有 clone 介面,不用自己實現 對比 js 中的原型 prototype prototype 可以理解為 es6 class 的一種底層原理 而 class 是實現面 ...
  • 狀態模式 一個對象有狀態變化 每次狀態變化都會觸發一個邏輯 不能總是用 if...else 來控制 示例:交通信號燈的不同顏色變化 傳統的 UML 類圖 javascript 中的 UML 類圖 javascript class State { constructor(color) { this.c ...
  • 迭代器模式 順序訪問一個集合 使用者無需知道集合內部結構(封裝) jQuery 示例 傳統 UML 類圖 javascript 中的 UML 類圖 使用場景 jQuery each 上面的 jQuery 代碼就是 ES6 Iterator ES6 Iterator 為何存在? es6 語法中,有序集 ...
  • 圖解Java設計模式之設計模式七大原則 2.1 設計模式的目的 2.2 設計模式七大原則 2.3 單一職責原則 2.3.1 基本介紹 2.3.2 應用實例 2.4 介面隔離原則(Interface Segregation Principle) 2.4.1 基本介紹 2.4.2 應用實例 2.5 依賴 ...
  • 觀察者模式 發佈&訂閱 一對多 示例:點好咖啡之後坐等被叫 傳統 UML 類圖 javascript 中的 UML 類圖 應用場景 網頁事件綁定 promise jQuery callback nodejs 自定義事件 nodejs 處理文件 其他應用場景 nodejs 中:處理 http 請求,多 ...
  • 外觀模式 為子系統的一組介面提供了提個高層介面 使用者使用這個高層介面 示例:去醫院看病,接待員區掛號,門診,劃價,取藥 UML類圖 場景 設計原則驗證 + 不符合單一職責原則和開放封閉原則,因此謹慎使用,不可濫用 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...