1. 程式人生 > >java面試題之synchronized和lock有什麽區別

java面試題之synchronized和lock有什麽區別

作用範圍 out inter mutex 虛擬 add moni 私有 p s

synchronized和lock的區別:

類別 synchronized lock
存在層次 java的關鍵字,在jvm層面上 是一個類
鎖的釋放

1、以獲取鎖的線程執行完同步代碼,釋放鎖

2、線程執行發生異常,jvm會讓線程釋放鎖

在finally中必須釋放鎖,不然容易造成線程死鎖
鎖的獲取

假設A線程獲得鎖,B線程等待,

如果A線程阻塞,B線程會一直等待

分情況而定,lock有多個鎖獲取的方法,可以嘗試獲得鎖,

線程可以不用功一直等待

鎖狀態 無法判斷 可以判斷
鎖類型 可以重入,不可以中斷,非公平 可重入 可以判斷 可公平
性能 少量同步 大量同步

synchronized使用方式及原理:

作用在方法上:

public synchronized void test(){}//作用在方法上JVM采用ACC_SYNCHRONIZED標記符來實現同步的;

作用在代碼塊上:

synchronized (SynchronizedTest.class){}//作用在同步代碼塊上JVM是采用monitorenter和monitorexit兩個指令來實現同步的;

java對象頭:

  synchronized用的鎖是存在java對象頭裏的,java對象頭一般占有兩個機器碼(在32位虛擬機中,1個機器碼等於4字節,也就是32bit),那麽什麽是java對象頭呢?HotSpot虛擬機的對象頭主要包括兩部分數據:Mark Word(標記字段)、Klass Pointer(類型指針)。

  • Klass Pointer是對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例;
  • Mark Word 用於存儲對象自身的運行時數據,它是實現輕量級鎖和偏向鎖的關鍵(例如hashCode、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等等)

Mark Word會隨著程序的運行發生變化,變化狀態如下:

鎖狀態 25bit 4bit 1bit 2bit
23bit 2bit 是否是偏向鎖 鎖標誌位
無鎖狀態 對象hashCode、對象分代年齡 01
輕量級鎖 指向鎖記錄的指針 00
重量級鎖 指向重量級鎖的指針 10
GC標記 空,不需要記錄信息 11
偏向鎖 線程ID Epoch 對象分代年齡 1 01

Monitor:

  什麽是monitor?我們可以把它理解為一個同步工具,也可以描述為一種同步機制,它通常被描述為一個對象。與一切皆對象一樣,所有的java對象是天生的monitor,每一個java對象都有稱為monitor的潛質,

因為在java的設計中,每一個java對象自打娘胎裏出來就帶了一把看不見的鎖,它叫做內部鎖或者monitor鎖。

  monitor是線程私有的數據結構,每一個線程都有一個可用monitor record列表,同時還有一個全局的可用列表。每一個被鎖住的對象都會和一個monitor關聯(對象頭的Mark Word中的LockWord指向monitor的起始地址),

同時monitor中有一個Owner字段存放擁有該鎖的線程的唯一標識,標識該鎖被這個線程占用,其結構如下:

Owner
EntryQ
RcThis
Nest
HashCode
Candidate

  • Owner:初始值為null表示當前沒有任何線程擁有該monitor record,當線程成功擁有該鎖後保存線程唯一標識,當鎖被釋放時又設置為null;
  • EntryQ:關聯一個系統互斥鎖,阻塞所有試圖鎖住monitor record 失敗的線程;
  • RcThis:表示blocked或waiting在該monitor record上的所有線程的個數;
  • Nest:用來實現重入鎖的計數;
  • HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age);
  • Candidate:用來避免不必要的阻塞或等待線程喚醒,因為每一次只有一個線程能夠成功擁有鎖,如果每次前一個釋放鎖的線程喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒然後因為競爭鎖失敗又被阻塞)從而導致性能嚴重下降。Candidate只有兩種可能的值:0表示沒有需要喚醒的線程;1表示要喚醒一個繼任線程來競爭鎖。

我們都知道synchronized是重量級鎖,效率不怎麽好,同時這個觀念也一直存在我們腦海裏,不過在jdk1.6中對synchronized的實現進行了各種優化,使得它顯得不是那麽重了,那麽JVM采用了哪些優化手段呢?

鎖優化:

  jdk1.6對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。

  鎖主要存在四種狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨著競爭的激烈而逐漸升級。(註意!鎖只可以升級不可以降級,這種策略是為了提高獲得鎖和釋放鎖的效率)

自旋鎖

  線程的阻塞和喚醒需要CPU從用戶態轉為核心態,頻繁的阻塞和喚醒對CPU來說是一件負擔很重的工作,勢必會給系統的並發性能帶來很大的壓力。同時我們發現在許多應用上面,對象鎖的鎖狀態只會持續很短一段時間,為了這一段很短的時間頻繁的阻塞和喚醒線程是非常不值得的。所以引入自旋鎖。何謂自旋鎖?

  所謂自旋鎖,就是讓該線程等待一段時間,不會被立即掛起,看持有鎖的線程是否會很快釋放。怎麽等待呢?執行一段無意義的循環即可(自選)。自旋等待不能替代阻塞,雖然它可以避免線程切換帶來的開銷,但是它占用了處理器的時間。如果持有鎖的線程很快就釋放了鎖,那麽自旋的效率就非常好了,反之,自旋的線程就會白白消耗掉處理的資源,它不會做任何有意義的工作,典型的占著茅坑不拉屎,這樣反而會帶來性能上的而浪費。所以說,自旋等待的時間(自旋次數)必須要有一個限制,如果自旋超過了定義的時間仍然沒有獲取到鎖,則應該被掛起。

  自旋鎖在jdk 1.4.2中引入,默認關閉,但是可以使用-XX:+UseSpinning開啟,在jdk1.6中默認開啟。同時默認的次數為10次,可以通過參數-XX:PreBlockSpin來調整;如果通過參數-XX:preBlockSpin來調整自旋鎖的自旋次數,會帶來諸多不便。假如我將參數調整為10,但是系統很多線程都是等你剛剛退出的時候就釋放了鎖(假如你多自旋一兩次就可以獲取鎖),你是不是很尷尬,於是jdk1.6引入自適應的自旋鎖,讓虛擬機會變得越來越聰明。

適應自旋鎖:

  jdk1.6引入了更加聰明的自旋鎖,即自適應的自旋鎖。所謂自適應就意味著自旋的次數不再是固定的,他是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定的。它怎麽做的呢?線程如果自旋成功了,那麽下次自旋的次數會更加多,因為虛擬機認為既然上次成功了,那麽這次自旋也很有可能會再次成功,那麽他就會允許自旋等待持續的次數更多。反之,如果對於某個鎖,很少有自旋成功的,那麽在以後要獲取這個鎖的時候自旋的次數會減少甚至省略掉自旋的過程,以免浪費處理器資源。

  有了自適應自旋鎖,隨著程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的狀況預測會越來越準確,虛擬機會變得越來越聰明。

鎖消除:

  為了保證數據的完整性,我們在進行操作時需要對這部分操作進行同步控制,但是在有些情況下,JVM檢測到不可能存在共享數據競爭,這是JVM會對這些同步鎖進行所消除,鎖消除的依據是逃逸分析的數據支持。

  如果不存在競爭,為什麽還需要加鎖呢?所以鎖消除可以節省毫無意義的請求鎖的時間。變量是否逃逸,對於虛擬機來說需要使用功能數據流分析來確定,但是對於開發者來說這還不清楚麽?有時候我們雖然沒有顯示使用鎖,但是我們在使用一些jdk的內置api時,如StringBuffer、Vector、HashTable等,這個時候回存在隱形的加鎖操作。比如StringBuffer的append()方法,Vector的add()方法:

public void vectorTest(){
     Vector<String> vector = new Vector<String>();
     for(int i = 0 ; i < 10 ; i++){
         vector.add(i + "");
     }
 
     System.out.println(vector);
 }

在運行這段代碼時,JVM可以明顯檢測到變量vector沒有逃逸出方法vectorTest方法之外,所以JVM可以大膽地將vector內部的加鎖操作消除。

鎖粗化:

  我們知道在使用同步鎖的時候,需要讓同步塊的作用範圍盡可能小,僅在共享數據的實際作用於中才進行同步,這樣做的目的是為了使需要同步的操作數量盡可能縮小,如果存在鎖競爭,那麽等待鎖的線程也能盡快拿到鎖。但是如果一系列的連續加鎖解鎖操作,可能導致不必要的性能損耗,索引引入鎖粗化的概念。

  鎖粗化:就是將多個連續的加鎖、解鎖的操作連接在一起,擴展成一個範圍更大的鎖。

輕量級鎖:

  引入輕量級鎖的主要目的是在多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。當關閉偏向鎖功能或者多個線程競爭偏向鎖導致偏向鎖升級為輕量級鎖,則會嘗試獲取輕量級鎖,

  下圖是輕量級鎖的獲取和釋放過程:

  技術分享圖片

偏向鎖:  

  引入偏向鎖主要目的是:為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執行路徑。

  下圖是偏向鎖的獲取和釋放流程:

技術分享圖片

重量級鎖:

  重量級鎖通過對象內部的監視器(monitor)實現的,其中monitor的本質是依賴於底層操作系統的Mutex Lock實現,操作系統實現線程之間的切換需要從用戶態到內核態的切換,切換成本非常高。

java面試題之synchronized和lock有什麽區別