Java中Atomic原子類型的詳細講解(一)-劉宇
- 一、什麼是原子類型
- 二、原子類型的實作原理
- 三、利用volatile關鍵字示範原子性問題
-
- 示範
- 為什麼多個線程會輸出相同的結果
- 四、利用AtomicInteger解決原子性問題
- 五、AtomicInteger的基本使用
-
- 1、AtomicInteger的建立和get()方法
- 2、set()
- 3、getAndSet(int)
- 4、getAndAdd(int)
- 5、addAndGet(int)
- 6、getAndIncrement()
- 7、incrementAndGet()
- 9、getAndDecrement()
- 10、decrementAndGet()
- 11、compareAndSet(int expect, int update)
- 六、AtomicBoolean的基本使用
-
- 1、AtomicInteger的建立和get()方法
- 2、set()
- 3、getAndSet(int)
- 4、compareAndSet(boolean expect, boolean update)
- 七、AtomicBoolean的作用
- 八、AtomicLong的基本使用
-
- AtomicLong與AtomicInteger的差別
- VMSupportsCS8()方法的作用
- 九、compareAndSwap算法(CAS)詳解
- 十、利用compareAndSet方法來解決synchronized會使其他線程block住無法幹其他事情的現象
作者:劉宇
CSDN部落格位址:https://blog.csdn.net/liuyu973971883
有部分資料參考,如有侵權,請聯系删除。如有不正确的地方,煩請指正,謝謝。
一、什麼是原子類型
- 原子操作是指不會被線程排程機制打斷的操作,這種操作一旦開始,就一直運作到結束,中間不會有任何線程上下文切換。
- Java從JDK 1.5開始提供了java.util.concurrent.atomic包(以下簡稱Atomic包),這個包中的原子操作類提供了一種用法簡單、性能高效、線程安全地更新一個變量的方式。
- Atomic包大緻可以屬于4種類型的原子更新方式,分别是
- 原子更新基本類型
- 原子更新數組
- 原子更新引用
- 原子更新屬性
二、原子類型的實作原理
Atomic包裡的類基本都是使用Unsafe實作的包裝類,進而達到了原子性的操作。然後通過将内部的value變量用volatile關鍵字修飾,進而達到了可見性、防止重排序、原子性。
三、利用volatile關鍵字示範原子性問題
示範
使用三個線程分别對同一個變量進行加1操作,原本理想的結果應該是輸出1500個元素,而真實結果卻隻輸出了1498個,原因是有線程輸出了同樣的結果。是因為volatile關鍵字可以添加記憶體屏障有效防止重排序、記憶體可見性,但是不能確定其原子性,而value+=1時,看似是一行代碼,實則是分成了多步執行的。
package com.brycen.concurrency03.atomic;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerTest {
private static volatile int value = 0;
private static Set<Integer> set = Collections.synchronizedSet(new HashSet<Integer>());
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
int x= 0;
while(x<500) {
int temp = value += 1;
set.add(temp);
System.out.println(Thread.currentThread().getName()+":"+temp);
x++;
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
int x= 0;
while(x<500) {
int temp = value += 1;
set.add(temp);
System.out.println(Thread.currentThread().getName()+":"+temp);
x++;
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
int x= 0;
while(x<500) {
int temp = value += 1;
set.add(temp);
System.out.println(Thread.currentThread().getName()+":"+temp);
x++;
}
}
});
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}
}
輸出結果:
我們可以看到,原本應該輸出1500個,但是結果卻隻輸出了1498個。這是因為volatile關鍵字可以添加記憶體屏障有效防止重排序、記憶體可見性,但是不能確定其原子性,而value+=1時,看似是一行代碼,實則是分成了多步執行的
為什麼多個線程會輸出相同的結果
volatlie關鍵字沒有原子性,那麼value+=1這行代碼實則會被分為4步執行
- 擷取value的值
- 将擷取到的值+1
- 将最新值指派給value
- 将value的值刷入記憶體
假設當時value值為1,當線程1執行完+=操作的第1步時,cpu執行權被線程2搶走,然後線程2執行+=操作,直至輸入記憶體,并輸出2,這時cpu執行被線程1搶走,繼續執行沒有完成的+=操作,那麼這時線程1會根據第一步拿到的1進行+1操作,那麼傳回輸出的同樣是2。
四、利用AtomicInteger解決原子性問題
package com.brycen.concurrency03.atomic;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerTest {
private static Set<Integer> set = Collections.synchronizedSet(new HashSet<Integer>());
private static AtomicInteger value = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
int x= 0;
while(x<500) {
int temp = value.getAndIncrement();
set.add(temp);
System.out.println(Thread.currentThread().getName()+":"+temp);
x++;
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
int x= 0;
while(x<500) {
int temp = value.getAndIncrement();
set.add(temp);
System.out.println(Thread.currentThread().getName()+":"+temp);
x++;
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
int x= 0;
while(x<500) {
int temp = value.getAndIncrement();
set.add(temp);
System.out.println(Thread.currentThread().getName()+":"+temp);
x++;
}
}
});
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}
}
輸出結果:
五、AtomicInteger的基本使用
1、AtomicInteger的建立和get()方法
- AtomicInteger的建立分為兩種:
- 無參的,預設值0
- 有參的,指定預設值
- get():用于擷取目前值,該方法是不需要鎖的,因為他隻是去取值
AtomicInteger i = new AtomicInteger();
System.out.println(i.get());//輸出0
AtomicInteger j = new AtomicInteger(10)
System.out.println(j.get());//輸出10
2、set()
- 該方法是不需要鎖的,因為set方法相當于初始化該變量了
AtomicInteger i = new AtomicInteger();
i.set(12);
System.out.println(i.get());//輸出12
3、getAndSet(int)
- 先取值,再設定值。
AtomicInteger i = new AtomicInteger();
int result = i.getAndSet(10);
System.out.println(result);//輸出0
System.out.println(i.get());//輸出10
4、getAndAdd(int)
- 先拿到值并且後加num。
AtomicInteger i = new AtomicInteger(10);
int result = i.getAndAdd(10);
System.out.println(result);//輸出10
System.out.println(i.get());//輸出20
5、addAndGet(int)
- 先自動加num,再取值。
AtomicInteger i = new AtomicInteger(10);
int result = i.addAndGet(10);
System.out.println(result);//輸出20
System.out.println(i.get());//輸出20
6、getAndIncrement()
- 先拿到值然後再+1
AtomicInteger i = new AtomicInteger();
int result = i.getAndIncrement();
System.out.println(result);//輸出0
System.out.println(i.get());//輸出1
7、incrementAndGet()
- 先拿到值然後再+1
AtomicInteger i = new AtomicInteger();
int result = i.incrementAndGet();
System.out.println(result);//輸出1
System.out.println(i.get());//輸出1
9、getAndDecrement()
- 先拿到值然後再-1
AtomicInteger i = new AtomicInteger(10);
int result = i.getAndDecrement();
System.out.println(result);//輸出10
System.out.println(i.get());//輸出9
10、decrementAndGet()
- 先-1再取值
AtomicInteger i = new AtomicInteger();
int result = i.decrementAndGet();
System.out.println(result);//輸出9
System.out.println(i.get());//輸出9
11、compareAndSet(int expect, int update)
- 快速失敗政策,是用于判斷期望值是否與變量實際值相等,如果相等則将update指派給變量,否則失敗。
//成功案例
AtomicInteger atomicInteger = new AtomicInteger(10);
boolean result = atomicInteger.compareAndSet(10, 12);
System.out.println(result);//輸出true
System.out.println(atomicInteger.get());//輸出12
//失敗案例
AtomicInteger atomicInteger1 = new AtomicInteger(10);
boolean result1 = atomicInteger1.compareAndSet(11, 12);
System.out.println(result1);//輸出false
System.out.println(atomicInteger1.get());//輸出10
六、AtomicBoolean的基本使用
隻有兩種值,為0和1,即真或假。
1、AtomicInteger的建立和get()方法
- AtomicInteger的建立分為兩種:
- 無參的,預設值0
- 有參的,指定預設值
- get():用于擷取目前值,該方法是不需要鎖的,因為他隻是去取值
AtomicBoolean bool = new AtomicBoolean();
System.out.println(bool.get());//輸出false
AtomicBoolean bool = new AtomicBoolean(true);
System.out.println(bool.get());//輸出true
2、set()
- 該方法是不需要鎖的,因為set方法相當于初始化該變量了
AtomicBoolean bool = new AtomicBoolean();
bool.set(true);
System.out.println(bool.get());//輸出true
3、getAndSet(int)
- 先取值,再設定值。
AtomicBoolean bool = new AtomicBoolean(true);
boolean result = bool.getAndSet(false);
System.out.println(result);//輸出true
System.out.println(bool.get());//輸出false
4、compareAndSet(boolean expect, boolean update)
- 快速失敗政策,是用于判斷期望值是否與變量實際值相等,如果相等則将update指派給變量,否則失敗。
//成功案例
AtomicBoolean bool = new AtomicBoolean(true);
boolean result = bool.compareAndSet(true, false);
System.out.println(result);//輸出true
System.out.println(bool.get());//輸出false
//失敗案例
AtomicBoolean bool1 = new AtomicBoolean(true);
boolean result1 = bool1.compareAndSet(false, true);
System.out.println(result1);//輸出false
System.out.println(bool1.get());//輸出true
七、AtomicBoolean的作用
可以當做多線程中的開關flag,進而來代替sychronic這樣比較重的鎖
案例
- 如果這個案例中使用基本資料類型的boolean作為标志位,則線程永遠不會停止。
package com.brycen.concurrency03.atomic;
import java.util.concurrent.atomic.AtomicBoolean;
public class AtomicBooleanFlag {
private static AtomicBoolean flag = new AtomicBoolean(true);
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
int i = 0;
while (flag.get()) {
i++;
}
System.out.println("i="+i);
}
}).start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
flag.set(false);
}
}
輸出
八、AtomicLong的基本使用
他基本上與AtomicInteger的使用方法差不多,這邊就不示範了,簡單說一下他們的差別
AtomicLong與AtomicInteger的差別
- AtomicLong比AtomicInteger多了一個VM_SUPPORTS_LONG_CAS的boolean變量,對應的值是VMSupportsCS8(),這個是由JVM調用的。
VMSupportsCS8()方法的作用
- 因為long類型的資料是64位的,那麼在32位CPU中,就需要分高位和低位擷取一個long類型的資料,那麼就會無法保證原子性的操作。VMSupportsCS8()這個方法就是判斷該jvm、cpu是否支援long類型的lockless的CompareAndSet,如果支援,則VMSupportsCS8()方法就不會對資料總線進行加鎖了,如果不支援則需要對資料總線進行加鎖來保證原子性。
九、compareAndSwap算法(CAS)詳解
其實在原子類型中涉及到改變變量數值的操作都會進行CAS算法校驗。它是利用Unsafe發送彙編指令達到無鎖的狀态,其實他是有鎖的,隻不過他是CPU級别的,不是系統級别的,性能會非常高。它會對比我們的目前值是否和變量值一緻,如果一緻才會将最新值指派給變量,否則會快速失敗,然後進行循壞嘗試。
下面是Integer原子類型中改變數值時的部分源代碼,其中compareAndSet中就用到了CAS算法
for(;;){
int current = get();
int next = current + delta;
if (compareAndSet(current, next))
return current;
}
步驟分析
時序 | 線程1 | 線程2 |
---|---|---|
int current = get(); | current=1 | current=1 |
int next = current + delta; | next=2 | next=2 |
if (compareAndSet(current, next)) | 釋放CPU執行權 | current==value:判斷變量的值value和目前值current是否一緻。一緻則将要next的值指派給value |
if (compareAndSet(current, next)) | current==value:判斷變量的值value和目前值current是否一緻。而線程1中的current值為1,value已經變為2,是以不等,則繼續循環,直至相等 | 釋放CPU執行權 |
十、利用compareAndSet方法來解決synchronized會使其他線程block住無法幹其他事情的現象
1. 自定義一個異常
package com.brycen.concurrency03.atomic;
public class GetLockException extends Exception {
public GetLockException() {
super();
}
public GetLockException(String message) {
super(message);
}
}
2. 自定義一個利用compareAndSet算法的鎖
package com.brycen.concurrency03.atomic;
import java.util.concurrent.atomic.AtomicInteger;
public class CompareAndSetLock {
private final AtomicInteger value = new AtomicInteger();
private Thread currentThread = null;
public void tryLock() throws GetLockException{
boolean success = value.compareAndSet(0, 1);
if (!success) {
// 不相等則抛出異常,表示有其他線程使用
throw new GetLockException();
}
currentThread = Thread.currentThread();
}
public void unLock() {
if (0 == value.get()) {
return;
}
// 確定解鎖的是拿到鎖的那個線程
if (currentThread == Thread.currentThread()) {
value.compareAndSet(1, 0);
}
}
}
3. 自定義一個利用compareAndSet算法的鎖
package com.brycen.concurrency03.atomic;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerDetailTest2 {
private static CompareAndSetLock lock = new CompareAndSetLock();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
doSomeThing2();
}
}).start();
}
}
//這個方法是利用synchronized實作的鎖,會使其他線程進入block狀态
private static void doSomeThing() {
synchronized (AtomicIntegerDetailTest2.class) {
System.out.println(Thread.currentThread().getName()+": get the lock");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
private static void doSomeThing2() {
try {
lock.tryLock();
System.out.println(Thread.currentThread().getName()+": get the lock");
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (GetLockException e) {
// TODO Auto-generated catch block
// e.printStackTrace();
System.out.println(Thread.currentThread().getName()+": do other things");
}finally {
lock.unLock();
}
}
}
輸出結果
Thread-0: get the lock
Thread-3: do other things
Thread-4: do other things
Thread-1: do other things
Thread-2: do other things