1吊说、結(jié)論
volatile具有可見性
和防止指令重排
的能力曙咽,但是在某些場景下不能保證線程安全(無法替代synchronized關(guān)鍵字)
2去枷、原因簡析
1久免、線程安全問題中有三個概念:原子性(Atomicity)底瓣、可見性(Visibility)谢揪、有序性(Ordering)。
2捐凭、synchronized關(guān)鍵字可以保證原子性拨扶、可見性和有序性;volatile只能保證可見性和有序性(有序性體現(xiàn)在 防止指令重排 上)茁肠。
3患民、使用volatile修飾的(多線程共享的)變量進(jìn)行的是原子的修改操作時,這時volatile可以保證線程安全垦梆;除此之外匹颤,單一地使用volatile不保證線程安全。
4托猩、volatile會對總線(主存)加上LOCK前綴指令(觀察匯編源碼得知)印蓖,LOCK不是內(nèi)存屏障,但是完成的事情是類似內(nèi)存屏障(也叫內(nèi)存柵欄)的功能京腥。LOCK可以理解成是CPU一級的鎖赦肃,加上LOCK后,其他CPU對該內(nèi)存地址的原子的讀寫請求
都會被阻塞公浪,直到鎖釋放他宛。(《碼出高效Java開發(fā)手冊》P232中描述使用了volatile后“...任何對此變量的操作都會在內(nèi)存中進(jìn)行,不會產(chǎn)生副本”欠气,筆者認(rèn)為描述有問題)
5厅各、單一地使用synchronized(來保證線程安全)會有一定的效能損耗,可以用volatile搭配使用synchronized減少(因為要保證線程安全帶來的)效能損耗晃琳,也可以搭配CAS(比如自旋鎖運用了CAS -- Compare-And-Swap)讯检。
3、背景
計算機(jī)在對內(nèi)存進(jìn)行操作時卫旱,會存在主內(nèi)存(有些地方叫物理內(nèi)存)和高速緩存的概念人灼。主存中的變量值對所有線程可見,高速緩存是線程私有的--對其他線程不可見的顾翼。CPU對內(nèi)存進(jìn)行操作的時候投放,單個線程會從主存(總線)中讀取目標(biāo)內(nèi)存地址中的數(shù)據(jù),copy到高速緩存(作為副本)适贸,后續(xù)的一系列操作都是基于這個“副本”灸芳,操作完后涝桅,將副本的值同步回主存。
內(nèi)存柵欄實現(xiàn)了 可見性 和 防止指令重排 的效果
4烙样、代碼驗證
4.1冯遂、這是一段線程不安全的代碼
public class Test {
public static volatile int inc = 0;
public static void increase() throws InterruptedException {
inc++;
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 5; i++){
try {
increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("----A過程---" + Test.inc);
}
}
});
Thread thread2 = new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 5; i++){
try {
increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("----B過程---" + Test.inc);
}
}
});
thread1.start();
thread2.start();
Thread.sleep(2000);
System.out.println("--終態(tài)--" + inc);
}
}
4.2、對#4.1代碼的優(yōu)化
·#4.1的代碼不能復(fù)現(xiàn)出問題谒获,猜測可能是機(jī)器的CPU性能較好蛤肌。所以優(yōu)化了下代碼,如下
public class Test {
public static volatile int inc = 0;
public static void increase() throws InterruptedException {
Thread.sleep(1);
inc ++;
}
public static void main(String[] args) throws InterruptedException {
for (int k=0;k<10;k++){
new Thread(new Runnable(){
@Override
public void run() {
for (int i=0 ; i < 500; i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2);
increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("----A過程---" + inc);
}
}).start();
}
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
for (int i=0 ; i < 500; i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2);
increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("----B過程---" + inc);
}
}).start();
}
}
}).start();
}
Thread.sleep(5000);
System.out.println("--終態(tài)--" + inc);
}
}
4.3批狱、#4.2的運行結(jié)果
4.4、原因
因為#4.1和#4.2的模型一樣赔硫,#4.1的邏輯更簡單炒俱,故以#4.1為例講
4.4.1、首先爪膊,問題出在這一行
4.4.2权悟、其次,inc++非原子操作
4.4.3惊完、出現(xiàn)異常(結(jié)果不合預(yù)期)的情況
4.5僵芹、反思
從結(jié)果看來,在這個場景中小槐,volatile沒有發(fā)揮任何作用嘛?
我們?nèi)サ?4.2代碼中的volatile關(guān)鍵字荷辕,發(fā)現(xiàn)結(jié)果也是少于10,000
我認(rèn)為凿跳,volatile還是發(fā)揮作用的(只是沒有它沒有讓結(jié)果達(dá)到預(yù)期),舉個例子
去掉volatile后疮方,step-4的線程B不是無效掉前兩步的操作控嗜,而是將自己的副本(inc=2)更新到主存中,這時主存中的inc值又被更新了一次(2 -> 2);
假設(shè)在線程競爭中骡显,線程B獲得的CPU時間片輪遠(yuǎn)少于線程A時疆栏,當(dāng)線程A對inc更新過好幾輪了后(假設(shè)此時主存中的inc=4),線程B仍然對主存更新為2。這時主存中的inc值經(jīng)歷了幾個階段
4.6惫谤、比較明確地體現(xiàn)volatile的可見性作用的例子
1壁顶、狀態(tài)標(biāo)記量
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
2、雙重檢測鎖
class Singleton{
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
5溜歪、volatile適用的場景
1)對變量的寫操作不依賴于當(dāng)前值
2)該變量沒有包含在具有其他變量的不變式中
針對這兩點約束若专,個人還不是很理解,具體參考# volatile的適用場景
6蝴猪、參考來源
1调衰、 Java并發(fā)編程:volatile關(guān)鍵字解析
2膊爪、 volatile 和 內(nèi)存屏障
3、《碼出高效Java開發(fā)手冊》