不可不知的JUC:原子操作類AtomicLong的原理分析

2021-03-02 後端Dancer

微信公眾號:Zhongger
我是Zhongger,一個在網際網路行業摸魚寫代碼的打工人!
關注我,了解更多你不知道的【Java後端】打工技巧、職場經驗、生活感悟等

一、前言

java.util.concurrent 包裡提供了一系列的原子性操作類,這些類都是使用CAS機制實現的,相當於使用鎖實現原子性操作在性能上有了很大的提高。由於原子性操作類的原理都大致相同,所以本文主要對AtomicLong類的常用方法進行分析。

二、AtomicLong

AtomicLong是原子性遞增或者遞減類,其內部使用Unsafe類來實現。

public class AtomicLong extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 1927816293512124184L;
    //(1).獲取Unsafe類實例
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //(2).存放變量value的偏移量
    private static final long valueOffset;
    //(3).native方法來判斷JVM是否支持Long類型的無鎖CAS
    static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();
    private static native boolean VMSupportsCS8();

    static {
        try {
            //(4).獲取value在AtomicLong中的偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicLong.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    //(5).實際變量值
    private volatile long value;
    public AtomicLong(long initialValue) {
        value = initialValue;
    }

由(1)處代碼,獲取Unsafe類對象的方法,是採用Unsafe.getUnsafe();獲取到的,因為AtomicLong類同Unsafe類一樣,是在rt.jar包下的,它也是是通過BootStrap類加載器進行加載的。可以在IDE中看下AutomicLong類的位置確認一下:


科普一下,這裡的類加載器涉及到JVM的知識。關於類加載器加載與所加載的類的對應關係見下圖


代碼(2)和(4)中獲取value變量在AtomicLong類中的偏移量
代碼(5)中的value變量被聲明為volatile,這是為了在多線程下保證內存可見性,value是具體存放計數的變量。二、AtomicLong中的主要方法1、遞增和遞減操作

    //(6).調用unsafe的getAndAddLong方法,原子性設置value值為原始值+1,返回遞增後的值
      public final long incrementAndGet() {
        return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
    }

    //(7).調用unsafe的getAndAddLong方法,原子性設置value值為原始值-1,返回遞減後的值
    public final long decrementAndGet() {
        return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
    }

    //(8).調用unsafe的getAndAddLong方法,原子性設置value值為原始值+1,返回原始值
    public final long getAndIncrement() {
        return unsafe.getAndAddLong(this, valueOffset, 1L);
    }
   //(9).調用unsafe的getAndAddLong方法,原子性設置value值為原始值-1,返回原始值
    public final long getAndDecrement() {
        return unsafe.getAndAddLong(this, valueOffset, -1L);
    }

上述代碼內部都是通過調用Unsafe的getAndAddLong方法來實現操作,getAndAddLong方法是原子性操作,第一個參數是AtomicLong實例的引用,第二個參數為value在AtomicLong中的偏移量,第三個參數是要設置的第二個變量的值。

簡單看一下getAndAddLong 方法的實現:

      public final long getAndAddLong(Object var1, long var2, long var4) {
        long var6;
        do {
            var6 = this.getLongVolatile(var1, var2);
        } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

        return var6;
    }

其中的getLongVolatile和compareAndSwapLong方法都是native方法,交給底層語言去實現。

2、compareAndSet 方法

看一下另外一個比較常用的compareAndSet方法:

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

同樣也是調用了compareAndSwapLong方法。如果原子變量中的value值等於expect,則使用update值更新value值並返回true,否則返回false。

3、多線程使用AtomicLong統計0的個數

package AtomicLongLearn;

import java.util.concurrent.atomic.AtomicLong;

/**
 * Created by Zhong Mingyi on 2020/11/12.
 */
public class AtomicLongTest {
    //(10)創建Long型原子計數器
    private static AtomicLong atomicLong = new AtomicLong();
    //(11)數據源
    private static Integer[] arr1 = new Integer[]{0,1,2,3,0,5,6,0,56,0};
    private static Integer[] arr2 = new Integer[]{10,1,2,3,0,5,6,0,56,0};

    public static void main(String[] args) throws InterruptedException {
        //(12)統計arr1中0的個數
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < arr1.length; i++) {
                if (arr1[i] == 0) {
                    atomicLong.incrementAndGet();
                }
            }
        });
        //(13)統計arr2中0的個數
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < arr2.length; i++) {
                if (arr2[i] == 0) {
                    atomicLong.incrementAndGet();
                }
            }
        });

        //(14)線程執行
        thread1.start();
        thread2.start();

        //(15)等待線程執行完畢
        thread1.join();
        thread2.join();

        System.out.println("兩個數組中0的個數為:"+atomicLong.get());
    }
}

輸出結果如下:

兩個數組中0的個數為:7

如上代碼中的兩個線程各自統計自所持數據中0的個數,每當找到一個0就會調用AtomicLong的原子性遞增方法。

如果沒有原子操作類,多線程下實現計數器需要使用比如synchronized的同步措施,會對性能有一定損耗;但原子操作類都使用基於CAS的非阻塞算法,因此會獲取較好的性能。

但是在高並發情況下,AtomicLong還是會存在性能問題,JDK8提供了一個在高並發下性能更好的原子類,我們下一期再來揭曉~

三、總結

本期學習了java.util.concurrent 包裡提供的原子性操作類之一——AtomicLong,它其中的主要方法均使用非阻塞算法CAS實現,這比起使用鎖實現原子性操作在性能上有了很大的提升;然後我們使用了AtomicLong類來實現多線程下統計0的個數的需求,並以此更好來理解AtomicLong類;但是在高並發下,AtomicLong類仍然有一定的性能瓶頸,JDK8中有更好的類去彌補它在高並發下性能的不足。

我是Zhongger,一個在網際網路行業摸魚寫代碼的打工人, 求個【關注】和【在看】,你們的支持是我創作的最大動力,我們下期見~

往期回顧:
闖禍了,我用了一個『<>』操作符引發的線上Bug
Java中的「八鎖」,你知道幾個?
非計算機科班的學生如何系統學習Java後端開發技術棧



相關焦點

  • 走進C++11(三十七)原子操作之 std::atomic
    C++11提供了一個原子類型std::atomic<T>,可以使用任意類型作為模板參數,C++11內置了整型的原子變量
  • Atomic系列原子類
    問題或建議,請公眾號留言;Atomic原子類的底層Unsafe類Unsafe類是在JDK底層的一個類,不允許我們去實例化他以及使用他裡面的方法的,首先他的構造函數是私有化,不能自己手動去實例化他,其次,如果用Unsafe.getUnsafe()方法來獲取一個實例,也是不行的,只能JDK自己使用源碼AtomicInteger代碼片段一、
  • Java中的13個原子操作類
    來源:https://www.iteye.com/blog/xiaoheng-2509522atomic 包中的 13 個類,屬於 4 中類型的原子更新方式.原子更新基本類型原子更新數組原子更新引用原子更新屬性atomic 包裡的類基本都是使用 Unsafe 實現的包裝類.
  • 儀器分析——原子吸收光譜法
    但其作為一種分析方法卻是從1955年澳大利亞物理學家A.Walsh發表了「原子吸收光譜在化學分析中的應用」的論文以後才開始的。2 原子吸收光譜法的基本原理2.1基本概念共振吸收線:當電子吸收一定能量從基態躍遷到能量最低的激發態時所產生的吸收譜線,簡稱共振線。
  • java並發編程-原子類
    原子類  原子操作是指不會被線程調度機制打斷的操作,這種操作一旦開始,就一直運行到結束,中間不會有任何線程上下文切換。原子操作可以是一個步驟,也可以是多個操作步驟,但是其順序不可以被打亂,也不可以被切割而只執行其中的一部分,將整個操作視作一個整體是原子性的核心特徵。而 java.util.concurrent.atomic 下的類,就是具有原子性的類,可以原子性地執行添加、遞增、遞減等操作。
  • 原子操作和互斥鎖的區別
    為了實現這樣的嚴謹性,原子操作僅會由一個獨立的CPU指令代表和完成。原子操作是無鎖的,常常直接通過CPU指令直接實現。事實上,其它同步技術的實現常常依賴於原子操作。Go對原子操作的支持Go 語言的sync/atomic包提供了對原子操作的支持,用於同步訪問整數和指針。下面的示例演示如何使用AddInt32函數對int32值執行添加原子操作。
  • 並發類 AtomicLong 使用入門及源碼詳解
    AtomicLong 介紹AtomicLong可以原子更新的 Long 值。AtomicLong用於諸如原子遞增的序列號之類的應用程式中,並且不能用作Long的替代品。但是,此類確實擴展了Number,以允許通過處理基於數字的類的工具和實用程序進行統一訪問。
  • 線程安全之std::atomic探索
    從不同線程訪問某個原子對象是良性(well-defined) 行為,而通常對於非原子類型而言,並發訪問某個對象(如果不做任何同步操作)會導致未定義(undefined) 行為發生。因此,從實現的原理上來,可以理解為原子類型在自己內部添加了互斥鎖。我們先創建一個CMake工程,並創建兩個線程來操作同一個全局變量,然後來編譯運行,查看它的結果。
  • Java從零開始學 - 第23天:JUC中原子類,一篇就夠了
    atomic 翻譯成中文是原子的意思。在化學上,我們知道原子是構成一般物質的最小單位,在化學反應中是不可分割的。在我們這裡 atomic 是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程幹擾,所以,所謂原子類說簡單點就是具有原子操作特徵的類,原子操作類提供了一些修改數據的方法,這些方法都是原子操作的,在多線程情況下可以確保被修改數據的正確性。
  • LWN 翻譯:Atomic Mode Setting 設計簡介(下)
    原子更新(Atomic Update)與核操作(Nuclear Option)與 Android ADF 相比,upstream atomic mode-setting 的一個重大變化,就是 upstream 允許通過 atomic ioctl() 修改任意 display state,而 ADF 的 atomic 操作只允許修改 plane
  • 原子類Atomic家族,細看成員還不少
    在編程領域裡,原子性意味著「一組操作要麼全都操作成功,要麼全都失敗,不能只操作成功其中的一部分」。而 java.util.concurrent.atomic 下的類,就是具有原子性的類,可以原子性地執行添加、遞增、遞減等操作。
  • 深入分析volatile是如何實現可見性和有序性的
    什麼是原子性原子(atomic)本意是不能被進一步分割的最小粒子,而原子操作意為不可被中斷的一個或者一系列操作。volatile是不能保證原子性,但是在特定場景就是在32位處理器裡,對double和long型的變量的讀寫操作加了volatile修飾可以保證原子性。
  • 由淺入深講解JUC下 CAS + Lock
    CAS什麼是原子(atomic)操作:多線程中的原子操作類似於資料庫中的同時執行AB兩個語句,要麼同時執行成功,要麼同時執行失敗。synchronized 的不足:如果出現大量競爭會消耗CPU,同時帶來死鎖或其他安全隱患。
  • C+11多線程編程(四)——原子操作
    今天和大家說說C++多線程中的原子操作。首先為什麼會有原子操作呢?這純粹就是C++這門語言的特性所決定的,C++這門語言是為性能而生的,它對性能的追求是沒有極限的,它總是想盡一切辦法提高性能。互斥鎖是可以實現數據的同步,但同時是以犧牲性能為代價的。口說無憑,我們做個實驗就知道了。
  • C++ atomic memory model和Arm實現方式
    C++  atomic memory model  和 Arm構架實現C++的memory model
  • 多線程程序中操作的原子性
    背景原子操作就是不可再分的操作。在多線程程序中原子操作是一個非常重要的概念,它常常用來實現一些同步機制,同時也是一些常見的多線程Bug的源頭。本文主要討論了三個問題:1. 多線程程序中對變量的讀寫操作是否是原子的?2.
  • 【乾貨】原子結構(Atomic Structur)相關知識
    原子質量(Atomic mass) 3 元素的相對原子質量是該元素的6.023×1023個原子(阿伏伽德羅數NA)的質量,單位為克。元素周期表中最下面的數字為相對原子質量。
  • 原子吸收光譜儀的構造原理
    原子吸收光譜儀結構簡單,原理易懂。
  • 詳解java並發原子類AtomicInteger(基於jdk1.8源碼分析)
    java並發包裡面的類一直是學習和面試的重點,這篇文章主要是對java並發包的其中一個類AtomicInteger的講解。從為什麼要出現AtomicInteger再到其底層原理來一個分析。一、從a++說起為什麼使用AtomicInteger我們知道java並發機制中主要有三個特性需要我們去考慮,原子性、可見性和有序性。
  • atom亞當英文名稱中包含原子,看你五分鐘能不能記下這些詞
    影片由史蒂文·史匹柏監製,裡面的機器人亞當atom的名字取自「原子」;中國引入的第一部國外動畫片叫:鐵臂阿童木Atom;古希臘人認為,物質是不可能無限地分割下去的,這種不可再分的最小微粒就是「原子」。直到20世紀早期,這一結論被推翻。比原子更小的是夸克.所以後面出來了單詞: anatomy (解剖)。詞根tom元音弱化後變形tem。