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