Java并發(fā)-locks包源碼剖析1-Lock和ReentrantLock概述

前面幾篇文章分析了java.util.concurrent.atomic包下的原子類和synchronized同步鎖,這篇分析JUC的locks包下的鎖類。java.util.concurrent.locks下的類不是很多,但是比較復(fù)雜,定義了基本的鎖Lock,對(duì)線程進(jìn)行park和unpark的LockSupport和核心的AQS框架(AbstractQueuedSynchronizer)。

\color{blue}{1\ Lock}

先看下Lock的源碼:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

是只有六個(gè)方法的接口。Lock和synchronized具有同樣的含義和功能,synchronized 鎖在退出塊時(shí)自動(dòng)釋放,而Lock 需要手動(dòng)釋放,Lock更加靈活。synchronized 是系統(tǒng)關(guān)鍵字,Lock則是jdk1.5以來提供的一個(gè)接口。

synchronized缺點(diǎn)很明顯:一個(gè)正在等候獲得synchronized鎖的線程無法被中斷;也無法通過投票得到鎖,如果想要得到鎖那么就必須得等下去直到釋放鎖;synchronized還要求鎖的釋放只能在與獲得鎖所在的堆棧幀相同的堆棧幀中進(jìn)行。
而Lock(如ReentrantLock )除了與Synchronized 具有相同的語義外,還支持鎖投票、定時(shí)鎖等候和可中斷鎖等候(就是說在等待鎖的過程中,可以被中斷)的一些特性。調(diào)用lockInterruptibly后,或者獲得鎖,或者被中斷后拋出異常。優(yōu)先響應(yīng)異常。

Lock 接口有 3 個(gè)實(shí)現(xiàn)它的類:ReentrantLock、ReetrantReadWriteLock.ReadLock 和 ReetrantReadWriteLock.WriteLock,即重入鎖、讀鎖和寫鎖,下面介紹下ReentrantLock。

\color{blue}{2\ ReentrantLock}

ReentrantLock,意思是“可重入鎖”,ReentrantLock實(shí)現(xiàn)了Lock接口,并且ReentrantLock提供了更多的方法。ReentrantLock的鎖內(nèi)部實(shí)現(xiàn)通過NonfairSync和FairSync實(shí)現(xiàn),它提供了兩個(gè)構(gòu)造器:

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

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

無參數(shù)構(gòu)造器采用默認(rèn)的NonfairSync機(jī)制,第二個(gè)構(gòu)造器根據(jù)參數(shù)來決定使用公平鎖還是非公平鎖。

synchronized 采用的同步策略稱為阻塞同步,它屬于一種悲觀的并發(fā)策略,即線程獲得的是獨(dú)占鎖。獨(dú)占鎖意味著其他線程只能依靠阻塞來等待線程釋放鎖。而在 CPU 轉(zhuǎn)換線程阻塞時(shí)會(huì)引起線程上下文切換,當(dāng)有很多線程競爭鎖的時(shí)候,會(huì)引起 CPU 頻繁的上下文切換導(dǎo)致效率很低。

隨著指令集的發(fā)展,我們有了另一種選擇:基于沖突檢測的樂觀并發(fā)策略,通俗地講就是先進(jìn)性操作,如果沒有其他線程爭用共享數(shù)據(jù),那操作就成功了,如果共享數(shù)據(jù)被爭用,產(chǎn)生了沖突,那就再進(jìn)行其他的補(bǔ)償措施(最常見的補(bǔ)償措施就是不斷地自旋重拾,直到試成功為止),這種樂觀的并發(fā)策略的許多實(shí)現(xiàn)都不需要把線程掛起,因此這種同步被稱為非阻塞同步。ReetrantLock 采用的便是這種并發(fā)策略加上LockSupport提供的park/unPark操作。

“欲知天道,察其數(shù)”,想知其原理,先看其表現(xiàn),那么我們先通過例子看下如何正確使用ReentrantLock,然后再探究其原理。測試代碼如下:

public class Fs {
    private int cnt = 0;

    public static void main(String args[]) {
        Fs fs = new Fs();
        int threadCnt = 10;
        Thread[] threads = new Thread[threadCnt];
        CountDownLatch cdt = new CountDownLatch(threadCnt);
        for (int i = 0; i < threadCnt; i++) {
            threads[i] = new Thread(() -> {
                Lock lock = new ReentrantLock();
                lock.lock();
                try {
                    for (int j = 0; j < 10000; j++) {
                        fs.add();
                    }
                } catch (Exception e) {
                } finally {
                    lock.unlock();
                }
                cdt.countDown();
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
//        for (Thread i : threads) {
//            try {
//                i.join();
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//        }
//        try {
//            cdt.await();
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
        System.out.println(fs.cnt + "  ");
    }

    private void add() {
        ++cnt;
    }
}

Java并發(fā)-synchronized從入門到精通這篇文章結(jié)尾介紹了JVM對(duì)鎖優(yōu)化的手段包括鎖清除,根據(jù)鎖清除和代碼逃逸原理,各位朋友猜猜看上面的代碼能輸出我們期望的值10,000嗎?

答案是不能。lock屬于一個(gè)局部變量不會(huì)從當(dāng)前線程中逃逸出去,因此也不會(huì)被其他線程所使用,因此不可能存在共享資源競爭的情景,JVM會(huì)自動(dòng)將其鎖消除。所以輸出結(jié)果總是小于10,000。正確的代碼如下:

public class Fs {
    private int cnt = 0;
    private Lock lock = new ReentrantLock();

    public static void main(String args[]) {
        Fs fs = new Fs();
        int threadCnt = 10;
        Thread[] threads = new Thread[threadCnt];
        CountDownLatch cdt = new CountDownLatch(threadCnt);
        for (int i = 0; i < threadCnt; i++) {
            threads[i] = new Thread(() -> {
                fs.lock.lock();
                try {
                    for (int j = 0; j < 10000; j++) {
                        fs.add();
                    }
                } catch (Exception e) {
                } finally {
                    fs.lock.unlock();
                }
                cdt.countDown();
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        System.out.println(fs.cnt + "  ");
    }

    private void add() {
        ++cnt;
    }
}

關(guān)于lock()方法的使用,需要注意以下幾點(diǎn):

  1. lock()unlock()必須成對(duì)出現(xiàn),如果只出現(xiàn)lock()會(huì)導(dǎo)鎖不會(huì)被釋放,如果只出現(xiàn)unlock()會(huì)拋出異常java.lang.IllegalMonitorStateException at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151) at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261) at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457) at Fs.lambda$main$0(Fs.java:25) at java.lang.Thread.run(Thread.java:745)。
  2. lock()unlock()之間的同步代碼塊一定要用try-catch處理,就算你百分之一萬的確定同步代碼塊不會(huì)發(fā)生異常,也最好加上try-catch包裹起來,如果不用try-catch,那么萬一出現(xiàn)異常的話,unlock就不會(huì)執(zhí)行而導(dǎo)致鎖無法被釋放,而且unlock要放到finally語句里。
  3. lock()不會(huì)響應(yīng)中斷,如果想要響應(yīng)中斷,需要使用lockInterruptibly()方法。

關(guān)于synchronized和Lock,需要再提一下:

  1. 等待synchronized鎖的線程無法被中斷(中斷標(biāo)記位無法被設(shè)置為true),獲得synchronized鎖的線程可以被中斷;
  2. 等待Lock鎖的線程可以被中斷(這里是指中斷標(biāo)記位被設(shè)置為true),但是lock()方法無法響應(yīng)中斷,lockInterruptibly()可以響應(yīng)中斷。

關(guān)于線程的中斷,你需要理解這三個(gè)方法才能繼續(xù)往下閱讀:

//中斷線程(實(shí)例方法)
public void Thread.interrupt();
//判斷線程是否被中斷(實(shí)例方法)
public boolean Thread.isInterrupted();
//判斷是否被中斷并清除當(dāng)前中斷狀態(tài)(靜態(tài)方法)
public static boolean Thread.interrupted();

為了證明等待Lock鎖的線程可以被中斷,我寫了個(gè)程序?qū)iT測試下:

public class Fg implements Runnable {
    @Override
    public void run() {
        lock.lock();
        f();
        //中斷判斷
        System.out.println("等待鎖線程執(zhí)行完畢" + Thread.currentThread().isInterrupted());
        lock.unlock();
    }

    Fg() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                f();
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("獲得了鎖的線程執(zhí)行完畢");
                lock.unlock();
            }
        }, "獲得了鎖的線程開始執(zhí)行").start();
    }

    private void f() {
        System.out.println(Thread.currentThread().getName() + " " + Math.random());
    }

    private int cnt = 0;
    private Lock lock = new ReentrantLock();

    public static void main(String args[]) {
        Fg fg = new Fg();
        Thread t = new Thread(fg, "等待鎖線程開始執(zhí)行");
        t.start();
        t.interrupt();
        System.out.println(t.isInterrupted());
    }
}

代碼輸出是:

true
獲得了鎖的線程開始執(zhí)行 0.40285651579626425
獲得了鎖的線程執(zhí)行完畢
等待鎖線程開始執(zhí)行 0.8003535914600582
等待鎖線程執(zhí)行完畢true

上述代碼中,線程t啟動(dòng)前,它的內(nèi)部已經(jīng)啟動(dòng)了一個(gè)內(nèi)部線程并且獲得了lock鎖,t啟動(dòng)的時(shí)候也企圖獲得lock鎖,因此會(huì)被阻塞,然后調(diào)用t.interrupt();中斷t,日志顯示中斷標(biāo)記為已經(jīng)是true說明中斷成功,內(nèi)部線程3秒后釋放鎖,線程t獲得鎖后執(zhí)行后面的代碼??梢钥吹?,等待鎖的線程t能被中斷,但是如果調(diào)用lock()方法獲取鎖失敗,也是會(huì)自旋重試的(外觀效果和阻塞是一樣的,但是CAS的自旋比阻塞高效很多),直到獲取鎖成功,如果想要響應(yīng)中斷而不被自旋等待,需要使用lockInterruptibly方法。

為了證明獲得Lock鎖的線程也能被中斷(lock()方法同樣無法響應(yīng)中斷,lockInterruptibly()可以響應(yīng)中斷,我這里的被中斷意思是說中斷標(biāo)記位被成功設(shè)置為true),我又寫了個(gè)程序?qū)iT測試下:

public class Fs {
    private int cnt = 0;
    private Lock lock = new ReentrantLock();

    public static void main(String args[]) {
        Fs fs = new Fs();
        int threadCnt = 1;
        Thread[] threads = new Thread[threadCnt];
        CountDownLatch cdt = new CountDownLatch(threadCnt);
        for (int i = 0; i < threadCnt; i++) {
            threads[i] = new Thread(() -> {
                fs.lock.lock();
                try {
                    while(true) {
                        if (Thread.currentThread().isInterrupted()) {//這個(gè)if不會(huì)重置中斷標(biāo)記位,想要重置中斷標(biāo)記位需要使用Thread.interrupted()
                            System.out.println("中斷線程!! " + Thread.currentThread().isInterrupted() + " " + threads[0].isInterrupted());
                            break;
                        } else {
                            System.out.println(Thread.currentThread().getName() + Math.random());
                        }
                    }
                } catch (Exception e) {
                    System.out.println("異常了:" + e.toString());
                } finally {
                    fs.lock.unlock();
                }
                cdt.countDown();
            });
            threads[i].start();
        }
        threads[0].interrupt();
        System.out.println("threads[0].isInterrupted():   " + threads[0].isInterrupted());
        System.out.println("fs.cnt   " + fs.cnt);
    }
}

輸出結(jié)果是(輸出順序不固定,因?yàn)槿齻€(gè)輸出的代碼是運(yùn)行在兩個(gè)線程中的)

threads[0].isInterrupted():   true
中斷線程!! true true
fs.cnt   0

輸出結(jié)果給我們兩點(diǎn)提示

  1. threads[0]獲得了鎖,而且中斷標(biāo)記位能被設(shè)置為true;
  2. 可以看到被中斷的線程threads[0]的中斷標(biāo)記位一直是true,因此我們知道interrupt不會(huì)重置中斷標(biāo)記位,如果想要重置中斷標(biāo)記位,那么需要if條件需要使用Thread.interrupted()而不是Thread.currentThread().isInterrupted(),在while(true)后面的if條件語句你可以使用Thread.interrupted()代替,看下輸出結(jié)果回是這樣的:
threads[0].isInterrupted():   true
中斷線程!! false false
fs.cnt   0

即中斷標(biāo)記位被重置(為啥第一行輸出的標(biāo)記位是true,我猜測是Thread.interrupted()重置標(biāo)記位需要一定的時(shí)間,即還沒有重置為false第一行日志就打印出來了,如果在最后延遲1秒后再打印threads[0]的中斷標(biāo)記位,就是false了,這個(gè)猜測我是親自試過的,是對(duì)的)。

tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失?。存i已被其他線程獲?。?,則返回false,也就說這個(gè)方法無論如何都會(huì)立即返回。在拿不到鎖時(shí)不會(huì)一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區(qū)別在于這個(gè)方法在拿不到鎖時(shí)會(huì)等待一定的時(shí)間,在時(shí)間期限之內(nèi)如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內(nèi)拿到了鎖,則返回true。

\color{blue}{3\ ReentrantLock}條件變量實(shí)現(xiàn)線程間協(xié)作

synchronized可以配合使用 Object 對(duì)象的 wait()和 notify()或 notifyAll()方法來實(shí)現(xiàn)線程間協(xié)作。Java 5 之后,我們可以用 Reentrantlock 鎖配合 Condition 對(duì)象上的 await()和 signal()或 signalAll()方法來實(shí)現(xiàn)線程間協(xié)作。在 ReentrantLock 對(duì)象上 newCondition()可以得到一個(gè) Condition 對(duì)象,可以通過在 Condition 上調(diào)用 await()方法來掛起一個(gè)任務(wù)(線程),通過在 Condition 上調(diào)用 signal()來通知任務(wù),從而喚醒一個(gè)任務(wù),或者調(diào)用 signalAll()來喚醒所有在這個(gè) Condition 上被其自身掛起的任務(wù)。另外,如果使用了公平鎖,signalAll()的與 Condition 關(guān)聯(lián)的所有任務(wù)將以 FIFO 隊(duì)列的形式獲取鎖,如果沒有使用公平鎖,則獲取鎖的任務(wù)是隨機(jī)的,這樣我們便可以更好地控制處在 await 狀態(tài)的任務(wù)獲取鎖的順序。與 notifyAll()相比,signalAll()是更安全的方式。另外,它可以指定喚醒與自身 Condition 對(duì)象綁定在一起的任務(wù)。

下面是生產(chǎn)者——消費(fèi)者模型的代碼:

class Info { // 定義信息類
    private String name = "name";//定義name屬性,為了與下面set的name屬性區(qū)別開
    private String content = "content";// 定義content屬性,為了與下面set的content屬性區(qū)別開
    private boolean flag = true;   // 設(shè)置標(biāo)志位,初始時(shí)先生產(chǎn)
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition(); //產(chǎn)生一個(gè)Condition對(duì)象

    public void set(String name, String content) {
        lock.lock();
        try {
            while (!flag) {
                condition.await();
            }
            this.name = name;    // 設(shè)置名稱
            this.content = content;  // 設(shè)置內(nèi)容
            Thread.sleep(300);
            System.out.println("生產(chǎn) " + this.name + " --> " + this.content);
            flag = false; // 改變標(biāo)志位,表示可以取走
            condition.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void get() {
        lock.lock();
        try {
            while (flag) {
                condition.await();
            }
            System.out.println("消費(fèi) " + this.name + " --> " + this.content);
            Thread.sleep(300);
            flag = true;  // 改變標(biāo)志位,表示可以生產(chǎn)
            condition.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

class Producer implements Runnable { // 通過Runnable實(shí)現(xiàn)多線程
    private Info info = null;      // 保存Info引用

    public Producer(Info info) {
        this.info = info;
    }

    public void run() {
        for (int i = 0; i < 10; i++) {
            this.info.set("姓名--" + i, "內(nèi)容--" + +i);    // 設(shè)置名稱
        }
    }
}

class Consumer implements Runnable {
    private Info info = null;

    public Consumer(Info info) {
        this.info = info;
    }

    public void run() {
        for (int i = 0; i < 10; i++) {
            this.info.get();
        }
    }
}

public class ThreadCaseDemo {
    public static void main(String args[]) {
        Info info = new Info(); // 實(shí)例化Info對(duì)象
        Producer pro = new Producer(info); // 生產(chǎn)者
        Consumer con = new Consumer(info); // 消費(fèi)者
        new Thread(pro).start();
        //啟動(dòng)了生產(chǎn)者線程后,再啟動(dòng)消費(fèi)者線程
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(con).start();
    }
}

輸出:

生產(chǎn) 姓名--0 --> 內(nèi)容--0
消費(fèi) 姓名--0 --> 內(nèi)容--0
生產(chǎn) 姓名--1 --> 內(nèi)容--1
消費(fèi) 姓名--1 --> 內(nèi)容--1
生產(chǎn) 姓名--2 --> 內(nèi)容--2
消費(fèi) 姓名--2 --> 內(nèi)容--2
生產(chǎn) 姓名--3 --> 內(nèi)容--3
消費(fèi) 姓名--3 --> 內(nèi)容--3
生產(chǎn) 姓名--4 --> 內(nèi)容--4
消費(fèi) 姓名--4 --> 內(nèi)容--4
生產(chǎn) 姓名--5 --> 內(nèi)容--5
消費(fèi) 姓名--5 --> 內(nèi)容--5
生產(chǎn) 姓名--6 --> 內(nèi)容--6
消費(fèi) 姓名--6 --> 內(nèi)容--6
生產(chǎn) 姓名--7 --> 內(nèi)容--7
消費(fèi) 姓名--7 --> 內(nèi)容--7
生產(chǎn) 姓名--8 --> 內(nèi)容--8
消費(fèi) 姓名--8 --> 內(nèi)容--8
生產(chǎn) 姓名--9 --> 內(nèi)容--9
消費(fèi) 姓名--9 --> 內(nèi)容--9

上面代碼通過條件變量conditionawait()signal()達(dá)到生產(chǎn)者線程和消費(fèi)者線程間的同步與協(xié)作。

\color{blue}{4\ ReentrantLock}與 synchronized 性能比較

在 JDK1.5 中,synchronized 是性能低效的。因?yàn)檫@是一個(gè)重量級(jí)操作,它對(duì)性能最大的影響是阻塞的是實(shí)現(xiàn),掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成,這些操作給系統(tǒng)的并發(fā)性帶來了很大的壓力。相比之下使用Java 提供的 Lock 對(duì)象,性能更高一些。Brian Goetz 對(duì)這兩種鎖在 JDK1.5、單核處理器及雙 Xeon 處理器環(huán)境下做了一組吞吐量對(duì)比的實(shí)驗(yàn),發(fā)現(xiàn)多線程環(huán)境下,synchronized的吞吐量下降的非常嚴(yán)重,而ReentrankLock 則能基本保持在同一個(gè)比較穩(wěn)定的水平上。但與其說 ReetrantLock 性能好,倒不如說 synchronized 還有非常大的優(yōu)化余地,于是到了 JDK1.6,發(fā)生了變化,對(duì) synchronize 加入了很多優(yōu)化措施,有自適應(yīng)自旋,鎖消除,鎖粗化,輕量級(jí)鎖,偏向鎖等等。導(dǎo)致在 JDK1.6 上 synchronize 的性能并不比 Lock 差。官方也表示,他們也更支持 synchronize,在未來的版本中還有優(yōu)化余地,所以還是提倡在 synchronized 能實(shí)現(xiàn)需求的情況下,優(yōu)先考慮使用 synchronized 來進(jìn)行同步。

前面講了很多,但是并沒有解釋ReentrantLock的鎖機(jī)制,關(guān)于ReentrantLock的鎖機(jī)制,我打算放到下一篇文章中分析,不然文章太長,讀起來會(huì)不會(huì)覺得有點(diǎn)累。


參考文獻(xiàn)

  1. 并發(fā)新特性—Lock 鎖與條件變量
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容