雙重檢查鎖定問題:Double-checked Locking
1致讥、先來看問題代碼
- 代碼
// 單例模式構(gòu)建對(duì)象
public static LocalCache getInstance(){
// 雙重檢測(cè)鎖仅仆,提高運(yùn)行效率
if (instance == null){
synchronized (LocalCache.class){
if (instance == null) {
instance = new LocalCache();
}
}
}
return instance;
}
代碼(該方法)的預(yù)期目標(biāo)是使用 懶漢模式 的單例設(shè)計(jì)模式獲取對(duì)象,阿里插件顯示這段代碼是線程不安全的
2垢袱、改進(jìn)方案
2.1墓拜、懶漢模式改為餓漢模式
這種思路有幾種實(shí)現(xiàn)方式,這里貼一種请契,是靜態(tài)代碼塊實(shí)例化一次對(duì)象
- 代碼
static {
instance = new LocalCache();
}
// 單例模式構(gòu)建對(duì)象
public static LocalCache getInstance(){
return instance;
}
2.2咳榜、同為懶漢模式下的代碼改進(jìn)
可行的方案
instance變量加上 volatile 關(guān)鍵字夏醉,保證變量值的可見性
/**
* volatile的可見性,可以確保拿到 instance 的最終值
*/
private static volatile LocalCache instance;
// 單例模式構(gòu)建對(duì)象
public static LocalCache getInstance(){
// 雙重檢測(cè)鎖涌韩,提高運(yùn)行效率
if (instance == null){
synchronized (LocalCache.class){
if (instance == null) {
instance = new LocalCache();
}
}
}
return instance;
}
否定的方案(這個(gè)方案也不能保證線程安全畔柔,這里有點(diǎn)不太懂)
// 單例模式構(gòu)建對(duì)象
public static LocalCache getInstance(){
// 雙重檢測(cè)鎖,提高運(yùn)行效率
if (instance == null){
synchronized (LocalCache.class){
if (instance == null) {
LocalCache localCache = new LocalCache();
instance = localCache;
}
}
}
return instance;
}
3臣樱、雙重檢測(cè)鎖定問題產(chǎn)生的原因
3.1靶擦、簡(jiǎn)單來說
instance = new LocalCache();
這行代碼有三步操作(《碼出高效Java開發(fā)手冊(cè)》P233頁(yè)提到有兩步操作,個(gè)人感覺不太好解釋):
- 初始化 LocalCache 實(shí)例
- 為本來為null的instance變量開辟內(nèi)存空間雇毫,并確定默認(rèn)大行丁(這一點(diǎn)《碼出高效》P233頁(yè)書中并沒有提到)
- 將對(duì)象地址寫進(jìn) instance 字段
這三步操作并不是原子化的
3.2、舉個(gè)例子
- 線程A進(jìn)入到
if(instance == null){
的時(shí)候棚放,instance為null - 線程A進(jìn)入同步代碼塊(synchronized括起來的代碼塊)枚粘,到
instance = new LocalCache();
時(shí)執(zhí)行了 為instance開辟內(nèi)存空間 和 將對(duì)象的引用存入內(nèi)存空間 的動(dòng)作,但是沒有實(shí)例化LocalCache對(duì)象 - 線程B執(zhí)行到
if(instance == null){
的時(shí)候飘蚯,instance不為null
(但是實(shí)際沒有指向某個(gè)堆內(nèi)的內(nèi)存馍迄,簡(jiǎn)而言之,這塊內(nèi)存空間(棧的內(nèi)存空間)的引用地址指向的(堆的)內(nèi)存空間中沒有實(shí)際對(duì)象)局骤,所以直接return了一個(gè)中間態(tài)(我自己起的名字攀圈。。)的instance - 線程B中庄涡,接下來的代碼邏輯中量承,拿到instance的值其實(shí)是有問題的(有啥問題?-TODO- 反正是有問題的-_-)
這篇blog有相關(guān)介紹
所以這里涉及到指令重排
的問題(可能也有叫“指令優(yōu)化”的-《碼出高效》P232-P232有提到)穴店,即#3.1的三步操作撕捍,CPU在執(zhí)行的時(shí)候并不會(huì)根據(jù)代碼里理解的順序(從上到下、從左到右)執(zhí)行泣洞,會(huì)判斷怎樣的組合可以提高效率忧风,重新排列指令執(zhí)行的順序(如圖)
指令重排/指令優(yōu)化 導(dǎo)致的線程安全問題
3.3、使用了volatile之后
這里用到的是volatile的防止指令重排的能力(JDK1.5之后才有的)-- volatile還有一個(gè)
可見性
的能力球凰,這里貌似沒有體現(xiàn)(下篇文章探討volatile 可見性/指令重排 問題)
- 線程A進(jìn)入到
if(instance == null){
的時(shí)候狮腿,instance為null - 線程A進(jìn)入同步代碼塊(synchronized括起來的代碼塊),到
instance = new LocalCache();
時(shí)執(zhí)行了 為instance開辟內(nèi)存空間 和 實(shí)例化LocalCache對(duì)象 的動(dòng)作呕诉,但是沒有將對(duì)象的引用存入內(nèi)存空間 - 線程B執(zhí)行到
if(instance == null){
的時(shí)候缘厢,instance為null
- 接下去就是預(yù)期的執(zhí)行流程了
4、復(fù)現(xiàn)雙重檢測(cè)問題的方式-供參考
KΥ臁贴硫!實(shí)際復(fù)現(xiàn)過程中并沒有復(fù)現(xiàn)問題,嚴(yán)重懷疑是復(fù)現(xiàn)方式還可以改進(jìn),以下復(fù)現(xiàn)方式僅供參考
懶漢模式加載的單例對(duì)象類
public class LocalCache {
private static LocalCache instance;
// 構(gòu)造方法私有化英遭,防止實(shí)例化
private LocalCache() {}
// 單例模式構(gòu)建對(duì)象
public static LocalCache getInstance() throws InterruptedException {
// // 雙重檢測(cè)鎖间护,提高運(yùn)行效率
if (instance == null){
synchronized (LocalCache.class){
if (instance == null) {
instance = new LocalCache();
}
}
}
return instance;
}
}
建了兩個(gè)線程工廠,每個(gè)工廠里面有兩根線程(總共4根)挖诸,模擬多線程環(huán)境(有更簡(jiǎn)便的寫法)
public class TestJava {
public static void main(String[] args) {
BlockingQueue blockingDeque = new LinkedBlockingDeque(2);
TestThreadFactory firstFactory = new TestThreadFactory("第一個(gè)線程池");
TestThreadFactory secondFactory = new TestThreadFactory("第二個(gè)線程池");
TestRejectHandler testRejectHandler = new TestRejectHandler();
ThreadPoolExecutor firstThreadPool = new ThreadPoolExecutor(2, 2, Integer.MAX_VALUE, TimeUnit.SECONDS, blockingDeque, firstFactory, testRejectHandler);
ThreadPoolExecutor secondThreadPool = new ThreadPoolExecutor(2, 2, Integer.MAX_VALUE, TimeUnit.SECONDS,
blockingDeque, secondFactory, testRejectHandler);
Task task = new Task();
for (int i = 0; i < 2; i++){
firstThreadPool.execute(task);
secondThreadPool.execute(task);
}
}
/**
* 線程工廠
*/
public static class TestThreadFactory implements ThreadFactory{
private final String namePrefix;
private final AtomicInteger nextId = new AtomicInteger(1);
public TestThreadFactory(String namePrefix) {
this.namePrefix = "TestThreadFactory's " + namePrefix + "-worker-";
}
@Override
public Thread newThread(Runnable task) {
String name = namePrefix + nextId.getAndIncrement();
Thread thread = new Thread(null, task, name, 0);
System.out.println(thread);
return thread;
}
}
/**
* 實(shí)際執(zhí)行任務(wù)
*/
public static class Task implements Runnable{
private final AtomicLong count = new AtomicLong(0L);
@Override
public void run() {
try {
LocalCache instance = LocalCache.getInstance();
// todo 這里做一些instance對(duì)象的操作
System.out.println("running_" + count.getAndIncrement() + ", instance: " + instance);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 當(dāng)線程異常的時(shí)候汁尺,可以打印線程異常堆棧
*/
public static class TestRejectHandler implements RejectedExecutionHandler{
@Override
public void rejectedExecution(Runnable task, ThreadPoolExecutor executor) {
System.out.println("task rejected. " + executor.toString());
}
}
}