java 面試知識點筆記(十)多線程與併發-原理 上篇

問:線程安全問題的主要誘因?java

  1. 存在共享數據(也稱臨界資源)
  2. 存在多條線程共同操做這些共享數據

解決方法:同一時刻有且只有一個線程在操做共享數據,其餘線程必須等到該線程處理完數據後再對共享數據進行操做緩存

互斥鎖的特徵:安全

  1. 互斥性:即在同一時間只容許一個線程持有某個對象鎖,經過這種特性來實現多線程協調機制,這樣在同一時間只有一個線程對須要同步的代碼塊(複合操做)進行訪問。互斥性也稱爲操做的原子性。
  2. 可見性:必須確保在鎖被釋放以前,對共享變量所作的修改,對於隨後得到該鎖的另外一個線程是可見的(即在得到鎖時應該得到最新共享變量的值),不然另外一個線程多是在本地緩存的某個副本上繼續操做,從而引發不一致。

ps:synchronized 鎖的不是代碼,鎖的是對象多線程

獲取鎖的分類:獲取對象鎖、獲取類鎖併發

獲取對象鎖的兩種用法:框架

  1. 同步代碼塊(synchronized(this),synchronized(類實例對象)),鎖是小括號中的實例對象
  2. 同步非靜態方法(synchronized method) 鎖是當前對象的實例對象

獲取類鎖的兩種用法:jvm

  1. 同步代碼塊(synchronized(類.class)),鎖是小括號中的類對象(Class對象)
  2. 同步非靜態方法(synchronized static method) 鎖是當前對象的類對象(Class對象)

類鎖和對象鎖在鎖同一個對象的時候表現行爲是同樣的,由於class也是對象鎖,只是比較特殊,全部的實例共享同一個類(同一個class對象)佈局

若是鎖的是不一樣對象(同一個class的不一樣實例)表現就不同了,類鎖是全同步的,對象鎖是按對象區分同步的性能

類鎖和對象鎖互不干擾的,由於對象實例和類是兩個不一樣的對象優化

對象鎖和類鎖的終結:

  1. 有線程訪問對象的同步代碼塊時,另外的線程能夠訪問該對象的非同步代碼塊
  2. 若鎖住的是同一個對象,一個線程在訪問對象的同步代碼塊時,另外一個訪問對象的同步代碼塊的線程會被阻塞
  3. 若鎖住的是同一個對象,一個線程在訪問對象的同步方法時候另外一個訪問對象同步方法的線程會被阻塞
  4. 若鎖住的是同一個對象,一個線程在訪問對象的同步代碼塊時,另外一個線程訪問對象同步方法會被阻塞,反之亦然
  5. 同一個類的不一樣對象鎖互不干擾
  6. 類鎖因爲是一種特殊的對象鎖,所以表現和上述一、二、三、4一致,而因爲一個類只有一把對象鎖,因此同一個類的不一樣對象使用類鎖將會是同步的
  7. 類鎖和對象鎖互不干擾

樂觀鎖

樂觀鎖是一種樂觀思想,即認爲讀多寫少,遇到併發寫的可能性低,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,採起在寫時先讀出當前版本號,而後加鎖操做(比較跟上一次的版本號,若是同樣則更新),若是失敗則要重複讀-比較-寫的操做。

java中的樂觀鎖基本都是經過CAS操做實現的,CAS是一種更新的原子操做,比較當前值跟傳入值是否同樣,同樣則更新,不然失敗。

悲觀鎖

悲觀鎖是就是悲觀思想,即認爲寫多,遇到併發寫的可能性高,每次去拿數據的時候都認爲別人會修改,因此每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會block直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嚐試cas樂觀鎖去獲取鎖,獲取不到,纔會轉換爲悲觀鎖,如RetreenLock。

阻塞代價

java的線程是映射到操做系統原生線程之上的,若是要阻塞或喚醒一個線程就須要操做系統介入,須要在戶態與核心態之間切換,這種切換會消耗大量的系統資源,由於用戶態與內核態都有各自專用的內存空間,專用的寄存器等,用戶態切換至內核態須要傳遞給許多變量、參數給內核,內核也須要保護好用戶態在切換時的一些寄存器值、變量等,以便內核態調用結束後切換回用戶態繼續工做。

  • 若是線程狀態切換是一個高頻操做時,這將會消耗不少CPU處理時間;
  • 若是對於那些須要同步的簡單的代碼塊,獲取鎖掛起操做消耗的時間比用戶代碼執行的時間還要長,這種同步策略顯然很是糟糕的。


synchronized會致使爭用不到鎖的線程進入阻塞狀態,因此說它是java語言中一個重量級的同步操縱,被稱爲重量級鎖,爲了緩解上述性能問題,JVM從1.5開始,引入了輕量鎖與偏向鎖,默認啓用了自旋鎖,他們都屬於樂觀鎖。
 

深刻理解synchronized底層實現原理:

Java對象頭和Monitor是實現synchronized的基礎

hotspot中對象在內存的佈局是分3部分 

  1. 對象頭
  2. 實例數據
  3. 對其填充

這裏主要講對象頭:通常而言synchronized使用的鎖對象是存儲在對象頭裏的,對象頭是由Mark Word和Class Metadata Address組成

要詳細瞭解java對象的結構點擊:http://www.javashuo.com/article/p-gpknmlyb-hd.html

mark word存儲自身運行時數據,是實現輕量級鎖和偏向鎖的關鍵,默認存儲對象的hasCode、分代年齡、鎖類型、鎖標誌位等信息。

mark word數據的長度在32位和64位的虛擬機(未開啓壓縮指針)中分別爲32bit和64bit,它的最後2bit是鎖狀態標誌位,用來標記當前對象的狀態,對象的所處的狀態,決定了markword存儲的內容,以下表所示:

因爲對象頭的信息是與對象定義的數據沒有關係的額外存儲成本,因此考慮到jvm的空間效率,mark word 被設計出一個非固定的存儲結構,以便存儲更多有效的數據,它會根據對象自己的狀態複用本身的存儲空間(輕量級鎖和偏向鎖是java6後對synchronized優化後新增長的)

Monitor:每一個Java對象天生就自帶了一把看不見的鎖,它叫內部鎖或者Monitor鎖(監視器鎖)。上圖的重量級鎖的指針指向的就是Monitor的起始地址。

每一個對象都存在一個Monitor與之關聯,對象與其Monitor之間的關係存在多種實現方式,如Monitor能夠和對象一塊兒建立銷燬、或當線程獲取對象鎖時自動生成,當線程獲取鎖時Monitor處於鎖定狀態。

Monitor是虛擬機源碼裏面用C++實現的

源碼解讀:_WaitSet 和_EntryList就是以前學的等待池和鎖池,_owner是指向持有Monitor對象的線程。當多個線程訪問同一個對象的同步代碼的時候,首先會進入到_EntryList集合裏面,當線程獲取到對象Monitor後就會進入到_object區域並把_owner設置成當前線程,同時Monitor裏面的_count會加一。當調用wait方法會釋放當前對象的Monitor,_owner恢復成null,_count減一,同時該線程實例進入_WaitSet集合中等待喚醒。若是當前線程執行完畢也會釋放Monitor鎖並復位對應變量的值。

接下來是字節碼的分析:

package interview.thread;

/**
 * 字節碼分析synchronized
 * @Author: cctv
 * @Date: 2019/5/20 13:50
 */
public class SyncBlockAndMethod {
    public void syncsTask() {
        synchronized (this) {
            System.out.println("Hello");
        }
    }

    public synchronized void syncTask() {
        System.out.println("Hello Again");
    }
}

而後控制檯輸入 javac thread/SyncBlockAndMethod.java

而後反編譯 javap -verbose thread/SyncBlockAndMethod.class

先看看syncsTask方法裏的同步代碼塊

從字節碼中能夠看出 同步代碼塊 使用的是 monitorenter 和 monitorexit ,當執行monitorenter指令時當前線程講試圖獲取對象的鎖,當Monitor的count 爲0時將獲的monitor,並將count設置爲1表示取鎖成功。若是當前線程以前有這個monitor的持有權它能夠重入這個Monnitor。monitorexit指令會釋放monitor鎖並將計數器設爲0。爲了保證正常執行monitorenter 和 monitorexit 編譯器會自動生成一個異常處理器,該處理器能夠處理全部異常。主要保證異常結束時monitorexit(字節碼中多了個monitorexit指令的目的)釋放monitor鎖

ps:重入是從互斥鎖的設計上來講的,當一個線程試圖操做一個由其餘線程持有的對象鎖的臨界資源時,將會處於阻塞狀態,當一個線程再次請求本身持有對象鎖的臨界資源時,這種狀況屬於重入。就像以下狀況:hello2也是會輸出的,並不會鎖住。

再看看syncTask同步方法

解讀:這個字節碼中沒有monitorenter和monitorexit指令而且字節碼也比較短,其實方法級的同步是隱式實現的(無需字節碼來控制)ACC_SYNCHRONIZED是用來區分一個方法是否同步方法,若是設置了ACC_SYNCHRONIZED執行線程將持有monitor,而後執行方法,不管方法是否正常完成都會釋放調monitor,在方法執行期間,其餘線程都沒法在得到這個monitor。若是同步方法在執行期間拋出異常並且在方法內部沒法處理此異常,那麼這個monitor將會在異常拋到方法以外時自動釋放。

 

java6以前Synchronized效率低下的緣由:

在早期版本Synchronized屬於重量級鎖,性能低下,由於監視器鎖(monitor)是依賴於底層操做系統的的MutexLock實現的。

而操做系統切換線程時須要從用戶態轉換到核心態,時間較長,開銷較大

java6之後Synchronized性能獲得了很大提高(hotspot從jvm層面作了較大優化,減小重量級鎖的使用):

  1. Adaptive Spinning 自適應自旋
  2. Lock Eliminate 鎖消除
  3. Lock Coarsening 鎖粗化
  4. Lightweight Locking 輕量級鎖
  5. Biased Locking偏向鎖
  6. ……

自旋鎖:

  • 許多狀況下,共享數據的鎖定狀態持續時間較短,切換線程不值得
  • 經過讓線程執行while循環等待鎖的釋放,不讓出CPU
  • java4就引入了,不過默認是關閉的,java6後默認開啓的
  • 自旋本質和阻塞狀態並不相同,若是鎖佔用時間很是短,那自旋鎖性能會很好
  • 缺點:若鎖被其餘線程長時間佔用,會帶來許多性能上的開銷,由於自旋一直會佔用CPU資源且白白消耗掉CPU資源。
  • 若是線程超過了限定次數尚未獲取到鎖,就該使用傳統方式掛起線程(能夠設置VM的PreBlockSpin參數來更改限定次數)

 

引用閱讀:http://www.javashuo.com/article/p-eawupvdg-ba.html

相關文章
相關標籤/搜索