- 64位寫入原子性
舉一個簡單的例子,對於一個long型變量的賦值和取值操作而言,在多線程場景下,線程A調用set(100),線程B調用get(),在某些場景下,返回值可能不是100。
package com.lc.test02;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author liuchao
* @date 2020/7/3
*/
public class Test01 {
private long a = 0;
public void set(long a) {//線程1設置值
this.a = a;
}
public long get() {//線程2獲取值,返回值可能不是100
return a;
}
public static void main(String[] args) {
Test01 test = new Test01();
ExecutorService executor = Executors.newFixedThreadPool(2);
CountDownLatch latch = new CountDownLatch(1);
executor.execute(() -> {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long i = 100L;
test.set(i);
System.out.println("-----設置:" + i);
});
executor.execute(() -> {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("----獲取:" + test.get());
});
latch.countDown();
executor.shutdown();
}
}
爲什麼獲取到的值有可能不是100呢,是因爲JVM規範並未要求64位的long和double寫入是原子性的,64位的數據有可能被拆分成兩個32位的數據寫入,這樣另一個線程拿到的數據有可能是32位的不完整的數據,如果在long屬性前面加上volatile關鍵字就可以解決此問題。
- 內存可見性
JVM將內存組織爲主內存和工作內存兩個部分。
針對主內存中的變量,線程A操作後線程B看到要經過的流程
線程A操作後數據存儲在線程A工作內存=》save到主內存=》線程B從主內存load到線程B工作內存
那在整個操作過程中是非原子性操作的,有可能線程A修改後是10,但是線程B讀取到的數據是非10,因爲線程A修改後的數據還未save到主內存,那要解決這個問題也比較簡單就是直接在屬性前面加上volatile關鍵字,也就是解決了內存可見性問題
- 禁止重排序
在經典的單線程安全的寫法上DCL
//注意,此代碼有安全問題
package com.lc.test02;
/**
* @author liuchao
* @date 2020/7/3
*/
public class Test01 {
private static Test01 instance;
public static Test01 getInstance() {
if (null == instance) {
synchronized (instance) {//爲了性能驗證 使用lock
if (null == instance) {
instance = new Test01();//有問題的代碼
}
}
}
return instance;
}
}
上述的instance = new Test01();代碼有問題:其底層會分爲三個操作:
(1)分配一塊內存。
(2)在內存上初始化成員變量。
(3)把instance引用指向內存。
在這三個操作中,操作(2)和操作(3)可能重排序,即先把instance指向內存,再初始化成員變量,因爲二者並沒有先後的依賴關係。此時,另外一個線程可能拿到一個未完全初始化的對象。這時,直接訪問裏面的成員變量,就可能出錯。這就是典型的“構造函數溢出”問題。解決辦法也很簡單,就是爲instance變量加上volatile修飾