Java併發讀書筆記:線程安全與互斥同步

本篇參考許多著名的書籍,造成讀書筆記,便於加深記憶。java

前文傳送門:Java併發讀書筆記:JMM與重排序編程

致使線程不安全的緣由

當一個變量被多個線程讀取,且至少被一個線程寫入時,若是讀寫操做不遵循happens-before規則,那麼就會存在數據競爭的隱患,若是不給予正確的同步手段,將會致使線程不安全。api

什麼是線程安全

Brian Goetz在《Java併發編程實戰》中是這樣定義的:安全

當多個線程訪問一個類時,若是不用考慮這些線程在運行時環境下的調度和交替執行,而且不須要額外的同步及在調用方代碼沒必要作其餘的協調,這個類的行爲仍然是正確的,那麼這個類就是線程安全的。多線程


周志明在《深刻理解Java虛擬機》中提到:多個線程之間存在共享數據時,這些數據能夠按照線程安全程度進行分類:併發

不可變

不可變的對象必定是線程安全的,只要一個不可變的對象被正確地構建出來,那麼它在多個線程中的狀態就是一致的。例如用final關鍵字修飾對象:app

  • 修飾的是基本數據類型,final修飾不可變。
  • 修飾的是一個對象,就須要保證其狀態不發生變化。

JavaAPI中符合不可變要求的類型:String類,枚舉類,數值包裝類型(如Double)和大數據類型(BigDecimal)。工具

絕對線程安全

即徹底知足上述對於線程安全定義的。性能

知足該定義其實須要付出不少代價,Java中標註線程安全的類,實際上絕大多數都不是線程安全的(如Vector),由於它仍須要在調用端作好同步措施。Java中絕對線程安全的類:CopyOnWriteArrayListCopyOnWriteArraySet

相對線程安全

即咱們一般所說的線程安全,Java中大部分的線程安全類都屬於該範疇,如VectorHashTableCollections集合工具類的synchronizedCollection()方法包裝的集合等等。就拿Vector舉例:若是有個線程在遍歷某個Vector、有個線程同時在add這個Vector,99%的狀況下都會出現ConcurrentModificationException,也就是fail-fast機制。

線程兼容

對象自己並非線程安全的,能夠經過在調用段正確同步保證對象在併發環境下安全使用。如咱們以前學的分別與Vector和HashTable對應的ArrayListHashMap


對象經過synchronized關鍵字修飾,達到同步效果,自己是安全的,但相對來講,效率會低不少。

線程對立

不管調用端是否採起同步措施,都沒法正確地在多線程環境下執行。Java典型的線程對立:Thread類中的suspend()和resume()方法:若是兩個線程同時操控一個線程對象,一個嘗試掛起,一個嘗試恢復,將會存在死鎖風險,已經被棄用

常見的對立:System.setIn()System.setOut()System.runFinalizersOnExit()

互斥同步實現線程安全

互斥同步也被稱作阻塞同步(由於互斥同步會由於線程阻塞和喚醒產生性能問題),它是實現線程安全的其中一種方法,還有一種是非阻塞同步,以後再作學習。

互斥同步:保證併發下,共享數據在同一時刻只被一個線程使用。

synchronized內置鎖

其中使用synchronized關鍵字修飾方法或代碼塊是最基本的互斥同步手段。

synchronized是Java提供的一種強制原子性的內置鎖機制,以synchronized代碼塊的定義方式來講:

synchronized(lock){
    //訪問或修改被鎖保護的共享狀態
}

它包含了兩部分:一、鎖對象的引用 二、鎖保護的代碼塊。

每一個Java對象均可以做爲用於同步的鎖對象,咱們稱該類的鎖爲監視器鎖(monitor locks),也被稱做內置鎖。

能夠這樣理解:線程在進入synchronized以前須要得到這個鎖對象,在線程正常結束或者拋出異常都會釋放這個鎖。

而這個鎖對象很好地完成了互斥,假設A持有鎖,這時若是B也想訪問這個鎖,B就會陷入阻塞。A釋放了鎖以後,B纔可能中止阻塞。

鎖即對象

  • 對於普通同步方法,鎖是當前實例對象(this)。
//普通同步方法
public synchronized void do(){}
  • 對於靜態同步方法,鎖是當前的類的Class對象。
//靜態同步方法
public static synchronized void f(){}
  • 對於同步方法塊,鎖的是括號裏配置的對象。
//鎖對象爲TestLock的類對象
synchronized (TestLock.class){    
    f();
}

明確:synchronized方法和代碼塊本質上沒啥不一樣,方法只是對跨越整個方法體的代碼塊的簡短描述,而這個鎖是方法所在對象自己(static修飾的方法,對象是當前類對象)。這個部分能夠參考:Java併發之synchronized深度解析

是否要釋放鎖

釋放鎖的狀況:

  • 線程執行完畢。
  • 遇到return、break終止。
  • 拋出未處理的異常或錯誤。
  • 調用了當前對象的wait()方法。

不釋放鎖的狀況:

  • 調用了Thread.sleep()和Thread.yield()暫停執行不會釋放鎖。
  • 調用suspend()掛起線程,不會釋放鎖,已被棄用。

實現原理

JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步,但二者實現細節不一樣。

代碼塊同步使用monitorentermonitorexit兩個指令實現,JVM的要求以下:

  • monitorenter指令會在編譯後插入到同步代碼塊的開始位置,而monitorexit則會插入到方法結束和異常處。
  • 每一個對象都有一個monitor與之關聯,且當一個monitor被持有以後,他會處於鎖定狀態。
  • 線程執行到monitorenter時,會嘗試獲取對象對應monitor的全部權。

  • 在獲取鎖時,若是對象沒被鎖定,或者當前線程已經擁有了該對象的鎖(可重進入,不會鎖死本身),將鎖計數器加一,執行monitorexit時,鎖計數器減一,計數爲零則鎖釋放。
  • 獲取對象鎖失敗,則當前線程陷入阻塞,直到對象鎖被另一個線程釋放。

啥是重進入?

重進入意味着:任意線程在獲取到鎖以後可以再次獲取該鎖而不會被鎖阻塞synchronized是隱式支持重進入的,所以不會出現鎖死本身的狀況。

這就體現了鎖計數器的做用:得到一次鎖加一,釋放一次鎖減一,不管得到仍是釋放多少次,只要計數爲零,就意味着鎖被成功釋放

ReentrantLock(重入鎖)

ReentrantLock位於java.util.concurrent(J.U.C)包下,是Lock接口的實現類。基本用法與synchronized類似,都具有可重入互斥的特性,但擁有擴展的功能。

Lock接口的實現提供了比使用synchronized方法和代碼塊更普遍的鎖操做。容許更靈活的結構,具備徹底不一樣的屬性,而且可能支持多個關聯的Condition對象。

RenntrantLock官方推薦的基本寫法:

class X {
    //定義鎖對象
    private final ReentrantLock lock = new ReentrantLock();
    // ...
    //定義須要保證線程安全的方法
    public void m() {
        //加鎖
        lock.lock();  
        try{
        // 保證線程安全的代碼
        }
        // 使用finally塊保證釋放鎖
        finally {
            lock.unlock()
        }
    }
}

API層面的互斥鎖

ReentrantLock表現爲API層面的互斥鎖,經過lock()unlock()方法完成,是顯式的,而synchronized表現爲原生語法層面的互斥鎖,是隱式的。

等待可中斷

當持有線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待處理其餘事情

公平鎖

ReentrantLock鎖是公平鎖,即保證等待的多個線程按照申請鎖的時間順序依次得到鎖,而synchronized是不公平鎖。

鎖綁定

一個ReentrantLock對象能夠同時綁定多個Condition對象


JDK1.6以前,ReentrantLock在性能方面是要領先於synchronized鎖的,可是JDK1.6版本實現了各類鎖優化技術,後續性能改進會更加偏向於原生的synchronized。

參考數據:《Java併發編程實戰》、《Java併發編程的藝術》、《深刻理解Java虛擬機》

相關文章
相關標籤/搜索