對(duì)象共享:Java并發(fā)環(huán)境中的煩心事

相關(guān)文章: 多線程安全性:每個(gè)人都在談,但是不是每個(gè)人都談地清

并發(fā)的意義在于多線程協(xié)作完成某項(xiàng)任務(wù)涯穷,而線程的協(xié)作就不可避免地需要共享數(shù)據(jù)。今天我們就來討論下如何發(fā)布和共享類對(duì)象编饺,使其可以被多個(gè)線程安全地訪問呀酸。

之前,我們討論了同步操作在多線程安全中如何保證原子性该贾,其實(shí)關(guān)鍵字synchronized不光實(shí)現(xiàn)了原子性羔杨,還實(shí)現(xiàn)內(nèi)存可見性(Memory Visibility)。也就是在同步的過程中杨蛋,不僅要防止某個(gè)線程正在使用的狀態(tài)被另一個(gè)線程修改兜材,還要保證一個(gè)線程修改了對(duì)象狀態(tài)之后理澎,其他線程能獲得更新之后的狀態(tài)。

1. 內(nèi)存可見性

在單個(gè)線程環(huán)境中曙寡,對(duì)某個(gè)變量寫入值后糠爬,在沒有其他寫操作的情況下,讀取該變量的值總是相同举庶;但是在多線程環(huán)境中情況并非如此执隧,雖然難以接受且違反直觀,但是很多問題就是這樣發(fā)生的灯变,這都是由于沒有使用同步機(jī)制保證可見性殴玛。

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            //內(nèi)部靜態(tài)類可以直接使用外部類的靜態(tài)域
            while (!ready){
                // 線程讓步,使當(dāng)前線程從執(zhí)行狀態(tài)(運(yùn)行狀態(tài))變?yōu)榭蓤?zhí)行態(tài)(就緒狀態(tài))。
                // 就是說當(dāng)一個(gè)線程使用了這個(gè)方法之后添祸,它就會(huì)把自己CPU執(zhí)行的時(shí)間讓掉滚粟,
                // 讓自己或者其它的線程運(yùn)行。
                Thread.yield();
            }
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        //JVM可能對(duì)一些語句進(jìn)行重排序
        number = 42;
        ready = true;
    }
}

上面的期望的代碼結(jié)果是:因主線程執(zhí)行ready = true刃泌,匿名子線程退出循環(huán)凡壤,打印number。但是很可能事與愿違:由于匿名線程和主線程并不是一個(gè)線程環(huán)境耙替,雖然主線程中更新了ready變量的值亚侠,但是由于缺少同步機(jī)制,更新之后的值不一定對(duì)匿名子線程是可見的俗扇,匿名子線程很可能就由于使用了失效的數(shù)據(jù)而不能正常工作.

失效數(shù)據(jù)是由于Java的內(nèi)存機(jī)制導(dǎo)致的:在沒有同步機(jī)制的情況下硝烂,在多線程的環(huán)境中,每個(gè)進(jìn)程單獨(dú)使用保存在自己的線程環(huán)境中的變量拷貝铜幽。正因如此滞谢,當(dāng)多線程共享一個(gè)可變狀態(tài)時(shí),該狀態(tài)就會(huì)有多份拷貝除抛,當(dāng)一個(gè)線程環(huán)境中的變量拷貝被修改了狮杨,并不會(huì)立刻就去更新其他線程中的變量拷貝。

有些情況下到忽,上面的程序會(huì)輸出0橄教,這是由于重排序的發(fā)生,也就是JVM根據(jù)優(yōu)化的需要調(diào)整“不相關(guān)”代碼的執(zhí)行順序喘漏。在主線程中护蝶,number = 42ready = true看似是不相關(guān)的,不相互依賴翩迈,所以可能被JVM在編譯時(shí)顛倒執(zhí)行順序滓走,所以才會(huì)出現(xiàn)這個(gè)奇怪結(jié)果。

重排序和變量多拷貝可能看上去是一種奇怪的設(shè)計(jì)帽馋,但是這樣做的目的是希望JVM能充分利用多核處理器強(qiáng)大的性能搅方,Java內(nèi)存模型更為具體的內(nèi)容將會(huì)在未來的篇章中為大家詳細(xì)介紹。

1.1 加鎖和可見性

正像前文提到同步控制那樣绽族,加鎖的含義也不僅僅局限于建立互斥性以保證原子性姨涡,還涉及到內(nèi)存可見性。為確保所有線程都能看到共享變量的最新值吧慢,所有對(duì)該變量執(zhí)行讀操作和寫操作的線程都必須在同一個(gè)鎖上同步涛漂。

1.2 Volatile變量

加鎖當(dāng)然是多線程安全的完備方法,但是有的時(shí)候只需要確保少數(shù)狀態(tài)變量的可見性即可检诗,使用加鎖機(jī)制未免有些大材小用匈仗,因此Java語言提供一種稍弱的同步機(jī)制——Volatile變量。當(dāng)變量被聲明為Volatile類型后逢慌,在編譯時(shí)和運(yùn)行時(shí)悠轩,JVM都會(huì)注意到這是一個(gè)共享變量,既不會(huì)在編譯時(shí)對(duì)該變量的操作進(jìn)行重排序攻泼,也不會(huì)緩存該變量到其他線程不可見的地方火架,保證所有線程都能讀取到該變量的最新狀態(tài)。

訪問Volatile變量時(shí)并沒使用加鎖操作忙菠,不會(huì)阻塞線程的運(yùn)行何鸡,所以性能遠(yuǎn)遠(yuǎn)優(yōu)于同步代碼塊和上鎖機(jī)制,只比訪問正常變量略高牛欢,不過這是犧牲原子性為代價(jià)的骡男。

加鎖機(jī)制可以確保可見性傍睹、原子性和不可重排序性隔盛,但是Volatile變量只能確保可見性不可重排序性

使用Volatile變量時(shí)需要謹(jǐn)慎焰望,一定要確保以下所有條件:

  1. 對(duì)當(dāng)前變量的寫操作骚亿,不依賴變量的當(dāng)前值(比如++操作就不符合要求),或者確保只有一個(gè)進(jìn)程更新該變量狀態(tài)熊赖;
  2. 該變量不會(huì)和其他變量一起納入不變性條件中来屠;
  3. 訪問該變量不需要加鎖;

實(shí)際使用中震鹉,Volatile變量多使用在會(huì)發(fā)生狀態(tài)翻轉(zhuǎn)的標(biāo)志位上俱笛。

2. 發(fā)布與逸出

對(duì)象的可見性是保證對(duì)象的最新狀態(tài)被共享,同時(shí)我們還應(yīng)該注意防止不應(yīng)該被共享的對(duì)象被暴露在多線程環(huán)境中传趾。

發(fā)布對(duì)象意味著該對(duì)象能在當(dāng)前作用域之外的代碼中被使用迎膜,比如,將類內(nèi)部的對(duì)象傳給其他類使用浆兰,或者一個(gè)非私有方法返回了該對(duì)象的引用等等磕仅。Java中強(qiáng)調(diào)類的封裝性就是希望能合理的發(fā)布對(duì)象珊豹,保護(hù)類的內(nèi)部信息。發(fā)布類內(nèi)部狀態(tài)榕订,在多線程的環(huán)境下可能問題不大店茶,但是在并發(fā)環(huán)境中卻用可能嚴(yán)重地破壞多線程安全。

某個(gè)不該發(fā)布的對(duì)象被發(fā)布了劫恒,這種情況被稱為逸出.
我們來一起看看幾種逸出的例子:

class UnsafeStates {
    private String[] states = new String[]{
        "AK", "AL" /*...*/
    };

    public String[] getStates() {
        return states;
    }
}

上面的例子中贩幻,雖然states是私有變量,但是其被共有方法所暴露两嘴,數(shù)組中的元素都可以被任意修改丛楚,這就是一種逸出的情況。

當(dāng)一個(gè)對(duì)象被發(fā)布時(shí)憔辫,該對(duì)象的非私有域中的所有引用都會(huì)被發(fā)布趣些,即間接發(fā)布

有一種逸出是比較隱蔽的螺垢,就是This逸出:

public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        });
    }
}

內(nèi)部的匿名類是隱私持有外部類的this引用的喧务,這就無意中將this發(fā)布給內(nèi)部類,如果內(nèi)部類再被發(fā)布枉圃,則外部類就可能逸出功茴,無意間造成內(nèi)存泄漏和多線程安全問題。

具體來說孽亲,只有當(dāng)構(gòu)造器執(zhí)行結(jié)束后坎穿,this對(duì)象完成初始化后才能發(fā)布,否者就是一種不正確的構(gòu)造返劲,存在多線程安全隱患玲昧。

解決這個(gè)問題最常見的方法就是工廠模式

public class SafeListener {
    private final EventListener listener;

    private SafeListener() {
        listener = new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        };
    }

    public static SafeListener newInstance(EventSource source) {
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        return safe;
    }
}

上例中,外部類的構(gòu)造器被設(shè)置為私有的篮绿,其他類執(zhí)行外部類的公有靜態(tài)方法在構(gòu)造器執(zhí)行完畢之后才返回對(duì)象的引用孵延,避免了this對(duì)象的逸出問題。

相對(duì)而言亲配,對(duì)象安全發(fā)布的問題比可見性問題更容易被忽視尘应,接下來就討論下如何才能安全發(fā)布對(duì)象。

3. 線程封閉

對(duì)象的發(fā)布既然是個(gè)頭疼的問題吼虎,所以我們應(yīng)該避免泛濫地發(fā)布對(duì)象犬钢,最簡(jiǎn)單的方式就是盡可能把對(duì)象的使用范圍都控制在單線程環(huán)境中,也就是線程封閉思灰。

常見的線程封閉方法有:

  1. Ad-hoc線程封閉玷犹,也就是維護(hù)線程封閉性的責(zé)任完全由編程承擔(dān),這種方法是不推薦的洒疚;
  2. 局部變量封閉歹颓,很多人容易忽視一點(diǎn)坯屿,局部變量的固有屬性之一就是封閉在執(zhí)行線程內(nèi),無法被外界引用晴股,所以盡量使用局部變量可以減少逸出的發(fā)生愿伴;
  3. ThreadLocal,這是一種更為規(guī)范的方法电湘,該類將把進(jìn)程中的某個(gè)值和保存值的對(duì)象關(guān)聯(lián)起來,并提供get和set方法鹅经,保證get方法獲得的值都是當(dāng)前進(jìn)程調(diào)用set方法設(shè)置的最新值寂呛。

需要說明的是,看起來是ThreadLocal類似于一種 Map<Thread, T>對(duì)象瘾晃,來保存特定于線程的值贷痪,但實(shí)際上這些值** **,其生命周期和Thread對(duì)象一致蹦误,一旦線程終止后劫拢,線程對(duì)象中的值都會(huì)被回收。

ThreadLoacl在JDBC和J2EE容器中有著大量的應(yīng)用强胰。比如舱沧,在JDBC中,ThreadLoacl用來保證每個(gè)線程只能有一個(gè)數(shù)據(jù)庫(kù)連接偶洋,再如在J2EE中熟吏,用以保存線程的上下文,方便線程切換等玄窝。

4. 不變性

如果一定要將發(fā)布對(duì)象牵寺,那么不可變的對(duì)象是首選,因?yàn)槠湟欢ㄊ嵌嗑€程安全的恩脂,可以放心地被用來數(shù)據(jù)共享帽氓。這是因?yàn)椴蛔兊膶?duì)象的狀態(tài)只有一種狀態(tài),并且該狀態(tài)由其構(gòu)造器控制俩块。

對(duì)象不可變要求滿足以下條件:

  1. 該對(duì)象是正確創(chuàng)建的黎休,沒有this逸出問題;
  2. 該對(duì)象的所有狀態(tài)在創(chuàng)建之后不能修改典阵,也就是其set方法應(yīng)該為私有的奋渔,或者該域直接是final的。

下面這個(gè)類就是不可變的:

@Immutable
 public final class ThreeStooges {
    private final Set<String> stooges = new HashSet<String>();

    public ThreeStooges() {
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
    }

    public boolean isStooge(String name) {
        return stooges.contains(name);
    }

    public String getStoogeNames() {
        List<String> stooges = new Vector<String>();
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
        return stooges.toString();
    }
}

《Effective Java》建議在類設(shè)計(jì)時(shí)應(yīng)該盡可能減少可變的域:除非必須壮啊,域都應(yīng)該是私有域嫉鲸;除非可變,域都應(yīng)該是final域歹啼。

5. 安全發(fā)布

要安全地發(fā)布一個(gè)對(duì)象玄渗,對(duì)象的引用以及對(duì)象的狀態(tài)必須同時(shí)對(duì)其他線程可見座菠。一個(gè)正確構(gòu)造的對(duì)象可以通過以下方式安全地發(fā)布:

  1. 在靜態(tài)初始化函數(shù)中初始化一個(gè)對(duì)象的引用(態(tài)初始化函數(shù)由JVM在初始化階段執(zhí)行,JVM為其提供同步機(jī)制)藤树;
  2. 將對(duì)象的引用保存在Volatile域或AtomicReference對(duì)象中浴滴;
  3. 將對(duì)象的引用保存在某個(gè)正確構(gòu)造對(duì)象的final域中;
  4. 將對(duì)象的引用保存到一個(gè)由鎖保護(hù)的域中岁钓;
  5. 將對(duì)象的引用保存到線程安全容器中升略;

6. 總結(jié)

在討論過可見性和安全發(fā)布之后,我們來總結(jié)下安全共享對(duì)象的策略:

  1. 線程封閉:線程封閉的對(duì)象只能由一個(gè)線程擁有屡限,對(duì)象封閉在線程中品嚣,并且只能由該線程修改。
  2. 只讀共享:共享不可變的只讀對(duì)象钧大,只要保證可見性即可翰撑,可以不需要額外的同步操作。
  3. 線程安全共享:線程安全的對(duì)象在其內(nèi)部封裝同步機(jī)制啊央,多線程通過公有接口訪問數(shù)據(jù)眶诈;對(duì)象發(fā)布的內(nèi)部狀態(tài)必須是安全發(fā)布的,且可變的狀態(tài)需要鎖來保護(hù)瓜饥;對(duì)象的引用和對(duì)象的狀態(tài)都是可見的逝撬。

后續(xù)預(yù)告:Java內(nèi)存模型

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市压固,隨后出現(xiàn)的幾起案子球拦,更是在濱河造成了極大的恐慌,老刑警劉巖帐我,帶你破解...
    沈念sama閱讀 217,084評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坎炼,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡拦键,警方通過查閱死者的電腦和手機(jī)谣光,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芬为,“玉大人萄金,你說我怎么就攤上這事∶碾” “怎么了氧敢?”我有些...
    開封第一講書人閱讀 163,450評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)询张。 經(jīng)常有香客問我孙乖,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,322評(píng)論 1 293
  • 正文 為了忘掉前任唯袄,我火速辦了婚禮弯屈,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘恋拷。我一直安慰自己资厉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,370評(píng)論 6 390
  • 文/花漫 我一把揭開白布蔬顾。 她就那樣靜靜地躺著宴偿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪阎抒。 梳的紋絲不亂的頭發(fā)上酪我,一...
    開封第一講書人閱讀 51,274評(píng)論 1 300
  • 那天,我揣著相機(jī)與錄音且叁,去河邊找鬼。 笑死秩伞,一個(gè)胖子當(dāng)著我的面吹牛逞带,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播纱新,決...
    沈念sama閱讀 40,126評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼展氓,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了脸爱?” 一聲冷哼從身側(cè)響起遇汞,我...
    開封第一講書人閱讀 38,980評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎簿废,沒想到半個(gè)月后空入,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,414評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡族檬,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,599評(píng)論 3 334
  • 正文 我和宋清朗相戀三年歪赢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片单料。...
    茶點(diǎn)故事閱讀 39,773評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡埋凯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出扫尖,到底是詐尸還是另有隱情白对,我是刑警寧澤,帶...
    沈念sama閱讀 35,470評(píng)論 5 344
  • 正文 年R本政府宣布换怖,位于F島的核電站甩恼,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜媳拴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,080評(píng)論 3 327
  • 文/蒙蒙 一黄橘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧屈溉,春花似錦塞关、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至线梗,卻和暖如春椰于,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背仪搔。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工瘾婿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人烤咧。 一個(gè)月前我還...
    沈念sama閱讀 47,865評(píng)論 2 370
  • 正文 我出身青樓偏陪,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親煮嫌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子笛谦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,689評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容