面試題-自旋鎖,以及jvm對synchronized的優化

背景

想要弄清楚這些問題,須要弄清楚其餘的不少問題。
好比,對象,而對象自己又能夠延伸出不少其餘的問題。php

咱們平時不過只是在使用對象而已,怎麼使用?就是new 對象。這只是語法層面的使用,至關於會了一門編程語言而已。html

對象更深層次的問題以下:
1.源碼實現
2.內存
3.字節碼java

由於只有瞭解對象,特別是深入的理解對象,才能理解同步的問題。c++

對象

內存
包括三部分
1.頭
2.數據
3.空白填充編程

頭是對象的元數據,也就是,除了數據之外的數據。就像各類協議同樣,每一種協議,都有頭,好比,tcp/ip協議 http協議,都有頭字段。
咱們這裏主要關心的是鎖字段。除了鎖字段,其實還有,對象的hashcode(惟一標識一個對象),線程等等。bash

數據,就是數據咯。多線程

空白填充,是一段沒有使用的內存。爲何要沒有使用的內存,不是浪費嗎?由於不少場合,須要申請的內存是什麼的倍數,好比,對象須要是8個字節的倍數。因此,內存不夠,填充空白字節便可,直到達到8個字節的倍數要求爲止。併發


源碼實現
這裏也只講,與同步相關的部分。
好比,c++源碼裏,與同步關鍵字synchronized相關的代碼是對象監視器ObjectMonitor.cpp。與此相關的邏輯,主要包含如下幾個字段:
1.哪一個線程擁有這個鎖
2.計數器
3.線程排隊集合
4.線程等待集合jvm

計數器,狀況以下。初始值是0,1.第一次獲取,加1 2.若是當前線程再次獲取,每次自增1 3.若是是否鎖,減1。
同一個線程,再次獲取,這就是鎖的可重入特性。tcp

線程排隊集合,狀況以下。1.若是獲取到鎖,那麼持有鎖 2.若是沒有,那麼進入排隊集合。

等待集合,狀況以下。1.線程獲取到鎖 2.調用wait()方法,釋放鎖。加入等待集合。3.等待被別的線程喚醒,即調用notify或notifyAll()方法。


字節碼
1.魔數這些東西
2.鎖的進入指令enterLock和退出指令exitLock


jvm-如何訪問對象


總結
java是由c++實現的。
若要理解java語法是怎麼實現的?閱讀c++源碼,即jvm是如何實現的。

內置鎖

內置鎖主要包含兩個方面
1.哪一個鎖
2.鎖哪一個數據


鎖的本質
鎖,其實就是對象。咱們在說哪一個鎖,其實本質上是在問使用哪一個對象。對象就是鎖,鎖就是對象。每一個對象都有一個內置鎖,兩者一一對應,且互相只有一個。


鎖哪一個數據?
鎖的目的是,要鎖住哪一個數據。即多線程不能篡改數據。

可是,具體的表現形式是,鎖住的是一個代碼塊。不過,代碼塊的本質,也是訪問數據/操做數據,因此,最終仍然仍是爲了鎖數據。


內置鎖的源碼實現
1.對象
2.計數


對象
就是鎖。


計數 每一個對象,關聯一個計數器


鎖的可重入性
可重入指的是同一個對象/鎖,能夠重複獲取,誰來獲取?固然是線程。

獲取鎖的粒度是線程!只有線程才能獲取鎖!


鎖和要保護的變量之間的關係
一一對應。也就是說,最好保護一個變量就只使用那一個鎖來保護那個變量。而不要一個鎖同時保護多個變量,那麼可能出問題。

顯式鎖

顯式鎖和內置鎖,本質上是同樣的。
只不過,一個是使用關鍵字(jvm實現了獲取鎖和釋放鎖的方法),一個是使用鎖封裝類的lock()和unlock()方法。

synchronized-源碼實現

其實就是上文提到的監視器對象MonitorObject.cpp

jvm對synchronized的優化

1.鎖可重入
當前線程可從新獲取同一個對象的鎖。即同一個鎖。
2.自旋鎖
自旋鎖就是循環獲取鎖。
鎖的源碼實現類,封裝了lock()和unlock()方法。獲取鎖的實現,就是循環獲取。
3.重量級鎖
即普通的對象鎖。


一步步是如何優化的,如何進入到下一步
先是鎖可重入。這個是針對同一個線程。

其次,若是是不一樣的線程,那麼此刻是自旋鎖。即循環指定次數(幾十次)的獲取鎖。

最後,不行,就正常的普通的鎖,也就是所謂的重量級鎖。該線程進入排隊隊列。


自旋鎖的源碼實現
1.普通的鎖
獲取鎖的時候,只獲取一次。

2.自旋鎖
獲取鎖的時候,獲取屢次。

自旋鎖和普通鎖的惟一區別,就是獲取鎖的時候,獲取幾回的問題。


自旋鎖的使用
具體使用的時候,自旋鎖和普通鎖,沒有任何區別。全部的顯式鎖,都是如下兩步:
1.獲取鎖lock()
2.釋放鎖unlock()

自旋鎖,咱們本身也能夠實現。其實就是一個自旋鎖類,封裝了兩個方法1.獲取鎖2.釋放鎖。

synchronized的自旋鎖底層實現,也是同一個道理。


代碼實現
1.普通鎖

lock(){
    獲取鎖 //只獲取一次,不行,就進入排隊隊列
}
複製代碼

2.自旋鎖

lock(){
    while(次數){
        獲取鎖 //獲取鎖的代碼是同樣的。主要是修改兩個字段1.哪一個線程持有鎖2.計數器
    }
}
複製代碼

爲何要弄一個自旋鎖,由於普通鎖,獲取一次,獲取不到,該線程就進入線程排隊隊列。再次執行該線程的時候,會發生線程上下文的切換,由於線程進入排隊隊列的期間,對應的cpu就去執行其餘的線程去了。如今又要從新回來執行這個線程,這個過程就發生了至少兩次線程上下文切換。而線程上下文切換,是很是耗資源的,具體耗費資源的緣由就是,須要在用戶態和內核態之間來回切換,線程上下文切換就是由內核來實現上下文切換這個操做的。

互斥鎖和非互斥鎖

在一般狀況下咱們說的鎖都指的是「互斥」鎖,由於在還存在一些特殊的鎖,好比「讀寫鎖」,不徹底是互斥的。這篇文章說的鎖專指互斥鎖。


讀寫鎖


讀寫鎖如何實現

延伸-內存溢出和內存泄露的區別:內存泄露是內存溢出的一種

內存問題,說白了,就是不夠用的問題。

因此,全部的內存問題,都是由於內存不夠用致使的。

內存溢出,就是對象/數據不斷地增多。而內存有限。因此,纔會溢出嘛。

而內存泄露,其實,本質上,也是內存溢出/不夠。只不過有一點細微的區別,就是內存泄露是由於已有的對象,沒有作到很好的釋放掉。好比,鏈表裏的數據,只釋放了第一個數據的內存,後面的數據都沒有釋放內存。這個時候,會致使剩下的全部數據,都不能被釋放。

鎖的本質是等待

先理解一下什麼是自旋,所謂自旋就是線程在不知足某種條件的狀況下,一直循環作某個動做。因此對於自旋鎖來鎖,當線程在沒有獲取鎖的狀況下,一直循環嘗試獲取鎖,直到真正獲取鎖。

在聊聊高併發(三)鎖的一些基本概念 咱們提到鎖的本質就是等待,那麼如何等待呢,有兩種方式

  1. 線程阻塞

  2. 線程自旋

阻塞的缺點顯而易見,線程一旦進入阻塞(Block),再被喚醒的代價比較高,性能較差。自旋的優勢是線程仍是Runnable的,只是在執行空代碼。固然一直自旋也會白白消耗計算資源,因此常見的作法是先自旋一段時間,還沒拿到鎖就進入阻塞。JVM在處理synchrized實現時就是採用了這種折中的方案,並提供了調節自旋的參數。


鎖的本質是等待,等待的本質是線程阻塞
不論是鎖,仍是線程阻塞,仍是等待。最終落實到源碼層面,就是監視器對象MonitorObject的1.線程排隊集合2.線程等待集合(即調用了wait()方法)。


Object.wait()方法
Wait方法的本質,也是線程加入到線程等待集合。由於,最底層,jvm仍是要調用MonitorObject.cpp的wait()方法,把線程加入到線程等待集合。


線程阻塞的本質是什麼?
待補充。


參考
www.jianshu.com/p/f4454164c…
www.jianshu.com/p/3256473f5…
blog.csdn.net/raintungli/…
coderbee.net/index.php/c…

這些參考文章都寫的很差,僅供參考。

參考

baijiahao.baidu.com/s?id=161214…

blog.csdn.net/u010372981/…

blog.csdn.net/hellozhxy/a…
blog.csdn.net/iter_zc/art…

www.cnblogs.com/YDDMAX/p/56…

相關文章
相關標籤/搜索