理論
線程并發(fā)執(zhí)行下的原子性担扑、可見(jiàn)性、順序性問(wèn)題
- 原子性:線程上下文切換導(dǎo)致指令交錯(cuò)執(zhí)行而產(chǎn)生代碼執(zhí)行非原子性問(wèn)題鸭丛,可以通過(guò)加synchorized鎖來(lái)保障原子性罐监,線程1獲取到鎖之后,線程2就不能在執(zhí)行臨界區(qū)中的代碼贯吓,需要等待線程1釋放鎖之后線程2獲取鎖才可以執(zhí)行臨界區(qū)代碼懈凹,臨界區(qū)代碼始終只能有一個(gè)線程在執(zhí)行,從而保障原子性悄谐。
- 可見(jiàn)性:java內(nèi)存模型是由主內(nèi)存和工作內(nèi)存組成的介评,主內(nèi)存中存放的變量是所有線程都可以訪問(wèn)的,工作內(nèi)存中的變量是每個(gè)線程獨(dú)有的爬舰。線程從主內(nèi)存中取出變量后會(huì)在自己的工作內(nèi)存中做緩存们陆,緩存之后線程再訪問(wèn)數(shù)據(jù)就會(huì)從自己的緩存中取數(shù)據(jù)寒瓦,如果另外一個(gè)線程對(duì)主內(nèi)存中的變量進(jìn)行寫操作可能不會(huì)把修改之后的變量同步到其它線程的工作內(nèi)存中從而導(dǎo)致線程的不可見(jiàn)性∑撼穑可以通過(guò)加鎖或者加volatile關(guān)鍵字來(lái)保障變量的可見(jiàn)性孵构。
- 有序性:cpu為了提升指令執(zhí)行的吞吐量,會(huì)使用并發(fā)的方式執(zhí)行指令烟很,為了提升指令的并發(fā)執(zhí)行效率會(huì)對(duì)指令進(jìn)行重排序颈墅。指令重排之后會(huì)導(dǎo)致代碼的執(zhí)行順序被打亂,如果多線程交錯(cuò)執(zhí)行指令時(shí)就會(huì)導(dǎo)致一些問(wèn)題雾袱⌒羯福可以通過(guò)添加volatile的方式防止指令重排。synchorized也可以保障代碼的有序性芹橡,但是不能阻止臨界區(qū)內(nèi)的指令重排毒坛,前提是成員變量要完全交給synchorized管理。
volatile原理
volatile 可以保障指令執(zhí)行的可見(jiàn)性和有序性林说,變量如果用volatile修飾后會(huì)煎殷,如果對(duì)變量進(jìn)行讀操作時(shí)會(huì)加上讀屏障讀屏障下面的代碼直接從主內(nèi)存中讀數(shù)據(jù)并且讀屏障下面的代碼不會(huì)被重排到讀屏障上面。對(duì)變量進(jìn)行寫操作時(shí)會(huì)加上寫屏障腿箩,寫屏障上面的代碼會(huì)直接把變量寫到主存中并且寫屏障上面的代碼不會(huì)被重排到寫屏障下面豪直。從而可以保障指令執(zhí)行的可見(jiàn)性和有序性。
volatile只有一個(gè)線程執(zhí)行寫操作珠移,其它單個(gè)或多個(gè)線程執(zhí)行讀操作的情況弓乙,如果存在多個(gè)線程對(duì)添加volatile修飾的變量進(jìn)行寫操作還有可能會(huì)存在臟讀等問(wèn)題。
代碼解析
案例一
單例模式j(luò)ava代碼
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
if(INSTANCE == null) { // t2
// 首次訪問(wèn)會(huì)同步钧惧,而之后的使用沒(méi)有 synchronized
synchronized(Singleton.class) {
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
單例模式j(luò)ava字節(jié)碼解析(上面的java代碼編譯后的字節(jié)碼)
- 17行 在椣救停空間創(chuàng)建新的對(duì)象的引用變量
- 20行 復(fù)制一份新建的對(duì)象的引用變量
- 21行 復(fù)制的引用變量調(diào)用構(gòu)造方法
- 24行 新建的引用變量復(fù)制給靜態(tài)成員變量
- 也許jvm會(huì)對(duì)指令進(jìn)行優(yōu)化優(yōu)化后21行和24行的執(zhí)行順序調(diào)換。
如上圖如果字節(jié)碼執(zhí)行時(shí)發(fā)生指令重排21行和24行的執(zhí)行順序發(fā)生調(diào)換浓瞪,假如現(xiàn)在有一個(gè)線程t1執(zhí)行到了引用變量賦值給靜態(tài)變量INSTANCE但是懈玻,引用變量還沒(méi)來(lái)得及調(diào)用構(gòu)造方法創(chuàng)建對(duì)象,此時(shí)發(fā)生了上下文切換乾颁,t2線程執(zhí)行g(shù)etInstance方法判斷INSTANCE已經(jīng)不為NULL了就會(huì)直接返回,但是事實(shí)上INSTANCE賦值的對(duì)象還沒(méi)來(lái)得及調(diào)用構(gòu)造方法創(chuàng)建對(duì)象钮孵。所以返回的對(duì)象用起來(lái)就會(huì)出現(xiàn)問(wèn)題。
案例二
指令重排證明及解決方案
int num = 0;
boolean ready = false;
@Actor
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
}
上面代碼通過(guò)測(cè)試工具測(cè)試結(jié)果如下圖
如上圖按照正常運(yùn)行就算是多線程并發(fā)執(zhí)行這段代碼所能出現(xiàn)的結(jié)果可能就是1或者4但是實(shí)際運(yùn)行過(guò)程中還會(huì)存在一種可能就是0历涝,那么什么時(shí)候會(huì)出現(xiàn)0的結(jié)果呢,如果發(fā)生指令重排導(dǎo)致num=2;和ready=true;這兩行代碼的執(zhí)行順序交換然后就會(huì)可能出現(xiàn)結(jié)果為0的情況。
給ready 成員變量添加volatile修飾測(cè)試的結(jié)果就不會(huì)再出現(xiàn)0的結(jié)果荧库,因?yàn)閷?duì) volatile 變量的寫指令后會(huì)加入寫屏障對(duì)volatile變量的讀指令前會(huì)加入讀屏障。
int num = 0;
volatile boolean ready = false;
@Actor
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
}
volatile修飾過(guò)的變量添加了都屏障和寫屏障所以可以保障指令執(zhí)行時(shí)的有序性
happens-before
happerns-before指的是一個(gè)線程對(duì)共享變量的寫操作對(duì)于其它線程讀共享變量都是可見(jiàn)的分衫,一共有7種情況,如果不屬于這7種情況的共享變量的讀操作都是不可見(jiàn)的蚪战。
單例實(shí)現(xiàn)問(wèn)題分析
實(shí)現(xiàn)1
// 問(wèn)題1:為什么加 final
// 問(wèn)題2:如果實(shí)現(xiàn)了序列化接口, 還要做什么來(lái)防止反序列化破壞單例
public final class Singleton implements Serializable {
// 問(wèn)題3:為什么設(shè)置為私有? 是否能防止反射創(chuàng)建新的實(shí)例?
private Singleton() {}
// 問(wèn)題4:這樣初始化是否能保證單例對(duì)象創(chuàng)建時(shí)的線程安全?
private static final Singleton INSTANCE = new Singleton();
// 問(wèn)題5:為什么提供靜態(tài)方法而不是直接將 INSTANCE 設(shè)置為 public, 說(shuō)出你知道的理由
public static Singleton getInstance() {
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}
- 添加final是為了保障單例類不會(huì)被其它類繼承然后修改方法創(chuàng)建多個(gè)對(duì)象牵现。
- 通過(guò)readResolve方法來(lái)防止反序列化破壞單例。
- 不能繁殖反射創(chuàng)建新的實(shí)例
- 可以保障單例對(duì)象創(chuàng)建時(shí)的線程安全邀桑,因?yàn)閟tatic修飾的變量默認(rèn)是jvm加載類對(duì)象時(shí)創(chuàng)建對(duì)象,jvm可以保障創(chuàng)建單例對(duì)象的線程安全性壁畸。
- 面向?qū)ο蟮乃枷耄梢栽讷@取INSTANCE時(shí)添加一些自己的實(shí)現(xiàn)捏萍。
實(shí)現(xiàn)2
// 問(wèn)題1:枚舉單例是如何限制實(shí)例個(gè)數(shù)的
// 問(wèn)題2:枚舉單例在創(chuàng)建時(shí)是否有并發(fā)問(wèn)題
// 問(wèn)題3:枚舉單例能否被反射破壞單例
// 問(wèn)題4:枚舉單例能否被反序列化破壞單例
// 問(wèn)題5:枚舉單例屬于懶漢式還是餓漢式
// 問(wèn)題6:枚舉單例如果希望加入一些單例創(chuàng)建時(shí)的初始化邏輯該如何做
enum Singleton {
INSTANCE;
}
- 由上圖字節(jié)碼可以看到枚舉類型編譯成字節(jié)碼后實(shí)際上也是在自己的類內(nèi)部新建了一個(gè)靜態(tài)變量,靜態(tài)變量會(huì)在jvm 加載類是創(chuàng)建對(duì)象并且復(fù)制給靜態(tài)變量走敌,所以由jvm保障單例創(chuàng)建的安全性这揣。
- 沒(méi)有并發(fā)問(wèn)題悔常。
- 不能被反射破壞單例给赞。
- 不能被反序列化破壞單例矫户。
- 屬于餓漢式。
- 可以通過(guò)布爾類型的構(gòu)造方法柑蛇。
實(shí)現(xiàn)3
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
// 分析這里的線程安全, 并說(shuō)明有什么缺點(diǎn)
public static synchronized Singleton getInstance() {
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
是線程安全的,缺點(diǎn)是線程每次獲取單例對(duì)象都要獲取鎖和釋放鎖會(huì)造成系統(tǒng)資源的浪費(fèi)耻台。
實(shí)現(xiàn)4
public final class Singleton {
private Singleton() { }
// 問(wèn)題1:解釋為什么要加 volatile ?
private static volatile Singleton INSTANCE = null;
// 問(wèn)題2:對(duì)比實(shí)現(xiàn)3, 說(shuō)出這樣做的意義
public static Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (Singleton.class) {
// 問(wèn)題3:為什么還要在這里加為空判斷, 之前不是判斷過(guò)了嗎
if (INSTANCE != null) { // t2
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
}
- 加volatile為了保障可見(jiàn)性和有序性
- 跟實(shí)現(xiàn)3相比縮小了鎖臨界區(qū)的范圍空另,如果單例對(duì)象已經(jīng)創(chuàng)建成功了其它線程再獲取實(shí)例時(shí)就不會(huì)再獲取一次鎖,可以提升程序執(zhí)行的效率。
- 如果多線程并發(fā)執(zhí)行時(shí)如果t1獲取鎖但是還未創(chuàng)建單例對(duì)象成功時(shí)發(fā)生上下文切換坝咐,t2線程也獲取鎖并且進(jìn)入blocked狀態(tài)析恢。t1執(zhí)行完之后t2獲取到鎖并且執(zhí)行時(shí)如果沒(méi)有為空判斷會(huì)再一次創(chuàng)建單例對(duì)象。
實(shí)現(xiàn)5
public final class Singleton {
private Singleton() { }
// 問(wèn)題1:屬于懶漢式還是餓漢式
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 問(wèn)題2:在創(chuàng)建時(shí)是否有并發(fā)問(wèn)題
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
- 屬于懶漢式泽篮,jvm加載class類時(shí)并不是一上來(lái)就加載class類而是用時(shí)才會(huì)加載,所以在jvm加載singleton時(shí)并不會(huì)立刻加載LazyHolder柑船,所以基于這個(gè)原理實(shí)現(xiàn)懶漢式創(chuàng)建單例對(duì)象的。
- 創(chuàng)建時(shí)沒(méi)有并發(fā)問(wèn)題椎组,因?yàn)閖vm加載LazyHolder內(nèi)部類時(shí)會(huì)初始化靜態(tài)變量并且給靜態(tài)變量賦值,jvm會(huì)保障類加載時(shí)初始化靜態(tài)變量的線程安全性专筷。