目錄
- 引出
- Java中鎖升級
- synchronized與lock鎖區別
- 緩存三兄弟:緩存擊穿、穿透、雪崩
- 緩存擊穿
- 緩存穿透
- 緩存雪崩
- 總結
引出
Java進階(鎖)——鎖的升級,synchronized與lock鎖區別
Java中鎖升級
看一段代碼:
public class App {public static void main(String[] args) throws InterruptedException {Calculate cal = new Calculate();long start = System.currentTimeMillis();Thread t1 = new Thread(()->{for (int i = 0; i < 1000_0000; i++) {cal.increase();}});Thread t2 = new Thread(()->{for (int i = 0; i < 1000_0000; i++) {cal.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println("time = " + (System.currentTimeMillis()-start));System.out.println("cal.getNum() = " + cal.getNum());}
}
public class Calculate {private int num;public int getNum() {return num;}// 多線程執行:public void increase() 會有線程安全問題// synchronized 鎖解決,鎖的是什么?public void increase(){synchronized (this) {num++;}}
}
分析:
對象組成:
Mark Word用于存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等等,占用內存大小與虛擬機位長一致。Mark Word對應的類型是markOop
。源碼位于markOop.hpp
中。在64位虛擬機下,Mark Word是64bit大小的,其存儲結構如下:
對象頭在64位虛擬機 8個字節
package cn.wn.juc;
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
public class App {// 鎖升級演示public static void main(String[] args) throws InterruptedException {User user01 = new User();System.out.println("無狀態(001):" + ClassLayout.parseInstance(user01).toPrintable());// 從jdk6開始,jvm默認延遲4s自動開啟開啟偏向鎖。通過-XX:BiasedLockingStartupDelay=0設置取消延遲// 如果不要偏向鎖:-XX:-UseBiasedLocking=falseTimeUnit.SECONDS.sleep(5);User user02 = new User();System.out.println("啟用偏向鎖(101):" + ClassLayout.parseInstance(user02).toPrintable());for (int i = 0; i < 2; i++) {synchronized (user02) {System.out.println("偏向鎖(101)帶線程ID:" + ClassLayout.parseInstance(user02).toPrintable());}// 偏向鎖釋放,對象頭不會變化,一直存在, (偏向線程id) 下次執行判斷是否同一個線程如果是直接執行System.out.println("偏向鎖(101)釋放線程ID:" + ClassLayout.parseInstance(user02).toPrintable());}// 多個線程加鎖,升級為輕量級鎖new Thread(() -> {synchronized (user02) {System.out.println("輕量級鎖(00):" + ClassLayout.parseInstance(user02).toPrintable());try {System.out.println("=====休眠3秒======");TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("輕量-->重量(10):" + ClassLayout.parseInstance(user02).toPrintable());}}).start();TimeUnit.SECONDS.sleep(1);new Thread(() -> {synchronized (user02) {System.out.println("重量級鎖(10):" + ClassLayout.parseInstance(user02).toPrintable());}}).start();}
}
<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.10</version>
</dependency>
鎖的狀態總共有四種,級別由低到高依次為:無鎖、偏向鎖、輕量級鎖、重量級鎖,這四種鎖狀態分別代表什么,為什么會有鎖升級?其實在 JDK 1.6之前,synchronized 還是一個重量級鎖,是一個效率比較低下的鎖,但是在JDK 1.6后,Jvm為了提高鎖的獲取與釋放效率對(synchronized )進行了優化,引入了 偏向鎖 和 輕量級鎖 ,從此以后鎖的狀態就有了四種(無鎖、偏向鎖、輕量級鎖、重量級鎖),并且四種狀態會隨著競爭的情況逐漸升級,而且是不可逆的過程,即不可降級,也就是說只能進行鎖升級(從低級別到高級別),不能鎖降級(高級別到低級別),意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。
無鎖
無鎖是指沒有對資源進行鎖定,所有的線程都能訪問并修改同一個資源,但同時只有一個線程能修改成功。
無鎖的特點是修改操作會在循環內進行,線程會不斷的嘗試修改共享資源。如果沒有沖突就修改成功并退出,否則就會繼續循環嘗試。如果有多個線程修改同一個值,必定會有一個線程能修改成功,而其他修改失敗的線程會不斷重試直到修改成功。
偏向鎖
初次執行到synchronized代碼塊的時候,鎖對象變成偏向鎖(通過CAS修改對象頭里的鎖標志位),字面意思是“偏向于第一個獲得它的線程”的鎖。執行完同步代碼塊后,線程并不會主動釋放偏向鎖。當第二次到達同步代碼塊時,線程會判斷此時持有鎖的線程是否就是自己(持有鎖的線程ID也在對象頭里),如果是則正常往下執行。由于之前沒有釋放鎖,這里也就不需要重新加鎖。如果自始至終使用鎖的線程只有一個,很明顯偏向鎖幾乎沒有額外開銷,性能極高。
偏向鎖是指當一段同步代碼一直被同一個線程所訪問時,即不存在多個線程的競爭時,那么該線程在后續訪問時便會自動獲得鎖,從而降低獲取鎖帶來的消耗,即提高性能。
當一個線程訪問同步代碼塊并獲取鎖時,會在 Mark Word 里存儲鎖偏向的線程 ID。在線程進入和退出同步塊時不再通過 CAS 操作來加鎖和解鎖,而是檢測 Mark Word 里是否存儲著指向當前線程的偏向鎖。輕量級鎖的獲取及釋放依賴多次 CAS 原子指令,而偏向鎖只需要在置換 ThreadID 的時候依賴一次 CAS 原子指令即可。
偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖,線程是不會主動釋放偏向鎖的。
輕量級鎖
輕量級鎖是指當鎖是偏向鎖的時候,卻被另外的線程所訪問,此時偏向鎖就會升級為輕量級鎖,其他線程會通過自旋(關于自旋的介紹見文末)的形式嘗試獲取鎖,線程不會阻塞,從而提高性能。
一旦有第二個線程加入鎖競爭,偏向鎖就升級為輕量級鎖(自旋鎖)。這里要明確一下什么是鎖競爭:如果多個線程輪流獲取一個鎖,但是每次獲取鎖的時候都很順利,沒有發生阻塞,那么就不存在鎖競爭。只有當某線程嘗試獲取鎖的時候,發現該鎖已經被占用,只能等待其釋放,這才發生了鎖競爭。
在輕量級鎖狀態下繼續鎖競爭,沒有搶到鎖的線程將自旋,即不停地循環判斷鎖是否能夠被成功獲取。獲取鎖的操作,其實就是通過CAS修改對象頭里的鎖標志位。先比較當前鎖標志位是否為“釋放”,如果是則將其設置為“鎖定”,比較并設置是原子性發生的。這就算搶到鎖了,然后線程將當前鎖的持有者信息修改為自己。
長時間的自旋操作是非常消耗資源的,一個線程持有鎖,其他線程就只能在原地空耗CPU,執行不了任何有效的任務,這種現象叫做忙等(busy-waiting)。如果多個線程用一個鎖,但是沒有發生鎖競爭,或者發生了很輕微的鎖競爭,那么synchronized就用輕量級鎖,允許短時間的忙等現象。這是一種折衷的想法,短時間的忙等,換取線程在用戶態和內核態之間切換的開銷。
重量級鎖
重量級鎖顯然,此忙等是有限度的(有個計數器記錄自旋次數,默認允許循環10次,可以通過虛擬機參數更改)。如果鎖競爭情況嚴重,某個達到最大自旋次數的線程,會將輕量級鎖升級為重量級鎖(依然是CAS修改鎖標志位,但不修改持有鎖的線程ID)。當后續線程嘗試獲取鎖時,發現被占用的鎖是重量級鎖,則直接將自己掛起(而不是忙等),等待將來被喚醒。
重量級鎖是指當有一個線程獲取鎖之后,其余所有等待獲取該鎖的線程都會處于阻塞狀態。
簡言之,就是所有的控制權都交給了操作系統,由操作系統來負責線程間的調度和線程的狀態變更。而這樣會出現頻繁地對線程運行狀態的切換,線程的掛起和喚醒,從而消耗大量的系統資
synchronized與lock鎖區別
總結如下:
1?? lock 是一個接口,而 synchronized 是 Java 的一個關鍵字,synchronized 是內置的語言實現。
2?? 異常是否釋放鎖:synchronized 在發生異常時候會自動釋放占有的鎖,因此不會出現死鎖;而 lock 發生異常時候,不會主動釋放占有的鎖,必須手動 unlock 來釋放鎖,可能引起死鎖的發生。(所以最好將同步代碼塊用 try catch 包起來,finally 中寫入 unlock,避免死鎖的發生。)
3?? 是否響應中斷
lock 等待鎖過程中可以用 interrupt 來中斷等待,而 synchronized 只能等待鎖的釋放,不能響應中斷。
4?? 是否知道獲取鎖:Lock 可以通過 trylock 來知道有沒有獲取鎖,而 synchronized 不能。
5?? Lock 可以提高多個線程進行讀操作的效率。(可以通過 ReadWriteLock 實現讀寫分離)
6?? 在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當競爭資源非常激烈時(即有大量線程同時競爭),此時 Lock 的性能要遠遠優于 synchronized。所以說,在具體使用時要根據適當情況選擇。
7?? synchronized 使用 Object 對象本身的 wait 、notify、notifyAll 調度機制,而 Lock 可以使用 Condition 進行線程之間的調度。
性能區別
synchronized 和 lock 性能區別
synchronized 是托管給 JVM 執行的,而 lock 是 Java 寫的控制鎖的代碼。在 Java1.5 中,synchronized 是性能低效的。因為這是一個重量級操作,但是到了 Java1.6,發生了變化,進行了很多優化,有適應自旋,輕量級鎖,偏向鎖等等。
synchronized 原始采用的是 CPU悲觀鎖機制,即線程獲得的是排他鎖。排他鎖意味著其他線程只能依靠阻塞來等待線程釋放鎖。
而 Lock 用的是樂觀鎖機制。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。樂觀鎖實現的機制就是 CAS 操作(Compare and Swap)。進一步研究 ReentrantLock 的源碼,會發現其中比較重要的獲得鎖的一個方法是 compareAndSetState。這里其實就是調用的 CPU 提供的特殊指令。
用途區別
synchronized 和 lock 用途區別
synchronized 原語和 ReentrantLock 在一般情況下沒有什么區別,但是在非常復雜的同步應用中,請考慮使用 ReentrantLock ,特別是遇到下面兩種需求的時候。
1?? 某個線程在等待一個鎖的控制權的這段時間需要中斷。
2?? 分開處理一些 wait-notify,ReentrantLock 里面的 Condition 應用,能夠控制 notify 哪個線程。
3?? 公平鎖功能,每個到來的線程都將排隊等候。
先說第一種情況,ReentrantLock 的 lock 機制有 2 種,忽略中斷鎖和響應中斷鎖,這帶來了很大的靈活性。比如:如果 A、B 兩個線程去競爭鎖,A 線程得到了鎖,B 線程等待,但是 A 線程這個時候實在有太多事情要處理,就是一直不返回,B 線程可能就會等不及了,想中斷自己,不再等待這個鎖了,轉而處理其他事情。
這個時候 ReentrantLock 就提供了兩種機制:可中斷/可不中斷:
①B 線程中斷自己(或者別的線程中斷它),但是 ReentrantLock 不去響應,繼續讓 B 線程等待,你再怎么中斷,我全當耳邊風(synchronized 原語就是如此);
②B 線程中斷自己(或者別的線程中斷它),ReentrantLock 處理了這個中斷,并且不再等待這個鎖的到來,完全放棄。
緩存三兄弟:緩存擊穿、穿透、雪崩
緩存擊穿
緩存擊穿:redis中沒有,但是數據庫有
順序:先查緩存,判斷緩存是否存在;如果緩存存在,直接返回數據;如果緩存不存在,則查詢數據庫,將數據庫的數據存入到緩存
解決方案:將熱點數據設置過期時間長一點;針對數據庫的熱點訪問方法上分布式鎖;
緩存穿透
緩存穿透:redis中沒有,數據庫也沒有
解決方案:
(1)將不存在的key,在redis設置值為null;
(2)使用布隆過濾器;
原理:https://zhuanlan.zhihu.com/p/616911933
布隆過濾器:
如果確認key不存在于redis中,那么就一定不存在;
它說key存在,就有可能存在,也可能不存在! (誤差)
布隆過濾器
1、根據配置類中的 key的數量 ,誤差率,計算位圖數組【二維數組】
2、通過布隆過濾器存放key的時候,會計算出需要多少個hash函數,由hash函數算出多少個位圖位置需要設定為1
3、查詢時,根據對應的hash函數,判斷對應的位置值是否都為1;如果有位置為0,則表示key一定不存在于該redis服務器中;如果全部位置都為1,則表示key可能存在于redis服務器中;
緩存雪崩
緩存雪崩:
Redis的緩存雪崩是指當Redis中大量緩存數據同時失效或者被清空時,大量的請求會直接打到數據庫上,導致數據庫瞬時壓力過大,甚至宕機的情況。
造成緩存雪崩的原因主要有兩個:
1.相同的過期時間:當Redis中大量的緩存數據設置相同的過期時間時,這些數據很可能會在同一時間點同時失效,導致大量請求直接打到數據庫上。
2.緩存集中失效:當服務器重啟、網絡故障等因素導致Redis服務不可用,且緩存數據沒有自動進行容錯處理,當服務恢復時大量的數據同時被重新加載到緩存中,也會導致大量請求直接打到數據庫上。
預防緩存雪崩的方法主要有以下幾種:
1.設置不同的過期時間:可以將緩存數據的過期時間分散開,避免大量緩存數據在同一時間點失效。
2.使用加鎖:可以將所有請求都先進行加鎖操作,當某個請求去查詢數據庫時,如果還沒有加載到緩存中,則只讓單個線程去執行加載操作,其他線程等待該線程完成后再次進行判斷,避免瞬間都去訪問數據庫從而引起雪崩。
3.提前加載預熱:在系統低峰期,可以提前將部分熱點數據加載到緩存中,這樣可以避免在高峰期緩存數據失效時全部打到數據庫上。
4.使用多級緩存:可以在Redis緩存之上再使用一層緩存,例如本地緩存等,當Redis緩存失效時,還能夠從本地緩存中獲取數據,避免直接打到數據庫上。
本地緩存:ehcache oscache spring自帶緩存 持久層框架的緩存
總結
Java進階(鎖)——鎖的升級,synchronized與lock鎖區別