天天看點

多線程程式設計學習四(Lock 的使用)

一、前言

    本文要介紹使用Java5中 Lock 對象,同樣也能實作同步的效果,而且在使用上更加友善、靈活,主要包括 ReentrantLock 類的使用和ReentrantReadWriteLock 類的使用。

    lock 與 synchronized 關鍵字的差別?

    • synchronized 是java内置關鍵字;Lock是個java類。
    • synchronized 會自動釋放鎖;Lock需在finally中手工釋放鎖(unlock()方法釋放鎖)
    • synchronized 的鎖可重入、不可中斷等待、非公平;而Lock鎖可重入、可中斷等待(利用逾時機制)、可公平。
    • synchronized 的 wait() 和 notify() 或 notifyAll() 方法可以實作一個隐含的條件。而 Lock鎖可以同時綁定多個 Condition 對象。

    等待可中斷是指當持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,改為處理其他事情,可中斷特性對處理執行時間非常長的同步塊很有幫助。

    在Java 5之前,當一個線程擷取不到鎖而被阻塞在 synchronized 之外時,對該線程進行中斷操作,此時該線程的中斷标志位會被修改,但線程依舊會阻塞在synchronized上,等待着擷取鎖;在Java 5中,AQS 同步器提供了 acquireInterruptibly(int arg) 方法,這個方法在等待擷取同步狀态時,如果目前線程被中斷,會立刻傳回,并抛出 InterruptedException。

二、使用ReentrantLock 類

1、在java多線程中,可以使用 synchronized 關鍵字來實作線程之間同步互斥,但在JDK1.5中新增加的 ReentrantLock(重入鎖) 也能達到同樣的效果,并且在擴充功能上也更加強大,比如具有嗅探鎖定、多路分支通知等功能,而且在使用上也比 synchronized 更加的靈活。

2、調用lock.lock()代碼的線程就持有了“對象螢幕”,即lock 持有的是對象鎖,依賴于該類的執行個體存在。

多線程程式設計學習四(Lock 的使用)
多線程程式設計學習四(Lock 的使用)
public class MyService {
    private Lock lock=new ReentrantLock();
    public void testMethod(){
        lock.lock();
        for(int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+(i+1));
        }
        lock.unlock();
    }
}      

View Code

3、關鍵字synchronized 與wait() 和 notify()/notifyAll() 方法相結合可以實作等待/通知模式,類ReentrantLock 也可以實作同樣的功能,但需要借助于Condition對象。

Object類中的 wait() 方法相當于 Condition 類中的 await() 方法

Object類中的 wait(long timeout) 方法相當于Condition類中的 await(long time,TimeUnit unit) 方法

Object類中的 notify() 方法相當于 Condition 類中的 signal() 方法

Object類中的 notifyAll() 方法相當于 Condition 類中的 signalAll() 方法

多線程程式設計學習四(Lock 的使用)
多線程程式設計學習四(Lock 的使用)
public class Myservice {
    private Lock lock=new ReentrantLock();
    private Condition condition=lock.newCondition();

    //等待
    public void waitMethod(){
        try {
            lock.lock();
            System.out.println("A");
            condition.await();//調用的Condition的await等待方法也需要在同步方法中,否則會報錯
            System.out.println("B");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    //喚醒
    public void signal(){
        try {
            lock.lock();
            System.out.println("現在開始喚醒...");
            condition.signal();
        }finally {
            lock.unlock();
        }
    }
}      

4、使用多個 Condition 對象 實作線程之間的選擇性通知。

多線程程式設計學習四(Lock 的使用)
多線程程式設計學習四(Lock 的使用)
public class MyService {
    private Lock lock=new ReentrantLock();
    //通過定義多個Condition實作選擇性通知,可以喚醒指定種類的線程,這是
    //控制部分線程行為的友善形式
    private Condition conditionA=lock.newCondition();
    private Condition conditionB=lock.newCondition();

    public void awaitA(){
        try {
            lock.lock();
            System.out.println("awaitA begin");
            conditionA.await();
            System.out.println("awaitA end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void awaitB(){
        try {
            lock.lock();
            System.out.println("awaitB begin");
            conditionB.await();
            System.out.println("awaitB end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public void signalA(){
        try {
            lock.lock();
            System.out.println("現在開始喚醒awaitA");
            conditionA.signalAll();
        }finally {
            lock.unlock();
        }
    }

    public void signalB(){
        try {
            lock.lock();
            System.out.println("現在開始喚醒awaitB");
            conditionB.signalAll();
        }finally {
            lock.unlock();
        }
    }
}      
多線程程式設計學習四(Lock 的使用)
多線程程式設計學習四(Lock 的使用)
public class Run
{
    public static void main(String[] args) throws InterruptedException
    {
        MyService myService=new MyService();
        Thread threadA=new Thread(){
            @Override
            public void run()
            {
                super.run();
                myService.awaitA();
            }
        };

        Thread threadB=new Thread(){
            @Override
            public void run()
            {
                super.run();
                myService.awaitB();
            }
        };

        threadA.start();
        threadB.start();
        Thread.sleep(1000);
        myService.signalA();
        Thread.sleep(1000);
        myService.signalB();

    }
}      

5、公平鎖和非公平鎖

公平鎖:表示線程獲得鎖的順序是按照線程加鎖的順序來配置設定的,即先來先得的FIFO先進先出順序。

非公平鎖:一種獲得鎖的搶占機制,是随機擷取鎖的,和公平鎖不一樣的就是先來的不一定先得到鎖,這種方式可能造成某些線程一直拿不到鎖,結果也就是不公平的了。

多線程程式設計學習四(Lock 的使用)
多線程程式設計學習四(Lock 的使用)
public class Service {
    private Lock lock;

    public Service(boolean isFair)
    {
        //通過這種方式建立公平鎖(true)和非公平鎖(false)
        lock=new ReentrantLock(isFair);
    }

    public void methodA(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"正在運作");
        }finally {
            lock.unlock();
        }
    }
}      
多線程程式設計學習四(Lock 的使用)
多線程程式設計學習四(Lock 的使用)
public class Run {
    public static void main(String[] args)
    {
        final Service service=new Service(true);
        Runnable runnable=new Runnable() {
            @Override
            public void run()
            {
             service.methodA();
            }
        };

        Thread[] threads=new Thread[10];
        for (int i=0;i<10;i++){
            threads[i]=new Thread(runnable);
            threads[i].setName("線程"+(i+1));
            threads[i].start();
        }
    }
}      
多線程程式設計學習四(Lock 的使用)

    公平鎖保證了鎖的擷取按照FIFO原則,而代價是進行大量的線程切換。非公平性鎖雖然可能造成線程“饑餓”,但極少的線程切換,保證了其更大的吞吐量。

 6、ReentrantLock 常用方法介紹

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

(2) int getQueueLength() 傳回正等待此鎖定的線程估計數,比如有5個線程,1個線程正占用了這個Lock鎖在執行,則調用此方法傳回的就是4。該值僅是估計的數字,因為在此方法周遊内部資料結構的同時,線程的數目可能動态地變化。此方法用于監視系統狀态,不用于同步控制。

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

多線程程式設計學習四(Lock 的使用)
多線程程式設計學習四(Lock 的使用)
public class Service {
    private ReentrantLock lock=new ReentrantLock();
    private Condition condition=lock.newCondition();

    public void methodA(){
        try {
            lock.lock();
            System.out.println("A getHoldCount 調用lock的次數=>"+lock.getHoldCount());
            Thread.sleep(2000);
            System.out.println("A getQueueLength 正在等待的線程數=>"+lock.getQueueLength());
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    //測試getWaitQueueLength方法
    public Integer methodC(){
        try {
            lock.lock();
            return lock.getWaitQueueLength(condition);
        }finally {
            lock.unlock();
        }

    }
}      
多線程程式設計學習四(Lock 的使用)
多線程程式設計學習四(Lock 的使用)
public class Run{

    public static void main(String[] args) throws InterruptedException {
        Service service=new Service();
        Runnable runnable=new Runnable()
        {
            @Override
            public void run()
            {
               service.methodA();
            }
        };

        Thread[] threads=new Thread[5];
        for (int i=0;i<5;i++){
            threads[i]=new Thread(runnable);
            threads[i].start();
        }

        Thread.sleep(1000);
        System.out.println("執行了同一個Condition對象的的await()的線程有:"+service.methodC());
    }
}      

(4) boolean hasQueuedThread(Thread thread) 查詢指定的線程是否正在等待擷取此鎖定。

(5) boolean hasQueuedThreads() 查詢是否有線程正在等待擷取此鎖定。

(6) boolean hasWaiters(Condition condition) 查詢是否有線程正在等待與此鎖定有關的condition條件

(7) boolean isFair() 判斷是不是公平鎖。

(8) boolean isHeldByCurrentThread() 查詢目前線程是否保持此鎖定。

(9) boolean isLocked() 查詢此鎖定是否由任意線程保持。

(10) void lockInterruptibly() 如果目前線程未被中斷,則擷取鎖定,如果已經被中斷,則出現異常。和 lock() 方法的差別在于該方法會響應中斷,即在鎖的擷取中可以中斷目前線程。

(11) boolean tryLock() 僅在調用時鎖定未被另一個線程鎖定的情況下,才獲得此鎖定。

(12) boolean tryLock(long timeout,TimeUnit unit) 如果鎖定在給定等待時間内沒有被另一個線程保持,且目前線程未被中斷,則擷取該鎖定。

多線程程式設計學習四(Lock 的使用)
多線程程式設計學習四(Lock 的使用)
public class Service
{
    private ReentrantLock lock=new ReentrantLock();
    private Condition condition=lock.newCondition();

    //測試lockInterruptibly
    public void methodA(){
        try {
            lock.lockInterruptibly();
            System.out.println("methodA=》"+Thread.currentThread().getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            if (lock.isHeldByCurrentThread()){//如果目前線程依舊保持對此鎖的鎖定,則釋放
                lock.unlock();
            }
        }
    }
    //測試tryLock
    public void methodB(){
          if (lock.tryLock()){
              System.out.println(Thread.currentThread().getName()+"獲得鎖");
          }else{
              System.out.println(Thread.currentThread().getName()+"未獲得鎖");
          }
    }
}      
多線程程式設計學習四(Lock 的使用)
多線程程式設計學習四(Lock 的使用)
public class Run
{
    public static void main(String[] args) throws InterruptedException
    {
        Service service=new Service();
        Runnable runnable=new Runnable()
        {
            @Override
            public void run()
            {
                service.methodA();
                service.methodB();
            }
        };

        Thread threadA=new Thread(runnable);
        threadA.setName("A");
        threadA.start();

        Thread.sleep(1000);
        Thread threadB=new Thread(runnable);
        threadB.setName("B");
        threadB.start();
        threadB.interrupt();
    }
}      

(13) lock.awaitUninterruptibly():這個線程将不會被中斷,一直睡眠直到其他線程調用signal()或signalAll()方法。

(14) lock.awaitUntil(Date date):這個線程将會一直睡眠直到:

    • 它被中斷
    • 其他線程在這個condition上調用singal()或signalAll()方法
    • 指定的日期已經到了

    在擷取同步狀态時,同步器維護一個同步隊列,擷取狀态失敗的線程都會被加入到隊列中并在隊列中進行自旋;移出隊列(或停止自旋)的條件是前驅節點為頭節點且成功擷取了同步狀态。在釋放同步狀态時,同步器調用 tryRelease(int arg) 方法釋放同步狀态,然後喚醒頭節點的後繼節點。

三、使用ReentrantReadWriteLock 類 

      類 RenntrantLock 具有完全互斥排他的效果,即同一時間隻有一個線程在執行 RenntrantLock.lock() 方法後面的任務。這樣做雖然保證了執行個體變量的線程安全性,但效率卻是非常低下的,因為即使有時候鎖内沒有寫入内容,而也要等鎖釋放後,才能進行讀取。是以JDK提供了一種讀寫鎖 ReentrantReadWriteLock 類,使用它可以加快運作效率。             

      ReentrantReadWriteLock 有兩個鎖,都支援可重入,一個是讀操作相關的鎖,也稱為共享鎖;另一個是寫操作相關的鎖,也叫排他鎖。也就是多個讀鎖之間不互斥、讀鎖與寫鎖互斥、寫鎖與寫鎖互斥。在沒有線程 Thread 進行寫入操作時,進行讀取操作的多個Thread 都可以擷取讀鎖。而進行寫入操作的 Thread 隻有在擷取寫鎖後才能進行寫入操作。即多個 Thread 可以同時進行讀取操作,但是同一個時刻隻允許一個 Thread 進行寫入操作。

      讀寫鎖的自定義同步器需要在同步狀态(一個整型變量)上維護多個讀線程和一個寫線程的狀态,“按位切割使用”将這個變量切分成了兩個部分,高16位表示讀,低16位表示寫。

讀讀不互斥:

多線程程式設計學習四(Lock 的使用)
多線程程式設計學習四(Lock 的使用)
public class Read
{
    private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();

    public void read(){
        try {
            lock.readLock().lock();
            System.out.println(Thread.currentThread().getName()+"正在讀"+System.currentTimeMillis());
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
    }
}      
多線程程式設計學習四(Lock 的使用)
多線程程式設計學習四(Lock 的使用)
public class Run
{
    public static void main(String[] args)
    {
        Read read=new Read();
        Runnable runnable=new Runnable()
        {
            @Override
            public void run()
            {
                read.read();
            }
        };
        Thread[] threads=new Thread[10];
        for (int i=0;i<10;i++){
            threads[i]=new Thread(runnable);
            threads[i].start();
            //通過結果可以看到所有線程幾乎同時進入lock()方法
            //後面的代碼,讀讀不互斥,可以提高程式運作效率,允許
            //多個線程同時執行lock()方法後面的代碼
        }
    }
}      

寫寫互斥:

多線程程式設計學習四(Lock 的使用)
多線程程式設計學習四(Lock 的使用)
public class Write
{
    private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();

    public void write(){
        try {
            lock.writeLock().lock();
            System.out.println(Thread.currentThread().getName()+"正在寫"+System.currentTimeMillis());
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }
    }
}      
多線程程式設計學習四(Lock 的使用)
多線程程式設計學習四(Lock 的使用)
public class Run
{
    public static void main(String[] args)
    {
        Write write=new Write();
        Runnable runnable=new Runnable()
        {
            @Override
            public void run()
            {
                write.write();
            }
        };
        Thread[] threads=new Thread[10];
        for (int i=0;i<10;i++){
            threads[i]=new Thread(runnable);
            threads[i].start();
            //通過結果可以看到所有線程每隔兩秒運作一次,寫寫互斥,線程之間是同步運作的
        }
    }
}      

另外,寫讀、讀寫都是互斥的,就不舉例了。總之,隻要出現"寫"操作,就是互斥的!原因在于:讀寫鎖要確定寫鎖的操作對讀鎖可見,如果允許讀鎖在已被擷取的情況下對寫鎖的擷取,那麼正在運作的其他讀線程就無法感覺到目前寫線程的操作。

      鎖降級:鎖降級是指把持住(目前擁有的)寫鎖,再擷取到讀鎖,随後釋放(先前擁有的)寫鎖的過程。簡單來說就是同一個線程中,寫鎖處理資料的同時,對讀鎖擷取(防止其他寫線程争搶到鎖,進而改變資料)

public class LockDowngrade {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();

    private volatile Boolean update = false;

    public void processData() {
        readLock.lock();
        if (!update) {
            // 必須先釋放讀鎖
            readLock.unlock();
            // 鎖降級從寫鎖擷取到開始
            writeLock.lock();
            try {
                if (!update) {
                    // 準備資料的流程(略)
                    update = true;
                }
                readLock.lock();
            } finally {
                writeLock.unlock();
            }
            //鎖降級完成,寫鎖降級為讀鎖
        }
        try {
            // 使用資料的流程(略)
        } finally {
            readLock.unlock();
        }
    }
}      

四、Lock 鎖的實作原理

    一些同步元件(ReentrantLock、ReentrantReadWriteLock 和 CountDownLatch 等)的實作基本都是通過聚合了一個同步器(AbstractQueuedSynchronizer)的子類來完成線程通路控制的。可以這樣了解二者之間的關系:鎖或其它同步元件是面向使用者的,它定義了使用者與鎖互動的接口,隐藏了實作細節;同步器面向的是鎖的實作者,它簡化了鎖的實作方式,屏蔽了同步狀态管理、線程的排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實作者所需關注的領域。

    隊列同步器 AbstractQueuedSynchronizer(基于模闆方式模式),是用來建構鎖或者其他同步元件的基礎架構,它使用了一個 int成員變量(state)表示同步狀态,通過内置的 FIFO 隊列來完成資源擷取線程的排隊工作。

    同步器的主要使用方式是繼承,子類通過繼承同步器并實作它的抽象方法來管理同步狀态,在抽象方法的實作過程中免不了要對同步狀态進行更改,這時就需要使用同步器提供的 3 個方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))來進行操作,因為它們能夠保證狀态的改變是安全的。子類推薦被定義為自定義同步元件的靜态内部類,同步器自身沒有實作任何同步接口。它僅僅是定義了若幹同步狀态擷取和釋放的方法來供自定義同步元件使用,同步器既可以支援獨占式地擷取同步狀态,也可以支援共享式地擷取同步狀态。

多線程程式設計學習四(Lock 的使用)

    addWaiter() 的作用是把目前線程加入到AQS内部同步隊列的尾部;

    acquireQueued() 的作用是判斷自己是不是同步隊列中的第一個排隊的節點,則嘗試進行加鎖,如果成功,則把自己變成head node,如果失敗就阻塞目前線程。

    獨占鎖的擷取機制:

多線程程式設計學習四(Lock 的使用)

    在 Object 的螢幕模型上,一個對象擁有一個同步隊列和等待隊列,而并發包中的 Lock(更确切地說是同步器)擁有一個同步隊列和多個等待隊列。

多線程程式設計學習四(Lock 的使用)

    當調用,ConditionObject.await() 時,相當于同步隊列的首節點(擷取了鎖的節點)移動到Condition的等待隊列(看起來是移動,實際上是建構了一個新的 Node)中,然後釋放同步狀态,喚醒同步隊列中的後繼節點,然後目前線程會進入等待狀态 — LockSupport.park()。如果等待期間被中斷,會抛出 InterruptedException。

    當調用,ConditionObject.signal() 時,将會喚醒在等待隊列中等待時間最長的節點(首節點),在喚醒節點之前,會将節點移到同步隊列中(擷取鎖的過程),并使用 LockSupport.unpark 喚醒節點中的線程。

    當調用,ConditionObject.signalAll() 時,相當于對等待隊列中的每個節點均執行一次 signal()方法,效果就是将等待隊列中所有節點全部移動到同步隊列中,并喚醒每個節點的線程。