【源碼篇】深刻Lock鎖底層原理實現,手寫一個Lock鎖

synchronized與lock

lock是一個接口,而synchronized是在JVM層面實現的。synchronized釋放鎖有兩種方式:java

  1. 獲取鎖的線程執行完同步代碼,釋放鎖 。
  2. 線程執行發生異常,jvm會讓線程釋放鎖。

lock鎖的釋放,出現異常時必須在finally中釋放鎖,否則容易形成線程死鎖。lock顯式獲取鎖和釋放鎖,提供超時獲取鎖、可中斷地獲取鎖。node

synchronized是以隱式地獲取和釋放鎖,synchronized沒法中斷一個正在等待獲取鎖的線程。安全

synchronized原始採用的是CPU悲觀鎖機制,即線程得到的是獨佔鎖。獨佔鎖意味着其餘線程只能依靠阻塞來等待線程釋放鎖。而在CPU轉換線程阻塞時會引發線程上下文切換,當有不少線程競爭鎖的時候,會引發CPU頻繁的上下文切換致使效率很低。bash

Lock用的是樂觀鎖方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。樂觀鎖實現的機制就是CAS操做。多線程

具體的悲觀鎖和樂觀鎖的詳細介紹請參考這篇文章[]併發

JDK5中增長了一個Lock接口實現類ReentrantLock.它不只擁有和synchronized相同的併發性和內存語義,還多了鎖投票,定時鎖,等候和中斷鎖等.它們的性能在不一樣的狀況下會有不一樣。jvm

在資源競爭不是很激烈的狀況下,synchronized的性能要因爲ReentrantLock,可是在資源競爭很激烈的狀況下,synchronized的性能會降低得很是快,而ReentrantLock的性能基本保持不變.ide

接下來咱們會進一步研究ReentrantLock的源代碼,會發現其中比較重要的得到鎖的一個方法是compareAndSetState源碼分析

lock源碼

在閱讀源碼的成長的過程當中,有不少人會遇到不少困難,一個是源碼太多,另外一方面是源碼看不懂。在閱讀源碼方面,我提供一些我的的建議:性能

  1. 第一個是抓主舍次,看源碼的時候,不少人會發現源碼太長太多,看不下去,這就要求咱們抓住哪些是核心的方法,哪些是次要的方法。當捨去次要方法,就會發現代碼精簡和不少,會大大提升咱們閱讀源碼的信心。
  2. 第二個是不要死扣,有人看源碼會一行一行的死扣,當看到某一行看不懂,就一直停在那裏死扣,知道看懂爲止,其實不少時候,雖然看不懂代碼,可是能夠從變量名和方法名知道該代碼的做用,java中都是見名知意的。

接下來進入閱讀lock的源碼部分,在lock的接口中,主要的方法以下:

public interface Lock {
    // 加鎖
    void lock();
    // 嘗試獲取鎖
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 解鎖
    void unlock();
}
複製代碼

在lock接口的實現類中,最主要的就是ReentrantLock,來看看ReentrantLocklock()方法的源碼:

// 默認構造方法,非公平鎖
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    
    // 構造方法,公平鎖
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    
    // 加鎖
    public void lock() {
        sync.lock();
    }
複製代碼

在初始化lock實例對象的時候,能夠提供一個boolean的參數,也能夠不提供該參數。提供該參數就是公平鎖,不提供該參數就是非公平鎖。

什麼是非公平鎖和公平鎖呢?

非公平鎖就是不按照線程先來後到的時間順序進行競爭鎖,後到的線程也可以獲取到鎖,公平鎖就是按照線程先來後到的順序進行獲取鎖,後到的線程只能等前面的線程都獲取鎖完畢才執行獲取鎖的操做,執行有序。

咱們來看看lock()這個方法,這個有區分公平鎖和非公平鎖,這個二者的實現不一樣,先來看看公平鎖,源碼以下:

// 直接調用 acquire(1)
final void lock() {
     acquire(1);
 }
複製代碼

咱們來看看acquire(1)的源碼以下:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
複製代碼

這裏的判斷條件主要作兩件事:

  1. 通關過該方法tryAcquire(arg)嘗試的獲取鎖
  2. 如果沒有獲取到鎖,經過該方法acquireQueued(addWaiter(Node.EXCLUSIVE), arg)就將當前的線程加入到存儲等待線程的隊列中。

其中tryAcquire(arg)是嘗試獲取鎖,這個方法是公平鎖的核心之一,它的源碼以下:

protected final boolean tryAcquire(int acquires) {
             // 獲取當前線程 
            final Thread current = Thread.currentThread();
            // 獲取當前線程擁有着的狀態
            int c = getState();
            // 若爲0,說明當前線程擁有着已經釋放鎖
            if (c == 0) {
                 // 判斷線程隊列中是否有,排在前面的線程等待着鎖,如果沒有設置線程的狀態爲1。
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    // 設置線程的擁有着爲當前線程
                    setExclusiveOwnerThread(current);
                    return true;
                }
                // 如果當前的線程的鎖的擁有者就是當前線程,可重入鎖
            } else if (current == getExclusiveOwnerThread()) {
                // 執行狀態值+1
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                // 設置status的值爲nextc
                setState(nextc);
                return true;
            }
            return false;
        }
複製代碼

tryAcquire()方法中,主要是作了如下幾件事:

  1. 判斷當前線程的鎖的擁有者的狀態值是否爲0,若爲0,經過該方法hasQueuedPredecessors()再判斷等待線程隊列中,是否存在排在前面的線程。
  2. 如果沒有經過該方法 compareAndSetState(0, acquires)設置當前的線程狀態爲1。
  3. 將線程擁有着設爲當前線程setExclusiveOwnerThread(current)
  4. 如果當前線程的鎖的擁有者的狀態值不爲0,說明當前的鎖已經被佔用,經過current == getExclusiveOwnerThread()判斷鎖的擁有者的線程,是否爲當前線程,實現鎖的可重入。
  5. 如果當前線程將線程的狀態值+1,並更新狀態值。

公平鎖的tryAcquire(),實現的原理圖以下:

咱們來看看 acquireQueued()方法,該方法是將線程加入等待的線程隊列中,源碼以下:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 死循環處理
            for (;;) {
                // 獲取前置線程節點
                final Node p = node.predecessor();
                // 這裏又嘗試的去獲取鎖
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    // 直接return  interrupted
                    return interrupted;
                }
                // 在獲取鎖失敗後,應該將線程Park(暫停)
                if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
複製代碼

acquireQueued()方法主要執行如下幾件事:

  1. 死循環處理等待線程中的前置節點,並嘗試獲取鎖,如果p == head && tryAcquire(arg),則跳出循環,即獲取鎖成功。
  2. 如果獲取鎖不成功shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()就會將線程暫停。

acquire(int arg)方法中,最後如果條件成立,執行下面的源碼:

selfInterrupt();

// 實際執行的代碼爲
Thread.currentThread().interrupt();
複製代碼

即嘗試獲取鎖失敗,就會將鎖加入等待的線程隊列中,並讓線程處於中斷等待。公平鎖lock()方法執行的原理圖以下:

之因此畫這些原理的的緣由,是爲後面寫一個本身的鎖作鋪墊,由於你要實現和前人差很少的東西,你必須瞭解該東西執行的步驟,最後得出的結果,執行的過程是怎麼樣的。

有了流程圖,在後面的實現本身的東西才能一步一步的進行。這也是閱讀源碼的必要之一。

lock()方法,其實在lock()方法中,已經包含了兩方面:

  1. 鎖方法lock()
  2. 嘗試獲取鎖方法tryAquire()

接下來,咱們來看一下unlock()方法的源碼。

public void unlock() {
        sync.release(1);
    }
複製代碼

直接調用release(1)方法,來看release方法源碼以下:

public final boolean release(int arg) {
       // 嘗試釋放當前節點
        if (tryRelease(arg)) {
            // 取出頭節點
            Node h = head;
            if (h != null && h.waitStatus != 0)
                // 釋放鎖後要即便喚醒等待的線程來獲取鎖
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
複製代碼

經過調用tryRelease(arg),嘗試釋放當前節點,如果釋放鎖成功,就會獲取的等待隊列中的頭節點,就會即便喚醒等待隊列中的等待線程來獲取鎖。接下來看看tryRelease(arg)的源碼以下:

// 嘗試釋放鎖
 protected final boolean tryRelease(int releases) {
            // 將當前狀態值-1
            int c = getState() - releases;
            // 判斷當前線程是不是鎖的擁有者,若不是直接拋出異常,非法操做,直接一點的解釋就是,你都沒有擁有鎖,還來釋放鎖,這不是騙人的嘛
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //執行釋放鎖操做 1.若狀態值=0   2.將當前的鎖的擁有者設爲null
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 從新更新status的狀態值
            setState(c);
            return free;
        }
複製代碼

總結上面的幾個方法,unlock釋放鎖方法的執行原理圖以下:

對於非公平鎖與公平鎖的區別,在非公平鎖嘗試獲取鎖中不會執行 hasQueuedPredecessors()去判斷是否隊列中還有等待的前置節點線程。

以下面的非公平鎖,嘗試獲取鎖nonfairTryAcquire()源碼以下:

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // 直接就將status-1,並不會判斷是否還有前置線程在等待
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
複製代碼

以上就是公平鎖和非公平鎖的主要的核心方法的源碼,接下來咱們實現本身的一個鎖,首先依據前面的分析中,要實現本身的鎖,擁有的鎖的核心屬性以下:

  1. 狀態值status,0爲未佔用鎖,1未佔用鎖,而且是線程安全的。
  2. 等待線程隊列,用於存放獲取鎖的等待線程。
  3. 當前線程的擁有想者。

lock鎖的核心的Api以下:

  1. lock方法
  2. trylock方法
  3. unlock方法

依據以上的核心思想來實現本身的鎖,首先定義狀態值status,使用的是AtomicInteger原子變量來存放狀態值,實現該狀態值的併發安全和可見性。定義以下:

// 線程的狀態 0表示當前沒有線程佔用   1表示有線程佔用
	AtomicInteger status =new AtomicInteger();
複製代碼

接下來定義等待線程隊列,使用LinkedBlockingQueue隊列來裝線程,定義以下:

// 等待的線程
LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<Thread>();
複製代碼

最後的屬性未當前鎖的擁有者,直接就用Thread來封裝,定義以下:

// 當前線程擁有者
Thread ownerThread =null;
複製代碼

接下來定義lock()方法,依據上面的源碼分析,在lock方法中主要執行的幾件事以下:

  1. 死循環的處理等待線程隊列中的線程,知道獲取鎖成功,將該線程從隊列中刪除,跳出循環。
  2. 獲取鎖不成功,線程處於暫停等待。
@Override
	public void lock() {
		// TODO Auto-generated method stub
		// 嘗試獲取鎖
		if (!tryLock()) {
		    // 獲取鎖失敗,將鎖加入等待的隊列中
			waitersQueue.add(Thread.currentThread());
			// 死循環處理隊列中的鎖,不斷的獲取鎖
			for (;;) {
				if (tryLock()) {
				    // 直到獲取鎖成功,將該線程從等待隊列中刪除
					waitersQueue.poll();
					// 直接返回
					return;
				} else {
				    // 獲取鎖不成功,就直接暫停等待。
					LockSupport.park();
				}
			}
		}
	}
複製代碼

而後是trylock方法,依據上面的源碼分析,在trylock中主要執行的如下幾件事:

  1. 判斷當前擁有鎖的線程的狀態是否爲0,爲0,執行狀態值+1,並將當前線程設置爲鎖擁有者。
  2. 實現鎖可重入
@Override
	public boolean tryLock() {
		// 判斷是否有現成佔用
		if (status.get()==0) {
			// 執行狀態值加1
			if (status.compareAndSet(0, 1)) {
			    // 將當前線程設置爲鎖擁有者
				ownerThread = Thread.currentThread();
				return true;
			} else if(ownerThread==Thread.currentThread())  {
			    // 實現鎖可重入
				status.set(status.get()+1);
			}
		}
		return false;
	}
複製代碼

最後就是unlock方法,依據上面的源碼分析,在unlock中主要執行的事情以下:

  1. 判斷當前線程是不是鎖擁有者,若不是直接拋出異常。
  2. 判斷狀態值是否爲0,並將鎖擁有者清空,喚醒等待的線程。
@Override
	public void unlock() {
		// TODO Auto-generated method stub
		// 判斷當前線程是不是鎖擁有者
		if (ownerThread!=Thread.currentThread()) {
			throw new RuntimeException("非法操做");
		}
		// 判斷狀態值是否爲0
		if (status.decrementAndGet()==0) {
		    // 清空鎖擁有着
			ownerThread = null;
			// 從等待隊列中獲取前置線程
			Thread t = waitersQueue.peek();
			if (t!=null) {
			   // 並當即喚醒該線程
				LockSupport.unpark(t);
			}
		}
	}
複製代碼

以上就是實現本身的非公平的可重入鎖,lock的源碼其實並不複雜,只要認真看都能看懂,在閱讀源碼的過程當中,會遇到比較複雜的問題。遇到問題不要慌,網上查詢資料,相信不少都能找到答案,由於java的生態如此完善,幾乎90%的東西網上都會有,只要沉得住氣,相信必定會有所收穫。

相關文章
相關標籤/搜索