程式語言作為開發軟體的工具,與電腦溝通交流的工具。每個人都有選擇工具的理由。我選擇java的理由是:強規範,保證了很多低級錯誤,難以發覺的錯誤在編譯期被檢查;嚴格的記憶體管理,對記憶體的訪問java是有嚴格的規範的,它可以防止數組下標越界,錯誤的記憶體訪問;高效能的記憶體回收行程,java在cms並發收集器出現後效能上了一個台階,而且java虛擬機器還在不斷的發展進步,java的效能也會得到極大的提高,java的效能問題逐漸成了過去式;多線程,多線程是java優勢所在,java語言在這方面也是下了很大的功夫,使java的多線程效能和安全性都如此強大。這些原因使我非常熱愛在java語言上的學習,研究。
本文主要介紹java多線程方面的知識。正如很多java程式員知道的那樣,java多線程確實涉及到很多注意事項。這篇博文主要是引出一些話題,對他們進行討論。如果要深入討論的話,每一個話題都足以用幾篇博文來講解。下面是將要出現的一些話題:
1,為什麼要多線程(多線程的優勢)
2,多線程有什麼問題(多線程要解決的問題)
3,java對多線程的規範(java解決多線程問題對jvm作出的規範)
4,java解決多線程問題的手段,包括(synchronized,ReentrantLock,volatile,final,ThreadLocal,棧封閉,JUC技術等)
關於第一個話題:
要使用多線程的理由有很多,總體來說有如下(可能不全):
1,盡量利用系統資源,減少任務完成時間。(如,在IO阻塞的時候可以讓出cpu,分布式運算 mapreduce)
2,多任務系統,有些事情需要並存執行。(如,需要響應使用者插斷要求)
3,需要同時為更多人提供服務,不能因為一個長任務完全佔用系統資源,使一些短任務也不能服務。(如,web伺服器)
4,一台伺服器可以有很多cpu(單個cpu也可以支援超執行緒),所以多線程是必然的趨勢。
關於第二個話題:
多線程下確實有很多不確定性。也很難像序列化執行那麼偵錯工具。問題可能要一定的已耗用時間,一定的操作順序才能產生。所以很多程式員在迴避這個問題,很害怕去使用多線程。所以多線程要解決的第一個問題就是心理畏懼問題。對於技術上,多線程主要有兩個問題要解決。這也是開發多線程程式時一定要思考的問題。
1,共用資料的可見度(一個線程寫入的資料,何時對於其他線程可見)。
2,共用資源的競爭問題(多個線程同時請求一個資源,誰先誰後,如計數問題,轉賬問題等)。
注意這裡都有共用二字,也就是需要在多線程中同時操作資源才可能出問題。下面討論下這兩個問題產生的原因和情景。
對於可見度問題:
cpu的執行速度快的驚人,以致於電腦執行速度的瓶頸在IO上。所以除了cpu寄存器緩衝外,還有L1 ,L2——在cpu上的獨立緩衝,L3——同一個cpu槽內共用的緩衝。然後才是主存,磁碟,網路等。在cpu需要訪問資料時,會依次沿著上面的緩衝讀寫資料。就拿寫來說,假設一份資料在主存中,它會被一層層載入到寄存器緩衝中來,假設cpu0要改變這份資料的值,會先寫到寄存器緩衝中,這時候資料並不會馬上去修改主存中的資料,如果第二個cpu又來讀這份資料,它不會裡面讀到之前的改變值而是讀到一個"髒"值。也就是第一個操作對於第二個操作不可見。而且這時候第二個cpu可能根據條件判斷,對這個值做出一些處理。由於不可見,會引發判斷是失效,丟失的更新等等問題。不可見問題還有可能由重排序引起。
對於資源競爭問題:
資源競爭問題其實很好理解,如對一個變數計數。由於計數要分為讀取,改變(依賴原來值做修改),替換三個步驟。如果多個線程不加任何限制的執行這個三個步驟,就會使其它步驟失效。還有HashMap擴容問題,印表機這些只能獨佔資源等等。這些都是資源競爭的一些問題。資源競爭問題遠沒有這麼簡單,特別是在高並發下,還可能引發一些硬體層面的問題。這個也是多線程的痛點。資源競爭問題也可能由重排序引起。
關於第三個話題:
java對多線程提出來了自己的一套JMM模型。這個模型主要是對可見度約定——happens-before原則。happens-before有如下約定:
(1)同一個線程中的每條指令都happens-before於出現在其後的任何一個指令。
(2)對一個監視器的解鎖happens-before於每一個後續對同一個監視器的加鎖。
(3)對volatile欄位的寫入操作happens-before於後續這個欄位的讀操作。
(4)對於final修飾的變數,對它的構造happens-before於對它的第一次訪問之前。
(5)Thread.start()的調用會happens-before於啟動線程裡面的動作。
(6)Thread中的所有動作都happens-before於其他線程檢查到此線程結束或者Thread.join()中返回或者Thread.isAlive()==false。
(7)一個線程A調用另一個另一個線程B的interrupt()都happens-before於線程A發現B被A中斷(B拋出異常或者A檢測到B的isInterrupted()或者interrupted())。
(8)一個物件建構函數的結束happens-before與該對象的finalizer的開始
(9)如果A動作happens-before於B動作,而B動作happens-before與C動作,那麼A動作happens-before於C動作
這些happens-before的約定解決了共用變數的可見度問題。對於共用資源競爭性主要是靠有鎖的synchronized和lock,和無鎖的cas操作保證。
關於第四個話題:
被final修飾的變數,是不可變對象(如果是一個參考型別,只是引用不可變,其值任然可變)。所以通過final修飾共用變數,可以大大減少多線程訪問的複雜度。因為不可變對象一定是安全執行緒的。構造不可變對象是一個好的編程習慣。
棧封閉,其實就是說沒有共用變數,把所有變數的分配都在本線程的方法棧內使用。不傳遞給其他線程使用。這其實是迴避了多線程共用問題,很多時候我們仍然需要共用的。
ThreadLocal,這是另一個迴避多線程共用變數的技術(只對本線程共用)。它把對象綁定到執行緒區域,Thread上的ThreadLocalMap中。這是一個弱鍵引用的map,鍵是ThreadLocal,value是具體本地線程共用的對象。所以這樣的對象有兩種方式被回收。第一ThreadLocal執行個體失去了強引用,在gc時可能會把它回收,第二這個線程失去了強引用,隨著線程的回收而回收。所以ThreadLocal是不會記憶體溢出的。使用remove去移除不需要的變數是一種好習慣。如果線程長時間運行ThreadLocal通常也是不會失去強引用的,所以這裡的內容得不到回收。
volatile變數主要是對可見度的約束,即它的寫入總能立即被下一個讀取的線程看到。但是它不保證原子操作。
synchronized是在對象或類上的內建鎖。在synchronized範圍內的代碼不能被重排序,並且讀取共用變數能夠看到它的最新狀態,寫入共用變數能夠對持有這個鎖的其它對象立即可見。
ReentrantLock提供了與synchronized相同的功能。它們的主要區別是
1,ReentrantLock必須自己釋放鎖。通常在finally釋放。
2,ReentrantLock可以通過參數控制多線程訪問的公平性。
3,ReentrantLock可以通過tryLock(),非阻塞的獲得鎖,也可以在指定等待時間內獲得鎖。synchronized一旦進入鎖競爭就一定得等到鎖,所以更容易產生死結。
4,ReentrantLock可以響應中斷,而synchronized是無法響應中斷的。這一點很重要!一個好的線程設計都需要響應中斷。
JUC的包很多主要是一些多線程工具。這裡主要說下cas操作。像AtomicXXXX主要是利用cas操作,而不是鎖。所以它的效率會高於有鎖的同步。cas操作又主要是又cpu的硬體支援。JUC的內容很多在這裡不單獨討論,有興趣的同學可以看這篇部落格,http://blog.csdn.net/xieyuooo/article/details/8594713
關於多線程的討論暫時就到此了,如有任何紕漏還請多多指出,謝謝!