深入理解 JVM鎖 與 分布式鎖


鎖用來解決什么問題呢?

在我們編寫的應用程序或者高並發程序中,不知道大家有沒有想過一個問題,就是我們為什么需要引入鎖?鎖為我們解決了什么問題呢?

在很多業務場景下,我們編寫的應用程序中會存在很多的 資源競爭 的問題。而我們在高並發程序中,引入鎖,就是為了解決這些資源競爭的問題。

電商超賣問題

這里,我們可以列舉一個簡單的業務場景。比如,在電子商務(商城)的業務場景中,提交訂單購買商品時,首先需要查詢相應商品的庫存是否足夠,只有在商品庫存數量足夠的前提下,才能讓用戶成功的下單。下單時,我們需要在庫存數量中減去用戶下單的商品數量,並將庫存操作的結果數據更新到數據庫中。整個流程我們可以簡化成下圖所示。

這里,我也給出相應的代碼片段吧。我們可以使用下面的代碼片段來表示用戶的下單操作,我這里將商品的庫存信息保存在了Redis中。

@RequestMapping("/submitOrder")
public String submitOrder(){
    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
    if(stock > 0){
        stock -= 1;
        stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
        logger.debug("庫存扣減成功,當前庫存為:{}", stock);
    }else{
        logger.debug("庫存不足,扣減庫存失敗");
        throw new OrderException("庫存不足,扣減庫存失敗");
    }
    return "success";
}
View Code

注意:上述代碼片段比較簡單,只是為了方便大家理解,真正項目中的代碼就不能這么寫了。

上述的代碼看似是沒啥問題的,但是我們不能只從代碼表面上來觀察代碼的執行順序。這是因為在JVM中代碼的執行順序未必是按照我們書寫代碼的順序執行的。即使在JVM中代碼是按照我們書寫的順序執行,那我們對外提供的接口一旦暴露出去,就會有成千上萬的客戶端來訪問我們的接口。所以說,我們暴露出去的接口是會被並發訪問的。

試問,上面的代碼在高並發環境下是線程安全的嗎?答案肯定不是線程安全的,因為上述扣減庫存的操作會出現並行執行的情況。

我們可以使用Apache JMeter來對上述接口進行測試,這里,我使用Apache JMeter對上述接口進行測試。

 

在Jmeter中,我將線程的並發度設置為3,接下來的配置如下所示。

 

 以HTTP GET請求的方式來並發訪問提交訂單的接口。此時,運行JMeter來訪問接口,命令行會打印出下面的日志信息。

這里,我們明明請求了3次,也就是說,提交了3筆訂單,為什么扣減后的庫存都是一樣的呢?這種現象在電商領域有一個專業的名詞叫做  “超賣” 。

如果一個大型的高並發電商系統,比如淘寶、天貓、京東等,出現了超賣現象,那損失就無法估量了!架構設計和開發電商系統的人員估計就要通通下崗了。所以,作為技術人員,我們一定要嚴謹的對待技術,嚴格做好系統的每一個技術環節。


JVM中提供的鎖

JVM中提供的synchronized和Lock鎖,相信大家並不陌生了,很多小伙伴都會使用這些鎖,也能使用這些鎖來實現一些簡單的線程互斥功能。

那么,作為立志要成為架構師的你,是否了解過JVM鎖的底層原理呢?

JVM鎖原理

說到JVM鎖的原理,我們就不得不限說說Java中的對象頭了。

Java中的對象頭 

每個Java對象都有對象頭。如果是⾮數組類型,則⽤2個字寬來存儲對象頭,如果是數組,則會⽤3個字寬來存儲對象頭。在32位處理器中,⼀個字寬是32位;在64位虛擬機中,⼀個字寬是64位。 

 Mark Work的格式如下所示。

 可以看到:

當對象狀態為偏向鎖時, Mark Word 存儲的是偏向的線程ID;

當狀態為輕量級鎖時, Mark Word 存儲的是指向線程棧中 Lock Record 的指針;

當狀態為重量級鎖時, Mark Word 為指向堆中的monitor對象的指針 。

                                                          有關Java對象頭的知識,參考《深入淺出Java多線程》。 


JVM鎖原理

簡單點來說,JVM中鎖的原理如下。

在Java對象的對象頭上,有一個鎖的標記,比如,第一個線程執行程序時,檢查Java對象頭中的鎖標記,發現Java對象頭中的鎖標記為未加鎖狀態,於是為Java對象進行了加鎖操作,

將對象頭中的鎖標記設置為鎖定狀態。第二個線程執行同樣的程序時,也會檢查Java對象頭中的鎖標記,此時會發現Java對象頭中的鎖標記的狀態為鎖定狀態。於是,第二個線程會進入相應的阻塞隊列中進行等待。

這里有一個關鍵點就是Java對象頭中的鎖標記如何實現。

JVM鎖的短板

JVM中提供的synchronized和Lock鎖都是JVM級別的,大家都知道,當運行一個Java程序時,會啟動一個JVM進程來運行我們的應用程序。synchronized和Lock在JVM級別有效,也就是說,

synchronized和Lock在同一Java進程內有效。如果我們開發的應用程序是分布式的,那么只是使用synchronized和Lock來解決分布式場景下的高並發問題,就會顯得有點力不從心了。

synchronized和Lock支持JVM同一進程內部的線程互斥

synchronized和Lock在JVM級別能夠保證高並發程序的互斥,我們可以使用下圖來表示。

 

但是,當我們將應用程序部署成分布式架構,或者將應用程序在不同的JVM進程中運行時,synchronized和Lock就不能保證分布式架構和多JVM進程下應用程序的互斥性了。

synchronized和Lock不能實現多JVM進程之間的線程互斥

分布式架構和多JVM進程的本質都是將應用程序部署在不同的JVM實例中,也就是說,其本質還是多JVM進程。


分布式鎖

我們在實現分布式鎖時,可以參照JVM鎖實現的思想,JVM鎖在為對象加鎖時,通過改變Java對象的對象頭中的鎖的標志位來實現,也就是說,所有的線程都會訪問這個Java對象的對象頭中的鎖標志位。

 

 我們同樣以這種思想來實現分布式鎖,當我們將應用程序進行拆分並部署成分布式架構時,所有應用程序中的線程訪問共享變量時,

 都到同一個地方去檢查當前程序的臨界區是否進行了加鎖操作,而是否進行了加鎖操作,我們在統一的地方使用相應的狀態來進行標記。

 

可以看到,在分布式鎖的實現思想上,與JVM鎖相差不大。而在實現分布式鎖中,保存加鎖狀態的服務可以使用MySQL、Redis和Zookeeper實現。

但是,在互聯網高並發環境中, 使用Redis實現分布式鎖的方案是使用的最多的。 接下來,我們就使用Redis來深入解密分布式鎖的架構設計。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM