這是多線程系列第四篇,其餘請關注如下:java
java多線程-內存模型多線程
若是你看過前面幾篇關於線程的文字,會對線程的實現原理了然於胸,有了理論的支持會對實踐有更好的指導,那麼本篇會偏重於線程的實踐,對線程的幾種應用作個簡要的介紹。jvm
本篇主要內容:ide
- 線程安全的分類
- 線程同步的實現方式
- 鎖優化
線程安全並不是是一個非真既假的二元世界,若是按照線程安全的「安全程度」來排序的話,java中能夠分爲如下幾類性能
不可變。對數據類型修飾爲final類型的,就能夠保證其實不可變的(reference 對象除外,final對象屬性不保證,只保證內存地址)。不可變的對象必定是線程安全,好比String類,它是一個典型的不可變對象,當調用它的subString()、replace()和concat()的方法都不會影響它原來的值,只會返回一個新構造的字符串對象。優化
絕對線程安全。這個定義是極其嚴格的,一個類要達到,無論運行時環境如何,調用者都不須要任何額外的同步措施。spa
相對線程安全。這個就是咱們一般所講的線程安全,它保證對對象的單獨操做是線程安全的,不須要作額外保證工做,但對於特定順序的連續調用,可能須要調用端採用額外的同步手段來保證調用的正確性。操作系統
線程兼容。這是指對象自己並不是線程安全,能夠經過調用端正確地使用同步手段來保證對象在併發環境中能夠安全地使用。咱們經常使用的非線程安全類,都屬於這個範疇。
線程對立。這個是指不管調用端是否採用同步措施,都沒法在多線程環境中併發使用代碼。這種咱們應該避免。
上述中可能對絕對安全和相對安全,並非很好區分,咱們採用一個示例來區分:
public class VectorTest { private Vector<Integer> vector = new Vector<Integer>(); public void remove() { new Thread() { @Override public void run() { for (int i = 0; i < vector.size(); i++) { vector.remove(i); } } }.start(); } public void print() { new Thread() { @Override public void run() { for (int i = 0; i < vector.size(); i++) { System.out.println(vector.get(i)); } } }.start(); } public void add(int data) { vector.add(data); } public static void main(String[] args) { VectorTest test = new VectorTest(); for (int j=0;j<100;j++){ for (int i = 0; i < 10; i++) { test.add(i); } test.remove(); test.print(); } } }
上述代碼中運行會報錯:ArrayIndexOutOfBoundsException的異常,這個異常是在print方法裏面出現的,當remove線程刪除 一個元素以後,print方法正好執行到vector.get()方法,此時就會出現這個異常。
咱們知道vector是線程安全的,它的get()、remove()、size()、add()方法都採用synchronize 進行同步了,可是多線程的狀況下,若是不對方法調用端作額外同步的狀況下,仍然不是線程安全的。這就是咱們說的相對線程安全,它能不保證什麼時候,調用者都不須要任何額外的同步措施。
互斥同步是常見的一種併發正確性保障手段。同步是指多個線程併發訪問共享數據時,保證共享數據在同一個時刻只被一個線程使用。java中常見的互斥同步手段就是synchronize 和ReentrantLock。
想必這兩種加鎖方式對多線程有了解的人都知道。具體用法咱們再也不探討。咱們聊一聊這二者的不一樣和具體場景應用。
synchronize在前面的文字中咱們也講過了,它屬於重度鎖,因爲jvm線程是映射於操做系統原生線程,在阻塞或者喚醒線程時候,須要從用戶態轉換到內核上,這個耗費有時候會耗費時間超過代碼執行時間,因此jvm會對一些代碼執行短的同步代碼採用自旋鎖等方式,避免頻繁的切入到核心態之中。
synchronize是jvm提供的一種內置鎖,被jvm推薦使用,它寫出的代碼相對比較簡單緊湊,只有當內置鎖知足不了需求的時候,再來用ReentrantLock。
那麼ReentrantLock能提供哪些高級功能?咱們看個示例;
public void synA() { synchronized (lockA) { synchronized (lockB) { //doSomeThing.... } } } public void synB() { synchronized (lockB) { synchronized (lockA) { //doSomeThing.... } } }
上述經過synchronized的代碼,在多個線程分別調用synA和synB的時候容易發生死鎖的問題。要想避免,只能在編寫的時候強制要求全部的調用順序一致。而在ReentrantLock能夠採用輪詢鎖的方式來避免此種問題。
public void tryLockA() { long stopTime = System.currentTimeMillis() + 10000l while (true) { if (lockA.tryLock()) { try { if (lockB.tryLock()) { try { // doSomeThing..... return; } finally { lockB.unlock(); } } } finally { lockA.unlock(); } } if (System.currentTimeMillis() > stopTime) { return; } } }
上述trylock的方式,若是不能獲取到所須要的鎖,那麼能夠採用輪詢的方式來獲取,從而讓程序重新獲取控制權,並且會釋放已經得到的鎖。另外trylock還提供有定時重載方法,方便你在必定時間內得到鎖,若是指定時間內不能給出結果,會零程序結束。
ReentrantLock除了提供可輪詢,定時鎖之外,還能夠提供可中斷的鎖獲取操做,以便獲取可取消的操做中使用枷鎖。另外還提供了鎖獲取操做,公平隊列以及非塊結構的鎖。這些功能都極大的豐富了對鎖操做的可定製性。
固然了,你若是ReentrantLock的這些高級功能你並用不上,仍是推薦採用synchronized。在性能上synchronized在java6之後已經能夠和ReentrantLock相平衡了,並且據官方聽說,這一方面的性能將來還會增強,由於它屬於jvm的內置屬性,能執行一些優化,例如對線程封閉鎖對象的鎖消除優化,以及增長鎖的顆粒度來消除鎖同步等。這些在ReentrantLock是很可貴到實現的。
上述咱們也瞭解過,多線程對資源競爭的時候會令其餘沒有競爭到的線性進行阻塞等待,而阻塞以及喚醒又要須要內核的調度,這對有限的cpu來講代價過於龐大,因而jvm就在鎖優化上花費了大量的精力,以提升執行效率。
咱們看看常見的鎖優化方式。
在共享數據鎖定的狀態下,有不少方法都是隻會持有很短的一段時間,爲了這麼一小段時間而讓線程掛起和恢復很不值得。那麼jvm就讓等待鎖的線程稍等一下,但不放棄相應的執行時間。以此看等待的線程是否很快釋放,如此就減小了線程調度的壓力。若是鎖被佔用時間很短,這個效果就很好,若是時間過長,就白白浪費了循環的資源,並且會帶來資源浪費。
自旋鎖沒法依據鎖被佔用時間長短來處理,後續就引入了,自適應的自旋鎖,自選的時間再也不固定了,而是由前一次在同一個鎖的自選時間以及擁有的狀態來決定的。如此就會變的智能起來。
鎖消除是指jvm即時編譯器在運行時候,對一些代碼上要求同步,但被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除檢測主要依據是來源於逃逸分析的數據支持,若是判斷在一段代碼中,堆上的全部數據都不會逃逸出去,就把他們當作棧上數據對待,認爲是線程私有的,同步也就無需進行了。
編寫代碼的時候,老是推薦將同步塊做用範圍越小越好,若是一系列的操做都是對一個對象反覆枷鎖和解鎖,甚至出如今循環體中,及時沒有線程競爭,也會致使沒必要要的性能損耗。那麼對於此種代碼,jvm會擴大其鎖的顆粒度,對這一部分代碼只採用一個同步操做來進行。
輕量級鎖是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用操做系統互斥產生的性能消耗。jvm中對象header分爲兩部分信息,第一部分用於存儲對象自身的運行數據,稱爲」Mark Word」,它是實現輕量級鎖的關鍵。另一部分用於存儲執行方法區對象類型數據的指針。當代碼進入同步塊的時候,若是此同步對象沒有被鎖定,則把「Mark world」中指向鎖記錄的指針,標記爲「01」。
若是Mark Word更新成功,線程擁有了該對象的鎖,則把執行鎖標位的指針標記爲」00」,若是更新失敗,而且當前對象的Mark word 沒有指向當前線程的棧幀,則說明鎖對象已經被其餘線程搶佔了。若是有兩條以上 線程爭用同一個鎖,那輕量級鎖就再也不奏效了,鎖標記爲」10「,膨脹爲重量級鎖。
輕量級鎖是基於,絕大部分的鎖定,在同步週期內是不存在競爭的,因此以此來減輕互斥產生的性能消耗。固然若是存在鎖競爭,除了互斥還會避免使用互斥量的開銷,還有額外產生同步修改標記位的操做。
偏向鎖是在無競爭狀況下把整個同步都消除了,連CAS更新操做也不作了,它會偏向第一個得到它的線程,若是在接下來的執行過程當中,該鎖沒有被其餘的線程獲取,則持有偏向鎖的線程將永遠不須要同步,當另一個線程嘗試獲取這個鎖的時候,則宣告偏向模式結束。
-----------------------------------------------------------------------------
想看更多有趣原創的技術文章,掃描關注公衆號。
關注我的成長和遊戲研發,推進國內遊戲社區的成長與進步。