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

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

本文主要內容:微信

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

LockSupport位於java.util.concurrent簡稱juc)包中,算是juc中一個基礎類,juc中不少地方都會使用LockSupport,很是重要,但願你們必定要掌握。併發

關於線程等待/喚醒的方法,前面的文章中咱們已經講過2種了:ide

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

這2種方式,咱們先來看一下示例。高併發

使用Object類中的方法實現線程等待和喚醒

示例1:

package com.itsoku.chat10;

import java.util.concurrent.TimeUnit;

/**
 * 微信公衆號:javacode2018,獲取年薪50萬課程
 */
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關鍵字,看看有什麼效果:code

package com.itsoku.chat10;

import java.util.concurrent.TimeUnit;

/**
 * 微信公衆號:javacode2018,獲取年薪50萬課程
 */
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)token

示例3

喚醒方法在等待方法以前執行,線程可以被喚醒麼?代碼以下:

package com.itsoku.chat10;

import java.util.concurrent.TimeUnit;

/**
 * 微信公衆號:javacode2018,獲取年薪50萬課程
 */
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;

/**
 * 微信公衆號:javacode2018,獲取年薪50萬課程
 */
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;

/**
 * 微信公衆號:javacode2018,獲取年薪50萬課程
 */
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;

/**
 * 微信公衆號:javacode2018,獲取年薪50萬課程
 */
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;

/**
 * 微信公衆號:javacode2018,獲取年薪50萬課程
 */
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線程喚醒,輸出結果中一、3行結果相差5秒左右,說明t1線程等待5秒以後,被喚醒了。

LockSupport.park();無參數,內部直接會讓當前線程處於等待中;unpark方法傳遞了一個線程對象做爲參數,表示將對應的線程喚醒。

示例2

喚醒方法放在等待方法以前執行,看一下線程是否可以被喚醒呢?代碼以下:

package com.itsoku.chat10;

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

/**
 * 微信公衆號:javacode2018,獲取年薪50萬課程
 */
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();方法還未執行,說明喚醒方法在等待方法以前執行的;輸出結果中二、3行結果時間同樣,表示LockSupport.park();沒有阻塞了,是當即返回的。

說明:喚醒方法在等待方法以前執行,線程也可以被喚醒,這點是另外2中方法沒法作到的。Object和Condition中的喚醒必須在等待以後調用,線程才能被喚醒。而LockSupport中,喚醒的方法不論是在等待以前仍是在等待以後調用,線程都可以被喚醒。

示例3

park()讓線程等待以後,是否可以響應線程中斷?代碼以下:

package com.itsoku.chat10;

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

/**
 * 微信公衆號:javacode2018,獲取年薪50萬課程
 */
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從等待中被喚醒了,輸出結果中的一、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;

/**
 * 微信公衆號:javacode2018,獲取年薪50萬課程
 */
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 &lt;0x00000007180bfeb0&gt; (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高併發系列連載中,總計估計會有四五十篇文章,能夠關注公衆號:javacode2018,獲取最新文章。

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

相關文章
相關標籤/搜索