最近偶然間看見一道名爲史上最難的java面試題,這個題讓了我對線程安全的有了一些新的思考,給你們分享一下這個題吧:java
public class TestSync2 implements Runnable {
int b = 100;
synchronized void m1() throws InterruptedException {
b = 1000;
Thread.sleep(500); //6
System.out.println("b=" + b);
}
synchronized void m2() throws InterruptedException {
Thread.sleep(250); //5
b = 2000;
}
public static void main(String[] args) throws InterruptedException {
TestSync2 tt = new TestSync2();
Thread t = new Thread(tt); //1
t.start(); //2
tt.m2(); //3
System.out.println("main thread b=" + tt.b); //4
}
@Override
public void run() {
try {
m1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製代碼
推薦你們先別急着看下面的答案,試着看看這個題的答案是什麼?剛開始看這個題的時候,第一反應我擦嘞,這個是哪一個老鐵想出的題,如此混亂的代碼調用,真是驚爲天人。固然這是一道有關於多線程的題,最低級的錯誤,就是一些人對於.start()和.run不熟悉,直接會認爲.start()以後run會佔用主線程,因此得出答案等於:面試
main thread b=2000
b=2000
複製代碼
比較高級的錯誤:瞭解start(),可是忽略了或者不知道synchronized,在那裏瞎在想sleep()有什麼用,有可能得出下面答案:sql
main thread b=1000
b=2000
複製代碼
總而言之問了不少人,大部分第一時間都不能得出正確答案,其實正確答案以下:數據庫
main thread b=2000
b=1000
or
main thread b=1000
b=1000
or
b=1000
main thread b=2000
複製代碼
有人沒測出來b=2000這裏給你們看看:編程
解釋這個答案以前,這種題其實在面試的時候遇到不少,依稀記得再學C++的時候,考地址,指針,學java的時候又在考i++,++i,"a" == b等於True? 這種題家常便飯,想必你們作這種題都知道靠死記硬背是解決不來的,由於這種變化實在太多了,因此要作這種比較模棱兩可的題目,必需要會其意,方得齊解。尤爲是多線程,若是你不知道其原理,不只僅在面試中過不了,就算僥倖過了,在工做中如何不能很好的處理線程安全的問題,只能致使你的公司出現損失。安全
這個題涉及了兩個點:bash
若是對這幾個不熟悉的同窗不要着急下面我都會講,下面我解釋一下整個流程:網絡
狀況A:有可能t線程已經在執行了,可是因爲m2先進入了同步代碼塊,這個時候t進入阻塞狀態,而後主線程也將會執行輸出,這個時候又有一個爭議究竟是誰先執行?是t先執行仍是主線程,這裏有小夥伴就會把第3點拿出來講,確定是先輸出啊,t線程不是阻塞的嗎,調度到CPU確定來不及啊?不少人忽略了一點,synchronized實際上是在1.6以後作了不少優化的,其中就有一個自旋鎖,就能保證不須要讓出CPU,有可能恰好這部分時間和主線程輸出重合,而且在他以前就有可能發生,b先等於1000,這個時候主線程輸出其實就會有兩種狀況。2000 或者 1000。多線程
狀況B:有可能t還沒執行,tt.m2()一執行完,他恰好就執行,這個時候仍是有兩種狀況。b=2000或者1000併發
6.在t線程中不論哪一種狀況,最後確定會輸出1000,由於此時沒有修改1000的地方了。
整個流程以下面所示:
對於上面的題的代碼,雖然在咱們實際場景中很難出現,但保不齊有哪位同事寫出了相似的,到時候有可能排坑的仍是你本身,因此針對此想聊聊一些線程安全的事。
咱們用《java concurrency in practice》中的一句話來表述:當多個線程訪問一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替執行,也不須要進行額外的同步,或者在調用方進行任何其它的協調操做,調用這個對象的行爲均可以得到正確的結果,那這個對象就是線程安全的。
從上咱們能夠得知:
咱們能夠按照java共享對象的安全性,將線程安全分爲五個等級:不可變、絕對線程安全、相對線程安全、線程兼容、線程對立:
在java中Immutable(不可變)對象必定是線程安全的,這是由於線程的調度和交替執行不會對對象形成任何改變。一樣不可變的還有自定義常量,final及常池中的對象一樣都是不可變的。
在java中通常枚舉類,String都是常見的不可變類型,一樣的枚舉類用來實現單例模式是天生自帶的線程安全,在String對象中你不管調用replace(),subString()都沒法修改他原來的值
咱們來看看Brian Goetz的《Java併發編程實戰》對其的定義:當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些線程將如何交替進行,而且在主調代碼中不須要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼稱這個類是線程安全的。
周志明在<<深刻理解java虛擬機>>中講到,Brian Goetz的絕對線程安全類定義是很是嚴格的,要實現一個絕對線程安全的類一般須要付出很大的、甚至有時候是不切實際的代價。同時他也列舉了Vector的例子,雖然Vectorget和remove都是synchronized修飾的,但仍是展示了Vector其實不是絕對線程安全。簡單介紹下這個例子:
public Object getLast(Vector list) {
return list.get(list.size() - 1);
}
public void deleteLast(Vector list) {
list.remove(list.size() - 1);
}
複製代碼
若是咱們使用多個線程執行上面的代碼,雖然remove和get是同步保證的,可是會出現這個問題有可能已經remove掉了最後一個元素,可是list.size()這個時候已經獲取了,其實get的時候就會拋出異常,由於那個元素已經remove。
周志明認爲這個定義能夠適當弱化,把「調用這個對象的行爲」限定爲「對對象單獨的操做」,這樣一來就能夠獲得相對線程安全的定義。其須要保證對這個對象單獨的操做是線程安全的,咱們在調用的時候不須要作額外的操做,可是對於一些特定的順序連續調用,須要額外的同步手段。咱們能夠將上面的Vector的調用修改成:
public synchronized Object getLast(Vector list) {
return list.get(list.size() - 1);
}
public synchronized void deleteLast(Vector list) {
list.remove(list.size() - 1);
}
複製代碼
這樣咱們做爲調用方額外加了同步手段,其Vector就符合咱們的相對安全。
線程兼容是指其對象並非線程安全,可是能夠經過調用端正確地使用同步手段,好比咱們能夠對ArrayList進行加鎖,同樣能夠達到Vector的效果。
線程對立是指不管調用端是否採起了同步措施,都沒法在多線程環境中併發使用的代碼。因爲Java語言天生就具有多線程特性,線程對立這種排斥多線程的代碼是不多出現的,並且一般都是有害的,應當儘可能避免。
對於解決線程安全通常來講有幾個辦法:互斥阻塞(悲觀,加鎖),非阻塞同步(相似樂觀鎖,CAS),不須要同步(代碼寫得好,徹底不須要考慮同步)
同步是指在多個線程併發訪問共享數據時,保證共享數據在同一個時刻只被一條線程(或是一些,使用信號量的時候)線程使用。
互斥是一種悲觀的手段,由於他擔憂他訪問的時候時刻有人會破壞他的數據,因此他須要經過某種手段進行將這個數據在這個時間段給佔爲獨有,不能讓其餘人有接觸的機會。臨界區(CriticalSection)、互斥量(Mutex)和信號量(Semaphore)都是主要的互斥實現方式。在Java中通常用ReentrantLock和synchronized 實現同步。 而實際業務當中,推薦使用synchronized,在第一節的代碼其實也是使用的synchronized ,爲何推薦使用synchronized 的呢?
若是你在業務中須要等待可中斷,等待超時,公平鎖等功能的話,那你能夠選擇這個ReentrantLock。
固然在咱們的Mysql數據庫中排他鎖其實也是互斥同步的實現,當加上排他鎖,其餘事務都不能進行訪問其數據。
非阻塞同步是一種樂觀的手段,在樂觀的手段中他會先去嘗試操做,若是沒有人在競爭,就成功,不然就進行補償(通常就是死循環重試或者循環屢次以後跳出),在互斥同步最重要的問題就是進行線程阻塞和喚醒所帶來的性能問題,而樂觀同步策略解決了這一問題。
可是上面就有個問題操做和檢測是否有人競爭這兩個操做必定得保證原子性,這就須要咱們硬件設備的支持,例如咱們java中的cas操做其實就是操做的硬件底層的指令。
在JDK1.5以後,Java程序中才可使用CAS操做,該操做由sun.misc.Unsafe類裏面的compareAndSwapInt()和compareAndSwapLong()等幾個方法包裝提供,虛擬機在內部對這些方法作了特殊處理,即時編譯出來的結果就是一條平臺相關的處理器CAS之類,沒有方法調用的過程,或者能夠認爲是無條件內聯進去了
要保證線程安全,並不必定就要進行同步,二者沒有因果關係。同步只是保障共享數據爭用時的正確性手段,若是一個方法原本就不涉及共享數據,那它天然就無須任何同步措施去保證正確性,所以會有一些代碼天生就是現場安全的。 通常分爲兩類:
public int sum(){
return 1+2;
}
複製代碼
例如這種代碼就是可重入代碼,可是在咱們本身的代碼中其實出現得不多
上面寫得都比較官方,下面說說從一些真實的經驗中總結出來的:
本文從最開始的一道號稱史上最難的面試題,引入了咱們工做中最爲重要之一的線程安全。但願你們後續能夠好好的閱讀周志明的《深刻理解jvm虛擬機》的第13章線程安全和鎖優化,相信讀完以後必定會有一個新的提高。因爲做者本人水平有限,若是有什麼錯誤,還請指正。
最後打個廣告,若是你以爲這篇文章對你有文章,能夠關注個人技術公衆號,最近做者收集了不少最新的學習資料視頻以及面試資料,關注以後便可領取。
若是你們以爲這篇文章對你有幫助,或者你有什麼疑問想提供1v1免費vip服務,均可以關注個人公衆號,關注便可免費領取海量最新java學習資料視頻,以及最新面試資料,你的關注和轉發是對我最大的支持,O(∩_∩)O: