Java併發(三):共享受限資源

Java併發(三):共享受限資源

一、Java併發共享受限資源的問題
在使用Java併發時,如果兩個或者多個線程需要訪問共享資源,那麼就會出現共享資源競爭的問題,多個線程試圖同時訪問同一個共享資源
在這裏插入圖片描述
當線程A與線程B都是對共享資源進行寫入數據,那麼由於線程調度機制的不確定性,在線程A寫入一半數據時可能出現線程B調度,並寫入數據,這將導致數據錯誤。

二、解決共享資源競爭
使用線程時的一個基本問題是:你永遠不知道一個線程何時在運行。因此對於併發工作,需要某種方式來防止兩個任務訪問相同的資源,至少在關鍵階段不能出現競爭的狀況。
防止衝突的方法就是當資源被一個任務使用時,在其上加鎖。第一個訪問某項資源的任務必須鎖定這項資源,使其他任務在其被解鎖之前無法訪問它,而在其被解鎖之時,另一個任務就可以鎖定並使用它,以此類推。
在這裏插入圖片描述
基本上所有的併發模式在解決線程衝突問題的時候,都是採用序列化訪問共享資源的方案,這意味着在給定時刻只允許一個任務訪問共享資源

  1. 使用synchronized上鎖
    Java以提供關鍵字synchronized的形式,爲防止資源衝突提供了內置支持。當任務要執行被synchronized關鍵字保護的代碼片段時,它將檢查鎖是否可用,然後獲取鎖,執行代碼,釋放鎖。如果某個任務處於一個標記爲synchronized的方法的調用中,那麼在這個線程從該方法返回之前,其他所有要調用類中任何標記爲synchronize的方法的線程都會被阻塞
    在這裏插入圖片描述
    所有對象都自動含有單一的鎖,當對象上調用其任意synchronized方法時,此對象被加鎖,這時該對象上的其他synchronized方法只有等到前一個方法調用完畢並釋放鎖之後才能被調用。例如上面的代碼,某個任務調用了對象的f(),那麼Demo對象被上鎖,其他線程無法調用f()和g()。
    在這裏插入圖片描述
    注意,在使用併發時,將域設置爲private是非常重要的,否則synchronized關鍵字就不能防止其他任務直接訪問域,這樣就會產生衝突。
    在這裏插入圖片描述
    一個任務可以多次獲得對象的鎖。如果一個方法在同一個對象上調用了第一個方法,後者又調用了同一對象的另一個方法,就會發生這種情況。JVM負責跟蹤對象被加鎖的次數。如果一個對象被解鎖,其計數爲0,在任務第一次給對象加鎖時,計數爲1。每當這個相同的任務在這個對象上獲得鎖時,計數就會遞增。每當任務離開一個synchronized方法,計數遞減,當計數爲零時,鎖被完全釋放,此時別的任務才能夠使用該資源。
  2. 使用顯示的Lock對象
    Lock是顯示的互斥機制,其對象必須被顯示的創建、鎖定和釋放,與內建的鎖相比代碼缺乏優雅性,但是對於一些類型的問題來說更具靈活性。(注意:遞增程序是多個步驟的,即在Java中,遞增不是原子性的操作。)
    在這裏插入圖片描述
    如果在使用synchronized關鍵字時,某些事物失敗了,那麼就會拋出一個異常,而無法去做任何的清理工作,以維護系統使其處於良好的狀態;顯示的Lock對象可以在finally子句中將系統維護在正確的狀態
    顯示的Lock對象在加鎖和釋放鎖方面,相對於內建的synchronized鎖來說,賦予了更細緻的控制力,其能夠對嘗試着獲取鎖等操作。若嘗試上鎖失敗,即可離開該函數去執行其他事情而不是等待鎖的釋放。
    在這裏插入圖片描述

三、原子性、可見性與有序性
內存模型的三大特性爲:原子性、可見性與有序性。

  1. 原子性
    原子操作是不能被線程調度機制中斷的操作,一旦操作開始,那麼它一定可以在可能發生的「上下文切換」之前執行完畢。但是依賴原子性是很棘手且危險的。
    在這裏插入圖片描述
    原子性可以應用於除long和double之外的所有基本類型之上的「簡單操作」。對於讀取和寫入除long和double之外的基本類型變量這樣的操作,可以保證它們會被當做不可分的操作來操作內存,但是JVM可以將64位(long和double變量)的讀取和寫入當做兩個分離的32位操作,從而導致了可能發生上下文切換。
  2. 可見性
    可見性是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。Java內存模型是通過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值來實現可見性的
    主要有三種實現可見性的方式:
     volatile
     synchronize,對一個變量執行unlock操作之前,必須把變量值同步回主內存
     final,被final關鍵字修飾的字段在構造器中一旦初始化完成,並且沒有發生this逃逸,那麼其他線程就能看到final字段的值
  3. 有序性
    有序性是指在本線程內觀察,所有操作都是有序的。在一個線程觀察另一個線程,所有操作都是無序的,無序是因爲發生了指令重排。在Java內存模型中,允許編譯器和處理器對指令進行重排序,重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。
    volatile關鍵字通過添加內存屏障的方式來禁止指令重排,即重排序時不能把後面的指令放到內存屏障之前。也可以通過synchronized來保證有序性。
    在這裏插入圖片描述
  4. 關於volatile
    值得一提的是,synchronized能夠實現原子性和可見性,而volatile實現了內存可見性,但是卻無法保證操作的原子性
    volatile實現內存的可見性是通過store和load指令完成的;也就是對volatile變量執行寫操作時,會在寫操作後加入一條store指令,即強迫線程將最新的值刷新到主內存;而度操作時,會加入一條load指令,即強迫從主內存中讀取變量的值。
    被volatile修飾的變量具備了兩層語義:
     保證了不同線程對這個變量進行操作時的可見性
    禁止進行指令重排序

四、臨界區
有時候希望防止多個線程同時訪問方法內部的部分代碼而不是防止訪問整個方法,那麼可以通過synchronized對代碼進行分離,該代碼段稱爲臨界區。synchronized被用來指定某個對象,此對象的鎖被用來對花括號內的代碼進行同步控制。
在這裏插入圖片描述
這也被稱爲同步控制塊,在進入此段代碼之前,必須得到syncObjcet對象的鎖,如果其他線程已經得到這個鎖,那麼就等到鎖被釋放後才能進入臨界區。
通過使用同步控制塊,而不是對整個方法進行同步控制,可以使多個任務訪問對象的時間性能得到顯著提高

五、在其他對象上同步
前面提到的在方法前面使用synchronized關鍵字進行修飾,事實上是對方法對應類進行上鎖,等同於使用synchronized(this)對類對象進行上鎖
在這裏插入圖片描述
上述代碼中,方法f()和time()中的鎖的對象是不一致的,f()方法的鎖對象是類對象本身,而time()方法鎖對象是syncObject,因此可以在不同線程中對同一對象進行同步。
在這裏插入圖片描述

參考資料:《Java編程思想》
內存可見性和原子性:synchronized和volatile的比較
Java併發編程(三)volatile域