Java 多執行緒併發程式設計之互斥鎖 Reentrant Lock

hackeris.me發表於2017-04-25

Java 中的鎖通常分為兩種:

  • 通過關鍵字 synchronized 獲取的鎖,我們稱為同步鎖,上一篇有介紹到:Java 多執行緒併發程式設計 Synchronized 關鍵字
  • java.util.concurrent(JUC)包裡的鎖,如通過繼承介面 Lock 而實現的 ReentrantLock(互斥鎖),繼承 ReadWriteLock 實現的 ReentrantReadWriteLock(讀寫鎖)。

本篇主要介紹 ReentrantLock(互斥鎖)。

ReentrantLock(互斥鎖)

ReentrantLock 互斥鎖,在同一時間只能被一個執行緒所佔有,在被持有後並未釋放之前,其他執行緒若想獲得該鎖只能等待或放棄。

ReentrantLock 互斥鎖是可重入鎖,即某一執行緒可多次獲得該鎖。

公平鎖 and 非公平鎖

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

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

由 ReentrantLock 的建構函式可見,在例項化 ReentrantLock 的時候我們可以選擇例項化一個公平鎖或非公平鎖,而預設會構造一個非公平鎖。

公平鎖與非公平鎖區別在於競爭鎖時的有序與否。公平鎖可確保有序性(FIFO 佇列),非公平鎖不能確保有序性(即使也有 FIFO 佇列)。

然而,公平是要付出代價的,公平鎖比非公平鎖要耗效能,所以在非必須確保公平的條件下,一般使用非公平鎖可提高吞吐率。所以 ReentrantLock 預設的建構函式也是“不公平”的。

一般使用

DEMO1:

public class Test {

    private static class Counter {

        private ReentrantLock mReentrantLock = new ReentrantLock();

        public void count() {
            mReentrantLock.lock();
            try {
                for (int i = 0; i < 6; i++) {
                    System.out.println(Thread.currentThread().getName() + ", i = " + i);
                }
            } finally {
	            // 必須在 finally 釋放鎖
                mReentrantLock.unlock();
            }
        }
    }

    private static class MyThread extends Thread {

        private Counter mCounter;

        public MyThread(Counter counter) {
            mCounter = counter;
        }

        @Override
        public void run() {
            super.run();
            mCounter.count();
        }
    }

    public static void main(String[] var0) {
        Counter counter = new Counter();
        // 注:myThread1 和 myThread2 是呼叫同一個物件 counter
        MyThread myThread1 = new MyThread(counter);
        MyThread myThread2 = new MyThread(counter);
        myThread1.start();
        myThread2.start();
    }
}

DEMO1 輸出:

Thread-0, i = 0
Thread-0, i = 1
Thread-0, i = 2
Thread-0, i = 3
Thread-0, i = 4
Thread-0, i = 5
Thread-1, i = 0
Thread-1, i = 1
Thread-1, i = 2
Thread-1, i = 3
Thread-1, i = 4
Thread-1, i = 5

DEMO1 僅使用了 ReentrantLock 的 lock 和 unlock 來提現一般鎖的特性,確保執行緒的有序執行。此種場景 synchronized 也適用。

鎖的作用域

DEMO2:

public class Test {

    private static class Counter {

        private ReentrantLock mReentrantLock = new ReentrantLock();

        public void count() {
            for (int i = 0; i < 6; i++) {
                mReentrantLock.lock();
                // 模擬耗時,突出執行緒是否阻塞
                try{
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName() + ", i = " + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
	                // 必須在 finally 釋放鎖
                    mReentrantLock.unlock();
                }
            }
        }

        public void doOtherThing(){
            for (int i = 0; i < 6; i++) {
                // 模擬耗時,突出執行緒是否阻塞
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " doOtherThing, i = " + i);
            }
        }
    }

    public static void main(String[] var0) {
        final Counter counter = new Counter();
        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.count();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.doOtherThing();
            }
        }).start();
    }
}

DEMO2 輸出:

Thread-0, i = 0
Thread-1 doOtherThing, i = 0
Thread-0, i = 1
Thread-1 doOtherThing, i = 1
Thread-0, i = 2
Thread-1 doOtherThing, i = 2
Thread-0, i = 3
Thread-1 doOtherThing, i = 3
Thread-0, i = 4
Thread-1 doOtherThing, i = 4
Thread-0, i = 5
Thread-1 doOtherThing, i = 5

DEMO3:

public class Test {

    private static class Counter {

        private ReentrantLock mReentrantLock = new ReentrantLock();

        public void count() {
            for (int i = 0; i < 6; i++) {
                mReentrantLock.lock();
                // 模擬耗時,突出執行緒是否阻塞
                try{
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName() + ", i = " + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 必須在 finally 釋放鎖
                    mReentrantLock.unlock();
                }
            }
        }

        public void doOtherThing(){
            mReentrantLock.lock();
            try{
                for (int i = 0; i < 6; i++) {
                    // 模擬耗時,突出執行緒是否阻塞
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " doOtherThing, i = " + i);
                }
            }finally {
                mReentrantLock.unlock();
            }

        }
    }

    public static void main(String[] var0) {
        final Counter counter = new Counter();
        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.count();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.doOtherThing();
            }
        }).start();
    }
}

DEMO3 輸出:

Thread-0, i = 0
Thread-0, i = 1
Thread-0, i = 2
Thread-0, i = 3
Thread-0, i = 4
Thread-0, i = 5
Thread-1 doOtherThing, i = 0
Thread-1 doOtherThing, i = 1
Thread-1 doOtherThing, i = 2
Thread-1 doOtherThing, i = 3
Thread-1 doOtherThing, i = 4
Thread-1 doOtherThing, i = 5

結合 DEMO2 和 DEMO3 輸出可見,鎖的作用域在於 mReentrantLock,因為所來自於 mReentrantLock。

可終止等待

DEMO4:

public class Test {

    static final int TIMEOUT = 300;

    private static class Counter {

        private ReentrantLock mReentrantLock = new ReentrantLock();

        public void count() {
            try{
                //lock() 不可中斷
                mReentrantLock.lock();
                // 模擬耗時,突出執行緒是否阻塞
                for (int i = 0; i < 6; i++) {
                    long startTime = System.currentTimeMillis();
                    while (true) {
                        if (System.currentTimeMillis() - startTime > 100)
                            break;
                    }
                    System.out.println(Thread.currentThread().getName() + ", i = " + i);
                }
            } finally {
                // 必須在 finally 釋放鎖
                mReentrantLock.unlock();
            }
        }

        public void doOtherThing(){
            try{
                //lockInterruptibly() 可中斷,若執行緒沒有中斷,則獲取鎖
                mReentrantLock.lockInterruptibly();
                for (int i = 0; i < 6; i++) {
                    // 模擬耗時,突出執行緒是否阻塞
                    long startTime = System.currentTimeMillis();
                    while (true) {
                        if (System.currentTimeMillis() - startTime > 100)
                            break;
                    }
                    System.out.println(Thread.currentThread().getName() + " doOtherThing, i = " + i);
                }
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " 中斷 ");
            }finally {
                // 若當前執行緒持有鎖,則釋放
                if(mReentrantLock.isHeldByCurrentThread()){
                    mReentrantLock.unlock();
                }
            }
        }
    }

    public static void main(String[] var0) {
        final Counter counter = new Counter();
        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.count();
            }
        }).start();
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                counter.doOtherThing();
            }
        });
        thread2.start();
        long start = System.currentTimeMillis();
        while (true){
            if (System.currentTimeMillis() - start > TIMEOUT) {
                // 若執行緒還在執行,嘗試中斷
                if(thread2.isAlive()){
                    System.out.println(" 不等了,嘗試中斷 ");
                    thread2.interrupt();
                }
                break;
            }
        }
    }
}

DEMO4 輸出:

Thread-0, i = 0
Thread-0, i = 1
Thread-0, i = 2
不等了,嘗試中斷
Thread-1 中斷
Thread-0, i = 3
Thread-0, i = 4
Thread-0, i = 5

執行緒 thread2 等待 300ms 後 timeout,中斷等待成功。

若把 TIMEOUT 改成 3000ms,輸出結果:(正常執行)

Thread-0, i = 0
Thread-0, i = 1
Thread-0, i = 2
Thread-0, i = 3
Thread-0, i = 4
Thread-0, i = 5
Thread-1 doOtherThing, i = 0
Thread-1 doOtherThing, i = 1
Thread-1 doOtherThing, i = 2
Thread-1 doOtherThing, i = 3
Thread-1 doOtherThing, i = 4
Thread-1 doOtherThing, i = 5

定時鎖

DEMO5:

public class Test {

    static final int TIMEOUT = 3000;

    private static class Counter {

        private ReentrantLock mReentrantLock = new ReentrantLock();

        public void count() {
            try{
                //lock() 不可中斷
                mReentrantLock.lock();
                // 模擬耗時,突出執行緒是否阻塞
                for (int i = 0; i < 6; i++) {
                    long startTime = System.currentTimeMillis();
                    while (true) {
                        if (System.currentTimeMillis() - startTime > 100)
                            break;
                    }
                    System.out.println(Thread.currentThread().getName() + ", i = " + i);
                }
            } finally {
                // 必須在 finally 釋放鎖
                mReentrantLock.unlock();
            }
        }

        public void doOtherThing(){
            try{
                //tryLock(long timeout, TimeUnit unit) 嘗試獲得鎖
                boolean isLock = mReentrantLock.tryLock(300, TimeUnit.MILLISECONDS);
                System.out.println(Thread.currentThread().getName() + " isLock:" + isLock);
                if(isLock){
                    for (int i = 0; i < 6; i++) {
                        // 模擬耗時,突出執行緒是否阻塞
                        long startTime = System.currentTimeMillis();
                        while (true) {
                            if (System.currentTimeMillis() - startTime > 100)
                                break;
                        }
                        System.out.println(Thread.currentThread().getName() + " doOtherThing, i = " + i);
                    }
                }else{
                    System.out.println(Thread.currentThread().getName() + " timeout");
                }
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " 中斷 ");
            }finally {
                // 若當前執行緒持有鎖,則釋放
                if(mReentrantLock.isHeldByCurrentThread()){
                    mReentrantLock.unlock();
                }
            }
        }
    }

    public static void main(String[] var0) {
        final Counter counter = new Counter();
        new Thread(new Runnable() {
            @Override
            public void run() {
                counter.count();
            }
        }).start();
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                counter.doOtherThing();
            }
        });
        thread2.start();
    }
}

DEMO5 輸出:

Thread-0, i = 0
Thread-0, i = 1
Thread-0, i = 2
Thread-1 isLock:false
Thread-1 timeout
Thread-0, i = 3
Thread-0, i = 4
Thread-0, i = 5

tryLock() 嘗試獲得鎖,tryLock(long timeout, TimeUnit unit) 在給定的 timeout 時間內嘗試獲得鎖,若超時,則不帶鎖往下走,所以必須加以判斷。

ReentrantLock or synchronized

ReentrantLock 、synchronized 之間如何選擇?

ReentrantLock 在效能上 比 synchronized 更勝一籌。

ReentrantLock 需格外小心,因為需要顯式釋放鎖,lock() 後記得 unlock(),而且必須在 finally 裡面,否則容易造成死鎖。

synchronized 隱式自動釋放鎖,使用方便。

ReentrantLock 擴充套件性好,可中斷鎖,定時鎖,自由控制。

synchronized 一但進入阻塞等待,則無法中斷等待。

相關文章