前言 併發編程是java中不可或缺的模塊。與串列程式相比,它們能使複雜的非同步代碼變得簡單,從而極大地簡化了複雜系統的開發。此外,想要充分發揮多處理器系統的強大計算能力,最簡單的方式就是使用線程。隨著處理器數量的持續增長,如何高效地使用蝙蝠正變得越來越重要。同時在當今互聯網的時代,大量的互聯網應用都面 ...
前言
併發編程是java中不可或缺的模塊。與串列程式相比,它們能使複雜的非同步代碼變得簡單,從而極大地簡化了複雜系統的開發。此外,想要充分發揮多處理器系統的強大計算能力,最簡單的方式就是使用線程。隨著處理器數量的持續增長,如何高效地使用蝙蝠正變得越來越重要。同時在當今互聯網的時代,大量的互聯網應用都面對著海量的訪問請求,因此,併發編程在我們的應用中占的比重越來越大。
為什麼會有併發安全
在講併發編程前,我們先來看段代碼:
public class UnsafeDemo {
Integer iCount = 0;
/**聲明不變對象,用於加鎖*/
final Object obj = new Object();
/**
* 加1操作
*/
public void addCount() {
iCount++;
}
/**
* 使用內置鎖 --加1操作
*/
public void addCount1() {
synchronized (obj){
iCount++;
}
}
/**
* 獲取count的值
*
* @return int
*/
public int getCount() {
return iCount;
}
/**
* 啟動方法
*
* @param args 參數
*/
public static void main(String[] args) throws InterruptedException {
UnsafeDemo unsafeDemo = new UnsafeDemo();
//新建2個線程執行+1的方法
Thread thread1 = new CountAddThread(unsafeDemo);
Thread thread2 = new CountAddThread(unsafeDemo);
thread1.start();
thread2.start();
//阻塞等待程式執行
Thread.sleep(1000);
System.out.println("count進行多線程相加後的結果:"+unsafeDemo.getCount());
/**
* count進行多線程相加後的結果:17296
* count進行多線程相加後的結果:11913
* count進行多線程相加後的結果:10375
*/
}
/**
* 定義一個私有的線程類
*/
private static class CountAddThread extends Thread {
private UnsafeDemo unsafeDemo;
public CountAddThread(UnsafeDemo unsafeDemo) {
this.unsafeDemo = unsafeDemo;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
unsafeDemo.addCount();
}
}
}
}
按照我們程式的思路,輸出的結果應該是20000,但實際上,輸出的結果並不確定。因為線程的運行是靠CPU來隨機決定的;i++是非線程安全的,非原子性的操作(一共分3步,獲取i的值,將i的值加1,將結果寫入到i中);
線程1首先將元空間的iCount值讀取到線程1的本地緩存中,然後線程1將iCount的值進行改變,然後再將改變後的iCount的值寫入到元空間中。線上程1執行的時候,可能線程2也在進行同樣的操作,就出現了值覆蓋的情況。由此可以引出併發安全的本質:
- 原子性:在一個操作中,cpu不可以在中途暫停然後再執行,即不可以被中斷操作,要麼執行完成,要麼不執行
- 可見性:必須確保釋放鎖之前對共用數據做出的更改對於隨後獲得該鎖的另一個線程是可見的 。(volatile只保證可見性不保證原子性)
- 有序性:線程之間必須是有序的訪問共用變數
對於這種情況,就需要對程式加鎖操作。今天我們就來聊聊synchronized內置鎖。
synchronized使用
synchronized
是Java提供的一個併發控制的關鍵字,可以修飾一個類、修飾一個方法、修飾代碼塊、修飾靜態代碼。保證了代碼的原子性和可見性以及有序性,但是不會處理重排序以及代碼優化的過程,但是在一個線程中執行肯定是有序的,因此是有序的。
synchronized原理
/**
* 對加1操作加上synchronized內置鎖,這樣在執行的時候,就能達到我們理想的效果,輸出結果為20000
*/
public void addCount() {
synchronized (this){
iCount++;
}
}
那為什麼synchronized能解決原子性,可見性,有序性這幾個問題呢?
先來瞭解一下幾個名詞:
無鎖狀態:沒有加鎖
偏向鎖:同步代碼塊第一次進入的時候,會生成偏向鎖
輕量級鎖:偏向鎖的線程沒有結束,此時又有其他的線程進入,進行鎖升級,其他線程自旋
重量級鎖:輕量級鎖沒有釋放,其他線程一直休眠沒有獲取鎖。重量級鎖是依賴對象內部的monitor鎖來實現的,而monitor又依賴操作系統的MutexLock(互斥鎖)來實現的,所以重量級鎖也稱為互斥鎖。
自旋:cpu空跑,讓等待的線程不放棄cpu執行時間,而是執行一個自旋(一般是空迴圈);即do{ }while(自旋的次數) 迴圈。自旋次數可以通過參數 -XX:PreBlockSpin 來修改。
鎖消除:虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共用數據競爭的鎖進行消除。一般根據逃逸分析的數據支持來作為判定依據。
在瞭解鎖的時候,首先需要瞭解java的對象頭,所有的鎖的實現都依賴於對象頭。
以32位的操作系統為例,對象頭的運行時數據(mark word)的預設存儲結構如下表所示:
回到上面的例子,來查看synchronized的原理,理解鎖膨脹的過程(即synchronized內置鎖的原理):
1.無鎖狀態:預設偏向鎖為0;
2.偏向鎖:當有一個線程進入同步代碼塊,將鎖對象頭的hashCode改成線程1的ID,同時標誌成偏向鎖。同時線程2也訪問到了同步代碼塊,發現鎖對象頭的ID並不是自己的ID;但是線程2還是要得到得到鎖,於是就去嘗試修改對象頭的HashCode值。
3.輕量級鎖:如果線程1剛好釋放鎖,那線程2就能修改成功,獲得鎖。但如果線程1一直持有鎖,線程2修改失敗,那線程2就進行鎖消除,同時虛擬機對線程1的鎖升級為輕量級鎖。
4.重量級鎖:虛擬機會有線程1和線程2分配一塊各自的記憶體空間,並且把鎖複製到各自的空間中,通知將鎖狀態變成空的。同時把鎖狀態變成線程對應的ID。此時線程2進入自旋(不釋放CPU),當線程2自旋到一定的程度(上面的自旋次數),線程2進入睡眠狀態,釋放CPU。
線程1執行完畢,釋放輕量級鎖,這時候發現,鎖對象的指針並不是指向自己,開始釋放鎖,並喚醒所有休眠的線程。
CAS(Compare And Swap)機制
要談CAS機制,還是先來段代碼,跟前文的代碼差別不大。就是將Int換成了AtomicInteger,將addCount和getCount方法改變了一下。可以看到執行出來的結果,跟使用synchronized的效果一樣。並且在某些情況下,代碼的性能會比synchronized更好。
public class VolatileDemo {
AtomicInteger count =new AtomicInteger(0);
public void addCount(){
count.incrementAndGet();//i++
}
public int getCount(){
return count.get();
}
/**
* 啟動方法
*
* @param args 參數
*/
public static void main(String[] args) throws InterruptedException {
VolatileDemo volatileDemo = new VolatileDemo();
//新建2個線程執行+1的方法
Thread thread1 = new VolatileDemo.CountAddThread(volatileDemo);
Thread thread2 = new VolatileDemo.CountAddThread(volatileDemo);
thread1.start();
thread2.start();
//阻塞等待程式執行
Thread.sleep(100);
System.out.println("count進行多線程相加後的結果:"+volatileDemo.getCount());
/**
* count進行多線程相加後的結果:20000
* count進行多線程相加後的結果:20000
* count進行多線程相加後的結果:20000
*/
}
/**
* 定義一個私有的線程類
*/
private static class CountAddThread extends Thread {
private VolatileDemo volatileDemo;
public CountAddThread( VolatileDemo volatileDemo) {
this.volatileDemo = volatileDemo;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
volatileDemo.addCount();
}
}
}
}
來看看AtomicInteger#incrementAndGet()的源碼:
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
可以看到getAndAddInt()裡面是使用了do{ }while()迴圈,也就是一個自旋。調用的compareAndSwapInt()是使用native修飾的,去記憶體中查詢新值和舊值並比較。
而Atomic操作類的底層正是用到了“CAS機制”。上文講到的自旋也是使用了CAS機制。CAS的核心是利用Unsafe對象實現的。Unsafe包是用來幫助java訪問操作系統底層資源的類。通過Unsafe,java具有了操作底層的能力,可以提升運行效率。
那麼什麼是CAS機制呢?
CAS機制中使用了3個基本操作數:記憶體地址V,舊的預期值A,要修改的新值B。
更新一個變數的時候,只有當變數的預期值A和記憶體地址V當中的實際值相同時,才會將記憶體地址V對應的值修改為B。否則不進行任何操作。
從思想上來說,synchronized屬於悲觀鎖,悲觀的認為程式中的併發情況嚴重,所以嚴防死守,CAS屬於樂觀鎖,樂觀地認為程式中的併發情況不那麼嚴重,所以讓線程不斷去重試更新。
CAS機制的優點:
- 直接操作底層,在併發量小的時候,可以提搞效率
CAS機制的缺點:
- 迴圈開銷大
- 只能保證單獨共用變數的原子操作
- ABA問題,當一個值從A變成B,又更新回A,普通CAS機制會誤判通過檢測。利用版本號比較可以有效解決ABA問題。
引用:
https://www.zhihu.com/question/39009953/answer/80186008
https://cnblogs.com/kismetv/p/10787228.html