Java多線程核心技術(四)Lock的使用

本文主要介紹使用Java5中Lock對象也能實現同步的效果,並且在使用上更加方便。html

本文着重掌握以下2個知識點:編程

  1. ReentrantLock 類的使用。
  2. ReentrantReadWriteLock 類的使用。

1. 使用ReentrantLock 類

在Java多線程中,可使用 synchronized 關鍵字來實現線程之間同步互斥,但在JDK1.5中新增長了 ReentrantLock 類也能達到一樣的效果,而且在擴展功能上也更增強大,好比具備嗅探鎖定、多路分支通知等功能,並且在使用上也比 synchronized 更加的靈活。安全

1.1 使用ReentrantLock實現同步

調用ReentrantLock對象的lock()方法獲取鎖,調用unlock()方法釋放鎖。多線程

下面是初步的程序示例:併發

public class Demo {
    private Lock lock = new ReentrantLock();

    public void test(){
        lock.lock();
        for (int i= 0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+" - "+i);
        }
        lock.unlock();
    }

    public static void main(String[] args) {
        Demo demo =  new Demo();
        for (int i = 0;i<5;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.test();
                }
            }).start();
        }
    }
}

運行結果:ide

Thread-0 - 0
Thread-0 - 1
Thread-0 - 2
Thread-0 - 3
Thread-0 - 4
Thread-1 - 0
Thread-1 - 1
Thread-1 - 2
Thread-1 - 3
Thread-1 - 4
Thread-2 - 0
Thread-2 - 1
Thread-2 - 2
Thread-2 - 3
Thread-2 - 4
Thread-3 - 0
Thread-3 - 1
Thread-3 - 2
Thread-3 - 3
Thread-3 - 4
Thread-4 - 0
Thread-4 - 1
Thread-4 - 2
Thread-4 - 3
Thread-4 - 4

從運行的結果來看,當前線程打印完畢後將鎖進行釋放,其餘線程才能夠繼續打印。學習

1.1.2 鎖住類的全部實例對象

上面的示例是全部線程調用一個ReentrantLock實例對象實現同步,若是每一個線程都調用各自ReentrantLock實例對象的同一段代碼呢?線程

示例代碼:code

public class MyService implements Runnable{
    private ReentrantLock lock = new ReentrantLock();

    public void method(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"鎖定...");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+"解鎖。");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public static void main(String[] args) {
        new Thread(new MyService()).start();
        new Thread(new MyService()).start();
        new Thread(new MyService()).start();
    }

    @Override
    public void run() {
        method();
    }
}

運行結果:htm

Thread-0鎖定...
Thread-2鎖定...
Thread-1鎖定...
Thread-2解鎖。
Thread-0解鎖。
Thread-1解鎖。

從運行結果來看,並無實現想要的方法同步的效果。若是咱們想要實現相似synchronized(class),也就是給Class類上鎖,能夠把 ReentrantLock 聲明爲 static 靜態變量。

示例代碼:

public class MyService implements Runnable{
    private static ReentrantLock lock = new ReentrantLock();

    public void method(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"鎖定...");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+"解鎖。");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public static void main(String[] args) {
        new Thread(new MyService()).start();
        new Thread(new MyService()).start();
        new Thread(new MyService()).start();
    }

    @Override
    public void run() {
        method();
    }
}

運行結果:

Thread-0鎖定...
Thread-0解鎖。
Thread-1鎖定...
Thread-1解鎖。
Thread-2鎖定...
Thread-2解鎖。

從運行結果來看,成功實現了預期的結果。

1.2 使用Condition 實現等待 / 通知

關鍵字 synchronized 與 wait() 和 notify() / notifyAll() 方法相結合能夠實現等待 / 通知模式,類 ReentrantLock 也能夠實現一樣的功能,但須要藉助於 Condition(即對象監視器)實例,線程對象能夠註冊在指定的 Condition 中,從而能夠有選擇性地進行線程通知,在調度線程上更加靈活。

在使用 notify() / notifyAll() 方法進行通知時,被通知的線程倒是由JVM隨機選擇的。但使用 ReentrantLock 結合 Condition 類是能夠實現前面介紹過的「選擇性通知」,這個功能是很是重要的,並且在 Condition 類中是默認提供的。

示例代碼:

public class Demo {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void await() {
        try {
            lock.lock();
            System.out.println("開始等待:" + System.currentTimeMillis());
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void signal() {
        try {
            lock.lock();
            System.out.println("結束等待:" + System.currentTimeMillis());
            condition.signal();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Demo demo = new Demo();
        new Thread(new Runnable() {
            @Override
            public void run() {
                demo.await();
            }
        }).start();
        Thread.sleep(3000);
        demo.signal();
    }
}

運行結果:

開始等待:1537352883839
結束等待:1537352886839

成功實現等待 / 通知模式。

在Object中,有wait() 、wait(long)、notify()、notifyAll()方法。

在Condition類中,有 await()、await(long)、signal()、signalAll()方法。

1.3使用多個Condition實現通知部分線程

示例代碼:

public class Demo {
    private Lock lock = new ReentrantLock();
    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();

    public void awaitA() {
        try {
            lock.lock();
            System.out.println("A開始等待:" + System.currentTimeMillis());
            conditionA.await();
            System.out.println("A結束等待:" + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void awaitB() {
        try {
            lock.lock();
            System.out.println("B開始等待:" + System.currentTimeMillis());
            conditionB.await();
            System.out.println("B結束等待:" + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void signalAll_B() {
        try {
            lock.lock();
            conditionB.signalAll();
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Demo demo = new Demo();
        new Thread(new Runnable() {
            @Override
            public void run() {
                demo.awaitA();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                demo.awaitB();
            }
        }).start();
        Thread.sleep(3000);
        demo.signalAll_B();
    }
}

運行結果:

A開始等待:1537354021740
B開始等待:1537354021741
B結束等待:1537354024738

能夠看到,只有B線程被喚醒了。

經過此實驗可知,使用 ReentrantLock 對象能夠喚醒指定種類的線程,這是控制部分線程行爲的方便行爲。

1.4 公平鎖和非公平鎖

鎖Lock分爲」公平鎖「和「非公平鎖」,公平鎖表示線程獲取鎖的順序是按照線程加載的順序來分配的,即先來先得的FIFO先進先出順序。而非公平鎖就是一種獲取鎖的搶佔機制,是隨機得到鎖的,和公平鎖不同的就是先來的不必定先獲得鎖,這個方式可能形成某些線程一直拿不到鎖,結果也就是不公平的了。

設置公平鎖:

Lock lock = new ReentrantLock(true);

使用ReentrantLock類設置公平鎖只須要在構造時傳入boolean參數便可。默認false。須要明白的是,即便設置爲true也不能保證百分百公平。

總結:

公平鎖:先去判斷等待隊列是否爲空,也就是是否有線程在等待,沒有就去獲取鎖,不然把本身加入等待隊列。

非公平鎖:先去嘗試獲取鎖,若是失敗再加入到等待隊列。

1.5 方法getHoldCount()、getQueryLength()和getWaitQueryLength()

1.方法getHoldCount() 的做用是查詢當前線程保持此鎖定的個數,也就是調用 lock() 方法的次數。

示例代碼:

public class Service {
    private ReentrantLock lock = new ReentrantLock();

    public void method() {
        try {
            lock.lock();
            System.out.println("getHoldCount() " + lock.getHoldCount());
            method2();
        } finally {
            lock.unlock();
        }
    }

    public void method2() {
        try {
            lock.lock();
            System.out.println("getHoldCount() " + lock.getHoldCount());
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        Service service = new Service();
        service.method();
    }

}

運行結果:

getHoldCount() 1
getHoldCount() 2

2.方法getQueryLength() 的做用是返回正等待獲取此鎖定的線程估計數。好比有5個方法,1個線程首先執行 await()方法,那麼在調用getQueueLength()方法後返回值是4,說明有4個線程同時在等待 lock 的釋放。

示例代碼:

public class Service {
    private ReentrantLock lock = new ReentrantLock();

    public void method() {
        try {
            lock.lock();
            System.out.println("Name: " + Thread.currentThread().getName());
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Service service = new Service();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                service.method();
            }
        };
        for (int i = 0; i < 5; i++) {
            new Thread(runnable).start();
        }
        Thread.sleep(1000);
        ReentrantLock lock = service.getLock();
        System.out.println("有多少線程在等待:"+lock.getQueueLength());
    }

    private ReentrantLock getLock() {
        return lock;
    }

}

運行結果:

Name: Thread-1
有多少線程在等待:4

3.方法getWaitQueryLength(condition) 的做用是返回等待與此鎖定相關的給定條件Condition的線程估計數,好比有5個線程,每一個線程都執行了同一個condition 對象的await() 方法,則調用 getWaitQueryLength(condition) 方法時返回的int值是5。

示例代碼:

public class Service {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void method() {
        try {
            lock.lock();
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void notifyMethod() {
        try {
            lock.lock();
            System.out.println("等待condition的線程數" + lock.getWaitQueueLength(condition));
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Service service = new Service();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                service.method();
            }
        };
        for (int i = 0; i < 5; i++) {
            new Thread(runnable).start();
        }
        Thread.sleep(1000);
        service.notifyMethod();
    }
    
}

運行結果:

等待condition的線程數5

1.6 方法hasQueuedThread()、hasQueuedThreads()和hasWaiters()

1.方法 boolean hasQueuedThread(Thread thread) 的做用是查詢指定的線程是否正在等待獲取此鎖定。

2.方法 boolean hasQueuedThreads() 的做用是查詢是否有線程正在等待獲取此鎖定。

一、2示例代碼:

public class Service {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void waitMethod(){
        try {
            lock.lock();
            Thread.sleep(Integer.MAX_VALUE);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public ReentrantLock getLock(){
        return lock;
    }

    public static void main(String[] args) throws InterruptedException {
        final Service service = new Service();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                service.waitMethod();
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        Thread.sleep(1000);
        ReentrantLock lock = service.getLock();
        System.out.println(lock.hasQueuedThreads());
        System.out.println(lock.hasQueuedThread(thread1));
        System.out.println(lock.hasQueuedThread(thread2));
    }

}

運行結果:

true
false
true

3.方法 boolean hasWaiters(Condition condition) 的做用是查詢是否有線程正在等待與此鎖定有關的 condition 條件。

示例代碼:

public class Service {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void waitMethod(){
        try {
            lock.lock();
            condition.await();
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void notifyMethod(){
        try {
            lock.lock();
            System.out.println("有沒有線程正在等待 condition ?" + lock.hasWaiters(condition) + " 線程數是多少?" + lock.getWaitQueueLength(condition));
            condition.signalAll();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Service service = new Service();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                service.waitMethod();
            }
        };
        for (int i = 0; i < 10; i++) {
            new Thread(runnable).start();
        }
        Thread.sleep(2000);
        service.notifyMethod();
    }

}

運行結果:

有沒有線程正在等待 condition ?true 線程數是多少?10

1.7 方法isFair()、isHeldByCurrentThread()和isLocked()

  1. 方法boolean isFair() 的做用是判斷是否是公平鎖。
  2. 方法boolean isHeldByCurrentThread() 的做用是查詢當前線程是否保持此鎖定。
  3. 方法boolean isLocked() 的做用是查詢此鎖定是否由任意線程保持。

更改上面的部分代碼:

System.out.println(lock.isHeldByCurrentThread());
System.out.println(lock.isLocked());
lock.lock();
System.out.println(lock.isLocked());
System.out.println(lock.isHeldByCurrentThread());

運行結果:

false
false
true
true

1.8 方法lockInterruptibly()、tryLock()和tryLock(long timeout, TimeUnit unit)

下面的三個方法都是對lock.lock()方法的另外一種變形:

  1. 方法void lockInterruptibly()的做用是:若是當前線程未被中斷,則獲取鎖定,若是已經被中斷則出現異常。

    而使用 lock() 方法,即便線程被中斷(調用thread.interrupt()方法),也不會出現異常。

  2. 方法boolean tryLock() 的做用是,僅在未被另外一個線程保持的狀況下,才獲取該鎖定。

    假設有兩個線程同時調用同一個lock對象的tryLock()方法,那麼除了第一個得到鎖(返回true),其它都獲取不到鎖(返回false)。

  3. 方法 boolean tryLock(long timeout, TimeUnit unit) 的做用是,若是鎖定在給定等待時間內沒有被另外一個線程保持,且當前線程未被中斷,則獲取該鎖定。

1.9 方法 condition.awaitUninterruptibly()的使用

前面講到,執行condition.await()方法後,線程進入等待狀態,若是這時線程被中斷(調用thread.interrupt()方法)則會拋出異常。而使用 condition.awaitUninterruptibly() 方法代替 condition.await() 方法則不會拋出異常。

1.10 方法 condition.awaitUntil(Date deadline)的使用

使用方法 condition.awaitUntil(Date deadline) 能夠代替 await(long time, TimeUnit unit) 方法進行線程等待,該方法在等待時間到達前是能夠被提早喚醒的。

1.11 使用Condition實現順序執行

使用Condition對象能夠對線程執行的業務進行排序規劃。

示例代碼:

public class DThread{
    volatile private static int nextPrintWho = 1;
    private static ReentrantLock lock = new ReentrantLock();
    final private static Condition conditionA = lock.newCondition();
    final private static Condition conditionB = lock.newCondition();
    final private static Condition conditionC = lock.newCondition();
    public static void main(String[] args) {
        Thread threadA = new Thread(){
            @Override
            public void run() {
                try {
                    lock.lock();
                    while (nextPrintWho != 1){
                        conditionA.await();
                    }
                    for (int i = 0;i<3;i++){
                        System.out.println("ThreadA "+(i+1));
                    }
                    nextPrintWho = 2;
                    conditionB.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        };
        Thread threadB = new Thread(){
            @Override
            public void run() {
                try {
                    lock.lock();
                    while (nextPrintWho != 2){
                        conditionA.await();
                    }
                    for (int i = 0;i<3;i++){
                        System.out.println("ThreadB "+(i+1));
                    }
                    nextPrintWho = 3;
                    conditionB.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        };
        Thread threadC = new Thread(){
            @Override
            public void run() {
                try {
                    lock.lock();
                    while (nextPrintWho != 3){
                        conditionA.await();
                    }
                    for (int i = 0;i<3;i++){
                        System.out.println("ThreadC "+(i+1));
                    }
                    nextPrintWho = 1;
                    conditionB.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        };
        for (int i= 0;i<5;i++){
            new Thread(threadA).start();
            new Thread(threadB).start();
            new Thread(threadC).start();
        }
    }

}

打印結果:

ThreadA 1
ThreadA 2
ThreadA 3
ThreadB 1
ThreadB 2
ThreadB 3
ThreadC 1
ThreadC 2
ThreadC 3
....

2.使用ReentrantReadWriteLock類

類 ReentrantLock 具備徹底互斥排他的效果,即同一時間只有一個線程在執行ReentrantLock.lock() 方法後面的任務。這樣作雖然保證了實例變量的線程安全性,但效率倒是很是低下的。因此在JDK中提供了一種讀寫鎖 ReentrantReadWriteLock 類,使用它能夠加快運行效率,在某些不須要操做實例變量的方法中,徹底可使用讀寫 ReentrantReadWriteLock 來提高該方法的代碼運行速度。
讀寫鎖表示也有兩個鎖,一個是讀操做相關的鎖,也稱爲共享鎖;另外一個是寫操做相關的鎖,也叫排他鎖。也就是多個讀鎖之間不互斥,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥。在沒有線程 Thread進行寫入操做時,進行讀取操做的多個 Thread 均可以獲取讀鎖,而進行寫入操做的 Thread 只有在獲取寫鎖後才能進行寫入操做。即多個 Thread能夠同時進行讀取操做可是同一時刻只容許一個 Thread 進行寫入操做。

總結起來就是:讀讀共享,寫寫互斥,讀寫互斥,寫讀互斥。

聲明讀寫鎖:

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

獲取讀鎖:

lock.readLock().lock();

獲取寫鎖:

lock.writeLock().lock();

3.文末總結

學習完本文徹底可使用Lock對象將 synchronized關鍵字替換掉,並且其具備的獨特功能也是 synchronized 所不具備的。在學習併發時,Lock是synchronized關鍵字的進階,掌握Lock有助於學習併發包中源代碼的實現原理,在併發包中大量的類使用了Lock 接口做爲同步的處理方式。

參考

《Java多線程編程核心技術》高洪巖著

擴展

Java多線程編程核心技術(一)Java多線程技能

Java多線程編程核心技術(二)對象及變量的併發訪問

Java多線程編程核心技術(三)多線程通訊

Java多線程核心技術(五)單例模式與多線程

Java多線程核心技術(六)線程組與線程異常

相關文章
相關標籤/搜索