兩種方式實現自己的可重入鎖


本篇文章將介紹兩種自己動手實現可重入鎖的方法。

我們都知道JDK中提供了一個類ReentrantLock,利用這個類我們可以實現一個可重入鎖,這種鎖相對於synchronized來說是一種輕量級鎖。

重入鎖的概念

重入鎖實際上指的就是一個線程在沒有釋放鎖的情況下,可以多次進入加鎖的代碼塊。

    public void a() {
        lock2.lock();
        System.out.println("a");
        b();
        lock2.unlock();
    }

    public void b() {
        lock2.lock();
        System.out.println("b");
        lock2.unlock();
    }
	new Thread(() -> {
		m.a();
    }).start();

這種情況下,如果我們加的鎖不是支持可重入的鎖,那么b方法中的代碼塊不會執行,如果我們的鎖是一個重入鎖,那么b方法中的打印代碼塊也會被執行。

土方法實現重入鎖

首先我們先實現一個沒有實現可重入的鎖,這個鎖實現接口Lock,代碼如下:

public class MyLock implements Lock {

	//鎖標記
    private boolean isLocked = false;

    @Override
    public synchronized void lock() {
    	//如果已經有一個線程獲得了鎖,那么線程就一直等待
        while (isLocked)
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        isLocked = true;
    }

    @Override
    public synchronized void unlock() {
    	//可以進入的一定使已經獲得鎖的線程,那么直接改變標志,喚醒其他等待的線程
    	isLocked = false;
    	notify();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }


    @Override
    public Condition newCondition() {
        return null;
    }
}

代碼中使用sychronized關鍵字,來控制只有一個線程可以獲得鎖。

由於sychronized關鍵字加在類的方法上的時候,內置鎖對象實際使當前類的對象,因此,我們需要使用同一個對象來調用加鎖、解鎖方法。

這樣我們就可以保證只有一個線程可以進入加解鎖方法內部,然后通過isLocked來標記是否已經有線程獲得了鎖。

當我們使用上面介紹重入鎖的測試方式來測驗代碼時,只會打印出a,之后將一直等待,無法打印b。

下面,我們將修改方法,讓其實現可重入。

實際上,所謂可重入的方法就是將獲得鎖的線程記錄下來,如果進入方法的線程可獲得鎖的線程是同一個線程,那么我們就可以直接獲得鎖,不需要等待。

實現方法如下:

    private boolean isLocked = false;
	//記錄獲得鎖的線程
    private Thread lockBy = null;
	//記錄獲得鎖的線程的重入次數
    private int count = 0;


    @Override
    public synchronized void lock() {
        //獲取當前線程
        Thread currentThread = Thread.currentThread();
		//已經有線程獲得鎖,並且獲得鎖的線程不是當前線程,那么不滿足獲得鎖,線程需要等待
        while (isLocked && currentThread != lockBy)
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

		//沒有線程獲得鎖,或者獲得鎖的線程就是當前線程
        isLocked = true;
        lockBy = currentThread;
        //記錄當前線程的重入次數
        count++;
    }

    @Override
    public synchronized void unlock() {
    	//釋放鎖時,只有當獲得鎖的線程和當前線程是同一線程時才是正確的
        if (lockBy == Thread.currentThread()) {
        	//線程重入次數減一
            count--;
            //只有當count變為0也就是所有獲取鎖的地方都已經釋放了,才能夠真正釋放鎖,修改標志位,喚醒其他線程
            if (count == 0) {
                notify();
                isLocked = false;
            }
        }
    }

當我們使用上面介紹重入鎖的測試方式來測驗代碼時,將會打印出a、b,因為鎖是可以重入的,不會出現一直等待的情況。

使用AQS類實現重入鎖

AQS類簡單介紹

AbstractQueuedSynchronizer類時JDK在1.5版本開始提供的一個可以用來實現依賴於先進先出 (FIFO) 等待隊列的阻塞鎖和相關同步器(信號量、事件,等等)框架,在這里我們不關注它的其他功能,重點介紹一下如何利用AQS實現阻塞鎖。

當我們要使用AQS實現一個鎖時,我們需要在我們的鎖類的內部聲明一個非公共的內部幫助類,讓這個類集成AbstractQueuedSynchronizer類,並實現其某些方法。
利用AQS類,我們可以實現兩種模式的鎖,一種是獨占鎖,一種是共享鎖。這兩種鎖的幫助類需要實現的方法是不同,如果是獨占鎖,那么需要實現tryAcquire(int)tryRelease(int)方法;如果是共享鎖,那么需要實現tryAcquireShared(int)tryReleaseShared(int)方法。

AQS類內部維護了一個FIFO的雙向鏈表,用來保存所有爭奪鎖的線程,AQS源碼中的Node類就是雙向鏈表中節點的數據結構。

當使用AQS類加鎖時,會調用方法acquire(int)方法中會調用,而acquire(int)方法中會調用tryAcquire(int)來嘗試獲得鎖,如果獲得鎖成功,方法結束,如果獲得鎖失敗,那么需要將線程維護到FIFO鏈表中,並且讓新增加的線程進入等待狀態,並且維護鏈表中線程的狀態。(這部分代碼比較復雜,可以參考網上對AQS的講解,之后會再寫一篇專門介紹AQS類的源碼解析)

注:這個方法是忽略中斷的,不忽略中斷的方法,這里不做介紹

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

當使用AQS釋放鎖時,會調用方法release(int)方法中會調用,而release(int)方法中會調用tryRelease(int)來嘗試釋放鎖,如果釋放鎖成功后,如果FIFO鏈表中有線程,那么,會喚醒所有等待狀態的線程。

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

利用AQS實現可重入鎖

實際上利用AQS實現一個可重入鎖是非常容易的,首先給出代碼。

實際上利用AQS實現鎖和用土方法實現鎖的思路大體上是相同的,只是,我們不需要關注線程的喚醒和等待,這些會有AQS幫助我們實現,我們只需要實現方法tryAcquire(int)tryRelease(int)就可以了。

這里我們實際上是應用AQS中的int值保存當前線程的重入次數。

加鎖思路:

如果第一個線程進入可以拿到鎖,可以返回true,

如果第二個線程進入,拿不到鎖,返回false,

有一種特例(實現可重入),如果當前進入線程和當前保存線程為同一個,允許拿到鎖,但是有代價,更新狀態值,也就是記錄線程的重入次數

public class MyLock2 implements Lock {
    private Helper helper = new Helper();
	//實現一個私有的幫助類,繼承AQS類
    private class Helper extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
        
            //AQS中的int值,當沒有線程獲得鎖時為0
            int state = getState();
            Thread t = Thread.currentThread();
			//第一個線程進入
            if (state == 0) {
            	//由於可能有多個線程同時進入這里,所以需要使用CAS操作保證原子性,這里不會出現線程安全性問題
                if (compareAndSetState(0, 1)) {
                	//設置獲得獨占鎖的線程
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
            } else if (getExclusiveOwnerThread() == t) {
            	//已經獲得鎖的線程和當前線程是同一個,那么state加一,由於不會有多個線程同時進入這段代碼塊,所以沒有線程安全性問題,可以直接使用setState方法
                setState(state + 1);
                return true;
            }
            //其他情況均無法獲得鎖
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {

            //鎖的獲取和釋放使一一對應的,那么調用此方法的一定是當前線程,如果不是,拋出異常
            if (Thread.currentThread() != getExclusiveOwnerThread()) {
                throw new RuntimeException();
            }
            
            int state = getState() - arg;

            boolean flag = false;
            
			//如果state減一后的值為0了,那么表示線程重入次數已經降低為0,可以釋放鎖了。
            if (state == 0) {
                setExclusiveOwnerThread(null);
                flag = true;
            }
            
			//無論是否釋放鎖,都需要更改state的值
            setState(state);
            
			//只有state的值為0了,才真正釋放了鎖,返回true
            return flag;
        }

        Condition newCondition() {
            return new ConditionObject();
        }
    }

    @Override
    public void lock() {
        helper.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        helper.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return helper.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return helper.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        helper.release(1);
    }

    @Override
    public Condition newCondition() {
        return helper.newCondition();
    }
}

至此,我們已經使用兩種方式實現了一個重入鎖。


免責聲明!

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



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