1、史上最難的題
推薦大家先別急著看下面的答案,試著看看這個題的答案是什么?剛開始看這個題的時候,第一反應(yīng)我擦嘞,這個是哪個老鐵想出的題,如此混亂的代碼調(diào)用,真是驚為天人。當(dāng)然這是一道有關(guān)于多線程的題,最低級的錯誤,就是一些人對于.start()和.run不熟悉,直接會認(rèn)為.start()之后run會占用主線程,所以得出答案等于:
比較高級的錯誤:了解start(),但是忽略了或者不知道synchronized,在那里瞎在想sleep()有什么用,有可能得出下面答案:
總而言之問了很多人,大部分第一時間都不能得出正確答案,其實(shí)正確答案如下:
這個題涉及了兩個點(diǎn):
如果對這幾個不熟悉的同學(xué)不要著急下面我都會講,下面我解釋一下整個流程: 1. 新建一個線程t, 此時線程t為new狀態(tài)。 2. 調(diào)用t.start(),將線程至于runnable狀態(tài)。 3. 這里有個爭議點(diǎn)到點(diǎn)是t線程先執(zhí)行還是tt.m2先執(zhí)行呢,我們知道此時線程t還是runnable狀態(tài),此時還沒有被cpu調(diào)度,但是我們的tt.m2()是我們本地的方法代碼,此時一定是tt.m2()先執(zhí)行。 4. 執(zhí)行tt.m2()進(jìn)入synchronized同步代碼塊,開始執(zhí)行代碼,這里的sleep()沒啥用就是混淆大家視野的,此時b=2000。 5. 在執(zhí)行tt.m2()的時候。有兩個情況: 情況A:有可能t線程已經(jīng)在執(zhí)行了,但是由于m2先進(jìn)入了同步代碼塊,這個時候t進(jìn)入阻塞狀態(tài),然后主線程也將會執(zhí)行輸出,這個時候又有一個爭議到底是誰先執(zhí)行?是t先執(zhí)行還是主線程,這里有小伙伴就會把第3點(diǎn)拿出來說,肯定是先輸出啊,t線程不是阻塞的嗎,調(diào)度到CPU肯定來不及啊?很多人忽略了一點(diǎn),synchronized其實(shí)是在1.6之后做了很多優(yōu)化的,其中就有一個自旋鎖,就能保證不需要讓出CPU,有可能剛好這部分時間和主線程輸出重合,并且在他之前就有可能發(fā)生,b先等于1000,這個時候主線程輸出其實(shí)就會有兩種情況。2000 或者 1000。 情況B:有可能t還沒執(zhí)行,tt.m2()一執(zhí)行完,他剛好就執(zhí)行,這個時候還是有兩種情況。b=2000或者1000 6. 在t線程中不論哪種情況,最后肯定會輸出1000,因?yàn)榇藭r沒有修改1000的地方了。 整個流程如下面所示: 2、線程安全對于上面的題的代碼,雖然在我們實(shí)際場景中很難出現(xiàn),但保不齊有哪位同事寫出了類似的,到時候有可能排坑的還是你自己,所以針對此想聊聊一些線程安全的事。 2.1 何為線程安全我們用《java concurrency in practice》中的一句話來表述:當(dāng)多個線程訪問一個對象時,如果不用考慮這些線程在運(yùn)行時環(huán)境下的調(diào)度和交替執(zhí)行,也不需要進(jìn)行額外的同步,或者在調(diào)用方進(jìn)行任何其它的協(xié)調(diào)操作,調(diào)用這個對象的行為都可以獲得正確的結(jié)果,那這個對象就是線程安全的。 從上我們可以得知: 1. 在什么樣的環(huán)境:多個線程的環(huán)境下。 2. 在什么樣的操作:多個線程調(diào)度和交替執(zhí)行。 3. 發(fā)生什么樣的情況: 可以獲得正確結(jié)果。 4. 誰:線程安全是用來描述對象是否是線程安全。 2.2 線程安全性我們可以按照java共享對象的安全性,將線程安全分為五個等級:不可變、絕對線程安全、相對線程安全、線程兼容、線程對立: 2.2.1 不可變在java中Immutable(不可變)對象一定是線程安全的,這是因?yàn)榫€程的調(diào)度和交替執(zhí)行不會對對象造成任何改變。同樣不可變的還有自定義常量,final及常池中的對象同樣都是不可變的。 在java中一般枚舉類,String都是常見的不可變類型,同樣的枚舉類用來實(shí)現(xiàn)單例模式是天生自帶的線程安全,在String對象中你無論調(diào)用replace(),subString()都無法修改他原來的值 2.2.2 絕對線程安全我們來看看Brian Goetz的《Java并發(fā)編程實(shí)戰(zhàn)》對其的定義:當(dāng)多個線程訪問某個類時,不管運(yùn)行時環(huán)境采用何種調(diào)度方式或者這些線程將如何交替進(jìn)行,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)同,這個類都能表現(xiàn)出正確的行為,那么稱這個類是線程安全的。 周志明在<深入理解java虛擬機(jī)>>中講到,Brian Goetz的絕對線程安全類定義是非常嚴(yán)格的,要實(shí)現(xiàn)一個絕對線程安全的類通常需要付出很大的、甚至有時候是不切實(shí)際的代價。同時他也列舉了Vector的例子,雖然Vectorget和remove都是synchronized修飾的,但還是展現(xiàn)了Vector其實(shí)不是絕對線程安全。簡單介紹下這個例子:
如果我們使用多個線程執(zhí)行上面的代碼,雖然remove和get是同步保證的,但是會出現(xiàn)這個問題有可能已經(jīng)remove掉了最后一個元素,但是list.size()這個時候已經(jīng)獲取了,其實(shí)get的時候就會拋出異常,因?yàn)槟莻€元素已經(jīng)remove。 2.2.3 相對安全周志明認(rèn)為這個定義可以適當(dāng)弱化,把“調(diào)用這個對象的行為”限定為“對對象單獨(dú)的操作”,這樣一來就可以得到相對線程安全的定義。其需要保證對這個對象單獨(dú)的操作是線程安全的,我們在調(diào)用的時候不需要做額外的操作,但是對于一些特定的順序連續(xù)調(diào)用,需要額外的同步手段。我們可以將上面的Vector的調(diào)用修改為:
這樣我們作為調(diào)用方額外加了同步手段,其Vector就符合我們的相對安全。 2.2.4 線程兼容線程兼容是指其對象并不是線程安全,但是可以通過調(diào)用端正確地使用同步手段,比如我們可以對ArrayList進(jìn)行加鎖,一樣可以達(dá)到Vector的效果。 2.2.5 線程對立線程對立是指無論調(diào)用端是否采取了同步措施,都無法在多線程環(huán)境中并發(fā)使用的代碼。由于Java語言天生就具備多線程特性,線程對立這種排斥多線程的代碼是很少出現(xiàn)的,而且通常都是有害的,應(yīng)當(dāng)盡量避免。 2.3 如何解決線程安全對于解決線程安全一般來說有幾個辦法:互斥阻塞(悲觀,加鎖),非阻塞同步(類似樂觀鎖,CAS),不需要同步(代碼寫得好,完全不需要考慮同步)
2.3.1 互斥同步互斥是一種悲觀的手段,因?yàn)樗麚?dān)心他訪問的時候時刻有人會破壞他的數(shù)據(jù),所以他需要通過某種手段進(jìn)行將這個數(shù)據(jù)在這個時間段給占為獨(dú)有,不能讓其他人有接觸的機(jī)會。臨界區(qū)(CriticalSection)、互斥量(Mutex)和信號量(Semaphore)都是主要的互斥實(shí)現(xiàn)方式。在Java中一般用ReentrantLock和synchronized 實(shí)現(xiàn)同步。 而實(shí)際業(yè)務(wù)當(dāng)中,推薦使用synchronized,在第一節(jié)的代碼其實(shí)也是使用的synchronized ,為什么推薦使用synchronized 的呢?
如果你在業(yè)務(wù)中需要等待可中斷,等待超時,公平鎖等功能的話,那你可以選擇這個ReentrantLock。 當(dāng)然在我們的Mysql數(shù)據(jù)庫中排他鎖其實(shí)也是互斥同步的實(shí)現(xiàn),當(dāng)加上排他鎖,其他事務(wù)都不能進(jìn)行訪問其數(shù)據(jù)。 2.3.2 非阻塞同步非阻塞同步是一種樂觀的手段,在樂觀的手段中他會先去嘗試操作,如果沒有人在競爭,就成功,否則就進(jìn)行補(bǔ)償(一般就是死循環(huán)重試或者循環(huán)多次之后跳出),在互斥同步最重要的問題就是進(jìn)行線程阻塞和喚醒所帶來的性能問題,而樂觀同步策略解決了這一問題。 但是上面就有個問題操作和檢測是否有人競爭這兩個操作一定得保證原子性,這就需要我們硬件設(shè)備的支持,例如我們java中的cas操作其實(shí)就是操作的硬件底層的指令。 在JDK1.5之后,Java程序中才可以使用CAS操作,該操作由sun.misc.Unsafe類里面的compareAndSwapInt()和compareAndSwapLong()等幾個方法包裝提供,虛擬機(jī)在內(nèi)部對這些方法做了特殊處理,即時編譯出來的結(jié)果就是一條平臺相關(guān)的處理器CAS之類,沒有方法調(diào)用的過程,或者可以認(rèn)為是無條件內(nèi)聯(lián)進(jìn)去了 2.3.3 無同步要保證線程安全,并不一定就要進(jìn)行同步,兩者沒有因果關(guān)系。同步只是保障共享數(shù)據(jù)爭用時的正確性手段,如果一個方法本來就不涉及共享數(shù)據(jù),那它自然就無須任何同步措施去保證正確性,因此會有一些代碼天生就是現(xiàn)場安全的。 一般分為兩類:
例如這種代碼就是可重入代碼,但是在我們自己的代碼中其實(shí)出現(xiàn)得很少
2.4 線程安全的一些其他經(jīng)驗(yàn)上面寫得都比較官方,下面說說從一些真實(shí)的經(jīng)驗(yàn)中總結(jié)出來的:
最后本文從最開始的一道號稱史上最難的面試題,引入了我們工作中最為重要之一的線程安全。希望大家后續(xù)可以好好的閱讀周志明的《深入理解jvm虛擬機(jī)》的第13章線程安全和鎖優(yōu)化,相信讀完之后一定會有一個新的提升。由于作者本人水平有限,如果有什么錯誤,還請指正。
架構(gòu)文摘 ID:ArchDigest 互聯(lián)網(wǎng)應(yīng)用架構(gòu)丨架構(gòu)技術(shù)丨大型網(wǎng)站丨大數(shù)據(jù)丨機(jī)器學(xué)習(xí) |
|