所謂鎖是用來控制多個線程訪問共享資源的**方式**,所以鎖在Java中并不僅僅指代的就是Java中的對象,例如*自旋鎖*用的就是指令的方式。通常的,一個鎖能夠防止多個線程同時訪問共享資源(也稱為臨界區)。
在JDK5之前,Java中的鎖是通過關鍵字Synchronized來實現的,而在JDK5之后Lock接口的出現使得鎖的使用更加的靈活。同時在JDK6開始對Synchronized關鍵字進行了鎖升級的優化,使其能夠適用于更多的場景,不再是嚴格意義上的*重量級鎖*。
## Synchronized關鍵字
意為同步的,Synchronized關鍵字可以隱性的將一個java對象設置為鎖,在《Java虛擬機規范》中并沒有對Synchronized做特定的約束,synchronized的實現依照各虛擬機產商而定。在Hotspot(常說的Java虛擬機基本就是指這個)中是在對象的頭部信息中使用mark word域(32位或者64位)來表示對象的鎖信息。在JDK6的優化之后,Synchronized關鍵字被引入了一個`鎖升級`的概念,其升級過程如下:
:-: 偏向鎖 --輕量級鎖 --重量級鎖
Synchronized鎖的升級與鎖對象的頭部信息的mark word字段息息相關,對象頭結構及各種狀態下的mark word域的結構如下圖:
:-: 
> 參考:《Java并發編程的藝術》P12
其中mark word可以用來儲存對象的hashcode、鎖信息、垃圾回收時的分代年齡等信息。Class Metadata Address保存對象的類型(class類)數據的指針。32位的Array Length用在數組對象中保存數組對象的長度。
不同狀態下mark word的內容:
:-: 
> 參考:《黑馬程序員-并發編程》
因此一個對象有如下四種鎖狀態:“無鎖狀態”、“偏向鎖狀態”、“輕量級鎖狀態”、“重量級鎖狀態”。其中:
* baised\_lock表示是否是偏向鎖,0表示不是*偏向鎖*,1表示是*偏向鎖*;
* age表示垃圾回收時的分代年齡;
* epoch用在批量重偏向中;
* threadID表示獲取*偏向鎖*的線程ID;
* ptr\_to\_lock\_record指向棧幀中的lock record記錄;
* ptr\_to\_monitor指向monitor對象。
### 偏向鎖狀態
*偏向鎖*指的是當鎖不存在多個線程競爭,并且經常由一個線程獲取鎖對象時,為了讓線程獲取鎖的代價更低,在鎖對象的mark word中保存了當前獲取鎖對象線程的線程ID,下次該線程要獲取鎖的時候可以不使用CAS的方式獲取鎖,而是直接通過比較線程ID來再次獲取鎖對象。
例如:
<iframe src="https://carbon.now.sh/embed?bg=rgba%2831%2C129%2C109%2C1%29&t=night-owl&wt=none&l=auto&ds=false&dsyoff=20px&dsblur=68px&wc=true&wa=true&pv=19px&ph=23px&ln=true&fl=1&fm=Hack&fs=14.5px&lh=143%25&si=false&es=2x&wm=false&code=public%2520class%2520Test%2520%257B%250A%2520%2520%2520%2520private%2520static%2520final%2520Object%2520lock%2520%253D%2520new%2520Object%28%29%253B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%250A%2520%2520%2520%2520%250A%2520%2520%2520%2520public%2520static%2520void%2520m1%28%29%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520synchronized%28lock%29%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520m2%28%29%253B%250A%2520%2520%2520%2520%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%250A%2520%2520%2520%2520public%2520static%2520void%2520m2%28%29%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520synchronized%28lock%29%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520m3%28%29%253B%250A%2520%2520%2520%2520%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%250A%2520%2520%2520%2520public%2520static%2520void%2520m3%28%29%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520synchronized%28lock%29%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%252F%252F%2520...%250A%2520%2520%2520%2520%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%250A%2520%2520%2520%2520public%2520static%2520void%2520main%28String%255B%255D%2520args%29%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520m1%28%29%253B%250A%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%250A%257D" style="width: 960px; height: 632px; border:0; transform: scale(1); overflow:hidden;" sandbox="allow-scripts allow-same-origin"> </iframe>
?
:-: 
判斷線程ID一樣之后就會直接獲取鎖,而不會采用cas的方式來自旋獲取鎖。同時采用這種方式*偏向鎖*即是可重入的鎖。
一個對象在剛被創建的時候處于無鎖狀態(**mark word后三位為:001**,其余全為0),但是幾秒之后會變成*偏向鎖*狀態(mark word后三位為:**101**,其余全為0),可以使用虛擬機參數:“**\-XX:BiasedLockingStartupDelay=0**”來關閉這種延遲的特性,同時如果想要關閉*偏向鎖*的話可以使用虛擬機參數:“**\-XX:-UseBiasedLocking**”來進行關閉。
例如:
<iframe src="https://carbon.now.sh/embed?bg=rgba%2831%2C129%2C109%2C1%29&t=night-owl&wt=none&l=auto&ds=false&dsyoff=20px&dsblur=68px&wc=true&wa=true&pv=19px&ph=23px&ln=true&fl=1&fm=Hack&fs=14.5px&lh=143%25&si=false&es=2x&wm=false&code=%252F**%250A%2520*%2520%25E4%25BD%25BF%25E7%2594%25A8%25E7%25AC%25AC%25E4%25B8%2589%25E6%2596%25B9%25E5%25B7%25A5%25E5%2585%25B7jol%25E6%259F%25A5%25E7%259C%258B%25E5%25AF%25B9%25E8%25B1%25A1%25E7%259A%2584%25E5%25A4%25B4%25E9%2583%25A8%25E4%25BF%25A1%25E6%2581%25AF%250A%2520*%252F%250A%250Apublic%2520class%2520TestBaisedLock%2520%257B%250A%2520%2520%2520%2520public%2520static%2520void%2520main%28String%255B%255D%2520args%29%2520%257B%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%250A%2520%2520%2520%2520%2520%2520%2520%2520Object%2520object%2520%253D%2520new%2520Object%28%29%253B%250A%2520%2520%2520%2520%2520%2520%2520%2520ClassLayout%2520classLayout%2520%253D%2520ClassLayout.parseInstance%28object%29%253B%250A%2520%2520%2520%2520%2520%2520%2520%2520%250A%2520%2520%2520%2520%2520%2520%2520%2520new%2520Thread%28%28%29%2520-%253E%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520System.out.println%28classLayout.toPrintable%28object%29%29%253B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520synchronized%2520%28object%29%2520%257B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520System.out.println%28classLayout.toPrintable%28object%29%29%253B%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%257D%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520System.out.println%28classLayout.toPrintable%28object%29%29%253B%250A%2520%2520%2520%2520%2520%2520%2520%2520%257D%29.start%28%29%253B%250A%250A%2520%2520%2520%2520%257D%250A%257D" style="width: 1024px; height: 492px; border:0; transform: scale(1); overflow:hidden;" sandbox="allow-scripts allow-same-origin"> </iframe>
?
輸出的mark word的主要內容如下:
~~~
?OFF ?SZ ? TYPE DESCRIPTION ? ? ? ? ? ? ? VALUE
? ?0 ? 8 ? ? ? (object header: mark) ? ? 0x0000000000000005 (biasable; age: 0)
? ?
?OFF ?SZ ? TYPE DESCRIPTION ? ? ? ? ? ? ? VALUE
? ?0 ? 8 ? ? ? (object header: mark) ? ? 0x00000296f840c005 (biased: 0x00000000a5be1030; epoch: 0; age: 0)
? ?
?OFF ?SZ ? TYPE DESCRIPTION ? ? ? ? ? ? ? VALUE
? ?0 ? 8 ? ? ? (object header: mark) ? ? 0x00000296f840c005 (biased: 0x00000000a5be1030; epoch: 0; age: 0)
~~~
說明:value字段的內容為16進制,最后的5用二進制位表示為:0101,即為*偏向鎖*狀態。(注意這里在測試的時候并沒有添加關閉偏向鎖延遲加載的虛擬機參數,但是可能由于程序啟動太慢了導致第一個顯示的也是偏向鎖狀態?)
添加了虛擬機參數“-XX:-UseBiasedLocking”關閉*偏向鎖*后的輸出:
~~~
?OFF ?SZ ? TYPE DESCRIPTION ? ? ? ? ? ? ? VALUE
? ?0 ? 8 ? ? ? (object header: mark) ? ? 0x0000000000000001 (non-biasable; age: 0)
? ?
?OFF ?SZ ? TYPE DESCRIPTION ? ? ? ? ? ? ? VALUE
? ?0 ? 8 ? ? ? (object header: mark) ? ? 0x0000004bd2bff420 (thin lock: 0x0000004bd2bff420)
? ?
?OFF ?SZ ? TYPE DESCRIPTION ? ? ? ? ? ? ? VALUE
? ?0 ? 8 ? ? ? (object header: mark) ? ? 0x0000000000000001 (non-biasable; age: 0)
~~~
說明:關閉偏向鎖之后在第一次調用synchronized時候直接變成了輕量級鎖狀態。
備注:jol依賴引入如下,注意scope屬性不要provider!
~~~
?<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
?<dependency>
? ? ?<groupId>org.openjdk.jol</groupId>
? ? ?<artifactId>jol-core</artifactId>
? ? ?<version>0.16</version>
?</dependency>
~~~
### 輕量級鎖
*輕量級鎖*狀態發生在當有多個線程訪問同步代碼塊,但是訪問時間是錯開的,彼此之間沒有競爭的時候。(注意*偏向鎖*是發生在**同一個線程**反復訪問同步代碼塊的時候。)
*輕量級鎖*狀態的記錄不再跟偏向鎖一樣在對象的mark word域中記錄thread id,而是使用了線程棧中的**棧幀的鎖記錄結構**來記錄*輕量級鎖*狀態。

1. 加鎖過程:讓鎖記錄中的對象引用指向對象地址,同時采用CAS的方式將鎖記錄的地址替換到對象的mark word域中,對象的hashcode和分代年齡等信息則放到鎖記錄中進行暫存。CAS操作有如下兩種情況:
- CAS成功則當前線程獲取鎖,對象頭的mark down信息存儲`鎖記錄地址和最后面的鎖狀態改為00`。

- 如果失敗,有兩種情況,一種是確實是當前線程獲取了鎖對象,這個時候就發生了鎖的重入,只需要多添加一條Lock Record作為鎖的重入計數即可。

另外一種是別的線程獲取了鎖對象正在訪問同步代碼塊,這個時候就會發生鎖膨脹,將輕量級鎖膨脹為為重量級鎖。
2. 解鎖過程:當退出同步代碼塊發現有取值為null的鎖記錄,則證明有重入現象,只要去掉即可。如果取值不為null,則用CAS操作將對象的mark word內容重置。該操作有兩種情況:
* 成功:則解鎖成功。
* 失敗:說明輕量級鎖已經膨脹成了重量級鎖,因為這個時候對象頭已經不再是指向鎖記錄了,而是保存了重量級鎖的monitor信息,進入重量級鎖的解鎖流程。
### 重量級鎖
#### Monitor對象
重量級鎖和一個Monitor對象有關。每個Java對象都可以關聯一個Monitor對象,如果使用synchronized關鍵字給對象加上重量級鎖的時候,鎖對象的mark word域就會指向Monitor對象。
Monitor對象的結構如下:

* Monitor剛創建的時候Owner為null。
* 當Thread-0執行synchronized(object)時,Monitor就會將Owner設置為Thread-0,Monitor只能有一個Owner。
* 在Thread-0上鎖的過程中,如果有其他線程Thread-1、Thread-2等線程來競爭鎖,就會進入EntryList Block中。
* 在Thread-0中執行完同步代碼塊后,會喚醒EntryList中的等待線程來競爭鎖,競爭的時候是非公平的。
* WaitSet的內容與wait-notify有關(等待/通知模型中要用到的等待隊列)。
> 注意:
> synchronized必須是進入同一個對象的monitor才有上述效果。
用下面代碼為例查看monitor起的作用
~~~
?static final Object obj = new Object();
?static int count = 0;
??
?public static void main(String[] args) {
? ? ?synchronized (obj) {
? ? ? ? ?count++;
? ? }
?}
~~~
對應的字節碼:
~~~
? Code:
? ? ? ?stack=2, locals=3, args_size=1
? ? ? ? ? 0: getstatic ? ? #7 ? ? ? ? ? ? ? ? // Field obj:Ljava/lang/Object;
? ? ? ? ? 3: dup
? ? ? ? ? 4: astore_1
? ? ? ? ? 5: monitorenter ?// 插入monitorenter指令
? ? ? ? ? 6: getstatic ? ? #13 ? ? ? ? ? ? ? ? // Field count:I
? ? ? ? ? 9: iconst_1
? ? ? ? ?10: iadd
? ? ? ? ?11: putstatic ? ? #13 ? ? ? ? ? ? ? ? // Field count:I
? ? ? ? ?14: aload_1
? ? ? ? ?15: monitorexit // 插入monitorexit指令
? ? ? ? ?16: goto ? ? ? ? ?24
? ? ? ? ?19: astore_2
? ? ? ? ?20: aload_1
? ? ? ? ?21: monitorexit
? ? ? ? ?22: aload_2
? ? ? ? ?23: athrow
? ? ? ? ?24: return
~~~
當synchronized處于重量級鎖的狀態的時候,JVM會在進入同步代碼塊之前插入monitorenter指令,保證線程獲取鎖;在退出代碼塊之前插入monitorexit指令,保證鎖被釋放。
當synchronized處于重量級鎖的狀態的時候,從源碼上來看會先自旋一定次數的CAS,然后再進入阻塞隊列中,并將線程阻塞住。
> 注意jdk1.8中輕量級鎖并不會自選,重量級鎖才會自選。
## Volatile關鍵字
Volatile是輕量級的synchronizd,能夠保證共享變量的”可見性“。所謂可見性:當一個線程修改一個共享變量的時候,另外一個線程能夠讀取到其修改的值。當一個字段被聲明成volatile時,Java線程內存模型確保所有的線程看到這個變量是一致的。
底層實現的相關術語:
* 內存屏障:一組處理器指令,用于實現對內存操作的順序限制。
* 原子操作:不可中斷的一個或一系列操作。
**Volatile保持可見性的原理:**
在被volatile聲明了的變量中,當要進行寫操作的時候,jvm會添加一條lock前綴的cpu的指令,該指令可實現如下兩個目的:
* 將當前處理器的緩存行寫回內存中。
* 這個寫回的操作使得其他cpu緩存了該內存地址的數據無效。通過嗅探技術監控自己緩存的數據是不是過期了。具體的協議就是MESI協議。
# Lock接口
從上面我們知道了synchronized關鍵字能夠隱性的獲取鎖,同時介紹了synchronized鎖升級的概念,鎖升級可以使得synchronized的性能得到大大的優化,不在是嚴格意義上的重量級鎖。但是,synchronized這種隱性獲取鎖的方式雖然簡化了同步的方式,同時也給訪問共享資源的擴展帶來了一定的困難。
而Lock接口(JDK5后提供)所代表的鎖就能夠在某些場景下具有較大的靈活性,能夠讓程序員顯式的來操作鎖對象。Lock鎖的使用方式大致如下:
~~~
?Lock lock = new ReentrantLock();
?lock.lock(); // 加鎖
?try {
? ? ?// 同步代碼塊
?} finally {
? ? ?lock.unlock(); ?// 解鎖
?}
~~~
**Lock接口的特性:**
1. 嘗試非阻塞的獲取鎖:當有多個線程競爭鎖的時候,如果這一時刻鎖沒有被其他線程獲取,則當前線程能夠成功并持有鎖;
2. 能被中斷的獲取鎖:synchronized是不能響應中斷,而使用了Lock接口的鎖是可以響應中斷的;
3. 超時獲取鎖:可以在創建鎖對象的時候設置超時時間,如果截止時間到了仍舊無法獲取鎖則返回。
**基本API**
1. 獲取鎖,在沒有其他線程獲取鎖的時候調用該方法的線程會獲取鎖。
~~~
?void lock();
~~~
2. 可中斷獲取鎖,該方法可以響應中斷,在鎖的獲取中可以響應中斷當前線程。
~~~
?void lockInterruptibly() throws InterruptedException
~~~
3. 嘗試非阻塞的獲取鎖,能夠成功獲取返回true,否則返回false。
~~~
?boolean tryLock();
~~~
4. 超時獲取鎖
~~~
?boolean tryLock(long time, TimeUnit unit) throws InterruptedException
~~~
5. 釋放鎖
~~~
?void unlock();
~~~
6. 獲取等待通知組件
~~~
?Condition newCondition();
~~~
- 第一章 Java基礎
- ThreadLocal
- Java異常體系
- Java集合框架
- List接口及其實現類
- Queue接口及其實現類
- Set接口及其實現類
- Map接口及其實現類
- JDK1.8新特性
- Lambda表達式
- 常用函數式接口
- stream流
- 面試
- 第二章 Java虛擬機
- 第一節、運行時數據區
- 第二節、垃圾回收
- 第三節、類加載機制
- 第四節、類文件與字節碼指令
- 第五節、語法糖
- 第六節、運行期優化
- 面試常見問題
- 第三章 并發編程
- 第一節、Java中的線程
- 第二節、Java中的鎖
- 第三節、線程池
- 第四節、并發工具類
- AQS
- 第四章 網絡編程
- WebSocket協議
- Netty
- Netty入門
- Netty-自定義協議
- 面試題
- IO
- 網絡IO模型
- 第五章 操作系統
- IO
- 文件系統的相關概念
- Java幾種文件讀寫方式性能對比
- Socket
- 內存管理
- 進程、線程、協程
- IO模型的演化過程
- 第六章 計算機網絡
- 第七章 消息隊列
- RabbitMQ
- 第八章 開發框架
- Spring
- Spring事務
- Spring MVC
- Spring Boot
- Mybatis
- Mybatis-Plus
- Shiro
- 第九章 數據庫
- Mysql
- Mysql中的索引
- Mysql中的鎖
- 面試常見問題
- Mysql中的日志
- InnoDB存儲引擎
- 事務
- Redis
- redis的數據類型
- redis數據結構
- Redis主從復制
- 哨兵模式
- 面試題
- Spring Boot整合Lettuce+Redisson實現布隆過濾器
- 集群
- Redis網絡IO模型
- 第十章 設計模式
- 設計模式-七大原則
- 設計模式-單例模式
- 設計模式-備忘錄模式
- 設計模式-原型模式
- 設計模式-責任鏈模式
- 設計模式-過濾模式
- 設計模式-觀察者模式
- 設計模式-工廠方法模式
- 設計模式-抽象工廠模式
- 設計模式-代理模式
- 第十一章 后端開發常用工具、庫
- Docker
- Docker安裝Mysql
- 第十二章 中間件
- ZooKeeper