Java并發(fā)編程(一)---原子性,可見(jiàn)性,有序性
摘要
并發(fā)編程世界里,由于CPU緩存導(dǎo)致的可見(jiàn)性問(wèn)題,線程切換導(dǎo)致的原子性問(wèn)題,以及編譯器重排序?qū)е碌挠行蛐詥?wèn)題是并發(fā)編程Bug的根源。
正文
可見(jiàn)性
一個(gè)線程對(duì)共享變量的修改。另外一個(gè)線程能夠立刻看到,我們稱之為可見(jiàn)性。共享變量指的是存放在堆內(nèi)存,由所有線程所共享的變量。比如:實(shí)例變量,靜態(tài)變量。
如圖所示:
共享變量V可以由線程A和線程B同時(shí)操作,線程A和B首先從各自的CPU緩存或者寄存器中讀取數(shù)值,然后由CPU的寄存器寫(xiě)入內(nèi)存中。
public class LongTest {
private static long atest = 0L;
public void countTest() {
for (int i = 0; i < 10000; i++) {
atest = atest + 1;
}
}
public static void main(String[] args) throws InterruptedException {
final LongTest longTest = new LongTest();
Thread threadA = new Thread(new Runnable() {
public void run() {
longTest.countTest();
}
});
Thread threadB = new Thread(new Runnable() {
public void run() {
longTest.countTest();
}
});
threadA.start();
threadB.start();
threadA.join();
threadB.join();
System.out.println("*******獲得到的atest值為=" + atest);
}
}
如上程序,運(yùn)行之后我們會(huì)發(fā)現(xiàn) atest 值永遠(yuǎn)都不會(huì)到達(dá)20000,而是在10000-20000之間的隨機(jī)數(shù)。
原因分析:假設(shè)線程A和線程B同時(shí)執(zhí)行 atest = atest + 1;, 線程A 讀取到的原值是0,執(zhí)行+1操作之后,得到新值1,同樣的,線程B也是讀取到的原值0,然后執(zhí)行+1操作,得到新值1。這樣就永遠(yuǎn)得不到結(jié)果2。
類(lèi)推的話,循環(huán)執(zhí)行10000次也是同理,線程A執(zhí)行+1操作時(shí)不能及時(shí)獲得線程B已經(jīng)寫(xiě)入的值,故導(dǎo)致值永遠(yuǎn)不可能達(dá)到20000。
原子性
由于一條高級(jí)語(yǔ)句在CPU中可能會(huì)分成若干條指令來(lái)執(zhí)行,每條指令執(zhí)行完之后就有可能會(huì)發(fā)生線程切換。故線程切換造成的原子性問(wèn)題。
例如: count=count+1 共有三個(gè)指令
指令一: 將count值從內(nèi)存加載到到CPU的寄存器中
指令二:在寄存器中+1操作
指令三 :將新值寫(xiě)入到內(nèi)存中(由于緩存機(jī)制,寫(xiě)入的可能是CPU的緩存而不是內(nèi)存)
操作系統(tǒng)做任務(wù)切換可以發(fā)生在任何一條CPU指令執(zhí)行完,是CPU指令執(zhí)行完。
我們將一個(gè)或者多個(gè)操作在CPU執(zhí)行過(guò)程中不被中斷的特性稱之為原子性。
有序性
編譯器重排序?qū)е碌挠行蛐詥?wèn)題:
例如:雙重加鎖中的:
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo(){
}
static SingletonDemo getSingletonDemo() {
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo(); //6
}
}
}
return instance;
}
}
這里會(huì)有有序性問(wèn)題:
問(wèn)題主要出在了 new SingletonDemo() 這一步
因?yàn)閕nstance = new SingletonDemo();主要有三個(gè)指令
分配內(nèi)存空間M
在內(nèi)存M上初始化對(duì)象
然后M的地址賦值給instance變量
正常順序是1-2-3
但是CPU重排序之后執(zhí)行順序可能變成了 1-3-2
步驟如下:
A首先進(jìn)入synchronized,由于instance為null,所以它執(zhí)行instance = new SingletonDemo();
然后線程 A執(zhí)行1->JVM先畫(huà)出了一些分配給SingletonDemo實(shí)例的空白內(nèi)存,并賦值給instance
在還沒(méi)有進(jìn)行第三步(將instance引用指向內(nèi)存空間)的時(shí)候,恰好發(fā)生了線程切換 ,切換到了線程B上,
如果此時(shí)線程B也執(zhí)行g(shù)etSingletonDemo()方法,那么線程B 在執(zhí)行第一個(gè)判斷是會(huì)發(fā)現(xiàn)instance!=null,所以直接返回了instance,而此時(shí)的instance是沒(méi)有初始化的。
總結(jié)
并發(fā)編程中主要的問(wèn)題就是可見(jiàn)性問(wèn)題, 原子性問(wèn)題,有序性問(wèn)題。本文介紹了這三種問(wèn)題的發(fā)生原因,以及發(fā)生的場(chǎng)景。
作者:碼農(nóng)飛哥
微信公眾號(hào):碼農(nóng)飛哥
