日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

一道號(hào)稱“史上最難”的java面試題引發(fā)的線程安全思考

 xujin3 2018-08-21

1、史上最難的題



  1. public class TestSync2 implements Runnable {

  2.    int b = 100;        


  3.    synchronized void m1() throws InterruptedException {

  4.        b = 1000;

  5.        Thread.sleep(500); //6

  6.        System.out.println('b=' + b);

  7.    }


  8.    synchronized void m2() throws InterruptedException {

  9.        Thread.sleep(250); //5

  10.        b = 2000;

  11.    }


  12.    public static void main(String[] args) throws InterruptedException {

  13.        TestSync2 tt = new TestSync2();

  14.        Thread t = new Thread(tt);  //1

  15.        t.start(); //2


  16.        tt.m2(); //3

  17.        System.out.println('main thread b=' + tt.b); //4

  18.    }


  19.    @Override

  20.    public void run() {

  21.        try {

  22.            m1();

  23.        } catch (InterruptedException e) {

  24.            e.printStackTrace();

  25.        }

  26.    }

  27. }


推薦大家先別急著看下面的答案,試著看看這個(gè)題的答案是什么?剛開(kāi)始看這個(gè)題的時(shí)候,第一反應(yīng)我擦嘞,這個(gè)是哪個(gè)老鐵想出的題,如此混亂的代碼調(diào)用,真是驚為天人。當(dāng)然這是一道有關(guān)于多線程的題,最低級(jí)的錯(cuò)誤,就是一些人對(duì)于.start()和.run不熟悉,直接會(huì)認(rèn)為.start()之后run會(huì)占用主線程,所以得出答案等于:


  1. main thread b=2000

  2. b=2000


比較高級(jí)的錯(cuò)誤:了解start(),但是忽略了或者不知道synchronized,在那里瞎在想sleep()有什么用,有可能得出下面答案:


  1. main thread b=1000

  2. b=2000


總而言之問(wèn)了很多人,大部分第一時(shí)間都不能得出正確答案,其實(shí)正確答案如下:


  1. main thread b=2000

  2. b=1000


  3. or


  4. main thread b=1000

  5. b=1000


解釋這個(gè)答案之前,這種題其實(shí)在面試的時(shí)候遇到很多,依稀記得再學(xué)C++的時(shí)候,考地址,指針,學(xué)java的時(shí)候又在考i++,++i,'a' == b等于True? 這種題屢見(jiàn)不鮮,想必大家做這種題都知道靠死記硬背是解決不來(lái)的,因?yàn)檫@種變化實(shí)在太多了,所以要做這種比較模棱兩可的題目,必須要會(huì)其意,方得齊解。尤其是多線程,如果你不知道其原理,不僅僅在面試中過(guò)不了,就算僥幸過(guò)了,在工作中如何不能很好的處理線程安全的問(wèn)題,只能導(dǎo)致你的公司出現(xiàn)損失。


這個(gè)題涉及了兩個(gè)點(diǎn):


  • synchronized

  • 線程的幾個(gè)狀態(tài):new,runnable(thread.start()),running,blocking(Thread.Sleep())


如果對(duì)這幾個(gè)不熟悉的同學(xué)不要著急下面我都會(huì)講,下面我解釋一下整個(gè)流程:


1. 新建一個(gè)線程t, 此時(shí)線程t為new狀態(tài)。


2. 調(diào)用t.start(),將線程至于runnable狀態(tài)。


3. 這里有個(gè)爭(zhēng)議點(diǎn)到點(diǎn)是t線程先執(zhí)行還是tt.m2先執(zhí)行呢,我們知道此時(shí)線程t還是runnable狀態(tài),此時(shí)還沒(méi)有被cpu調(diào)度,但是我們的tt.m2()是我們本地的方法代碼,此時(shí)一定是tt.m2()先執(zhí)行。


4. 執(zhí)行tt.m2()進(jìn)入synchronized同步代碼塊,開(kāi)始執(zhí)行代碼,這里的sleep()沒(méi)啥用就是混淆大家視野的,此時(shí)b=2000。


5. 在執(zhí)行tt.m2()的時(shí)候。有兩個(gè)情況:


情況A:有可能t線程已經(jīng)在執(zhí)行了,但是由于m2先進(jìn)入了同步代碼塊,這個(gè)時(shí)候t進(jìn)入阻塞狀態(tài),然后主線程也將會(huì)執(zhí)行輸出,這個(gè)時(shí)候又有一個(gè)爭(zhēng)議到底是誰(shuí)先執(zhí)行?是t先執(zhí)行還是主線程,這里有小伙伴就會(huì)把第3點(diǎn)拿出來(lái)說(shuō),肯定是先輸出啊,t線程不是阻塞的嗎,調(diào)度到CPU肯定來(lái)不及啊?很多人忽略了一點(diǎn),synchronized其實(shí)是在1.6之后做了很多優(yōu)化的,其中就有一個(gè)自旋鎖,就能保證不需要讓出CPU,有可能剛好這部分時(shí)間和主線程輸出重合,并且在他之前就有可能發(fā)生,b先等于1000,這個(gè)時(shí)候主線程輸出其實(shí)就會(huì)有兩種情況。2000 或者 1000。


情況B:有可能t還沒(méi)執(zhí)行,tt.m2()一執(zhí)行完,他剛好就執(zhí)行,這個(gè)時(shí)候還是有兩種情況。b=2000或者1000


6. 在t線程中不論哪種情況,最后肯定會(huì)輸出1000,因?yàn)榇藭r(shí)沒(méi)有修改1000的地方了。


整個(gè)流程如下面所示:



2、線程安全


對(duì)于上面的題的代碼,雖然在我們實(shí)際場(chǎng)景中很難出現(xiàn),但保不齊有哪位同事寫(xiě)出了類似的,到時(shí)候有可能排坑的還是你自己,所以針對(duì)此想聊聊一些線程安全的事。


2.1 何為線程安全


我們用《java concurrency in practice》中的一句話來(lái)表述:當(dāng)多個(gè)線程訪問(wèn)一個(gè)對(duì)象時(shí),如果不用考慮這些線程在運(yùn)行時(shí)環(huán)境下的調(diào)度和交替執(zhí)行,也不需要進(jìn)行額外的同步,或者在調(diào)用方進(jìn)行任何其它的協(xié)調(diào)操作,調(diào)用這個(gè)對(duì)象的行為都可以獲得正確的結(jié)果,那這個(gè)對(duì)象就是線程安全的。


從上我們可以得知:


1. 在什么樣的環(huán)境:多個(gè)線程的環(huán)境下。

2. 在什么樣的操作:多個(gè)線程調(diào)度和交替執(zhí)行。

3. 發(fā)生什么樣的情況: 可以獲得正確結(jié)果。

4. 誰(shuí):線程安全是用來(lái)描述對(duì)象是否是線程安全。


2.2 線程安全性


我們可以按照java共享對(duì)象的安全性,將線程安全分為五個(gè)等級(jí):不可變、絕對(duì)線程安全、相對(duì)線程安全、線程兼容、線程對(duì)立:


2.2.1 不可變


在java中Immutable(不可變)對(duì)象一定是線程安全的,這是因?yàn)榫€程的調(diào)度和交替執(zhí)行不會(huì)對(duì)對(duì)象造成任何改變。同樣不可變的還有自定義常量,final及常池中的對(duì)象同樣都是不可變的。


在java中一般枚舉類,String都是常見(jiàn)的不可變類型,同樣的枚舉類用來(lái)實(shí)現(xiàn)單例模式是天生自帶的線程安全,在String對(duì)象中你無(wú)論調(diào)用replace(),subString()都無(wú)法修改他原來(lái)的值


2.2.2 絕對(duì)線程安全


我們來(lái)看看Brian Goetz的《Java并發(fā)編程實(shí)戰(zhàn)》對(duì)其的定義:當(dāng)多個(gè)線程訪問(wèn)某個(gè)類時(shí),不管運(yùn)行時(shí)環(huán)境采用何種調(diào)度方式或者這些線程將如何交替進(jìn)行,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)同,這個(gè)類都能表現(xiàn)出正確的行為,那么稱這個(gè)類是線程安全的。


周志明在<深入理解java虛擬機(jī)>>中講到,Brian Goetz的絕對(duì)線程安全類定義是非常嚴(yán)格的,要實(shí)現(xiàn)一個(gè)絕對(duì)線程安全的類通常需要付出很大的、甚至有時(shí)候是不切實(shí)際的代價(jià)。同時(shí)他也列舉了Vector的例子,雖然Vectorget和remove都是synchronized修飾的,但還是展現(xiàn)了Vector其實(shí)不是絕對(duì)線程安全。簡(jiǎn)單介紹下這個(gè)例子:


  1. public  Object getLast(Vector list) {

  2.    return list.get(list.size() - 1);

  3. }

  4. public  void deleteLast(Vector list) {

  5.    list.remove(list.size() - 1);

  6. }


如果我們使用多個(gè)線程執(zhí)行上面的代碼,雖然remove和get是同步保證的,但是會(huì)出現(xiàn)這個(gè)問(wèn)題有可能已經(jīng)remove掉了最后一個(gè)元素,但是list.size()這個(gè)時(shí)候已經(jīng)獲取了,其實(shí)get的時(shí)候就會(huì)拋出異常,因?yàn)槟莻€(gè)元素已經(jīng)remove。


2.2.3 相對(duì)安全


周志明認(rèn)為這個(gè)定義可以適當(dāng)弱化,把“調(diào)用這個(gè)對(duì)象的行為”限定為“對(duì)對(duì)象單獨(dú)的操作”,這樣一來(lái)就可以得到相對(duì)線程安全的定義。其需要保證對(duì)這個(gè)對(duì)象單獨(dú)的操作是線程安全的,我們?cè)谡{(diào)用的時(shí)候不需要做額外的操作,但是對(duì)于一些特定的順序連續(xù)調(diào)用,需要額外的同步手段。我們可以將上面的Vector的調(diào)用修改為:


  1. public synchronized Object getLast(Vector list) {

  2.    return list.get(list.size() - 1);

  3. }

  4. public synchronized void deleteLast(Vector list) {

  5.    list.remove(list.size() - 1);

  6. }


這樣我們作為調(diào)用方額外加了同步手段,其Vector就符合我們的相對(duì)安全。


2.2.4 線程兼容


線程兼容是指其對(duì)象并不是線程安全,但是可以通過(guò)調(diào)用端正確地使用同步手段,比如我們可以對(duì)ArrayList進(jìn)行加鎖,一樣可以達(dá)到Vector的效果。


2.2.5 線程對(duì)立


線程對(duì)立是指無(wú)論調(diào)用端是否采取了同步措施,都無(wú)法在多線程環(huán)境中并發(fā)使用的代碼。由于Java語(yǔ)言天生就具備多線程特性,線程對(duì)立這種排斥多線程的代碼是很少出現(xiàn)的,而且通常都是有害的,應(yīng)當(dāng)盡量避免。


2.3 如何解決線程安全


對(duì)于解決線程安全一般來(lái)說(shuō)有幾個(gè)辦法:互斥阻塞(悲觀,加鎖),非阻塞同步(類似樂(lè)觀鎖,CAS),不需要同步(代碼寫(xiě)得好,完全不需要考慮同步)


同步是指在多個(gè)線程并發(fā)訪問(wèn)共享數(shù)據(jù)時(shí),保證共享數(shù)據(jù)在同一個(gè)時(shí)刻只被一條線程(或是一些,使用信號(hào)量的時(shí)候)線程使用。


2.3.1 互斥同步


互斥是一種悲觀的手段,因?yàn)樗麚?dān)心他訪問(wèn)的時(shí)候時(shí)刻有人會(huì)破壞他的數(shù)據(jù),所以他需要通過(guò)某種手段進(jìn)行將這個(gè)數(shù)據(jù)在這個(gè)時(shí)間段給占為獨(dú)有,不能讓其他人有接觸的機(jī)會(huì)。臨界區(qū)(CriticalSection)、互斥量(Mutex)和信號(hào)量(Semaphore)都是主要的互斥實(shí)現(xiàn)方式。在Java中一般用ReentrantLock和synchronized 實(shí)現(xiàn)同步。 而實(shí)際業(yè)務(wù)當(dāng)中,推薦使用synchronized,在第一節(jié)的代碼其實(shí)也是使用的synchronized ,為什么推薦使用synchronized 的呢?


  • 如果我們顯示的使用lock我們得手動(dòng)的進(jìn)行解鎖unlock()調(diào)用,但是很多人在實(shí)際開(kāi)發(fā)過(guò)程其實(shí)有可能出現(xiàn)忘記,所以推薦使用synchronized ,在易于編程方面Lock敗。


  • synchronized 在jdk1.6之后對(duì)其進(jìn)行了優(yōu)化會(huì)從偏向鎖,輕量級(jí)鎖,自旋適應(yīng)鎖,最后才到重量級(jí)鎖。而Lock一來(lái)就是重量鎖。在未來(lái)的jdk版本中,重點(diǎn)優(yōu)化的也是synchronized。在性能方便Lock也敗。


如果你在業(yè)務(wù)中需要等待可中斷,等待超時(shí),公平鎖等功能的話,那你可以選擇這個(gè)ReentrantLock。


當(dāng)然在我們的Mysql數(shù)據(jù)庫(kù)中排他鎖其實(shí)也是互斥同步的實(shí)現(xiàn),當(dāng)加上排他鎖,其他事務(wù)都不能進(jìn)行訪問(wèn)其數(shù)據(jù)。


2.3.2 非阻塞同步


非阻塞同步是一種樂(lè)觀的手段,在樂(lè)觀的手段中他會(huì)先去嘗試操作,如果沒(méi)有人在競(jìng)爭(zhēng),就成功,否則就進(jìn)行補(bǔ)償(一般就是死循環(huán)重試或者循環(huán)多次之后跳出),在互斥同步最重要的問(wèn)題就是進(jìn)行線程阻塞和喚醒所帶來(lái)的性能問(wèn)題,而樂(lè)觀同步策略解決了這一問(wèn)題。


但是上面就有個(gè)問(wèn)題操作和檢測(cè)是否有人競(jìng)爭(zhēng)這兩個(gè)操作一定得保證原子性,這就需要我們硬件設(shè)備的支持,例如我們java中的cas操作其實(shí)就是操作的硬件底層的指令。


在JDK1.5之后,Java程序中才可以使用CAS操作,該操作由sun.misc.Unsafe類里面的compareAndSwapInt()和compareAndSwapLong()等幾個(gè)方法包裝提供,虛擬機(jī)在內(nèi)部對(duì)這些方法做了特殊處理,即時(shí)編譯出來(lái)的結(jié)果就是一條平臺(tái)相關(guān)的處理器CAS之類,沒(méi)有方法調(diào)用的過(guò)程,或者可以認(rèn)為是無(wú)條件內(nèi)聯(lián)進(jìn)去了


2.3.3 無(wú)同步


要保證線程安全,并不一定就要進(jìn)行同步,兩者沒(méi)有因果關(guān)系。同步只是保障共享數(shù)據(jù)爭(zhēng)用時(shí)的正確性手段,如果一個(gè)方法本來(lái)就不涉及共享數(shù)據(jù),那它自然就無(wú)須任何同步措施去保證正確性,因此會(huì)有一些代碼天生就是現(xiàn)場(chǎng)安全的。 一般分為兩類:


  • 可重入代碼:可重入代碼也叫純代碼,可以隨時(shí)中斷,恢復(fù)控制權(quán)之后程序依然不會(huì)出任何錯(cuò)誤,可重入代碼的結(jié)果一般來(lái)說(shuō)是可預(yù)測(cè)的:


  1. public int sum(){

  2.        return 1+2;

  3.    }


例如這種代碼就是可重入代碼,但是在我們自己的代碼中其實(shí)出現(xiàn)得很少


  • 線程本地存儲(chǔ):而這個(gè)一般來(lái)說(shuō)是我們用得比較多的手段,我們可以通過(guò)保證類是無(wú)狀態(tài)的,所有的變量都存在于我們的方法之中,或者通過(guò)ThreadLocal來(lái)進(jìn)行保存。


2.4 線程安全的一些其他經(jīng)驗(yàn)


上面寫(xiě)得都比較官方,下面說(shuō)說(shuō)從一些真實(shí)的經(jīng)驗(yàn)中總結(jié)出來(lái)的:


  • 在使用某些對(duì)象作為單例的時(shí)候,需要確定這個(gè)對(duì)象是否是線程安全的: 比如我們使用SimpleDateFormate的時(shí)候,很多初學(xué)者都不注意將其作為單例一個(gè)工具類來(lái)使用,導(dǎo)致了我們的業(yè)務(wù)異常??梢詤⒖嘉业牧硗庖黄? 在Java中你真的會(huì)日期轉(zhuǎn)換嗎?


  • 如果發(fā)現(xiàn)其不是單例,需要進(jìn)行替換,比如HashMap用ConcurrentHashMap,queue用ArrayBlockingQueue進(jìn)行替換。


  • 注意死鎖,如果使用鎖一定記得釋放鎖,同時(shí)使用鎖的順序一定要注意,這里不僅僅說(shuō)的是單機(jī)的鎖,也要說(shuō)分布式鎖,一定要注意:一個(gè)線程先鎖A后鎖B,另一個(gè)線程先鎖B后鎖A這個(gè)情況。所以一般來(lái)說(shuō)分布式鎖會(huì)加上超時(shí)時(shí)間,避免由于網(wǎng)絡(luò)問(wèn)題釋放鎖失敗,而導(dǎo)致死鎖。


  • 鎖的粒度:同樣的不僅僅是說(shuō)單機(jī)的鎖,也包括了分布式鎖,不要圖方便直接從入口方法,不加分析的就開(kāi)始加鎖,這樣會(huì)嚴(yán)重影響性能。同樣的也不能過(guò)于細(xì)粒度,單機(jī)的鎖會(huì)增加上下文的切換,分布式鎖會(huì)增加網(wǎng)絡(luò)調(diào)用,都會(huì)導(dǎo)致我們性能的下降。


  • 適當(dāng)引入樂(lè)觀鎖:比如我們有個(gè)需求是給用戶扣款,為了防止多扣,這個(gè)時(shí)候會(huì)用悲觀鎖進(jìn)行鎖,但是效率比較低,因?yàn)橛脩艨劭钇鋵?shí)同時(shí)扣的情況是比較少的,我們就可以使用樂(lè)觀鎖,在用戶的賬戶表里面添加version字段,首先查詢version,然后更新的時(shí)候看看當(dāng)前version和數(shù)據(jù)庫(kù)的version是否一致,一致就更新不一致就證明已經(jīng)扣過(guò)了。


  • 如果想要在多線程環(huán)境下使用非線程安全對(duì)象,數(shù)據(jù)可以放在ThreadLocal,或者只在方法里面進(jìn)行創(chuàng)建,我們的ArrayList雖然不是線程安全的,但是一般我們使用的時(shí)候其實(shí)都是在方法里面進(jìn)行List list = new ArrayList()使用,用無(wú)同步的方式也保證了線程安全。


  • 毛主席曾說(shuō)過(guò):手里有糧,心里不慌。多多學(xué)習(xí)多線程知識(shí),這個(gè)也是最重要的,當(dāng)然可以關(guān)注我的公眾號(hào)來(lái)和共同進(jìn)步。


最后


本文從最開(kāi)始的一道號(hào)稱史上最難的面試題,引入了我們工作中最為重要之一的線程安全。希望大家后續(xù)可以好好的閱讀周志明的《深入理解jvm虛擬機(jī)》的第13章線程安全和鎖優(yōu)化,相信讀完之后一定會(huì)有一個(gè)新的提升。由于作者本人水平有限,如果有什么錯(cuò)誤,還請(qǐng)指正。


出處:https://mp.weixin.qq.com/s/71ccU9Hms0XbQsjjyxyiIQ



架構(gòu)文摘

ID:ArchDigest

互聯(lián)網(wǎng)應(yīng)用架構(gòu)丨架構(gòu)技術(shù)丨大型網(wǎng)站丨大數(shù)據(jù)丨機(jī)器學(xué)習(xí)

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多