1. 樂觀鎖和悲觀鎖
悲觀鎖
總是假設(shè)最壞的情況,為防止每次去拿數(shù)據(jù)別人修改镜遣,每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì)阻塞直到它拿到鎖(共享資源每次只給一個(gè)線程使用,其它線程阻塞,用完后再把資源轉(zhuǎn)讓給其它線程)边翼。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫里邊就用到了很多這種鎖機(jī)制,比如行鎖鸣剪,表鎖等,讀鎖丈积,寫鎖等筐骇,都是在做操作之前先上鎖。Java中synchronized
和ReentrantLock
等獨(dú)占鎖就是悲觀鎖思想的實(shí)現(xiàn)江滨。
Synchronized
雖然確保了線程的安全铛纬,但是在性能上卻不是最優(yōu)的,Synchronized
關(guān)鍵字會(huì)讓沒有得到鎖資源的線程進(jìn)入BLOCKED
狀態(tài)唬滑,而后在爭(zhēng)奪到鎖資源后恢復(fù)為RUNNABLE
狀態(tài)告唆,這個(gè)過程中涉及到操作系統(tǒng)用戶模式和內(nèi)核模式的轉(zhuǎn)換棺弊,代價(jià)比較高鞍盗。
樂觀鎖
總是假設(shè)最好的情況诸迟,每次拿數(shù)據(jù)的時(shí)候認(rèn)為別人不會(huì)更改數(shù)據(jù)内边,所以不對(duì)數(shù)據(jù)加鎖赐写,但是在更新數(shù)據(jù)的時(shí)候都要先判定一下這個(gè)數(shù)據(jù)在計(jì)算期間有沒有被別人修改(例如:判斷值是否與之前的一致)觅闽,如果被修改則再次重試計(jì)算巷查。
CAS(Compare and Swap疯坤,即比較再交換)是樂觀鎖的典型實(shí)現(xiàn)编曼,當(dāng)多個(gè)線程嘗試使用CAS同時(shí)更新同一個(gè)變量時(shí)僧凤,只有其中一個(gè)線程能更新變量的值畜侦,而其它線程都失敗,失敗的線程并不會(huì)被掛起躯保,而是被告知這次競(jìng)爭(zhēng)中失敗旋膳,并可以再次嘗試。
2. 什么是CAS途事?
CAS機(jī)制
CAS機(jī)制當(dāng)中使用了3個(gè)基本操作數(shù):內(nèi)存地址V验懊,舊的值A(chǔ),要修改的新值B盯孙。
更新一個(gè)變量的時(shí)候鲁森,只有當(dāng)變量的預(yù)期值A(chǔ)和內(nèi)存地址V當(dāng)中的實(shí)際值相同時(shí),才會(huì)將內(nèi)存地址V對(duì)應(yīng)的值修改振惰。
借用別人的一段代碼歌溉,感覺很形象,可以用來模擬CAS的過程:
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author hrabbit
* 2018/07/16.
*/
public class AtomicBooleanTest implements Runnable {
private static AtomicBoolean flag = new AtomicBoolean(true);
public static void main(String[] args) {
AtomicBooleanTest ast = new AtomicBooleanTest();
Thread thread1 = new Thread(ast);
Thread thread = new Thread(ast);
thread1.start();
thread.start();
}
@Override
public void run() {
System.out.println("thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
if (flag.compareAndSet(true,false)){
System.out.println(Thread.currentThread().getName()+""+flag.get());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag.set(true);
}else{
System.out.println("重試機(jī)制thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
run();
}
}
}
我們來看上面這段代碼骑晶,同時(shí)啟動(dòng)兩個(gè)線程痛垛,同時(shí)去更改AtomicBoolean
類型的屬性flag
的值(通過flag.compareAndSet(true,false)
將true值更新成false)。compareAndSet的官方解釋如下:
也就是說當(dāng)flag的值為true的時(shí)候桶蛔,會(huì)觸發(fā)更新操作將flag的值更新成false匙头。一旦有一個(gè)線程執(zhí)行了這個(gè)操作,其他線程將會(huì)等待if條件成立才會(huì)去執(zhí)行仔雷。
最終的輸出:
thread:Thread-1;flag:true
thread:Thread-0;flag:true
Thread-1false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:true
Thread-0false
通過以上的輸出可以看到蹂析,兩個(gè)線程Thread-0和Thread-1同時(shí)執(zhí)行run函數(shù),但是Thread1先執(zhí)行了flag.compareAndSet(true,false)
,更新了flag的值碟婆,這時(shí)候Thread1進(jìn)入sleep电抚,Thread0一直沒有進(jìn)入if的條件,當(dāng)Thread1 sleep完之后竖共,重置flag的值到true蝙叛,這時(shí)候flag.compareAndSet(true,false)
的條件滿足,Thread0才可以進(jìn)入if執(zhí)行公给。我們看到flag.compareAndSet(true,false)
跟flag.set(true)
相當(dāng)于一把鎖借帘,鎖住了中間的代碼蜘渣。
CAS的缺點(diǎn)
1.CPU開銷較大
在并發(fā)量比較高的情況下,如果許多線程反復(fù)嘗試更新某一個(gè)變量肺然,卻又一直更新不成功蔫缸,循環(huán)往復(fù),會(huì)給CPU帶來很大的壓力狰挡。
2.不能保證代碼塊的原子性
CAS機(jī)制所保證的只是一個(gè)變量的原子性操作捂龄,而不能保證整個(gè)代碼塊的原子性。比如需要保證3個(gè)變量共同進(jìn)行原子性的更新加叁,就不得不使用Synchronized了倦沧。
CAS對(duì)比Synchronized:
對(duì)于資源競(jìng)爭(zhēng)較少(線程沖突較輕)的情況,使用synchronized同步鎖進(jìn)行線程阻塞和喚醒切換以及用戶態(tài)內(nèi)核態(tài)間的切換操作額外浪費(fèi)消耗cpu資源它匕;而CAS基于硬件實(shí)現(xiàn)展融,不需要進(jìn)入內(nèi)核,不需要切換線程豫柬,操作自旋幾率較少告希,因此可以獲得更高的性能。
對(duì)于資源競(jìng)爭(zhēng)嚴(yán)重(線程沖突嚴(yán)重)的情況烧给,CAS自旋的概率會(huì)比較大燕偶,從而浪費(fèi)更多的CPU資源,效率低于synchronized础嫡。
補(bǔ)充: synchronized在jdk1.6之后指么,已經(jīng)改進(jìn)優(yōu)化。synchronized的底層實(shí)現(xiàn)主要依靠Lock-Free的隊(duì)列榴鼎,基本思路是自旋后阻塞伯诬,競(jìng)爭(zhēng)切換后繼續(xù)競(jìng)爭(zhēng)鎖,稍微犧牲了公平性巫财,但獲得了高吞吐量盗似。在線程沖突較少的情況下,可以獲得和CAS類似的性能平项;而線程沖突嚴(yán)重的情況下赫舒,性能遠(yuǎn)高于CAS。
Atomic類
在java.util.concurrent.atomic包下闽瓢,一系列以Atomic開頭的包裝類都是能夠保證原子性操作的号阿。例如AtomicBoolean
,AtomicInteger
鸳粉,AtomicLong
。它們分別用于Boolean
园担,Integer
届谈,Long
類型的原子性操作枯夜。
先看個(gè)例子:
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
//每個(gè)線程讓count自增100次
for (int i = 0; i < 100; i++) {
count.incrementAndGet();
}
}
}).start();
}
try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(count);
}
使用AtomicInteger之后,最終的輸出結(jié)果可以保證是200艰山,效果跟sychronized是一樣的湖雹,但是在某些情況下,代碼的性能會(huì)比Synchronized更好曙搬。
atomic操作的底層實(shí)現(xiàn)正是利用的CAS機(jī)制摔吏。
AtomicReference
AtomicReference類提供了一種讀和寫都是原子性的對(duì)象引用變量。原子意味著多個(gè)線程試圖改變同一個(gè)AtomicReference(例如比較和交換操作)將不會(huì)使得AtomicReference處于不一致的狀態(tài)纵装。
AtomicReference有一個(gè)非常有用的方法是compareAndSet()征讲。compareAndSet()方法可以將存儲(chǔ)在AtomicReference中的引用同你的預(yù)期值做一個(gè)比較,如果他們是相同的(not equal as in equals() but same as in ==)橡娄,那么在AtomicReference實(shí)例上會(huì)設(shè)置一個(gè)新的引用诗箍。
atomic的源碼
我們可以看到java.util.concurrent.atomic
包下面各個(gè)類的源碼都比較簡(jiǎn)單,主要利用了sun.misc.Unsafe
實(shí)現(xiàn)的CAS原理挽唉,其中value用volatile
聲明滤祖,使得value對(duì)各個(gè)線程可見。valueOffset
為內(nèi)存地址的偏移量瓶籽。sun.misc.Unsafe里關(guān)于對(duì)象字段訪問的方法把對(duì)象布局抽象出來匠童,它提供了objectFieldOffset()方法用于獲取某個(gè)字段相對(duì)Java對(duì)象的“起始地址”的偏移量。
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
//...省略若干行
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
sun.misc.Unsafe是JDK內(nèi)部用的工具類塑顺。它通過暴露一些Java意義上說“不安全”的功能給Java層代碼汤求,來讓JDK能夠更多的使用Java代碼來實(shí)現(xiàn)一些原本是平臺(tái)相關(guān)的、需要使用native語言(例如C或C++)才可以實(shí)現(xiàn)的功能茬暇。該類不應(yīng)該在JDK核心類庫之外使用首昔。
我們?cè)賮砜聪耮et和set方法:
/**
* Gets the current value.
*
* @return the current value
*/
public final int get() {
return value;
}
/**
* Sets to the given value.
*
* @param newValue the new value
*/
public final void set(int newValue) {
value = newValue;
}
我們看到get和set方法沒有調(diào)用unsafe的方法。
參考文獻(xiàn):
https://www.imooc.com/article/details/id/44217
https://www.cnblogs.com/snow-man/p/9970523.html