Java多執行緒之synchronized增強版——ReentrantLock

innoyiya發表於2018-02-07

接下來介紹比synchronized功能上更豐富的關鍵字:重入鎖

  • 靈活性:

    public class ReentrantLockTest implements Runnable{
        public static ReentrantLock lock = new ReentrantLock();
        public static int flag = 0;
    
        @Override
        public void run() {
            for (int i = 0; i < 10000000; i++) {
                lock.lock();
                try {
                    flag++;
                } finally {
                    lock.unlock();
                }
            }
        }
    
        public static void main(String args[]) throws InterruptedException {
            ReentrantLockTest test = new ReentrantLockTest();
            Thread first = new Thread(test);
            Thread second = new Thread(test);
            first.start();
            second.start();
            first.join();
            second.join();
            System.out.println(flag);
        }
    }
    複製程式碼

    lock.lock();這裡,通過重入鎖保護臨界區安全,以免發生執行緒安全問題。

    lock.unlock();這裡,必須手動指示釋放鎖的操作,否則其他執行緒將無法獲得。

    在這段程式碼裡,我們能見到重入鎖靈活的特點。但為什麼叫“重入”呢?

    看下段程式碼:

        @Override
        public void run() {
            for (int i = 0; i < 10000000; i++) {
                lock.lock();
                lock.lock();
                try {
                    flag++;
                } finally {
                    lock.unlock();
                    lock.unlock();
                }
            }
        }
    複製程式碼

    因為該鎖能反覆進進出出。但要注意一下:

    在上段程式碼中,鎖是可以重複獲取的。如果不允許,則該執行緒在第二次獲取鎖時會和自己產生死鎖問題。同時也要注意,執行緒獲取多少次鎖就要釋放多少此鎖。當獲取鎖的次數大於釋放鎖的次數、相當於該執行緒還持有鎖。當獲取鎖的次數少於釋放鎖的次數、則會得到一個java.lang.IllegalMonitorStateException異常。

  • 中斷響應:

    二話不說貼程式碼:

    public class ReentrantLockTest implements Runnable{
        public static ReentrantLock producer = new ReentrantLock();
        public static ReentrantLock consumer = new ReentrantLock();
        public int flag = 0;
        public ReentrantLockTest(int flag){
            this.flag = flag;
        }
        @Override
        public void run() {
            try {
                if (flag == 0) {
                    producer.lockInterruptibly();
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
    
                    }
                    consumer.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName() + "完成工作");
                } else {
                    consumer.lockInterruptibly();
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
    
                    }
                    producer.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName() + "完成工作");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (producer.isHeldByCurrentThread()) {
                    producer.unlock();
                }
                if (consumer.isHeldByCurrentThread()) {
                    consumer.unlock();
                }
                System.out.println(Thread.currentThread().getName() + ": 執行緒退出");
            }
        }
    
        public static void main(String args[]) throws InterruptedException {
            ReentrantLockTest producerThread = new ReentrantLockTest(1);
            ReentrantLockTest consumerThread = new ReentrantLockTest(0);
            Thread first = new Thread(producerThread);
            Thread second = new Thread(consumerThread);
            first.setName("producer");
            second.setName("consumer");
            first.start();
            second.start();
            Thread.sleep(1000);
            second.interrupt();
        }
    }
    複製程式碼

    這是一段容易造成死鎖的程式碼,具體原因大家應該懂。當執行到second.interrupt();時,second執行緒在等待鎖時被中斷,故second執行緒會放棄對鎖的申請、並對已持有資源進行釋放。first執行緒則能夠正常獲取所等待的鎖並繼續執行下去。

    結果如下:

            producer完成工作
            java.lang.InterruptedException
            consumer: 執行緒退出
            producer: 執行緒退出
        	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
        	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
        	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
        	at blog.ReentrantLockTest.run(ReentrantLockTest.java:22)
        	at java.lang.Thread.run(Thread.java:748)
    複製程式碼

    真正完成工作的只有producer執行緒。

  • 限時等待:

    除了用中斷避免死鎖問題外,還可以用限時等待鎖來避免。限時等待鎖有點像是系統自動完成執行緒中斷的感覺。先展示下限時等待鎖的使用:

    public class showWait implements Runnable {
        public static ReentrantLock lock = new ReentrantLock();
        @Override
        public void run() {
            try {
                if (lock.tryLock(5, TimeUnit.SECONDS)) {
                    Thread.sleep(6000);
                } else {
                    System.out.println(Thread.currentThread().getName() + " get lock failed");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String args[]) {
            showWait test = new showWait();
            Thread t1 = new Thread(test);
            Thread t2 = new Thread(test);
            t1.setName("producer");
            t2.setName("consumer");
            t1.start();
            t2.start();
        }
    }
    複製程式碼

    上述程式碼展示了lock.tryLock(5,TimeUnit.SECONDS);的使用,在這裡,該方法接收兩個引數,分別是時長和計時單位。

    該方法也可以不帶引數,當不帶引數時,當前執行緒會嘗試獲取鎖,如果鎖未被其他執行緒佔有則會申請成功並立即返回true。如果鎖被其他執行緒佔用則立即返回false。這種方法不會引起執行緒等待,所以不會產生死鎖問題。

  • 公平鎖:

    在多大數情況下,鎖的申請都是非公平性的,有時會造成執行緒飢餓問題。當我們使用synchronized時產生的鎖是非公平性的,但我們使用ReentrantLock時可以通過建構函式進行指定其公平性。 public ReentrantLock(boolean fair)

    當引數為true時為公平鎖,預設為非公平鎖。公平鎖看起來挺優美的,但其必然要維護一個等待佇列,其效能必然會降低

  • 整理:

    • lock()
    • lockInterruptibly()
    • tryLock()
    • tryLock(long time,TimeUnit unit)
    • unlock()

    大家回顧下這幾個方法吧。

相關文章