【併發編程系列11】Java中12個原子(Atomic)操作類實現原理分析 前言 原子更新基本類型 原子更新數組 原子更新引用類型 原子更新屬性 CAS操作ABA問題 總結

前言

我們知道i++操作實際上是線程不安全的,因爲一個i++操作分爲了三步:

  • 1、獲取的i的值
  • 2、執行i+1
  • 3、將i+1的結果賦值給i

而這三步不是一個原子操作,多線程環境下就會出現線程不安全性問題。

Java從JDK 1.5開始,在java.util.concurrent.atomic包下提供了12個對應的原子類操作,讓我們可以直接使用原子操作類來實現一個原子的i++操作。

Java中一共提供了12個原子類操作,可以分爲四種類型,分別是:

  • 原子更新基本類型
  • 原子更新數組
  • 原子更新引用
  • 原子更新屬性

下面就讓我們一起來看看這四大類中的12個原子操作類:

原子更新基本類型

原子更新基本數據類型有以下三個:

  • AtomicInteger:原子更新整型。
  • AtomicBoolean:原子更新布爾類型。
  • AtomicLong:原子更新長整型。

AtomicInteger

常用方法如下:

  • int addAndGet(int delta):
    以原子方式將傳入的數值與實例中的值(AtomicInteger裏的 value)相加,並返回結果。
  • boolean compareAndSet(int expect,int update):
    如果輸入的數值等於預期值,則以原子方式將該值設置爲輸入的值,成功返回true,替換失敗則返回false
  • int getAndIncrement():
    以原子方式將當前值自增1,並返回自增前的值
  • int getAndDecrement():
    以原子方式將當前值自減1,並返回自減前的值
  • void lazySet(int newValue):
    最終會設置成 newValue,注意這個方法是一個lazy方法,也就是說設置之後,並不會馬上去將值更新到主內存,那麼其他線程在一小段時間內可能看不到設置的值。
  • int getAndSet(int newValue):
    以原子方式將值設置爲newValue,並返回舊值

代碼示例

package com.zwx.concurrent.atomic;

import java.util.concurrent.atomic.AtomicInteger;

public class TestAtomicBasicData {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger(8);
        System.out.println("初始化:" + atomicInteger);//8
        atomicInteger.compareAndSet(8,10);
        System.out.println("CAS後:" + atomicInteger);//10
        System.out.println(atomicInteger.getAndIncrement());//自增1,返回自增前的值10
        System.out.println("自增後:" + atomicInteger);//11
        System.out.println(atomicInteger.getAndDecrement());//11
        System.out.println("自減後:" + atomicInteger);//10
    }
}

AtomicBoolean

更新boolean值,常用方法如下:

  • boolean compareAndSet(boolean expect,boolean update):
    如果輸入的數值等於預期值,則以原子方式將該值設置爲輸入的值,成功返回true,替換失敗則返回false。注意這裏面實際上會先將boolean值轉換爲int類型,0-否 1-是


  • void lazySet(boolan newValue):
    最終會設置成 newValue,注意這個方法是一個lazy方法,也就是說設置之後,並不會馬上去將值更新到主內存,那麼其他線程在一小段時間內可能看不到設置的值。

  • boolean getAndSet(boolean newValue):
    以原子方式將值設置爲newValue,並返回舊值

AtomicLong

這個和上面的AtomicInteger幾乎是一樣的,就不在舉例了。
原子操作都是利用Unsafe類中的CAS操作實現的,但是Unsafe中只提供了三種類型的CAS操作:


所以上面的boolean類型是轉換爲整型來CAS的,其他數據類型也可以先進行數據轉換之後再通過CAS實現原子操作。

原子更新數組

原子操作更新數組也提供了三種類型:

  • AtomicIntegerArray:原子更Integer類型數組裏的元素。
  • AtomicLongArray:原子更新Long類型數組裏的元素。
  • AtomicReferenceArray:原子更新引用類型數組裏的元素。

AtomicIntegerArray

 int[] arr = new int[]{1,2,3};
 AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(arr);

可以看到初始化之後將數組複製了一份,所以不會如果把值改變了,不會影響原有數組的值。

boolean compareAndSet(int i, int expect, int update):可以看到這個方法對比基本類型多了一個i,也就是index下標,其他方法也是一樣和基本類型數組相比,多了一個index參數。

代碼示例

package com.zwx.concurrent.atomic;

import java.util.concurrent.atomic.AtomicIntegerArray;

public class TestAtomicArray {
    public static void main(String[] args) {
        int[] arr = new int[]{1,2,3};
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(arr);
        atomicIntegerArray.compareAndSet(1,2,8);
        System.out.println(arr[1]);//原數組的值沒被改變,還是2
        System.out.println(atomicIntegerArray.get(1));//atomicIntegerArray值被改變成8
    }
}

AtomicLongArray

和基本類型AtomicLong相比,方法都一樣,也是多了一個index數組下標參數。

AtomicReferenceArray

這個方法也是一樣,唯一的區別是可以傳入一個泛型,也就是說數據中的元素時自定義的對象,而不是引用對象。

代碼示例:

package com.zwx.concurrent.atomic;

import com.alibaba.fastjson.JSONObject;
import java.util.concurrent.atomic.AtomicReferenceArray;

public class TestAtomicReferenceArray {
    public static void main(String[] args) {
        Man man = new Man(18,"張三");
        Man[] arr = new Man[]{man};
        AtomicReferenceArray<Man> atomicReferenceArray = new AtomicReferenceArray<>(arr);
        System.out.println("CAS前:" + JSONObject.toJSONString(atomicReferenceArray.get(0)));//{"age":18,"name":"張三"}

        Man updateMan = new Man(28,"李四");
        atomicReferenceArray.compareAndSet(0,man,updateMan);
        System.out.println("CAS前:" + JSONObject.toJSONString(atomicReferenceArray.get(0)));//{"age":28,"name":"李四"}
    }
}

class Man{
    protected volatile Integer age;
    private String name;

    public Man(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

原子更新引用類型

原子更新基本類型時每次只能更新一個變量,如果我們需要原子更新多個變量,怎麼做呢?這時候我們可以把多個變量和合成一個,那麼就需要使用這個原子更新引用類型提供的類來更新了。

原子更新引用類型也提供了3個類:

  • AtomicReference:原子更新引用類型。
  • AtomicMarkableReference:原子更新帶有標記位的引用類型。可以原子更新一個布爾類 型的標記位和引用類型。
  • AtomicStampedReference:原子更新帶有版本號的引用類型。該類將整數值與引用關聯起來,可用於原子的更新數據和數據的版本號,可以解決使用CAS進行原子更新時可能出現的ABA問題。

AtomicReference

需要先構造一個引用對象然後調用AtomicReference中的相關原子方法,我們先來看一段代碼示例:

代碼示例

package com.zwx.concurrent.atomic;

import com.alibaba.fastjson.JSONObject;
import java.util.concurrent.atomic.AtomicReference;

public class TestAtomicReference {
    public static void main(String[] args) {
        User oldUser = new User(18,"張三");
        AtomicReference<User> atomicReference = new AtomicReference<>(oldUser);
        System.out.println("CAS前:" + JSONObject.toJSONString(atomicReference.get()));//{"age":18,"name":"張三"}

        User upateUser = new User(28,"李四");
        boolean result =atomicReference.compareAndSet(oldUser,upateUser);
        System.out.println("CAS結果爲:" + result);//true
        System.out.println("CAS後:" + JSONObject.toJSONString(atomicReference.get()));//{"age":28,"name":"李四"}
    }
}

class User{
    volatile Integer age;
    private String name;

    public User(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

這裏的原理也是一樣,通過Uasafe的CAS操作Object對象實現原子操作:


AtomicMarkableReference

這個和上面AtomicReference基本一致,唯一的區別是多了一個mark標記,boolean類型。

示例

package com.zwx.concurrent.atomic;

import com.alibaba.fastjson.JSONObject;
import java.util.concurrent.atomic.AtomicMarkableReference;

public class TestAtomicReferenceMark {
    public static void main(String[] args) {
        Person person = new Person(18,"張三");
        AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference(person,false);
        System.out.println("是否被標記過:" + atomicMarkableReference.isMarked());

        System.out.println("CAS前:" + JSONObject.toJSONString(atomicMarkableReference.getReference()));//{"age":18,"name":"張三"}

        Person updatePerson = new Person(28,"李四");
        /**
         * arg1:表示預期的引用對象
         * arg2:表示即將更新的引用對象
         * arg3:表示預期的標記
         * arg4:表示更新的標記
         * 需要參數1和參數3都是預期值纔會CAS成功
         */
        atomicMarkableReference.compareAndSet(person,updatePerson,false,true);
        System.out.println("CAS後:" + JSONObject.toJSONString(atomicMarkableReference.getReference()));//{"age":28,"name":"李四"}
    }
}

class Person{
    private Integer age;
    private String name;

    public Person(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

AtomicMarkableReference原理分析

初始化對象的時候需要初始化一個引用對象和一個初始mark,而這兩個屬性又是通過其靜態內部類Pair來管理的:


AtomicStampedReference

這個和AtomicMarkableReference幾乎一模一樣,唯一的區別就是AtomicMarkableReference中的標記只有true和false,而AtomicStampedReference中的標記是一個int類型,可以視作版本號,可以解決CAS的ABA問題。


原子更新屬性

  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater:原子更新長整型字段的更新器。
  • AtomicReferenceFieldUpdater:原子更新引用類型裏的任意指定字段。

AtomicIntegerFieldUpdater

這個是用來更新引用對象中的int類型的屬性,利用反射修改屬性。有以下幾點需要注意:

  • 引用類型中的屬性必須是int,不能是包裝類Integer
  • 引用類型中的屬性必須被volatile修飾
  • 引用類型中的屬性不能被private修飾

代碼示例

package com.zwx.concurrent.atomic;

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class TestAtomicReferenceField {
    public static void main(String[] args) {
        //AtomicIntegerFieldUpdater
        Women women = new Women(18,"張三");
        //arg1:引用的對象類型 arg2:要修改的對象中的屬性名
        AtomicIntegerFieldUpdater atomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Women.class,"age");
        atomicIntegerFieldUpdater.compareAndSet(women,18,28);
        System.out.println("CAS後的值:" + women.getAge());//28
    }
}

class Women{
    volatile int age;
    private String name;

    public Women(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

newUpdater方法:


AtomicIntegerFieldUpdaterImpl中初始化中就是利用反射獲取屬性修改屬性,並進行了一些驗證:


AtomicLongFieldUpdater

這個和上面AtomicLongFieldUpdater基本一樣,用來更新long類型的屬性,同樣有以下幾點需要注意:

  • 引用類型中的屬性必須是long,不能是包裝類Long
  • 引用類型中的屬性必須被volatile修飾
  • 引用類型中的屬性不能被private修飾

AtomicReferenceFieldUpdater

上面兩個都是隻能更新指定數據類型,而這個可以更新任意指定類型的屬性。也有以下幾個注意點:

  • 引用類型中的屬性不能是原始數據類型,必須用對應包裝類(這點和上面的兩種相反)
  • 引用類型中的屬性必須被volatile修飾
  • 引用類型中的屬性不能被private修飾

示例

package com.zwx.concurrent.atomic;

import com.alibaba.fastjson.JSONObject;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class TestAtomicReferenceField {
    public static void main(String[] args) {
        /**
         * arg1:傳入引用對象類型
         * arg2:傳入引用對象的屬性類型
         * arg3:傳入要修改的屬性名
         */
        AtomicReferenceFieldUpdater atomicReferenceFieldUpdater1 = AtomicReferenceFieldUpdater.newUpdater(Women.class,Integer.class,"age");
        AtomicReferenceFieldUpdater atomicReferenceFieldUpdater2 = AtomicReferenceFieldUpdater.newUpdater(Women.class,String.class,"name");

        Women women = new Women(18,"張三");
        atomicReferenceFieldUpdater1.compareAndSet(women,18,28);
        atomicReferenceFieldUpdater2.compareAndSet(women,"張三","李四");
        System.out.println(JSONObject.toJSONString(women));//{"age":28,"name":"李四"}
    }
}

class Women{
    volatile Integer age;
    volatile String name;

    public Women(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

CAS操作ABA問題

前面在講述AQS同步隊列的文章裏提到了ABA問題可以通過引入一個版本號來和實際值拼在一起來避免ABA問題,那麼實際上我們同樣可以利用AtomicMarkableReference和AtomicStampedReference來實現,不過如果用AtomicMarkableReference只有true和false兩種標記,而對於AtomicStampedReference就更自由,實際可以根據業務需求進行選擇。

總結

本文介紹了Java中提供的12種原子操作類,原理均是通過Unsafe類中的CAS方法實現的,一般的CAS方法有可能會出現ABA問題,所以有一種帶標記,一種帶版本號的原子操作可以用於避免ABA問題的產生。

下一篇,將會介紹線程池的實現原理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章