前言
我們知道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問題的產生。
下一篇,將會介紹線程池的實現原理。