目錄
- 什麼是原子性問題
-
- 舉例說明一下
- 怎麼解決
-
- 自帶原子性保證
- synchronized 和 Lock鎖
- 原子操作類型
- 最好的方法還是使用無鎖程式設計
- 簡單總結
- 參考
什麼是原子性問題
原子性是指在一個操作中,cpu不可以在中途暫停然後再排程,要麼一次執行完成,要麼就不執行。
在Java中當我們讨論一個操作具有原子性問題,是一般就是指這個操作會被線程的随機排程打斷而産生的一系列的問題。
舉例說明一下
我們先來看一些例子,來了解什麼是原子性的操作
a = true; //原子性
a = 5; //原子性
a = b; //非原子性,分兩步完成,第一步加載b的值,第二步将b指派給a
a = b + 2; //非原子性,分三步完成
a ++; //非原子性,分三步完成:1、讀取a的值,2、計算a的值+1,3、指派
接下類我們看一個由于原子性導緻的問題:
public class Singleton {
private static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) { //判空
synchronized (Singleton.class) { //加鎖
if (singleton == null) { //再次判空
singleton = new Singleton();
}
}
}
return singleton;
}
}
以上是使用雙重校驗鎖實作的一個單例,按照常理,隻能由一個線程可以執行
new Singleton();
的操作,但是比較詭異的是,在一些時候,以上單例會出現空指針的異常。雖然大部分時間是正常的,但是隻要出現了問題,那麼編碼上一定是有問題的。
下面我們來分析為什麼會出現這樣的問題:
- 第一個線程判空成功之後,使用synchronized關鍵字對指派操作進行加鎖,第二次判空之後開始了建立對象并指派的操作
- 注意,這裡的操作并不是原子性的,簡單來說,這裡的操作可以分成三步:
- 建立記憶體空間
- 在記憶體空間内建立對象
- 将記憶體空間指派給變量singleton
- 上面的操作由于不是原子性,是以三步之間是可以被中斷的,再加上不是原子操作,是以可能會進行重排序,是以又産生了有序性問題
- 如果将步驟2和步驟3進行了重排序,建立完記憶體之後立刻指派,指派之後再進行對象的建立,而另外一個線程在指派和對象的建立之間對變量singleton 進行了通路,那麼他就會拿到一個半成品的對象。 這時就會出現空指針異常。
這個問題是由順序性和原子性兩個原因共同導緻的。
是以synchronized關鍵字對原子性的保證是從結果上保證的,因為對于整個指派操作,無論是否重排序,确實沒有影響結果,但是對于另外一個線程來講就不盡相同了。
摘抄一段
不是說synchronized是可以保證有序性的麼,這裡為什麼就不行了呢?
首先,可以明确的一點是:synchronized是無法禁止指令重排和處理器優化的。那麼他是如何保證的有序性呢?
這就要再把有序性的概念擴充一下了。Java程式中天然的有序性可以總結為一句話:如果在本線程内觀察,所有操作都是天然有序的。如果在一個線程中觀察另一個線程,所有操作都是無序的。
以上這句話也是《深入了解Java虛拟機》中的原句,但是怎麼了解呢?周志明并沒有詳細的解釋。這裡我簡單擴充一下,這其實和as-if-serial語義有關。
as-if-serial語義的意思指:不管怎麼重排序,單線程程式的執行結果都不能被改變。編譯器和處理器無論如何優化,都必須遵守as-if-serial語義。
這裡不對as-if-serial語義詳細展開了,簡單說就是,as-if-serial語義保證了單線程中,不管指令怎麼重排,最終的執行結果是不能被改變的。
那麼,我們回到剛剛那個雙重校驗鎖的例子,站在單線程的角度,也就是隻看Thread1的話,因為編譯器會遵守as-if-serial語義,是以這種優化不會有任何問題,對于這個線程的執行結果也不會有任何影響。
淦,跑題了
怎麼解決
自帶原子性保證
在Java中,對基本資料類型的變量的讀取和指派操作是原子性操作。
synchronized 和 Lock鎖
這兩個方法可以通過加鎖的方法保證單線程中最終的結果具有原子性,但是不能保證中間的操作是否被打斷
原子操作類型
public static class BankAccount {
//省略其他代碼
private AtomicDouble balance;
public double deposit(double amount) {
return balance.addAndGet(amount);
}
//省略其他代碼
}
JDK提供了很多原子操作類來保證操作的原子性。原子操作類的底層是使用CAS機制的,這個機制對原子性的保證和synchronized有本質的差別。CAS機制保證了整個指派操作是原子的不能被打斷的,而synchronized值能保證代碼最後執行結果的正确性,也就是說synchronized能消除原子性問題對代碼最後執行結果的影響。
最好的方法還是使用無鎖程式設計
提到托線程程式設計,就繞不開鎖,但是鎖本身又是性能殺手,是以就出現了無鎖程式設計這一龐大而又複雜的話題。而上面所說的CAS機制就是一種無鎖程式設計的應用。
但是CAS我還沒學會,挖個坑吧
簡單總結
在多線程程式設計環境下(無論是多核CPU還是單核CPU),對共享變量的通路存在原子性問題。這個問題可能會導緻程式錯誤的執行結果。JMM主要提供了如下的方式來保證操作的原子,保證程式不受原子性問題的影響。
- synchronized機制:保證程式最終正确性,是的程式不受原子性問題的影響;
- Lock接口:和synchronized類似;
- 原子操作類:底層使用CAS機制,能保證操作真正的原子性。
參考
程式員自由之路
HollisChuang’s Blog » 既生synchronized,何生volatile