線程與鎖(三)ReentrantLock及LockSupport等JUC工具類的使用

ReentrantLock / CountDownLatch / CyclicBarrier / Semaphore / LockSupport

  • java.util.concurrent.locks.ReentrantLock    可重入鎖,類似Synchronized(優勢是可精確喚醒)
  • java.util.concurrent.locks.Condition      線程間協作條件
  • java.util.concurrent.CountDownLatch      門栓(主線程控制,等待其他線程)
  • java.util.concurrent.CyclicBarrier      循環屏障(非主線程控制,等待其他線程)
  • java.util.concurrent.Semaphore      信號量(指定空位,線程競爭)
  • java.util.concurrent.locks.LockSupport      不可重入鎖,許可鎖機制

一、ReentrantLock與Synchronized比較:

相似點:都是加鎖阻塞式的同步,ReentranLock(顯示鎖)和synchronized(隱式鎖)都是可重入鎖,可重入鎖的一個優點是可在一定程度避免死鎖 。 隱式鎖:(即synchronized關鍵字使用的鎖)默認是可重入鎖(同步塊、同步方法)

區分

  • 底層實現:Synchronized是原生語法層面的互斥,JVM實現,涉及到鎖的升級(無鎖、偏向鎖、自旋鎖等)。而ReentrantLock 是從jdk1.5以來(java.util.concurrent.locks.Lock)提供的API層面可重入( 支持一個線程對資源的重複加鎖 )的互斥鎖,需要lock()/unlock()方法結合try/finally語塊來完成,通過利用CAS自旋機制保證線程操作的原子性和volatile保證數據可見性實現鎖的功能。
  • 公平鎖:Synchronized鎖非公平鎖,多個線程等待同一個鎖時要按申請鎖的時間順序獲取鎖。而ReetrantLock默認的構造函數是創建非公平鎖,也可以通過參數創建公平鎖,但公平鎖表現的性能不是很好。
  • 等待中斷:Synchronized長期等待(不能中斷)可能會出現死鎖的情況。而ReentrantLock持有鎖長期不釋放時,正在等待的線程可以選擇放棄等待,通過lock.lockInterruptibly()來實現。
  • 綁定條件:Synchronized是維護一個線程隊列,當多線程同時被喚醒,只能隨機喚醒或全部喚醒,無法進行精確線程的喚醒。而ReentrantLock是可以維護多個線程隊列(綁定多個條件),可以對線程精確喚醒

二、CountDownLatch | CyclicBarrier | Semaphore

JUC三個工具類:CountDownLatch | CyclicBarrier | Semaphore 底層都是AQS來實現的

1、CountDownLatch門栓(主線程等待其他線程countdown進行減操作,直到0時才被喚醒)

CountDownLatch主要有兩個方法,當一個或多個線程調用await方法時,這些線程會阻塞;其它線程調用countDown方法會將計數器減1(調用countDown方法的線程不會阻塞);計數器的值變爲0時,因await方法阻塞的線程會被喚醒,繼續執行

/*
	1
	2
	3
	4
	5
	6
	main	over
* */
public static void main(String[] args) throws Exception{
        CountDownLatch countDownLatch=new CountDownLatch(6);
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName());
                countDownLatch.countDown();
            },Thread.currentThread().getName()).start();

        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+"\t"+"over");
    }

2、CyclicBarrier(類似CountDownLatch,動作實施者爲其他線程)

CyclicBarrier的字面意思是可循環(Cyclic) 使用的屏障(barrier)。 它要做的事情是,讓一組線程到達一個屏障(也可以叫做同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,所有被屏障攔截的線程纔會繼續幹活,線程進入屏障通過CyclicBarrier的await()方法。和CountDownLatch比較相似,都表示完成工作之後進行下一步動作, 對於CountDownLatch,當計數爲0的時候,下一步的動作實施者是“主線程“;對於CyclicBarrier,下一步動作實施者是“其他線程”。

/*
	1
	2
	3
	4
	5
	6
	main	over
* */
public static void main(String[] args) {
	        // public CyclicBarrier(int parties, Runnable barrierAction) {}
	        CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()->{
	            System.out.println(Thread.currentThread().getName()+"/t"+"over");
	        });
	        for (int i = 1; i <=7; i++) {
	            final int temp=i;
	            new Thread(()->{
	                System.out.println(Thread.currentThread().getName());
	                try {
	                    cyclicBarrier.await();
	                } catch (InterruptedException e) {
	                    e.printStackTrace();
	                } catch (BrokenBarrierException e) {
	                    e.printStackTrace();
	                }
	            }).start();
	        }
	
	    }

3、Semaphore(信號量)

cquire(獲取) 當一個線程調用acquire操作時,它要麼通過成功獲取信號量(信號量減1),要麼一直等下去,直到有線程釋放信號量,或超時。release(釋放)實際上會將信號量的值加1,然後喚醒等待的線程。信號量主要用於兩個目的,一個是用於多個共享資源的互斥使用,另一個用於併發線程數的控制。
 

    public static void main(String[] args) {
        Semaphore semaphore=new Semaphore(3);
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName()+"\t搶之前空車位數:"+semaphore.availablePermits());
                    System.out.println(Thread.currentThread().getName()+"\t搶佔了車位");
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"\t離開了車位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName()+"\t離開後空車位數:"+semaphore.availablePermits());
                }
            },String.valueOf(i)).start();
        }
    }

三、ReentrantLock使用

下面結合ReentrantLock寫個案例:使三個線程按順序輸出ABC循環字母(線程1輸出A,線程2輸出B,線程3輸出C)


import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 循環ABC
 */
public class ReentrantLockTest {

    private static ReentrantLock lock = new ReentrantLock();

    private static Condition cA = lock.newCondition();
    private static Condition cB = lock.newCondition();
    private static Condition cC = lock.newCondition();

    //門栓
    private static CountDownLatch latchB = new CountDownLatch(1);
    private static CountDownLatch latchC = new CountDownLatch(1);

    public static void main(String[] args) {

        Thread threadA = new Thread(() -> {
            try{
                lock.lock();
                for(int i=0;i<10;i++){
                    System.out.print("A");
                    cB.signal();
                    if(i==0) latchB.countDown();
                    cA.await();
                }
                cB.signal();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }, "Thread A");

        Thread threadB = new Thread(() -> {
            try {
                latchB.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try{
                lock.lock();
                for(int i=0;i<10;i++){
                    System.out.print("B");
                    cC.signal();
                    if(i==0) latchC.countDown();
                    cB.await();
                }
                cC.signal();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }, "Thread B");

        Thread threadC = new Thread(() -> {
            try {
                latchC.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try{
                lock.lock();
                for(int i=0;i<10;i++){
                    System.out.print("C");
                    cA.signal();
                    cC.await();
                }
                cA.signal();
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }, "Thread C");

        threadA.start();
        threadB.start();
        threadC.start();
    }

}

1、必須先喚醒其他線程signal()再將自己轉爲等待狀態await()

cB.signal();
cA.await();

2、基於ReentrantLock,需要在方法前後加入lock(),unlock()

3、每個線程結束後要再喚醒一下其他線程,保證所有線程都執行完才能結束。否則始終處於執行等待狀態。

4、三個線程並不一定會按順序依次執行 threadA.start();/threadB.start();/threadC.start(); 爲了保證從一開始就按ABC循環輸出,就需要加入ReentrantLock的CountDownLatch(門栓,設置門栓await()後只會執行門栓上面的代碼,取消門栓countdown()後纔會執行門栓下面的代碼)上述代碼滿足需求只需要第一次設置門栓即可。

四、LockSupport

Synchronized的wait與notify組合的方式看起來是個不錯的解決方式,但其面向的主體是對象object,阻塞的是當前線程,而喚醒的是隨機的某個線程或所有線程,偏重於線程之間的通信交互。 假如換個角度,面向主體是線程的話,想要輕而易舉地對指定的線程進行阻塞喚醒(主體是線程),這個時候就需要LockSupport,它提供的park與unpark方法分別用於阻塞和喚醒。

LockSupport是用來創建鎖和其他同步類的基本線程阻塞原語,是JDK中用來實現線程阻塞和喚醒的工具。使用它可以在任何場合使線程阻塞,可以指定任何線程進行喚醒,並且不用擔心阻塞和喚醒操作的順序,但要注意連續多次喚醒的效果和一次喚醒是一樣的。JDK併發包下的鎖和其他同步工具的底層實現中大量使用了LockSupport進行線程的阻塞和喚醒,掌握它的用法和原理可以讓我們更好的理解鎖和其它同步工具的底層實現。

public class LockSupportTest {

    public static void main(String[] args) {
        Thread parkThread = new Thread(new ParkThread());
        parkThread.start();
        System.out.println("開始線程喚醒");
        LockSupport.unpark(parkThread);
        System.out.println("結束線程喚醒");

    }

    static class ParkThread implements Runnable{

        @Override
        public void run() {
            System.out.println("開始線程阻塞");
            LockSupport.park();
            System.out.println("結束線程阻塞");
        }
    }
}

LockSupport類爲線程阻塞喚醒提供了基礎,同時,在競爭條件問題上具有wait和notify無可比擬的優勢。使用wait和notify組合時,某一線程在被另一線程notify之前必須要保證此線程已經執行到wait等待點,錯過notify則可能永遠都在等待,另外notify也不能保證喚醒指定的某線程。反觀LockSupport,由於park與unpark引入了許可機制,許可邏輯爲:  

  • park將許可在等於0的時候阻塞,等於1的時候返回並將許可減爲0。 

  • unpark嘗試喚醒線程,許可加1

class Parker : public os::PlatformParker {
private:
  volatile int _counter ;
  ...
public:
  void park(bool isAbsolute, jlong time);
  void unpark();
  ...
}
class PlatformParker : public CHeapObj<mtInternal> {
  protected:
    pthread_mutex_t _mutex [1] ;
    pthread_cond_t  _cond  [1] ;
    ...
}

 

下面同樣模仿上面案例,循環輸出ABC


import java.util.concurrent.locks.LockSupport;

public class LockSupportTest1 {

    private static Thread threadA;
    private static Thread threadB;
    private static Thread threadC;

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

        threadA = new Thread(() -> {
           for(int i=0;i<10;i++){
               System.out.print("A");
               LockSupport.unpark(threadB);
               LockSupport.park(threadA);
           }
        });

        threadB = new Thread(() -> {
            LockSupport.park(threadB);
            for(int i=0;i<10;i++){
                System.out.print("B");
                LockSupport.unpark(threadC);
                LockSupport.park(threadB);
            }
        });

        threadC = new Thread(() -> {
            LockSupport.park(threadC);
            for(int i=0;i<10;i++){
                System.out.print("C");
                LockSupport.unpark(threadA);
                LockSupport.park(threadC);
            }
        });

        threadA.start();
        threadB.start();
        threadC.start();
    }

}

LockSupport的park與unpark組合真正解耦了線程之間的同步,不再需要另外的對象變量存儲狀態,並且也不需要考慮同步鎖,wait與notify要保證必須有鎖才能執行,而且執行notify操作釋放鎖後還要將當前線程扔進該對象鎖的等待隊列,LockSupport則完全不用考慮對象、鎖、等待隊列等問題。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章