java高併發系列 - 第14天:JUC中的LockSupport工具類,必備技能

路人甲Java發表於2019-07-20

這是java高併發系列第14篇文章。

本文主要內容:

  1. 講解3種讓執行緒等待和喚醒的方法,每種方法配合具體的示例
  2. 介紹LockSupport主要用法
  3. 對比3種方式,瞭解他們之間的區別

LockSupport位於java.util.concurrent簡稱juc)包中,算是juc中一個基礎類,juc中很多地方都會使用LockSupport,非常重要,希望大家一定要掌握。

關於執行緒等待/喚醒的方法,前面的文章中我們已經講過2種了:

  1. 方式1:使用Object中的wait()方法讓執行緒等待,使用Object中的notify()方法喚醒執行緒
  2. 方式2:使用juc包中Condition的await()方法讓執行緒等待,使用signal()方法喚醒執行緒

這2種方式,我們先來看一下示例。

使用Object類中的方法實現執行緒等待和喚醒

示例1:

package com.itsoku.chat10;

import java.util.concurrent.TimeUnit;

/**
 * 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分散式事務、非同步訊息服務、任務排程、分庫分表、大資料等),喜歡請關注!
 */
public class Demo1 {

    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 被喚醒!");
            }
        });
        t1.setName("t1");
        t1.start();
        //休眠5秒
        TimeUnit.SECONDS.sleep(5);
        synchronized (lock) {
            lock.notify();
        }
    }
}

輸出:

1563592938744,t1 start!
1563592943745,t1 被喚醒!

t1執行緒中呼叫lock.wait()方法讓t1執行緒等待,主執行緒中休眠5秒之後,呼叫lock.notify()方法喚醒了t1執行緒,輸出的結果中,兩行結果相差5秒左右,程式正常退出。

示例2

我們把上面程式碼中main方法內部改一下,刪除了synchronized關鍵字,看看有什麼效果:

package com.itsoku.chat10;

import java.util.concurrent.TimeUnit;

/**
 * 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分散式事務、非同步訊息服務、任務排程、分庫分表、大資料等),喜歡請關注!
 */
public class Demo2 {

    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 被喚醒!");
        });
        t1.setName("t1");
        t1.start();
        //休眠5秒
        TimeUnit.SECONDS.sleep(5);
        lock.notify();
    }
}

執行結果:

Exception in thread "t1" java.lang.IllegalMonitorStateException
1563593178811,t1 start!
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:502)
    at com.itsoku.chat10.Demo2.lambda$main$0(Demo2.java:16)
    at java.lang.Thread.run(Thread.java:745)
Exception in thread "main" java.lang.IllegalMonitorStateException
    at java.lang.Object.notify(Native Method)
    at com.itsoku.chat10.Demo2.main(Demo2.java:26)

上面程式碼中將synchronized去掉了,發現呼叫wait()方法和呼叫notify()方法都丟擲了IllegalMonitorStateException異常,原因:Object類中的wait、notify、notifyAll用於執行緒等待和喚醒的方法,都必須在同步程式碼中執行(必須用到關鍵字synchronized)

示例3

喚醒方法在等待方法之前執行,執行緒能夠被喚醒麼?程式碼如下:

package com.itsoku.chat10;

import java.util.concurrent.TimeUnit;

/**
 * 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分散式事務、非同步訊息服務、任務排程、分庫分表、大資料等),喜歡請關注!
 */
public class Demo3 {

    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock) {
                System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
                try {
                    //休眠3秒
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 被喚醒!");
            }
        });
        t1.setName("t1");
        t1.start();
        //休眠1秒之後喚醒lock物件上等待的執行緒
        TimeUnit.SECONDS.sleep(1);
        synchronized (lock) {
            lock.notify();
        }
        System.out.println("lock.notify()執行完畢");
    }
}

執行程式碼,輸出結果:

lock.notify()執行完畢
1563593869797,t1 start!

輸出了上面2行之後,程式一直無法結束,t1執行緒呼叫wait()方法之後無法被喚醒了,從輸出中可見,notify()方法在wait()方法之前執行了,等待的執行緒無法被喚醒了。說明:喚醒方法在等待方法之前執行,執行緒無法被喚醒。

關於Object類中的使用者執行緒等待和喚醒的方法,總結一下:

  1. wait()/notify()/notifyAll()方法都必須放在同步程式碼(必須在synchronized內部執行)中執行,需要先獲取鎖
  2. 執行緒喚醒的方法(notify、notifyAll)需要在等待的方法(wait)之後執行,等待中的執行緒才可能會被喚醒,否則無法喚醒

使用Condition實現執行緒的等待和喚醒

Condition的使用,前面的文章講過,對這塊不熟悉的可以移步JUC中Condition的使用,關於Condition我們準備了3個示例。

示例1

package com.itsoku.chat10;

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

/**
 * 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分散式事務、非同步訊息服務、任務排程、分庫分表、大資料等),喜歡請關注!
 */
public class Demo4 {

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 被喚醒!");
            } finally {
                lock.unlock();
            }
        });
        t1.setName("t1");
        t1.start();
        //休眠5秒
        TimeUnit.SECONDS.sleep(5);
        lock.lock();
        try {
            condition.signal();
        } finally {
            lock.unlock();
        }

    }
}

輸出:

1563594349632,t1 start!
1563594354634,t1 被喚醒!

t1執行緒啟動之後呼叫condition.await()方法將執行緒處於等待中,主執行緒休眠5秒之後呼叫condition.signal()方法將t1執行緒喚醒成功,輸出結果中2個時間戳相差5秒。

示例2

我們將上面程式碼中的lock.lock()、lock.unlock()去掉,看看會發生什麼。程式碼:

package com.itsoku.chat10;

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

/**
 * 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分散式事務、非同步訊息服務、任務排程、分庫分表、大資料等),喜歡請關注!
 */
public class Demo5 {

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 被喚醒!");
        });
        t1.setName("t1");
        t1.start();
        //休眠5秒
        TimeUnit.SECONDS.sleep(5);
        condition.signal();
    }
}

輸出:

Exception in thread "t1" java.lang.IllegalMonitorStateException
1563594654865,t1 start!
    at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.fullyRelease(AbstractQueuedSynchronizer.java:1723)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2036)
    at com.itsoku.chat10.Demo5.lambda$main$0(Demo5.java:19)
    at java.lang.Thread.run(Thread.java:745)
Exception in thread "main" java.lang.IllegalMonitorStateException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.signal(AbstractQueuedSynchronizer.java:1939)
    at com.itsoku.chat10.Demo5.main(Demo5.java:29)

有異常發生,condition.await();condition.signal();都觸發了IllegalMonitorStateException異常。原因:呼叫condition中執行緒等待和喚醒的方法的前提是必須要先獲取lock的鎖

示例3

喚醒程式碼在等待之前執行,執行緒能夠被喚醒麼?程式碼如下:

package com.itsoku.chat10;

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

/**
 * 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分散式事務、非同步訊息服務、任務排程、分庫分表、大資料等),喜歡請關注!
 */
public class Demo6 {

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.lock();
            try {
                System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 被喚醒!");
            } finally {
                lock.unlock();
            }
        });
        t1.setName("t1");
        t1.start();
        //休眠5秒
        TimeUnit.SECONDS.sleep(1);
        lock.lock();
        try {
            condition.signal();
        } finally {
            lock.unlock();
        }
        System.out.println(System.currentTimeMillis() + ",condition.signal();執行完畢");
    }
}

執行結果:

1563594886532,condition.signal();執行完畢
1563594890532,t1 start!

輸出上面2行之後,程式無法結束,程式碼結合輸出可以看出signal()方法在await()方法之前執行的,最終t1執行緒無法被喚醒,導致程式無法結束。

關於Condition中方法使用總結:

  1. 使用Condtion中的執行緒等待和喚醒方法之前,需要先獲取鎖。否者會報IllegalMonitorStateException異常
  2. signal()方法先於await()方法之前呼叫,執行緒無法被喚醒

Object和Condition的侷限性

關於Object和Condtion中執行緒等待和喚醒的侷限性,有以下幾點:

  1. 2中方式中的讓執行緒等待和喚醒的方法能夠執行的先決條件是:執行緒需要先獲取鎖
  2. 喚醒方法需要在等待方法之後呼叫,執行緒才能夠被喚醒

關於這2點,LockSupport都不需要,就能實現執行緒的等待和喚醒。下面我們來說一下LockSupport類。

LockSupport類介紹

LockSupport類可以阻塞當前執行緒以及喚醒指定被阻塞的執行緒。主要是通過park()unpark(thread)方法來實現阻塞和喚醒執行緒的操作的。

每個執行緒都有一個許可(permit),permit只有兩個值1和0,預設是0。

  1. 當呼叫unpark(thread)方法,就會將thread執行緒的許可permit設定成1(注意多次呼叫unpark方法,不會累加,permit值還是1)。
  2. 當呼叫park()方法,如果當前執行緒的permit是1,那麼將permit設定為0,並立即返回。如果當前執行緒的permit是0,那麼當前執行緒就會阻塞,直到別的執行緒將當前執行緒的permit設定為1時,park方法會被喚醒,然後會將permit再次設定為0,並返回。

注意:因為permit預設是0,所以一開始呼叫park()方法,執行緒必定會被阻塞。呼叫unpark(thread)方法後,會自動喚醒thread執行緒,即park方法立即返回。

LockSupport中常用的方法

阻塞執行緒

  • void park():阻塞當前執行緒,如果呼叫unpark方法或者當前執行緒被中斷,從能從park()方法中返回

  • void park(Object blocker):功能同方法1,入參增加一個Object物件,用來記錄導致執行緒阻塞的阻塞物件,方便進行問題排查

  • void parkNanos(long nanos):阻塞當前執行緒,最長不超過nanos納秒,增加了超時返回的特性

  • void parkNanos(Object blocker, long nanos):功能同方法3,入參增加一個Object物件,用來記錄導致執行緒阻塞的阻塞物件,方便進行問題排查

  • void parkUntil(long deadline):阻塞當前執行緒,直到deadline,deadline是一個絕對時間,表示某個時間的毫秒格式

  • void parkUntil(Object blocker, long deadline):功能同方法5,入參增加一個Object物件,用來記錄導致執行緒阻塞的阻塞物件,方便進行問題排查;

喚醒執行緒

  • void unpark(Thread thread):喚醒處於阻塞狀態的指定執行緒

示例1

主執行緒執行緒等待5秒之後,喚醒t1執行緒,程式碼如下:

package com.itsoku.chat10;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分散式事務、非同步訊息服務、任務排程、分庫分表、大資料等),喜歡請關注!
 */
public class Demo7 {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
            LockSupport.park();
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 被喚醒!");
        });
        t1.setName("t1");
        t1.start();
        //休眠5秒
        TimeUnit.SECONDS.sleep(5);
        LockSupport.unpark(t1);
        System.out.println(System.currentTimeMillis() + ",LockSupport.unpark();執行完畢");
    }
}

輸出:

1563597664321,t1 start!
1563597669323,LockSupport.unpark();執行完畢
1563597669323,t1 被喚醒!

t1中呼叫LockSupport.park();讓當前執行緒t1等待,主執行緒休眠了5秒之後,呼叫LockSupport.unpark(t1);將t1執行緒喚醒,輸出結果中1、3行結果相差5秒左右,說明t1執行緒等待5秒之後,被喚醒了。

LockSupport.park();無引數,內部直接會讓當前執行緒處於等待中;unpark方法傳遞了一個執行緒物件作為引數,表示將對應的執行緒喚醒。

示例2

喚醒方法放在等待方法之前執行,看一下執行緒是否能夠被喚醒呢?程式碼如下:

package com.itsoku.chat10;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

/**
 * 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分散式事務、非同步訊息服務、任務排程、分庫分表、大資料等),喜歡請關注!
 */
public class Demo8 {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
            LockSupport.park();
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 被喚醒!");
        });
        t1.setName("t1");
        t1.start();
        //休眠1秒
        TimeUnit.SECONDS.sleep(1);
        LockSupport.unpark(t1);
        System.out.println(System.currentTimeMillis() + ",LockSupport.unpark();執行完畢");
    }
}

輸出:

1563597994295,LockSupport.unpark();執行完畢
1563597998296,t1 start!
1563597998296,t1 被喚醒!

程式碼中啟動t1執行緒,t1執行緒內部休眠了5秒,然後主執行緒休眠1秒之後,呼叫了LockSupport.unpark(t1);喚醒執行緒t1,此時LockSupport.park();方法還未執行,說明喚醒方法在等待方法之前執行的;輸出結果中2、3行結果時間一樣,表示LockSupport.park();沒有阻塞了,是立即返回的。

說明:喚醒方法在等待方法之前執行,執行緒也能夠被喚醒,這點是另外2中方法無法做到的。Object和Condition中的喚醒必須在等待之後呼叫,執行緒才能被喚醒。而LockSupport中,喚醒的方法不管是在等待之前還是在等待之後呼叫,執行緒都能夠被喚醒。

示例3

park()讓執行緒等待之後,是否能夠響應執行緒中斷?程式碼如下:

package com.itsoku.chat10;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

/**
 * 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分散式事務、非同步訊息服務、任務排程、分庫分表、大資料等),喜歡請關注!
 */
public class Demo9 {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
            System.out.println(Thread.currentThread().getName() + ",park()之前中斷標誌:" + Thread.currentThread().isInterrupted());
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + ",park()之後中斷標誌:" + Thread.currentThread().isInterrupted());
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 被喚醒!");
        });
        t1.setName("t1");
        t1.start();
        //休眠5秒
        TimeUnit.SECONDS.sleep(5);
        t1.interrupt();

    }
}

輸出:

1563598536736,t1 start!
t1,park()之前中斷標誌:false
t1,park()之後中斷標誌:true
1563598541736,t1 被喚醒!

t1執行緒中呼叫了park()方法讓執行緒等待,主執行緒休眠了5秒之後,呼叫t1.interrupt();給執行緒t1傳送中斷訊號,然後執行緒t1從等待中被喚醒了,輸出結果中的1、4行結果相差5秒左右,剛好是主執行緒休眠了5秒之後將t1喚醒了。結論:park方法可以相應執行緒中斷。

LockSupport.park方法讓執行緒等待之後,喚醒方式有2種:

  1. 呼叫LockSupport.unpark方法
  2. 呼叫等待執行緒的interrupt()方法,給等待的執行緒傳送中斷訊號,可以喚醒執行緒

示例4

LockSupport有幾個阻塞放有一個blocker引數,這個引數什麼意思,上一個例項程式碼,大家一看就懂了:

package com.itsoku.chat10;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

/**
 * 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分散式事務、非同步訊息服務、任務排程、分庫分表、大資料等),喜歡請關注!
 */
public class Demo10 {

    static class BlockerDemo {
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            LockSupport.park();
        });
        t1.setName("t1");
        t1.start();

        Thread t2 = new Thread(() -> {
            LockSupport.park(new BlockerDemo());
        });
        t2.setName("t2");
        t2.start();
    }
}

執行上面程式碼,然後用jstack檢視一下執行緒的堆疊資訊:

"t2" #13 prio=5 os_prio=0 tid=0x00000000293ea800 nid=0x91e0 waiting on condition [0x0000000029c3f000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000007180bfeb0> (a com.itsoku.chat10.Demo10$BlockerDemo)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at com.itsoku.chat10.Demo10.lambda$main$1(Demo10.java:22)
        at com.itsoku.chat10.Demo10$$Lambda$2/824909230.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)

"t1" #12 prio=5 os_prio=0 tid=0x00000000293ea000 nid=0x9d4 waiting on condition [0x0000000029b3f000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304)
        at com.itsoku.chat10.Demo10.lambda$main$0(Demo10.java:16)
        at com.itsoku.chat10.Demo10$$Lambda$1/1389133897.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)

程式碼中,執行緒t1和t2的不同點是,t2中呼叫park方法傳入了一個BlockerDemo物件,從上面的執行緒堆疊資訊中,發現t2執行緒的堆疊資訊中多了一行- parking to wait for <0x00000007180bfeb0> (a com.itsoku.chat10.Demo10$BlockerDemo),剛好是傳入的BlockerDemo物件,park傳入的這個引數可以讓我們線上程堆疊資訊中方便排查問題,其他暫無他用。

LockSupport的其他等待方法,包含有超時時間了,過了超時時間,等待方法會自動返回,讓執行緒繼續執行,這些方法在此就不提供示例了,有興趣的朋友可以自己動動手,練一練。

執行緒等待和喚醒的3種方式做個對比

到目前為止,已經說了3種讓執行緒等待和喚醒的方法了

  1. 方式1:Object中的wait、notify、notifyAll方法
  2. 方式2:juc中Condition介面提供的await、signal、signalAll方法
  3. 方式3:juc中的LockSupport提供的park、unpark方法

3種方式對比:

Object Condtion LockSupport
前置條件 需要在synchronized中執行 需要先獲取Lock的鎖
無限等待 支援 支援 支援
超時等待 支援 支援 支援
等待到將來某個時間返回 不支援 支援 支援
等待狀態中釋放鎖 會釋放 會釋放 不會釋放
喚醒方法先於等待方法執行,能否喚醒執行緒 可以
是否能響應執行緒中斷
執行緒中斷是否會清除中斷標誌
是否支援等待狀態中不響應中斷 不支援 支援 不支援

java高併發系列

java高併發系列連載中,總計估計會有四五十篇文章,可以關注公眾號:javacode2018,獲取最新文章。
java高併發系列 - 第14天:JUC中的LockSupport工具類,必備技能

相關文章