Java原子類
技術標籤:Java多執行緒
1.為什麼需要原子類
當程式更新一個變數時,如果是多執行緒同時更新這個變數,可能得到的結果與期望值不同。比如:有一個變數i,A執行緒執行i+1,B執行緒也執行i+1,經過兩個執行緒的操作後,變數i的值可能不是期望的3,而是2。這是因為,可能在A執行緒和B執行緒執行的時候拿到的i的值都是1,這就是執行緒不安全的更新操作,通常我們會使用synchronized來解決這個問題,synchronized能保證多執行緒不會同時更新變數i.
從java1.5開始,jdk提供了java.util.concurrent.atomic包,這個包中的原子操作類,提供了一種用法簡單,效能高效,執行緒安全的更新一個變數的方式。
atomic包裡面一共提供了13個類,分為4種類型,分別是:原子更新基本型別,原子更新陣列,原子更新引用,原子更新屬性,這13個類都是使用Unsafe實現的包裝類。
2.原子更新基本型別類
atomic提供了3個類用於原子更新基本型別:分別是AtomicInteger原子更新整形,AtomicLong原子更新長整形,AtomicBoolean原子更新bool值。由於這三個類提供的方法幾乎是一樣的,我以AtomicInteger為例進行說明。
AtomicInteger的常用方法有:
- int addAndGet(int delta):以原子的方式將輸入的值與例項中的值相加,並把結果返回
- boolean compareAndSet(int expect,int update):如果輸入值等於預期值,以原子的方式將該值設定為輸入的值
- final int getAndIncrement():以原子的方式將當前值加1,並返回加1之前的值
- void lazySet(int newValue):最終會設定成newValue,使用lazySet設定值後,可能導致其他執行緒在之後的一小段時間內還是可以讀到舊的值。
- int getAndSet(int newValue):以原子的方式將當前值設定為newValue,並返回設定之前的舊值
public class AtomicIntegerTest { static AtomicInteger atomicInteger = new AtomicInteger(1); static int a = 1; public static void main(String[] args) { for (int i = 0; i < 10000; i++) { new Thread(() -> { atomicInteger.getAndIncrement(); }).start(); } try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("使用原子類的值:" + atomicInteger.get()); for (int i = 0; i < 10000; i++) { new Thread(() -> { a++; }).start(); } try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("不使用原子類的值:" + a); } }
不使用原子類計算的值會有一些出入
AtomicInteger 類主要利用 CAS (compare and swap) + volatile 和 native 方法來保證原子操作,從而避免 synchronized 的高開銷,執行效率大為提升。
CAS的原理是拿期望的值和原本的一個值作比較,如果相同則更新成新的值。UnSafe 類的 objectFieldOffset() 方法是一個本地方法,這個方法是用來拿到“原來的值”的記憶體地址。另外 value 是一個volatile變數,在記憶體中可見,因此 JVM 可以保證任何時刻任何執行緒總能拿到該變數的最新值。
3.原子更新陣列
atomic裡提供了三個類用於原子更新數組裡面的元素,分別是:
- AtomicIntegerArray:原子更新整形數組裡的元素
- AtomicLongArray:原子更新長整形數組裡的元素
- AtomicReferenceArray:原子更新引用數組裡的元素
因為每個類裡面提供的方法都一致,因此以AtomicIntegerArray為例來說明。AtomicIntegerArray主要提供了以原子方式更新數組裡的整數,常見方法如下:
- int addAndGet(int i,int delta):以原子的方式將輸入值與陣列中索引為i的元素相加
- boolean compareAndSet(int i,int expect,int update):如果當前值等於預期值,則以原子方式將陣列位置i的元素設定成update值。
public class AtomicIntegerArrayTest {
static int[] value = new int[]{1, 2};
static AtomicIntegerArray ai = new AtomicIntegerArray(value);
public static void main(String[] args) {
new Thread(() -> {
ai.getAndSet(0, 3);
//輸出3
System.out.println(ai.get(0));
//輸出1
System.out.println(value[0]);
}).start();
}
}
需要注意的是,陣列value通過構造方法傳遞進去,然後AtomicIntegerArray會將當前陣列複製一份,所以當AtomicIntegerArray對內部的陣列元素進行修改時,不會影響傳入的陣列。
4.引用型別原子類
原子更新基本型別的AtomicInteger只能更新一個變數,如果要原子更新多個變數,就需要使用原子更新引用型別提供的類了。原子引用型別atomic包主要提供了以下幾個類:
- AtomicReference:原子更新引用型別
- AtomicReferenceFieldUpdater:原子更新引用型別裡的欄位
- AtomicMarkableReference:原子更新帶有標記位的引用型別。可以原子更新一個布林型別的標記位和引用型別。構造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)注意:AtomicMarkableReference是將一個boolean值作是否有更改的標記,本質就是它的版本號只有兩個,true和false, 修改的時候在這兩個版本號之間來回切換,這樣做並不能解決ABA的問題,只是會降低ABA問題發生的機率而已
public class AtomicReferenceArrayTest {
private static AtomicReference<User> reference = new AtomicReference<>();
public static void main(String[] args) {
User user = new User("tom", 23);
reference.set(user);
User updateUser = new User("ketty", 34);
reference.compareAndSet(user, updateUser);
System.out.println(reference.get().getName());
System.out.println(reference.get().getAge());
}
static class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
}
5.原子更新欄位類
如果需要原子更新某個物件的某個欄位,就需要使用原子更新屬性的相關類,atomic中提供了一下幾個類用於原子更新屬性:
- AtomicIntegerFieldUpdater:原子更新整形屬性的更新器
- AtomicLongFieldUpdater:原子更新長整形的更新器
- AtomicStampedReference:原子更新帶有版本號的引用型別。該類將整數值與引用關聯起來,可用於原子的更新資料和資料的版本號,可以解決使用CAS進行原子更新時可能出現的ABA問題。
想要原子的更新欄位,需要兩個步驟:
1.因為原子更新欄位類都是抽象類,每次使用的時候必須使用靜態方法newUpdater()建立一個更新器,並且需要設定想要更新的類和屬性
2.更新類的欄位(屬性)必須使用public volatile修飾符
public class AtomicIntegerFieldUpdaterTest {
//建立原子更新器
private static AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
public static void main(String[] args){
User user = new User("ketty",21);
//ketty長了一歲
updater.getAndIncrement(user);
//輸出22
System.out.println(updater.get(user));
}
static class User{
private String name;
public volatile int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
}