Java併發編程(一):ReentrantLock的介紹與使用

前言

目前狀態:對《Java併發編程的藝術》全書進行的兩邊學習。第一次學習主要是初步瞭解Java內存模型、Java併發編程基礎以及Java併發編程工具包。而後對遺忘較多。尤其是對Java內存模型這塊忘記較多。隨後又對全書進行了第二次學習,在第一次學習瞭解併發衆多概念基礎上更細一步瞭解併發編程。而後發現對Java併發編程的瞭解還僅限於皮毛;爲此,我需要對併發工具包的特性做一個更細緻的瞭解和源碼的解讀,以進一步提高對併發編程的瞭解。其實在網上並不缺少相關技術文章,並且內容非常詳盡;我發佈此欄博文的目的有三個。
學習希望有所輸出,輸出是最好的輸入;準確和良好的輸出是已更加完成的輸入和整理爲基礎,以此來督促自己更加深入的完成相關的學習內容。
網絡上相關文章很多,內容也很翔實;甚至包括《Java併發編程的藝術》的作者也開立博客,刊登相關文章;對概念描述精準清晰,對內容全面詳細。但我的博客更多是大白話來表述我的理解,儘管在內容上不夠全面,卻希望初學者能夠以更低的門檻窺探到併發編程的美麗世界。
最後一點,若讀者朋友發現謬誤之處,忘讀者們及時指出文章的錯誤;我會虛心接受及時修改錯誤內容並及時提升自己,不要讓錯誤的內容誤導其他讀者,造成讀者的困惑。

此處我也不清楚我能堅持此博文能夠到哪裏一步,但是你們的閱讀和評論是對我最好的支持和監督;望所有讀者們能在我的文章中能夠得到對你有用的內容。

1、ReentrantLock與Condition的使用

什麼是ReentrantLock?

鎖是控制多線程安全方法共享資源訪問的工具。鎖有兩種實現方式,一種是使用synchronized(隱性鎖),另一種是Lock顯性鎖,這裏主要是講Lock的其中一個實現ReentrantLock。
相對與synchronized關鍵字,Lock鎖的lock()和unlock()方法包含的代碼等同於synchronized包含的代碼塊或者方法體。相比synchronized更加靈活,更加容易理解,但是在使用時更加容易出錯。
雖然synchronized的鎖,出現異常時能夠自動釋放鎖,使用不易出錯;但更推薦使用Lock鎖,因爲Lock接口有多個實現ReentrantLock、ReentrantReadWriteLock等多個鎖實現,在不同讀寫的情況下,效率往往比synchronized的效率要高。
我們用併發編程的目的也正是爲了能夠提高代碼執行效率嘛。(細粒度鎖能夠提高同步代碼訪問的效率)

什麼是Condition?

Condition是對Lock狀態的管理以及對鎖更精確的空中。
Condition中的await()方法相當於Object的wait()方法,Condition中的signal()方法相當於Object的notify()方法,Condition中的signalAll()相當於Object的notifyAll()方法。(這裏需要對synchronized鎖有一個初步瞭解)

public class Cliect {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        Thread t1 = new Thread(new Workder1(lock, condition));
        Thread t2 = new Thread(new Workder2(lock, condition));
        t1.start();
        t2.start();

    }
    static class Workder1 implements Runnable {
        private Lock lock;
        Condition condition;
        public Workder1(Lock lock, Condition condition) {
            this.lock = lock;
            this.condition = condition;
        }
        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.lock();
            try {
                condition.signal();
                System.out.println("worker1 notify...");
            } finally {
                lock.unlock();
            }
        }
    }
    static class Workder2 implements Runnable {
        private Lock lock;
        Condition condition;
        public Workder2(Lock lock, Condition condition) {
            this.lock = lock;
            this.condition = condition;
        }
        @Override
        public void run() {
            lock.lock();
            try {
                condition.await();
                System.out.println("worker2 wait...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

注意:這裏有兩點需要非常注意的。

  1. 在Lock代碼塊中不能錯誤使用condition.wait()和condition.notify(),這是隱性鎖對應同步對象的等待和喚醒機制。若錯誤使用會拋出java.lang.IllegalMonitorStateException異常,wait()和notify()無法找到對應synchronized修飾的同步代碼。
  2. condition對象的await()和signal()一定要在Lock代碼塊中,否則會拋出java.lang.IllegalMonitorStateException異常。

2、ReentrantLock中的公平鎖與非公平鎖

ReentrantLock有兩個子類實現,分別爲公平鎖和非公平鎖。
首先明確一個前提,在ReentrantLock中,每當一個線程去請求鎖時,若此時鎖被其他線程持有。當前線程會被加入到CLH等待隊列中,而公平鎖和非公平鎖的區別就在於這些等待線程請求鎖的過程。
公平鎖:線程遵循FIFO原則,當鎖釋放時,等待隊列中先等待的線程先獲得鎖,後面線程繼續排隊。
非公平鎖:線程遵循先到先得原則,當鎖釋放時,等待隊列中所有線程都嘗試請求鎖,最快的線程獲得鎖,其他線程繼續等待。
這就有點像去櫃檯買火車票,公平鎖就像去排隊購買,按照隊伍順序購買;非公平鎖就像大家一擁而上,誰力氣大動作快就可以先在窗口購票。
根據ReentrantLock構造函數可以看出默認的是非公平鎖,這裏可以猜測,作者在不清楚線程佔用鎖時長的情況下,傾向於以效率優先。

public ReentrantLock() {
        sync = new NonfairSync();
    }

非公平鎖相對於公平鎖的優缺點

  1. 非公平鎖比公平鎖的效率更高,加鎖解鎖次數越多,效率相差越大。
  2. 由於非公平鎖是所有線程都在搶佔鎖,可能會出現線程等待很久或者線程餓死。

公平鎖和非公平鎖執行順序觀察:根據日誌很明顯的可以看出公平鎖是根據啓動順序與獲取鎖順序相同。非公平鎖啓動與獲取鎖順序差異較大。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockClient {
    public static void main(String[] args) {
//        ReentrantLock lock = new ReentrantLock(true);//公平鎖
        ReentrantLock lock = new ReentrantLock(false);//非公平鎖
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(new Worker(lock));
            t.start();
        }
    }

    static class Worker implements Runnable {
        private Lock lock;

        public Worker(Lock lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "啓動");
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "獲得鎖");
            } finally {
                lock.unlock();
            }
        }
    }
}

公平鎖獲取日誌

Thread-0啓動
Thread-2啓動
Thread-1啓動
Thread-0獲得鎖
Thread-5啓動
Thread-4啓動
Thread-6啓動
Thread-3啓動
Thread-2獲得鎖
Thread-1獲得鎖
Thread-7啓動
Thread-8啓動
Thread-5獲得鎖
Thread-9啓動
Thread-4獲得鎖
Thread-6獲得鎖
Thread-3獲得鎖
Thread-7獲得鎖
Thread-8獲得鎖
Thread-9獲得鎖

非公平鎖獲取日誌

Thread-2啓動
Thread-1啓動
Thread-0啓動
Thread-1獲得鎖
Thread-2獲得鎖
Thread-7啓動
Thread-6啓動
Thread-0獲得鎖
Thread-5啓動
Thread-7獲得鎖
Thread-3啓動
Thread-4啓動
Thread-8啓動
Thread-9啓動
Thread-6獲得鎖
Thread-5獲得鎖
Thread-3獲得鎖
Thread-4獲得鎖
Thread-8獲得鎖
Thread-9獲得鎖

公平鎖和非公平鎖的效率比對
下面的代碼啓動了20個線程,每個線程對race變量進行了10w次的自增,每次都包含一次鎖獲取(lock())和釋放(unlock())。一共進行了20*10w=200w次鎖獲取/釋放。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockClient {
    public static Integer threadCount = 20;
    public static CountDownLatch cdl = new CountDownLatch(threadCount);
    public static ReentrantLock lock = new ReentrantLock(true);//公平鎖
    //    public static ReentrantLock lock = new ReentrantLock(false);//非公平鎖
    public static Integer race = 0;
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        for (int i = 0; i < threadCount; i++) {
            Thread t = new Thread(new Worker(lock, cdl));
            t.start();
        }
        cdl.await();
        long end = System.currentTimeMillis() - start;
        System.out.println(end);
    }
    public static void incrRace() {
        lock.lock();
        try {
            race++;
        } finally {
            lock.unlock();
        }
    }
    static class Worker implements Runnable {
        private Lock lock;
        private CountDownLatch cdl;

        public Worker(Lock lock, CountDownLatch cdl) {
            this.lock = lock;
            this.cdl = cdl;
        }
        @Override
        public void run() {
            for (int i = 0; i < 100000; i++) {
                incrRace();
            }
            cdl.countDown();
        }
    }
}

公平鎖耗時5915毫秒
公平鎖
非公平鎖耗時58毫秒
非公平鎖
公平鎖和非公平鎖在200w的鎖獲取/釋放中性能相差100倍。

3、ReentrantLock的可重入鎖

(1)可重入鎖:任意線程在獲取到鎖之後,再次獲取該鎖而不會被該鎖阻塞。這樣的鎖稱之爲可重入鎖。
可以使用 lock.getHoldCount() 方法統計鎖被重入次數。
下面代碼示例展示瞭如何獲取可重入鎖的重入次數

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockClient {
    public static ReentrantLock lock = new ReentrantLock();
    public static Integer count = 10;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Worker());
        t.start();
    }
    public static void getLock() {
        lock.lock();
        count--;
        try {
        	//統計鎖被重入次數
            System.out.println("鎖重入次數:" + lock.getHoldCount());
            if (count > 0) {
                getLock();
            }
        } finally {
            lock.unlock();
        }
    }
    static class Worker implements Runnable {
        @Override
        public void run() {
            getLock();
        }
    }
}

重入鎖日誌
上面的那段代碼啓用了一個新線程來重入鎖,下面的代碼直接在Main線程中重入鎖,更加直觀。

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockClient {
    public static void main(String[] args) {
        Worker worker = new Worker();
        worker.lockService();
    }

    static class Worker {
        public ReentrantLock lock = new ReentrantLock();
        public Integer count = 10;

        public void lockService() {
            lock.lock();
            count--;
            try {
                System.out.println("鎖重入次數:" + lock.getHoldCount());
                if (count > 0) {
                    lockService();
                }
            } finally {
                lock.unlock();
            }
            System.out.println("釋放後:" + lock.getHoldCount());
        }
    }
}

執行結果:
重入鎖2
(2)synchronized也具有可重入性
同一線程在調用類中其他synchronized方法/代碼塊或調用父類的synchronized方法/代碼塊都不會被該鎖阻塞,就是說同一線程對同一對象鎖是可重入的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章