1. 程式人生 > >Java多執行緒之原子操作類

Java多執行緒之原子操作類

在併發程式設計中很容易出現併發安全問題,最簡單的例子就是多執行緒更新變數i=1,多個執行緒執行i++操作,就有可能獲取不到正確的值,而這個問題,最常用的方法是通過Synchronized進行控制來達到執行緒安全的目的。但是由於synchronized是採用的是悲觀鎖策略,並不是特別高效的一種解決方案。實際上,在J.U.C下的Atomic包提供了一系列的操作簡單,效能高效,並能保證執行緒安全的類去更新多種型別。Atomic包下的這些類都是採用樂觀鎖策略CAS來更新資料。

CAS原理與問題

CAS操作(又稱為無鎖操作)是一種樂觀鎖策略。它假設所有執行緒訪問共享資源的時候不會出現衝突,因此不會阻塞其他執行緒的操作。那麼,如果出現衝突了怎麼辦?無鎖操作是使用CAS(compare and swap)來鑑別執行緒是否出現衝突,出現衝突就重試當前操作直到沒有衝突為止。

CAS的操作過程

舉例說明:
Atomic包中的AtomicInteger類,是通過Unsafe類下的native函式compareAndSwapInt自旋來保證原子性,
其中incrementAndGet函式呼叫的getAndAddInt函式如下所示:

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;
    }

CAS有3個運算元,記憶體值V,舊的預期值A,要修改的新值B。當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做。
可見只有自旋實現更新資料操作之後,while迴圈才能夠結束。

CAS的問題

  1. 自旋時間過長。由compareAndSwapInt函式可知,自旋時間過長會對效能是很大的消耗。
  2. ABA問題。因為CAS會檢查舊值有沒有變化,這裡存在這樣一個有意思的問題。比如一箇舊值A變為了成B,然後再變成A,剛好在做CAS時檢查發現舊值並沒有變化依然為A,但是實際上的確發生了變化。解決方案可以新增一個版本號可以解決。原來的變化路徑A->B->A就變成了1A->2B->3C,或使用AtomicStampedReference工具類。

Atomic包的使用

原子更新基本型別

Atomic包中原子更新基本型別的工具類:
AtomicBoolean:以原子更新的方式更新boolean;
AtomicInteger:以原子更新的方式更新Integer;
AtomicLong:以原子更新的方式更新Long;

這幾個類的用法基本一致,這裡以AtomicInteger為例總結常用的方法

  1. addAndGet(int delta):以原子方式將輸入的數值與例項中原本的值相加,並返回最後的結果;
  2. incrementAndGet() :以原子的方式將例項中的原值進行加1操作,並返回最終相加後的結果;
  3. getAndSet(int newValue):將例項中的值更新為新值,並返回舊值;
  4. getAndIncrement():以原子的方式將例項中的原值加1,返回的是自增前的舊值;

原理不再贅述,參考上文compareAndSwapInt函式。

AtomicInteger使用示例:

public class AtomicExample {

    private static AtomicInteger atomicInteger = new AtomicInteger(2);

    public static void main(String[] args) {
        System.out.println(atomicInteger.getAndIncrement());
        System.out.println(atomicInteger.incrementAndGet());
        System.out.println(atomicInteger.get());
    }
}
// 2 4 4

LongAdder

為了解決自旋導致的效能問題,JDK8在Atomic包中推出了LongAdder類。LongAdder採用的方法是,共享熱點資料分離的計數:將一個數字的值拆分為一個數組。不同執行緒會命中到陣列的不同槽中,各個執行緒只對自己槽中的那個值進行CAS操作,這樣熱點就被分散了,衝突的概率就小很多;要得到這個數字的話,就要把這個值加起來。相比AtomicLong,併發量大大提高。

優點:有很高效能的併發寫的能力
缺點:讀取的效能不是很高效,而且如果讀取的時候出現併發寫的話,結果可能不是正確的

原子更新陣列型別

Atomic包中提供能原子更新陣列中元素的工具類:
AtomicIntegerArray:原子更新整型陣列中的元素;
AtomicLongArray:原子更新長整型陣列中的元素;
AtomicReferenceArray:原子更新引用型別陣列中的元素

這幾個類的用法一致,就以AtomicIntegerArray來總結下常用的方法:

  1. addAndGet(int i, int delta):以原子更新的方式將陣列中索引為i的元素與輸入值相加;
  2. getAndIncrement(int i):以原子更新的方式將陣列中索引為i的元素自增加1;
  3. compareAndSet(int i, int expect, int update):將陣列中索引為i的位置的元素進行更新

AtomicIntegerArray與AtomicInteger的方法基本一致,只不過在前者的方法中會多一個指定陣列索引位i。

AtomicIntegerArray使用示例:

public class AtomicExample {

    private static int[] value = new int[]{1, 2, 3};
    private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value);

    public static void main(String[] args) {
        //對陣列中索引為2的位置的元素加3
        int result = integerArray.getAndAdd(2, 3);
        System.out.println(integerArray.get(2));
        System.out.println(result);
    }
}
// 6 3

原子更新引用型別

如果需要原子更新引用型別變數的話,為了保證執行緒安全,Atomic也提供了相關的類:

  1. AtomicReference
  2. AtomicReferenceFieldUpdater:原子更新引用型別裡的欄位;
  3. AtomicMarkableReference:原子更新帶有標記位的引用型別;

AtomicReference使用示例:

public class AtomicExample {

    private static AtomicReference<User> reference = new AtomicReference<>();

    public static void main(String[] args) {
        User user1 = new User("a", 1);
        reference.set(user1);
        User user2 = new User("b",2);
        User user = reference.getAndSet(user2);
        System.out.println(user);
        System.out.println(reference.get());
    }

    static class User {
        private String userName;
        private int age;

        public User(String userName, int age) {
            this.userName = userName;
            this.age = age;
        }

        @Override
        public String toString() {
            return "User{" +
                    "userName='" + userName + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}
// User{userName='a', age=1}
// User{userName='b', age=2}

AtomicReferenceFieldUpdater使用示例:

public class AtomicExample {

    public static void main(String[] args) {
        AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Dog.class, String.class, "name");
        Dog dog1 = new Dog();
        updater.compareAndSet(dog1, dog1.name, "cat");
        System.out.println(dog1.name);
    }
}

class Dog {
    volatile String name = "dog1";
}

原子更新欄位型別

如果需要更新物件的某個欄位,Atomic同樣也提供了相應的原子操作類:

  1. AtomicIntegeFieldUpdater:原子更新整型欄位類;
  2. AtomicLongFieldUpdater:原子更新長整型欄位類;

要想使用原子更新欄位需要兩步操作:
原子更新欄位型別類都是抽象類,只能通過靜態方法newUpdater來建立一個更新器,並且需要設定想要更新的類和屬性;
更新類的屬性必須使用public volatile進行修飾;

AtomicIntegerFieldUpdater使用示例:

public class AtomicExample {

    private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");

    public static void main(String[] args) {
        User user = new User("a", 1);
        System.out.println(updater.getAndAdd(user, 5));
        System.out.println(updater.addAndGet(user, 1));
        System.out.println(updater.get(user));
    }

    static class User {
        private String userName;
        public volatile int age;

        public User(String userName, int age) {
            this.userName = userName;
            this.age = age;
        }

        @Override
        public String toString() {
            return "User{" +
                    "userName='" + userName + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

解決CAS的ABA問題

AtomicStampedReference:原子更新引用型別,這種更新方式會帶有版本號,從而解決CAS的ABA問題

AtomicStampedReference使用示例:

public class AtomicExample {

    public static void main(String[] args) {
        Integer init1 = 1110;
//        Integer init2 = 126;
        AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(init1, 1);
        int curent1 = reference.getReference();
//        Integer current2 = reference.getReference();
        reference.compareAndSet(reference.getReference(), reference.getReference() + 1, reference.getStamp(), reference.getStamp() + 1);//正確寫法
//        reference.compareAndSet(current2, current2+1, reference.getStamp(), reference.getStamp() + 1);//正確寫法
//        reference.compareAndSet(1110, 1111, reference.getStamp(), reference.getStamp() + 1);//錯誤寫法
//        reference.compareAndSet(curent1, curent1+1, reference.getStamp(), reference.getStamp() + 1);//錯誤寫法
//        reference.compareAndSet(current2, current2 + 1, reference.getStamp(), reference.getStamp() + 1);
        System.out.println("reference.getReference() = " + reference.getReference());
    }
}

AtomicStampedReference踩過的坑

參考上面的程式碼,分享一個筆者遇到的一次坑。AtomicStampedReference的compareAndSet函式中,前兩個引數是使用包裝類的。所以當引數超過128時,而且傳入引數並不是reference.getReference()獲取的話,會導致expectedReference == current.reference為false,則無法進行更新。

public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

最後,限於筆者經驗水平有限,歡迎讀者就文中的觀點提出寶貴的建議和意見。如果想獲得更多的學習資源或者想和更多的是技術愛好者一起交流,可以關注我的公眾號『全菜工程師小輝』後臺回覆關鍵詞領取學習資料、進入前後端技術交流群和程式設計師副業群。同時也可以加入程式設計師副業群Q群:735764906 一起交流。

相關推薦

Java執行原子操作

在併發程式設計中很容易出現併發安全問題,最簡單的例子就是多執行緒更新變數i=1,多個執行緒執行i++操作,就有可能獲取不到正確的值,而這個問題,最常用的方法是通過Synchronized進行控制來達到執行緒安全的目的。但是由於synchronized是採用的是悲觀鎖策略,並不是特別高效的一種解決方案。實際上,

Java執行原子操作atomic的使用CAS(七)

3-5、java.util.concurrent.atomic:執行緒安全的原子操作包 在JDK1.5+的版本中,Doug Lea和他的團隊還為我們提供了一套用於保證執行緒安全的原子操作。我們都知道在多執行緒環境下,對於更新物件中的某個屬性、更新基本型別資料、更新陣列(

java 執行利用Thread建立執行(Day02)

前言:在一個程式中,如果一次只完成一件事情,很容易實現,但現實生活中很多事情都是同時進行的,所以在java中為了模擬這種狀態,引入了執行緒機制,簡單的說,當程式同時完成很多事情時,就是所謂的多執行緒。 實現執行緒的兩種方式:一是通過繼承Thread類來建立執行緒,另一種方法

java執行批量操作

第一次寫部落格,工作一年多,屬於新手型別,錯誤和不足之處大家多多提醒,謝謝啦。 作用簡介:  最直觀的效果就是大大減少了操作時間。 1.首先建立測試實體類 package com.ncq.entity; import java.io.Serializable; pub

Java執行併發工具

一、總論:在JDK中提供了幾種併發工具類 1)CountDownLatch(同步倒數計數器:等待多執行緒(或者多步驟)完成) 2)CyclicBarrier(迴圈屏障:同步屏障) 3)Semaphore(訊號量:控制併發程序數) 主要參考資料

java架構路(執行原子操作,Atomic與Unsafe魔術

  這次不講原理了,主要是一些應用方面的知識,和上幾次的JUC併發程式設計的知識點更容易理解. 知識回顧:   上次主要說了Semaphore訊號量的使用,就是一個票據的使用,我們舉例了看3D電影拿3D眼鏡的例子,還說了內部的搶3D眼鏡,和後續排隊的原始碼解析,還有CountDownLatch的使用,我們是用

Java執行記憶體可見性和原子性:Synchronized和Volatile的比較

在刷題時,碰到一題:關於volatile關鍵字的說法錯誤的是: A. 能保證執行緒安全 B volatile關鍵字用在多執行緒同步中,可保證讀取的可見性  C JVM保證從主記憶體載入到執行緒工做記憶體的值是最新的 D volatile能禁止指令進行指令重排序 答案:A 處

java執行Lock的使用

1.ReentrantLock類的使用    1.1ReentrantLock實現執行緒間同步 public class MyService { private Lock lock=new ReentrantLock(); public void service(){

java執行物件鎖、鎖、同步機制詳解

1.在java多執行緒程式設計中物件鎖、類鎖、同步機制synchronized詳解:     物件鎖:在java中每個物件都有一個唯一的鎖,物件鎖用於物件例項方法或者一個物件例項上面的。     類鎖:是用於一個類靜態方法或者class物件的,一個

Java執行join()方法

概要 本章,會對Thread中join()方法進行介紹。涉及到的內容包括: 1. join()介紹 2. join()原始碼分析(基於JDK1.7.0_40) 3. join()示例 來源:http://www.cnblogs.com/skywang12345/p/34792

白話理解java執行join()方法

join字面意思是加入,我理解為插隊. 舉例:媽媽在炒菜,發現沒喲醬油了,讓兒子去打醬油,兒子打完醬油,媽媽炒完菜,全家一起吃 package cn.yh.thread01; /** * * 打醬油的例子 */ public class Demo03 { public stat

細說Java 執行記憶體可見性

前言: 討論學習Java中的記憶體可見性、Java記憶體模型、指令重排序、as-if-serial語義等多執行緒中偏向底層的一些知識,以及synchronized和volatile實現記憶體可見性的原理和方法。 1、可見性介紹 可見性:一個執行緒對共用變數值的修改,能夠及時地被其他執行緒

java執行 執行協作

也是網上看的一道題目:關於假如有Thread1、Thread2、Thread3、Thread4四條執行緒分別統計C、D、E、F四個盤的大小,所有執行緒都統計完畢交給Thread5執行緒去做彙總,應當如何實現? 蒐集整理了網上朋友提供的方法,主要有: 1. 多執行緒都是Thread或

java執行鎖機制二

網上看到一個題目,題目是這樣:Java多執行緒,啟動四個執行緒,兩個執行加一,另外兩個執行減一。 針對該問題寫了一個程式,測試通過,如下: class Sync { static int count = 0; public void add() {

java執行鎖機制一

網上看了一篇關於java synchronized關鍵字使用的很好的文章,現將其簡要總結一下,加深理解。 先總結兩個規則: synchronized鎖住的是括號裡的物件,而不是程式碼。對於非static的synchronized方法,鎖的就是物件本身也就是this。 多個執行緒

java執行Phaser

java多執行緒技術提供了Phaser工具類,Phaser表示“階段器”,用來解決控制多個執行緒分階段共同完成任務的情景問題。其作用相比CountDownLatch和CyclicBarrier更加靈活,例如有這樣的一個題目:5個學生一起參加考試,一共有三道題,要求所有學生到齊才能開始考試,全部同學都

Java執行——ThreadLocal

ThreadLocal是什麼:每一個ThreadLocal能夠放一個執行緒級別的變數,也就是說,每一個執行緒有獨自的變數,互不干擾。以此達到執行緒安全的目的,並且一定會安全。 實現原理: 要了解實現原理,我們先看set方法 public void set(T value) { T

Java執行原理及Thread的使用

一、程序與執行緒的區別 1.程序是應用程式在記憶體總分配的空間。(正在執行中的程式) 2.執行緒是程序中負責程式執行的執行單元、執行路徑。 3.一個程序中至少有一個執行緒在負責程序的執行。 4.一個程序中有多個執行緒在執行的程式,為多執行緒程式。 5.多執行緒技術是為了解決多部分程式碼同時執行。

java執行Lock--顯式鎖

Lock與Synchronized簡介 Synchornized相信大家用的已經比較熟悉了,這裡我就不介紹它的用法了 Synchronized被稱為同步鎖或者是隱式鎖,隱式鎖與顯式鎖區別在於,隱式鎖的獲取和釋放都需要出現在一個塊結構中,而且是有順序的,獲取鎖的順序和釋放鎖的順序必須相反,就是說,

Java執行Executor框架

在前面的這篇文章中介紹了執行緒池的相關知識,現在我們來看一下跟執行緒池相關的框架--Executor。 一.什麼是Executor 1.Executor框架的兩級排程模型 在HotSpot VM的執行緒模型中,Java執行緒(java.lang.Thread)被一對一對映為本地作業系統執