Java中常用的鎖分析總結
Java中常用的鎖分析總結
1. ReentrantLock、ReentrantReadWriteLock及Sychronized簡介
(a) 類繼承結構
ReentrantLock類繼承結構:
ReentrantReadWriteLick類繼承結構:
簡述:通過類的繼承結構可以看出ReentrantLock 和 ReentrantReadWriteLock是擁有者兩個不同類繼承結構的體系,兩者並無關聯。
Ps:Sychronized是一個關鍵字
(b) 幾個相關概念
什麼是可重入鎖:可重入鎖的概念是自己可以再次獲取自己的內部鎖。舉個例子,比如一條執行緒獲得了某個物件的鎖,此時這個物件鎖還沒有釋放,當其再次想要獲取這個物件的鎖的時候還是可以獲取的(如果不可重入的鎖的話,此刻會造成死鎖)。說的更高深一點可重入鎖是一種遞迴無阻塞的同步機制。
什麼叫讀寫鎖:讀寫鎖拆成讀鎖和寫鎖來理解。讀鎖可以共享,多個執行緒可以同時擁有讀鎖,但是寫鎖卻只能只有一個執行緒擁有,而且獲取寫鎖的時候其他執行緒都已經釋放了讀鎖,而且該執行緒獲取寫鎖之後,其他執行緒不能再獲取讀鎖。簡單的說就是寫鎖是排他鎖,讀鎖是共享鎖。
獲取鎖涉及到的兩個概念即 公平和非公平:公平表示執行緒獲取鎖的順序是按照執行緒加鎖的順序來分配的,即先來先得的FIFO順序。而非公平就是一種獲取鎖的搶佔機制,和公平相對就是先來不一定先得,這個方式可能造成某些執行緒飢餓(一直拿不到鎖)。
(c) ReentrantLock,ReentrantReadWriteLock,Sychronized用法即作用
ReentrantLock: 類ReentrantLock實現了Lock,它擁有與Sychronized相同的併發性和記憶體語義,但是添加了類似鎖投票、定時鎖等候和可中斷等候的一些特性。此外,它還提供了在與激烈爭用情況下更佳的效能(說白了就是ReentrantLock和Sychronized差不多,執行緒間都是完全互斥的,一個時刻只能有一個執行緒獲取到鎖,執行被鎖住的程式碼,但ReentrantLock相對於Sychronized提供了更加豐富的功能並且線上程排程上做了優化,JVM排程使用ReentrantLock的執行緒會更快)
程式碼示例:ReentrantLockTest.java
/**
* ReentrantLock DEMO
* @author jianying.wcj
* @date 2013-5-20
*/
public class ReetrantLockTest {
/**
* 一個可重入鎖成員變數
*/
private ReentrantLocklock =new ReentrantLock();
public static void main(String[] args) {
ReetrantLockTestdalt = new ReetrantLockTest();
dalt.testLock();
}
public void testLock(){
for(int i = 0; i < 5; i++) {
Threadthread = new Thread(new Runnable(){
@Override
publicvoid run() {
sayHello();
}
},"thread"+i);
thread.start();
}
}
public void sayHello() {
/**
* 當一條執行緒不釋放鎖的時候,第二個執行緒走到這裡的時候就阻塞掉了
*/
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+" locking ...");
System.out.println("Hello world!");
System.out.println(Thread.currentThread().getName()+" unlocking ...");
}finally {
lock.unlock();
}
}
}
執行結果:
簡述:首先要操作ReentrantLock的加鎖(lock)和解鎖(unlock)必須是針對同一個ReentrantLock物件,要是new 兩個ReetrantLock來分別完成對同一資源的加鎖和解鎖是沒有意義的。比如LockA物件對 resource 加鎖,讓後LockB物件對Resource解鎖,這個是不對的,沒有意義的)。通過執行結果可以看出,當一個執行緒去lock資源的時候,必須是上一個執行緒對資源完成了unlock,這個和syncronized關鍵字啟動的作用是一樣的。 另外在使用時一個需要格外主意的點是 unlock方法的呼叫要放在finally程式碼塊裡,來保證鎖一定會釋放,否則可能造成某一個資源一直被鎖死,排查問題比較困難。
ReentrantReadWriteLock:類ReentrantReadWriteLock實現了ReadWirteLock介面。它和ReentrantLock是不同的兩套實現,在類繼承結構上並無關聯。和ReentrantLock定義的互斥鎖不同的是,ReentrantReadWriteLock定義了兩把鎖即讀鎖和寫鎖。讀鎖可以共享,即同一個資源可以讓多個執行緒獲取讀鎖。這個和ReentrantLock(或者sychronized)相比大大提高了讀的效能。在需要對資源進行寫入的時候在會加寫鎖達到互斥的目的。話不多說看DEMO:
ReentrantReadWriteLock.java:
public class ReadWriteLockTest {
/**
* 一個可重入讀寫鎖
*/
private ReentrantReadWriteLockreadWriteLock =new ReentrantReadWriteLock();
/**
* 讀鎖
*/
private ReadLockreadLock =readWriteLock.readLock();
/**
* 寫鎖
*/
private WriteLockwriteLock =readWriteLock.writeLock();
/**
* 共享資源
*/
private StringshareData ="寂寞等待中...";
public void write(String str) throws InterruptedException {
writeLock.lock();
System.err.println("ThreadName:"+Thread.currentThread().getName()+"locking...");
try {
shareData = str;
System.err.println("ThreadName:" + Thread.currentThread().getName()+"修改為"+str);
Thread.sleep(1);
}catch(InterruptedException e) {
e.printStackTrace();
}finally {
System.err.println("ThreadName:" + Thread.currentThread().getName()+" unlock...");
writeLock.unlock();
}
}
public String read() {
readLock.lock();
System.out.println("ThreadName:" + Thread.currentThread().getName()+"lock...");
try {
System.out.println("ThreadName:"+Thread.currentThread().getName()+"獲取為:"+shareData);
Thread.sleep(1);
}catch(InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("ThreadName:" + Thread.currentThread().getName()+"unlock...");
readLock.unlock();
}
returnshareData;
}
public static void main(String[] args) {
final ReadWriteLockTest shareData =new ReadWriteLockTest();
/**
* 起50條讀執行緒
*/
for(int i = 0; i < 50; i++) {
new Thread(new Runnable() {
publicvoid run() {
try {
Thread.sleep(1);
}catch (InterruptedException e) {
e.printStackTrace();
}
shareData.read();
}
},"get Thread-read"+i).start();
}
for(int i = 0; i < 5; i++) {
new Thread(new Runnable() {
publicvoid run() {
try {
Thread.sleep(1);
}catch (InterruptedException e1) {
e1.printStackTrace();
}
try {
shareData.write(new Random().nextLong()+"");
}catch (InterruptedException e) {
e.printStackTrace();
}
}
},"wirte Thread-write"+i).start();
}
}
}
執行結果:
簡述:Demo讀鎖和寫鎖都是ReentrantReadWriteLock類定義的內部公開類,要想讓讀鎖和讀鎖或者讀鎖跟寫鎖產生共享或者互斥關係,必須要求讀鎖和寫鎖是有同一個ReentrantReadWriteLock產生的,否則是沒有意義的。從執行結果中可以看出讀鎖之間的共享,寫鎖和寫鎖,寫鎖和讀鎖之間的互斥關係。
Synchronized關鍵字:
public class SychronizedTest implements Runnable{
public void run() {
synchronized(this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+"synchronized loop " + i);
}
}
}
public static void main(String[] args) {
SychronizedTest t1 = new SychronizedTest();
Thread ta = new Thread(t1,"A");
Thread tb = new Thread(t1,"B");
ta.start();
tb.start();
}
}
執行結果:
·
簡述:從執行記過來看,被sychronized包圍的程式碼是原子的。這個不多說,這個關鍵字大家應該都很熟悉。
2. ReentrantLock、ReentrantReadWriteLock及Sychronized實現原理(原始碼級別)
(a) 鎖機制的內部實現
ReentrantLock內部鎖機制實現相關類圖:
簡述:ReentrantLock鎖機制的實現是基於它的一個成員變數sync,這個Sync是AbstractQueuedSynchronized(AQS)的一個子類(ps:sync類是ReentrantLock自己定義的一個內部類)。另外在ReentrantLock內部還定義了另外兩個類,分別是FairSync和NonFairSync,這兩個類就是分別對應的鎖公平分配和不公平分配的兩個實現,它們都繼承自Sync(類圖已經清晰的描述出來了繼承結構)。有關鎖的分配和釋放邏輯都是封裝在了AQS裡面的(AQS是AbstractQueuedSynchronized的簡稱,是JSR166規範中提出的一個基礎的同步中心類或者說是同步框架,其在內部實現了大量的同步操作,而且使用者還可以在此類的基礎上自定義自己的同步類),可見Sync和AQS是鎖機制實現的核心類(AQS詳述見下文)。
ReentrantLock當中的部分例項程式碼:
1. 兩個建構函式(可見預設使用的非公平鎖的分配機制):
2. Lock方法的實現其實就是直接代理了Sync lock的實現:
3. TryLock方法也是一樣的,都是代理自Sync
4. 解鎖方法
Ps:說白了ReentrantLock就是基於Sync的,而Sync就是一種AQS,其中核心機制AQS都實現好了。
ReentrantReadWriteLock內部實現機制實現類圖:
ReentrantReadWriteLock的類圖和ReentrantLock的類圖感覺是一摸一樣的,唯一的區別就是Sync、FairSync、NonSync是ReentrantReadWriteLock自己定義的。因為ReentrantReadWriteLock要實現讀寫鎖機制,所以這裡的Sync和ReentrantLock的Sync肯定不會相同。其他的和ReentrantLock都是一樣的,核心的實現都是基於AQS的子類Sync(AQS分析見下文)
部分示例程式碼如下:
1.建構函式(內部定義了ReadLock和WriteLock,預設也採用鎖非公平分配的實現)
2. WriteLock當中的Lock方法:
Ps:上文簡單的貼了兩行程式碼主要為了說明一點,ReentrantLock和ReentrantReadWriteLock的實現是基於AQS的。下文再從原始碼角度分析一下具體實現。
Synchronized關鍵字:
簡述:Synchronized實現的同步和上面提到的AQS的方式是不同的,AQS實現了一套自己的演算法來實現共享資源的合理控制(具體演算法實現,下文分析),而Synchronized實現的同步控制是基於java 內部的物件鎖的。
Java內部物件鎖:JVM中每個物件和類實際上都與一把鎖與之相關聯,對於物件來說,監視的是這個物件變數,對於類來說,監視的是類變數。當虛擬機器裝載類時,會建立一個Class類的例項,鎖住的實際上是這個類對應的Class累的例項。物件鎖是可重入的,也就是說一個物件或者類上的鎖是可以累加的。
Ps:java中的同步是通過監視器模型來實現的,Java中的監視器實際上是一個程式碼塊.
Synchronized實現分析:這麼說還是有點抽象,那麼從程式碼角度來分析一下Synchronized是怎麼實現的。
(a) 先看看Synchronized程式碼快的方式:
SynchronizedTest1.java:
package test9;
/**
* @author jianying.wcj
* @date 2013-5-22
*/
public classSynchronizedTest1 {
public void sayHello(){
synchronized(this){
System.out.println("hello world!");
}
}
}
先用javac編譯成.class 然後再用javap–verbose SynchronizedTest1 檢視自己碼的彙編碼如下圖所示:
簡述:紅色標記出來的是兩條JVM命令,用來標識進入同步程式碼塊,和退出同步程式碼塊,由此可見Synchronized已經上升到JVM指令的級別和AQS的實現還是有很大差別的。上面這個是Synchronized程式碼塊的形式,Synchronized還有另一種使用方式就是同步方法。
(b) Synchronized同步方法的方式:
SynchronizedTest2.java:
package test9;
/**
* @author jianying.wcj
* @date 2013-5-22
*/
public class SychronizedTest{
public synchronized void sayHello(){
System.out.println("hello world!");
}
}
同樣通過javap命令檢視彙編碼如下:
簡述:通過看這段彙編碼,並沒有發現JVM的同步塊指令,可見同步方法和程式碼同步塊採用的是不同的實現方式。同步方法的實現是JVM定義了方法的訪問標誌 ACC_SYNCHRONIZED 在方法表中,JVM後將同步方法前面設定這個標誌,用於標識這個是一個同步方法。
3. Sync及AQS的核心實現(原始碼級別)
AQS核心思想是,如果被請求的共享資源空閒,則將當前請求資源的執行緒設定為有效的工作執行緒,並且將共享資源的設定為鎖定狀態。如果被請求的共享資源被佔用,那麼就需要一套執行緒阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH佇列鎖實現的,即將暫時獲取不到鎖的執行緒加入到佇列中。
那麼首先看一下CLH佇列鎖的資料結構及實現演算法。
(a)CLH佇列的資料結構(如圖):
簡述:CLH佇列是一個虛擬的雙向佇列(虛擬的雙向佇列即不存在佇列例項,僅存在結點之間的關聯關係)。AQS是將每條請求共享資源的執行緒封裝成一個CLH鎖佇列的一個結點(Node)來實現鎖的分配的。具體構建佇列的演算法是這樣的:
假設: 有共享資源S目前正被L3執行緒佔用,此時有L1、L2執行緒分別對資源S進行lock操作以及獲取鎖後進行unlock操作。具體的流程如下:
(1)由於目前資源S被佔用,所以將執行緒L1包裝成一個CLH佇列的Node,將這個Node的前驅(prev)指向當前對列裡的隊尾,放入隊尾這個操作採用了CAS原語(原子操作)。如果當前的隊尾為NULL,那麼就建一個虛擬的Header,然後將T1執行緒掛載到虛擬Header下。核心程式碼如下:
Ps: addWaiter就是放入佇列的操作。
Ps:採用CAS將節點加入到隊尾,如果隊尾為null進入enq操作。
Ps:建立了一個虛擬的Header
(2) L2執行緒請求資源S,那麼它和L1執行緒一樣將自己加入到隊尾,L2的prev指向L1,L1.next指向L2(雙向佇列嘛)。
(3) 當L3釋放資源即unlock的時候,喚醒與L3關聯的下一個節點,同時釋放當前節點。關鍵程式碼:
(b)每個結點類的屬性及方法資訊:
屬性簡述:CANCELLED:表示因為超時或者中斷,結點被設定為取消狀態,被取消的狀態結點不應該去競爭鎖。SIGNAL:表示這個結點的繼任結點被阻塞了,因為等待某個條件而被阻塞。CONDITION:表示這個結點在佇列中,因為等待某個條件而被阻塞。這幾個是常量屬性預設值為:
這幾個常量用來設定waitStatus屬性。
Thread屬性表示關聯到這個結點的執行緒。Prev和next就是關聯前後結點的索引變數。NextWaiter 記錄的是這個結點是獨佔式還是可共享的屬性。
4. 幾種鎖的效能比較及使用場景(應用級別)
對於效能的對比這篇部落格介紹的比較好: