Java多執行緒學習(六)——Lock的使用

  • 2019 年 10 月 4 日
  • 筆記

鎖是用於通過多個執行緒控制對共享資源的訪問的工具。通常,鎖提供對共享資源的獨佔訪問:一次只能有一個執行緒可以獲取鎖,並且對共享資源的所有訪問都要求首先獲取鎖。 但是,一些鎖可能允許並發訪問共享資源,如ReadWriteLock的讀寫鎖。Java5之後並發包中新增了Lock介面以及相關實現類來實現鎖功能。

雖然synchronized方法和語句的範圍機制使得使用監視器鎖更容易編程,並且有助於避免涉及鎖的許多常見編程錯誤,但是有時您需要以更靈活的方式處理鎖。例如,用於遍歷並發訪問的數據結構的一些演算法需要使用「手動」或「鏈鎖定」:您獲取節點A的鎖定,然後獲取節點B,然後釋放A並獲取C,然後釋放B並獲得D等。在這種場景中synchronized關鍵字就不那麼容易實現了,使用Lock介面容易很多。

Lock介面的特性

  • 嘗試非阻塞地獲取鎖:當前執行緒嘗試獲取鎖,如果這一時刻鎖沒有被其他執行緒獲取到,則成功獲取並持有鎖;
  • 能被中斷地獲取鎖:獲取到鎖的執行緒能夠響應中斷,當獲取到鎖的執行緒被中斷時,中斷異常將會被拋出,同時鎖會被釋放;
  • 超時獲取鎖:在指定的截止時間之前獲取鎖, 超過截止時間後仍舊無法獲取則返回。

ReentrantLock

ReentrantLock實現了Lock介面,並提供和synchronized相同的互斥性和記憶體可見性,與synchronized相比,ReentrantLock也有進入/退出同步程式碼塊相同的記憶體語義,也同樣的提供了可重入加鎖語義。ReentrantLock還為處理鎖的不可用性問題提供更高的靈活性。

public class LockTest {    private Lock lock = new ReentrantLock();      public void test(){        lock.lock();        try {            for (int i = 0; i < 5; i++) {                System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));            }        }finally {            lock.unlock();        }    }}public class Main {      public static void main(String[] args) {        LockTest lockTest = new LockTest();          for (int i=0; i<5; i++){            new Thread(lockTest::test, "Thread-"+i).start();        }    }}

Condition實現等待/通知機制

synchronized關鍵字與wait()和notify/notifyAll()方法相結合可以實現等待/通知機制,ReentrantLock類當然也可以實現,但是需要藉助於Condition介面與newCondition() 方法。比如可以實現多路通知功能也就是在一個Lock對象中可以創建多個Condition實例(即對象監視器),執行緒對象可以註冊在指定的Condition中,從而可以有選擇性的進行執行緒通知,在調度執行緒上更加靈活。

在使用notify/notifyAll()方法進行通知時,被通知的執行緒是有JVM選擇的,使用ReentrantLock類結合Condition實例可以實現「選擇性通知」,這個功能非常重要,而且是Condition介面默認提供的。

而synchronized關鍵字就相當於整個Lock對象中只有一個Condition實例,所有的執行緒都註冊在它一個身上。如果執行notifyAll()方法的話就會通知所有處於等待狀態的執行緒這樣會造成很大的效率問題,而Condition實例的signalAll()方法 只會喚醒註冊在該Condition實例中的所有等待執行緒。

import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;  /** * @author xiaosen * @date 2019/6/24 8:20 * @description */public class Myservice {    private Lock lock = new ReentrantLock();    public Condition condition = lock.newCondition();      public void await() {        lock.lock();        try {            System.out.println(" await時間為" + System.currentTimeMillis());            condition.await();            System.out.println("這是condition.await()方法之後的語句,condition.signal()方法之後我才被執行");        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }      public void signal() throws InterruptedException {        lock.lock();        try {            System.out.println("signal時間為" + System.currentTimeMillis());            condition.signal();            Thread.sleep(3000);            System.out.println("這是condition.signal()方法之後的語句");        } finally {            lock.unlock();        }    }  }  public class MyserviceTest {      public static void main(String[] args) throws InterruptedException{        Myservice myservice = new Myservice();        new Thread(() -> myservice.await()).start();        Thread.sleep(3000);        myservice.signal();    }}  // 輸出結果 await時間為1561335861608signal時間為1561335864608這是condition.signal()方法之後的語句這是condition.await()方法之後的語句,condition.signal()方法之後我才被執行

在使用wait/notify實現等待通知機制的時候我們知道必須執行完notify()方法所在的synchronized程式碼塊後才釋放鎖。在這裡也差不多,必須執行完signal所在的try語句塊之後才釋放鎖,condition.await()後的語句才能被執行。

注意:必須在condition.await()方法調用之前調用lock.lock()程式碼獲得同步監視器,不然會報錯。

多個Condition實現等待/通知機制

import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;  /** * @author xiaosen * @date 2019/6/24 8:29 * @description */public class MoreConditionNotify {      private Lock lock = new ReentrantLock();    public Condition conditionA = lock.newCondition();    public Condition conditionB = lock.newCondition();      public void awaitA() {        lock.lock();        try {            System.out.println("begin awaitA時間為" + System.currentTimeMillis()                    + " ThreadName=" + Thread.currentThread().getName());            conditionA.await();            System.out.println("  end awaitA時間為" + System.currentTimeMillis()                    + " ThreadName=" + Thread.currentThread().getName());        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }      public void awaitB() {        lock.lock();        try {            System.out.println("begin awaitB時間為" + System.currentTimeMillis()                    + " ThreadName=" + Thread.currentThread().getName());            conditionB.await();            System.out.println("  end awaitB時間為" + System.currentTimeMillis()                    + " ThreadName=" + Thread.currentThread().getName());        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock();        }    }      public void signalAll_A() {        lock.lock();        try {            System.out.println("  signalAll_A時間為" + System.currentTimeMillis()                    + " ThreadName=" + Thread.currentThread().getName());            conditionA.signalAll();        } finally {            lock.unlock();        }    }      public void signalAll_B() {        lock.lock();        try {            System.out.println("  signalAll_B時間為" + System.currentTimeMillis()                    + " ThreadName=" + Thread.currentThread().getName());            conditionB.signalAll();        } finally {            lock.unlock();        }    }}    public class MoreConditionNotifyMain {      public static void main(String[] args) throws InterruptedException{        MoreConditionNotify conditionNotify = new MoreConditionNotify();        new Thread(() -> conditionNotify.awaitA(), "A").start();        new Thread(() -> conditionNotify.awaitB(), "B").start();        Thread.sleep(3000);        conditionNotify.signalAll_A();    }}  // 輸出begin awaitA時間為1561336446748 ThreadName=Abegin awaitB時間為1561336446748 ThreadName=B  signalAll_A時間為1561336449748 ThreadName=main  end awaitA時間為1561336449748 ThreadName=A

此時執行緒一直處於掛起狀態,只有A執行緒被喚醒了。

實現生產者/消費者模式:一對一交替列印

package alternately;  import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.ReentrantLock;  /** * @author xiaosen * @date 2019/6/24 8:40 * @description */public class ConditionService {    private ReentrantLock lock = new ReentrantLock();    private Condition condition = lock.newCondition();    private boolean hasValue = false;      public void set(){        try {            lock.lock();            while (hasValue == true){                condition.await();            }            System.out.println("列印☆");            hasValue = true;            condition.signal();        }catch (InterruptedException e){            e.printStackTrace();        }finally {            lock.unlock();        }    }      public void get(){        try {            lock.lock();            while (hasValue == false){                condition.await();            }            System.out.println("列印★");            hasValue = false;            condition.signal();        }catch (InterruptedException e){            e.printStackTrace();        }finally {            lock.unlock();        }    }  }  public static void main(String[] args){        ConditionService service = new ConditionService();        new Thread(() -> {            for (int i=0; i<100; i++){                service.set();            }        }).start();        new Thread(() -> {            for (int i=0; i<100; i++){                service.get();            }        }).start();      }  // 輸出列印☆列印★列印☆列印★列印☆列印★列印☆。。。

公平鎖與非公平鎖

Lock鎖分為:公平鎖 和 非公平鎖。公平鎖表示執行緒獲取鎖的順序是按照執行緒加鎖的順序來分配的,即先來先得的FIFO先進先出順序。而非公平鎖就是一種獲取鎖的搶佔機制,是隨機獲取鎖的,和公平鎖不一樣的就是先來的不一定先得到鎖,這樣可能造成某些執行緒一直拿不到鎖,結果也就是不公平的了。

ReentrantReadWriteLock

ReentrantLock(排他鎖)具有完全互斥排他的效果,即同一時刻只允許一個執行緒訪問,這樣做雖然雖然保證了實例變數的執行緒安全性,但效率非常低下。ReadWriteLock介面的實現類-ReentrantReadWriteLock讀寫鎖就是為了解決這個問題。

讀寫鎖維護了兩個鎖,一個是讀操作相關的鎖也成為共享鎖,一個是寫操作相關的鎖 也稱為排他鎖。通過分離讀鎖和寫鎖,其並發性比一般排他鎖有了很大提升。

多個讀鎖之間不互斥,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥(只要出現寫操作的過程就是互斥的。)。在沒有執行緒Thread進行寫入操作時,進行讀取操作的多個Thread都可以獲取讀鎖,而進行寫入操作的Thread只有在獲取寫鎖後才能進行寫入操作。即多個Thread可以同時進行讀取操作,但是同一時刻只允許一個Thread進行寫入操作。

Github: https://github.com/lgsxiaosen/notes-code/tree/master/lock-test