Java無鎖的實現——原子變數
概述
對於併發控制來說,鎖是一種悲觀的策略。它總是假設每一次的臨界區操作會產生衝突,因此,必須對每次操作都小心翼翼。如果有多個執行緒同時訪問臨界區資源,就寧可犧牲效能讓執行緒進行等待,所以說鎖會阻塞執行緒執行。 而無鎖採用的是一種樂觀的策略,它會假設對資源的訪問是沒有衝突的,既然沒有衝突,所以不用等待。遇到衝突,無鎖採用的策略是一種叫做CAS的技術來鑑別執行緒衝突。
CAS
CAS全稱為compile and swap,它包含三個引數(V,E,N)。V表示要跟新的變數,E表示預期值,N表示新值。僅當V值等於E值的時候,才會將V的值設為N,如果V值和E值不同,則說明有其他執行緒做了跟新,則當前執行緒什麼都不做。
原子變數
無鎖的執行緒安全整數:AtomicInteger
就具體實現來說,Atomic中儲存了一個核心欄位
private volatile int value;
這裡以具體demo的形式來給出用法,下面的幾個原子變數一致。
package nolock; import java.util.concurrent.atomic.AtomicInteger; public class AtomicIntegerDemo { static AtomicInteger i = new AtomicInteger(); public static class AddThread implements Runnable{ public void run(){ for(int k = 0; k < 10000; k++){ i.incrementAndGet(); } } } public static void main(String[] args) throws InterruptedException{ Thread[] ts = new Thread[10]; for(int k = 0;k < 10; k++){ ts[k] = new Thread(new AddThread()); } for(int k = 0; k < 10; k++) ts[k].start(); for(int k = 0; k < 10; k++) ts[k].join(); System.out.println(i); } }
執行結果:
100000
無鎖的物件引用:AtomicReference
同理他是對物件的引用,但是這裡會有一個ABA問題。
如果A要訪問的物件值初始值為a,後來執行緒B改為b,再後來執行緒C改為a,此時就產生了髒讀了。所以但用這個AtomicReference解決不了問題,下面用這個例子來說明問題。
package nolock; import java.util.concurrent.atomic.AtomicReference; public class AtmoicReferenceDemo { static AtomicReference<Integer> money = new AtomicReference<Integer>(); public static void main(String[] args){ money.set(19); for(int i = 0; i < 3; i++){ new Thread(){ public void run(){ while(true){ while(true){ Integer m = money.get(); if(m < 20){ if(money.compareAndSet(m,m+20)){ System.out.println("餘額小於20元,充值成功,餘額:" + money.get() + "元"); break; } }else{ //無需充值 break; } } } } }.start(); new Thread(){ public void run(){ for(int i = 0; i < 100; i++){ while (true){ Integer m = money.get(); if(m > 10){ System.out.println("大於10元"); if(money.compareAndSet(m,m-10)){ System.out.println("成功消費10元,餘額:" + money.get()); break; } }else{ System.out.println("沒有足夠的金額"); break; } } try{ Thread.sleep(100); }catch (InterruptedException e){ e.printStackTrace(); } } } }.start(); } } }
這個例子的結果會無限迴圈下去,餘額值會一直浮動變化。
那麼用什麼來解決呢?
加一個時間戳stamp
AtomicStampedReference
帶有時間戳的物件引用:AtomicStampedReference
package nolock;
import java.util.concurrent.atomic.AtomicStampedReference;
public class AtomicStampedReferenceDemo {
static AtomicStampedReference<Integer> money = new AtomicStampedReference<Integer>(19,0);
public static void main(String[] args){
//模擬多個執行緒同時更新後臺資料庫,為使用者充值
for(int i = 0; i < 3; i++){
final int timestamp = money.getStamp();
new Thread(){
public void run(){
while(true){
while(true){
Integer m = money.getReference();
if(m < 20){
if(money.compareAndSet(m,m+20,timestamp,timestamp+1)){
System.out.println("餘額小於20元,充值成功,餘額:" + money.getReference() + "元");
break;
}
}else{
//無需充值
break;
}
}
}
}
}.start();
}
//使用者執行緒,模擬消費
new Thread(){
public void run(){
for(int i = 0; i < 100; i++){
while(true){
int timestamp = money.getStamp();
Integer m = money.getReference();
if(m > 10){
System.out.println("大於10元");
if(money.compareAndSet(m,m-10,timestamp,timestamp+1)){
System.out.println("成功消費10元,餘額:" + money.getReference());
break;
}
}else{
System.out.println("沒有足夠餘額");
break;
}
}
try{
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}.start();
}
}
用這個方法餘額就只會贈予一次。
陣列也能無鎖:AtomicIntegerArray
package nolock;
import java.util.concurrent.atomic.AtomicIntegerArray;
public class AtomicIntegerArrayDemo {
static AtomicIntegerArray arr = new AtomicIntegerArray(10);
public static class AddThread implements Runnable{
public void run(){
for(int k = 0;k < 10000;k++){
arr.getAndIncrement(k % arr.length());
}
}
}
public static void main(String[] args) throws InterruptedException{
Thread[] ts = new Thread[10];
for(int k = 0;k < 10; k++){
ts[k] = new Thread(new AddThread());
}
for(int k = 0;k < 10; k++)
ts[k].start();
for(int k = 0;k < 10; k++)
ts[k].join();
System.out.println(arr);
}
}
讓普通變數也享受原子操作:AtomicIntegerFieldUpdater
package nolock;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
public class AtomicIntegerFieldUpdaterDemo {
public static class Candidate{
int id;
volatile int score;//必須為volatile,並且不能private,也不能用static修飾
}
public final static AtomicIntegerFieldUpdater<Candidate> scoreUpdater
= AtomicIntegerFieldUpdater.newUpdater(Candidate.class,"score");
public static AtomicInteger allScore = new AtomicInteger(0);
public static void main(String[] args)throws InterruptedException{
final Candidate stu = new Candidate();
Thread[] t = new Thread[10000];
for(int i = 0; i < 10000; i++){
t[i] = new Thread(){
public void run(){
if(Math.random() > 0.4){
scoreUpdater.incrementAndGet(stu);
allScore.incrementAndGet();
}
}
};
t[i].start();
}
for(int i = 0; i < 10000; i++){
t[i].join();
}
System.out.println("score = "+ stu.score);
System.out.println("allScore = " + allScore);
}
}
因為AtomicIntegerFieldUpdater保證了Candidate.score的執行緒安全,所以Candidate.score的值總是和allscore的值相等。
AtomicIntegerFieldUpdater的使用注意事項:
1.必須為volatile
2.不能private(因為Updater是使用反射得到這個變數的,如果變數不可見就會出錯)
3.不能用static修飾(因為Unsafe.objectFieldOffset()不支援靜態變數)
參考文獻:《Java高併發程式設計》