1. 程式人生 > >Java多執行緒程式設計--使用Lock物件實現同步以及執行緒間通訊

Java多執行緒程式設計--使用Lock物件實現同步以及執行緒間通訊

前幾篇:

在《Java多執行緒程式設計-(4)-執行緒間通訊機制的介紹與使用》已經學習了,可以使用方法wait/notify 結合同步關鍵字synchronized實現同步和執行緒間通訊,下邊介紹一種更為方便的方式實現同步和執行緒間通訊的效果,那就是Lock物件。

Lock物件簡介

這裡為什麼說Lock物件哪?Lock其實是一個介面,在JDK1.5以後開始提供,其實現類常用的有ReentrantLock,這裡所說的Lock物件即是隻Lock介面的實現類,為了方便記憶或理解,都簡稱為Lock物件。

我們知道synchronized關鍵字可以實現執行緒間的同步互斥,從JDK1.5開始新增的ReentrantLock類能夠達到同樣的效果,並且在此基礎上還擴充套件了很多實用的功能,比使用synchronized更佳的靈活。

ReentrantLock的另一個稱呼就是“重入鎖”,Reentrant的英文釋義為:重入。

何為重入鎖,前幾篇在學習synchronized的時候,也談到了重入鎖,“一個物件一把鎖,多個物件多把鎖”,可重入鎖的概念就是:自己可以獲取自己的內部鎖。

ReentrantLock實現了Lock中的介面,繼承關係和方法屬性如下:

這裡寫圖片描述

下邊,就開始一起學習一下ReentrantLock物件。

使用ReentrantLock實現執行緒同步

public class Run {

    public static void main(String[] args) {

        Lock lock = new
ReentrantLock(); //lambda寫法 new Thread(() -> runMethod(lock), "thread1").start(); new Thread(() -> runMethod(lock), "thread2").start(); new Thread(() -> runMethod(lock), "thread3").start(); new Thread(() -> runMethod(lock), "thread4").start(); //常規寫法 new
Thread(new Runnable() { @Override public void run() { runMethod(lock); } }, "thread5").start(); } private static void runMethod(Lock lock) { lock.lock(); for (int i = 1; i <= 5; i++) { System.out.println("ThreadName:" + Thread.currentThread().getName() + (" i=" + i)); } System.out.println(); lock.unlock(); } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

執行結果:

ThreadName:thread1 i=1
ThreadName:thread1 i=2
ThreadName:thread1 i=3
ThreadName:thread1 i=4
ThreadName:thread1 i=5

ThreadName:thread2 i=1
ThreadName:thread2 i=2
ThreadName:thread2 i=3
ThreadName:thread2 i=4
ThreadName:thread2 i=5

ThreadName:thread3 i=1
ThreadName:thread3 i=2
ThreadName:thread3 i=3
ThreadName:thread3 i=4
ThreadName:thread3 i=5

ThreadName:thread4 i=1
ThreadName:thread4 i=2
ThreadName:thread4 i=3
ThreadName:thread4 i=4
ThreadName:thread4 i=5

ThreadName:thread5 i=1
ThreadName:thread5 i=2
ThreadName:thread5 i=3
ThreadName:thread5 i=4
ThreadName:thread5 i=5
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

可以看出,當前執行緒列印完畢之後釋放鎖,其他執行緒才可以獲取鎖然後進行列印。執行緒列印的資料是分組列印的,這是因為當前執行緒已經持有鎖,在當前執行緒列印完之後才會釋放鎖,但執行緒之間列印的順序是隨機的。

為了進一步說明使用ReentrantLock可以實現執行緒之間同步,測試程式碼如下:

public class Run {

    public static void main(String[] args) {

        Lock lock = new ReentrantLock();

        new Thread(() -> runMethod(lock, 0), "thread1").start();
        new Thread(() -> runMethod(lock, 5000), "thread2").start();
        new Thread(() -> runMethod(lock, 1000), "thread3").start();
        new Thread(() -> runMethod(lock, 5000), "thread4").start();
        new Thread(() -> runMethod(lock, 1000), "thread5").start();
    }

    private static void runMethod(Lock lock, long sleepTime) {
        lock.lock();
        try {
            Thread.sleep(sleepTime);
            System.out.println("ThreadName:" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

執行結果:

ThreadName:thread1
ThreadName:thread2
ThreadName:thread3
ThreadName:thread4
ThreadName:thread5
  • 1
  • 2
  • 3
  • 4
  • 5

可以看出,在sleep指定的時間內,當呼叫了lock.lock()方法執行緒就持有了”物件監視器”,其他執行緒只能等待鎖被釋放後再次爭搶,效果和使用synchronized關鍵字是一樣的。

使用Lock物件實現執行緒間通訊

上述,已經大致看了一下如何使用ReentrantLock實現執行緒之間的同步,下邊再看一下ReentrantLock是如何實現執行緒間通訊的。

在前文中我們已經知道可以使用關鍵字synchronized與wait()方法和notify()方式結合實現執行緒間通訊,也就是等待/通知模式。在ReentrantLock中,是藉助Condition物件進行實現的。

Condition的建立方式如下:

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
  • 1
  • 2

Condition按字面意思理解就是條件,當然,我們也可以將其認為是條件進行使用,這樣的話我們可以通過上述的程式碼建立多個Condition條件,我們就可以根據不同的條件來控制現成的等待和通知。而我們還知道,在使用關鍵字synchronized與wait()方法和notify()方式結合實現執行緒間通訊的時候,notify/notifyAll的通知等待的執行緒時是隨機的,顯然使用Condition相對靈活很多,可以實現”選擇性通知”。

這是因為,synchronized關鍵字相當於整個Lock物件只有一個單一的Condition物件,所有的執行緒都註冊到這個物件上。執行緒開始notifAll的時候,需要通知所有等待的執行緒,讓他們開始競爭獲得鎖物件,沒有選擇權,這種方式相對於Condition條件的方式在效率上肯定Condition較高一些。

下邊,我們首先看一個例項。

使用Lock物件和Condition實現等待/通知例項

主要方法對比如下:

(1)Object的wait()方法相當於Condition類中的await()方法; (2)Object的notify()方法相當於Condition類中的signal()方法; (3)Object的notifyAll()方法相當於Condition類中的signalAll()方法;

首先,使用Lock的時候,和《Java多執行緒程式設計-(4)-執行緒間通訊機制的介紹與使用》介紹的一樣,都需要先獲取鎖。

示例程式碼如下:

public class LockConditionDemo {

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {

        //使用同一個LockConditionDemo物件,使得lock、condition一樣
        LockConditionDemo demo = new LockConditionDemo();
        new Thread(() -> demo.await(), "thread1").start();
        Thread.sleep(3000);
        new Thread(() -> demo.signal(), "thread2").start();
    }

    private void await() {
        try {
            lock.lock();
            System.out.println("開始等待await! ThreadName:" + Thread.currentThread().getName());
            condition.await();
            System.out.println("等待await結束! ThreadName:" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    private void signal() {
        lock.lock();
        System.out.println("傳送通知signal! ThreadName:" + Thread.currentThread().getName());
        condition.signal();
        lock.unlock();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

執行結果:

開始等待await! ThreadName:thread1
傳送通知signal! ThreadName:thread2
等待await結束! ThreadName:thread1
  • 1
  • 2
  • 3

可以看出結果正確執行!

使用Lock物件和多個Condition實現等待/通知例項

示例程式碼如下:

public class LockConditionDemo {

    private Lock lock = new ReentrantLock();
    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {

        LockConditionDemo demo = new LockConditionDemo();

        new Thread(() -> demo.await(demo.conditionA), "thread1_conditionA").start();
        new Thread(() -> demo.await(demo.conditionB), "thread2_conditionB").start();
        new Thread(() -> demo.signal(demo.conditionA), "thread3_conditionA").start();
        System.out.println("稍等5秒再通知其他的執行緒!");
        Thread.sleep(5000);
        new Thread(() -> demo.signal(demo.conditionB), "thread4_conditionB").start();

    }

    private void await(Condition condition) {
        try {
            lock.lock();
            System.out.println("開始等待await! ThreadName:" + Thread.currentThread().getName());
            condition.await();
            System.out.println("等待await結束! ThreadName:" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    private void signal(Condition condition) {
        lock.lock();
        System.out.println("傳送通知signal! ThreadName:" + Thread.currentThread().getName());
        condition.signal();
        lock.unlock();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

執行結果:

開始等待await! ThreadName:thread1_conditionA
開始等待await! ThreadName:thread2_conditionB
傳送通知signal! ThreadName:thread3_conditionA
等待await結束! ThreadName:thread1_conditionA
稍等5秒再通知其他的執行緒!
傳送通知signal! ThreadName:thread4_conditionB
等待await結束! ThreadName:thread2_conditionB
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

可以看出實現了分別通知。因此,我們可以使用Condition進行分組,可以單獨的通知某一個分組,另外還可以使用signalAll()方法實現通知某一個分組的所有等待的執行緒。

公平鎖和非公平鎖

概念很好理解,公平鎖表示執行緒獲取鎖的順序是按照執行緒加鎖的順序來分配,即先進先出,那麼他就是公平的;非公平是一種搶佔機制,是隨機獲得鎖,並不是先來的一定能先得到鎖,結果就是不公平的。

ReentrantLock提供了一個構造方法,可以很簡單的實現公平鎖或非公平鎖,原始碼建構函式如下:

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

引數:fair為true表示是公平鎖,反之為非公平鎖,這裡不再寫程式碼測試。

ReentrantLock的其他方法

ReentrantLock原始碼結構如下:

這裡寫圖片描述

方法很簡單,看到名稱就可以想到作用是什麼,挑一些簡單介紹一下:

(1)getHoldCount()方法:查詢當前執行緒保持此鎖定的個數,也就是呼叫lock()的次數;

(2)getQueueLength()方法:返回正等待獲取此鎖定的執行緒估計數目;

(3)isFair()方法:判斷是不是公平鎖;

使用ReentrantReadWriteLock實現併發

上述的類ReentrantLock具有完全互斥排他的效果,即同一時間只能有一個執行緒在執行ReentrantLock.lock()之後的任務。

類似於我們集合中有同步類容器併發類容器,HashTable(HashTable幾乎可以等價於HashMap,並且是執行緒安全的)也是完全排他的,即使是讀也只能同步執行,而ConcurrentHashMap就可以實現同一時刻多個執行緒之間併發。為了提高效率,ReentrantLock的升級版ReentrantReadWriteLock就可以實現效率的提升。

ReentrantReadWriteLock有兩個鎖:一個是與讀相關的鎖,稱為“共享鎖”;另一個是與寫相關的鎖,稱為“排它鎖”。也就是多個讀鎖之間不互斥,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥。

在沒有執行緒進行寫操作時,進行讀操作的多個執行緒都可以獲取到讀鎖,而寫操作的執行緒只有獲取寫鎖後才能進行寫入操作。即:多個執行緒可以同時進行讀操作,但是同一時刻只允許一個執行緒進行寫操作。

ReentrantReadWriteLock鎖的特性:

(1)讀讀共享; (2)寫寫互斥; (3)讀寫互斥; (4)寫讀互斥;

ReentrantReadWriteLock例項程式碼

(1)讀讀共享

public class ReentrantReadWriteLockDemo {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) {

        ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();

        new Thread(() -> demo.read(), "ThreadA").start();
        new Thread(() -> demo.read(), "ThreadB").start();
    }

    private void read() {
        try {
            try {
                lock.readLock().lock();
                System.out.println("獲得讀鎖" + Thread.currentThread().getName()
                        + " 時間:" + System.currentTimeMillis());
                //模擬讀操作時間為5秒
                Thread.sleep(5000);
            } finally {
                lock.readLock().unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

執行結果:

獲得讀鎖ThreadA 時間:1507720692022
獲得讀鎖ThreadB 時間:1507720692022
  • 1
  • 2

可以看出兩個執行緒之間,獲取鎖的時間幾乎同時,說明lock.readLock().lock(); 允許多個執行緒同時執行lock()方法後面的程式碼。

(2)寫寫互斥

public class ReentrantReadWriteLockDemo {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) {

        ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();

        new Thread(() -> demo.write(), "ThreadA").start();
        new Thread(() -> demo.write(), "ThreadB").start();
    }

    private void write() {
        try {
            try {
                lock.writeLock().lock();
                System.out.println("獲得寫鎖" + Thread.currentThread().getName()
                        + " 時間:" + System.currentTimeMillis());
                //模擬寫操作時間為5秒
                Thread.sleep(5000);
            } finally {
                lock.writeLock().unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

執行結果:

獲得寫鎖ThreadA 時間:1507720931662
獲得寫鎖ThreadB 時間:1507720936662
  • 1
  • 2

可以看出執行結果大致差了5秒的時間,可以說明多個寫執行緒是互斥的。

(3)讀寫互斥或寫讀互斥

public class ReentrantReadWriteLockDemo {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) throws InterruptedException {
        ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();

        new Thread(() -> demo.read(), "ThreadA").start();
        Thread.sleep(1000);
        new Thread(() -> demo.write(), "ThreadB").start();
    }

    private void read() {
        try {
            try {
                lock.readLock().lock();
                System.out.println("獲得讀鎖" + Thread.currentThread().getName()
                        + " 時間:" + System.currentTimeMillis());
                Thread.sleep(3000);
            } finally {
                lock.readLock().unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void write() {
        try {
            try {
                lock.writeLock().lock();
                System.out.println("獲得寫鎖" + Thread.currentThread().getName()
                        + " 時間:" + System.currentTimeMillis());
                Thread.sleep(3000);
            } finally {
                lock.writeLock().unlock();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

執行結果:

獲得讀鎖ThreadA 時間:1507721135908
獲得寫鎖ThreadB 時間:1507721138908
  • 1
  • 2
  • 3

可以看出執行結果大致差了3秒的時間,可以說明讀寫執行緒是互斥的。