什麼是悲觀鎖、樂觀鎖?在java語言裏,總有一些名詞看語義跟本不明白是啥玩意兒,也就總有部分面試官拿着這樣的詞來忽悠面試者,以此來找優越感,其實理解清楚了,這些詞也就唬不住人了。java
synchronized是悲觀鎖,這種線程一旦獲得鎖,其餘須要鎖的線程就掛起的狀況就是悲觀鎖。node
CAS操做的就是樂觀鎖,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。web
在進入正題以前,咱們先理解下下面的代碼:面試
private static int count = 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();
}
//每一個線程讓count自增100次
for (int i = 0; i < 100; i++) {
count++;
}
}
}).start();
}
try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(count);
}
請問cout的輸出值是否爲200?答案是否認的,由於這個程序是線程不安全的,因此形成的結果count值可能小於200;算法
那麼如何改形成線程安全的呢,其實咱們可使用上Synchronized同步鎖,咱們只須要在count++的位置添加同步鎖,代碼以下:安全
private static int count = 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();
}
//每一個線程讓count自增100次
for (int i = 0; i < 100; i++) {
synchronized (ThreadCas.class){
count++;
}
}
}
}).start();
}
try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(count);
}
加了同步鎖以後,count自增的操做變成了原子性操做,因此最終的輸出必定是count=200,代碼實現了線程安全。微信
可是Synchronized雖然確保了線程的安全,可是在性能上卻不是最優的,Synchronized關鍵字會讓沒有獲得鎖資源的線程進入BLOCKED狀態,然後在爭奪到鎖資源後恢復爲RUNNABLE狀態,這個過程當中涉及到操做系統用戶模式和內核模式的轉換,代價比較高。多線程
儘管Java1.6爲Synchronized作了優化,增長了從偏向鎖到輕量級鎖再到重量級鎖的過分,可是在最終轉變爲重量級鎖以後,性能仍然較低。併發
所謂原子操做類,指的是
java.util.concurrent.atomic包下,一系列以Atomic開頭的包裝類。例如AtomicBoolean,AtomicInteger,AtomicLong。它們分別用於Boolean,Integer,Long類型的原子性操做。app
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();
}
//每一個線程讓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以後,最終的輸出結果一樣能夠保證是200。而且在某些狀況下,代碼的性能會比Synchronized更好。
而Atomic操做的底層實現正是利用的CAS機制,好的,咱們切入到這個博客的正點。
什麼是CAS機制
CAS是英文單詞Compare And Swap的縮寫,翻譯過來就是比較並替換。
CAS機制當中使用了3個基本操做數:內存地址V,舊的預期值A,要修改的新值B。
更新一個變量的時候,只有當變量的預期值A和內存地址V當中的實際值相同時,纔會將內存地址V對應的值修改成B。
CAS是英文單詞Compare And Swap的縮寫,翻譯過來就是比較並替換。
CAS機制當中使用了3個基本操做數:內存地址V,舊的預期值A,要修改的新值B。
更新一個變量的時候,只有當變量的預期值A和內存地址V當中的實際值相同時,纔會將內存地址V對應的值修改成B。
這樣說或許有些抽象,咱們來看一個例子:
1.在內存地址V當中,存儲着值爲10的變量。
2.此時線程1想要把變量的值增長1。對線程1來講,舊的預期值A=10,要修改的新值B=11。
3.在線程1要提交更新以前,另外一個線程2搶先一步,把內存地址V中的變量值率先更新成了11。
4.線程1開始提交更新,首先進行A和地址V的實際值比較(Compare),發現A不等於V的實際值,提交失敗。
5.線程1從新獲取內存地址V的當前值,並從新計算想要修改的新值。此時對線程1來講,A=11,B=12。這個從新嘗試的過程被稱爲自旋。
6.這一次比較幸運,沒有其餘線程改變地址V的值。線程1進行Compare,發現A和地址V的實際值是相等的。
7.線程1進行SWAP,把地址V的值替換爲B,也就是12。
從思想上來講,Synchronized屬於悲觀鎖,悲觀地認爲程序中的併發狀況嚴重,因此嚴防死守。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("重試機制thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
run();
}
}
}
輸出的結果:
thread:Thread-1;flag:true
thread:Thread-0;flag:true
Thread-1false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:true
Thread-0false
這裏不管怎麼運行,Thread-一、Thread-0都會執行if=true條件,並且還不會產生線程髒讀髒寫,這是如何作到的了,這就用到了咱們的compareAndSet(boolean expect,boolean update)方法 咱們看到當Thread-1在進行操做的時候,Thread一直在進行重試機制,程序原理圖:
這個圖中重最要的是compareAndSet(true,false)方法要拆開成compare(true)方法和Set(false)方法理解,是compare(true)是等於true後,就立刻設置共享內存爲false,這個時候,其它線程不管怎麼走都沒法走到只有獲得共享內存爲true時的程序隔離方法區。
看到這裏,這種CAS機制就是完美的嗎?這個程序其實存在一個問題,不知道你們注意到沒有?
可是這種得不到狀態爲true時使用遞歸算法是很耗cpu資源的,因此通常狀況下,都會有線程sleep。
CAS的缺點:
1.CPU開銷較大 在併發量比較高的狀況下,若是許多線程反覆嘗試更新某一個變量,卻又一直更新不成功,循環往復,會給CPU帶來很大的壓力。
2.不能保證代碼塊的原子性 CAS機制所保證的只是一個變量的原子性操做,而不能保證整個代碼塊的原子性。好比須要保證3個變量共同進行原子性的更新,就不得不使用Synchronized了。
![]()
你點的每個「在看」,我都當成了喜歡
本文分享自微信公衆號 - Java學習提高(javaxuexitisheng)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。