![](/img/trans.png)
[英]why java double check lock singleton must use the volatile keyword?
[英]double check lock without volatile is wrong?
我使用 jdk1.8。 我認為沒有 volatile 的雙重檢查鎖是正確的。 我多次使用 countdownlatch 測試,對象是單例。 如何證明它一定需要“volatile”?
更新 1
抱歉,我的代碼沒有格式化,因為我無法接收一些 JavaScript 公共類 DCLTest {
private static /*volatile*/ Singleton instance = null;
static class Singleton {
public String name;
public Singleton(String name) {
try {
//We can delete this sentence, just to simulate various situations
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.name = name;
}
}
public static Singleton getInstance() {
if (null == instance) {
synchronized (Singleton.class) {
if (null == instance) {
instance = new Singleton(Thread.currentThread().getName());
}
}
}
return instance;
}
public static void test() throws InterruptedException {
int count = 1;
while (true){
int size = 5000;
final String[] strs = new String[size];
final CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < size; i++) {
final int index = i;
new Thread(()->{
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Singleton instance = getInstance();
strs[index] = instance.name;
}).start();
}
Thread.sleep(100);
countDownLatch.countDown();
Thread.sleep(1000);
for (int i = 0; i < size-1; i++) {
if(!(strs[i].equals(strs[i+1]))){
System.out.println("i = " + strs[i] + ",i+1 = "+strs[i+1]);
System.out.println("need volatile");
return;
}
}
System.out.println(count++ + " times");
}
}
public static void main(String[] args) throws InterruptedException {
test();
}
}
您沒有看到的關鍵問題是指令可以重新排序。 因此,它們在源代碼中的順序與它們在內存中的應用順序不同。 CPU 和編譯器是原因或這種重新排序。
我不會詳細介紹雙重檢查鎖定示例的整個示例,因為有很多示例可用,但將為您提供足夠的信息來進行更多研究。
如果您有以下代碼:
if(singleton == null){
synchronized{
if(singleton == null){
singleton = new Singleton("foobar")
}
}
}
然后在引擎蓋下會發生這樣的事情。
if(singleton == null){
synchronized{
if(singleton == null){
tmp = alloc(Singleton.class)
tmp.value = "foobar"
singleton = tmp
}
}
}
到目前為止,一切都很好。 但以下重新排序是合法的:
if(singleton == null){
synchronized{
if(singleton == null){
tmp = alloc(Singleton.class)
singleton = tmp
tmp.value = "foobar"
}
}
}
所以這意味着尚未完全構造的單例(尚未設置值)已寫入單例全局變量。 如果一個不同的線程讀取這個變量,它可以看到一個部分創建的對象。
還有其他潛在的問題,如原子性(例如,如果值字段很長,它可能會被碎片化,例如讀/寫撕裂)。 還有可見性; 例如,編譯器可以優化代碼,以便優化內存中的加載/存儲。 請記住,從內存而不是緩存中讀取的思考從根本上是有缺陷的,也是我在 SO 上看到的最常遇到的誤解; 甚至很多前輩都弄錯了。 原子性、可見性和重新排序是 Java 內存模型的一部分,並且使單例變量可變,解決了所有這些問題。 它消除了數據競爭(您可以查找它以獲取更多詳細信息)。
如果你想成為真正的核心,在創建對象和分配給單例之間放置一個 [storestore] 屏障就足夠了,在讀取端放置一個 [loadload] 屏障就足夠了。 但這遠遠超出了大多數工程師的理解,並且在大多數情況下不會對性能產生太大影響。
如果您想檢查某些東西是否會損壞,請查看 JCStress:
https://github.com/openjdk/jcstress
它是一個很棒的工具,可以幫助您證明您的代碼已損壞。
如何證明它一定需要“volatile”?
作為一般規則,您無法通過測試來證明多線程應用程序的正確性。 您也許能夠證明不正確,但即使如此也不能保證。 正如你在觀察。
您沒有成功地使您的應用程序失敗的事實並不能證明它是正確的。
證明正確性的方法是在分析之前進行形式化(即數學)。
可以很直接地表明,當singleton
不是volatile
,在執行之前會發生缺失。 這可能會導致不正確的結果,例如多次進行初始化。 但不能保證您會得到不正確的結果。
另一方面是,如果使用volatile
,則發生在關系與代碼邏輯相結合之前發生的事情足以構建正式(數學)證明,您將始終得到正確的結果。
(我不打算在這里構造證明。太費力了。)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.