【轉】LockSupport深刻淺出

原文:https://www.cnblogs.com/qingquanzi/p/8228422.html

 

本篇是《本身動手寫把"鎖"》系列技術鋪墊的最後一個知識點。本篇主要講解LockSupport工具類,它用來實現線程的掛起和喚醒。

LockSupport是Java6引入的一個工具類,它簡單靈活,應用普遍。

 

1、簡單 html

俗話說,沒有比較就沒有傷害。這裏我們仍是經過對比來介紹LockSupport的簡單。java

在沒有LockSupport以前,線程的掛起和喚醒我們都是經過Object的wait和notify/notifyAll方法實現。面試

寫一段例子代碼,線程A執行一段業務邏輯後調用wait阻塞住本身。主線程調用notify方法喚醒線程A,線程A而後打印本身執行的結果。ide

 

執行這段代碼,不難發現這個錯誤:函數

Exception in thread "main" java.lang.IllegalMonitorStateException
    at java.lang.Object.notify(Native Method)

緣由很簡單,wait和notify/notifyAll方法只能在同步代碼塊裏用(這個有的面試官也會考察)。因此將代碼修改成以下就可正常運行了:工具

複製代碼
public class TestObjWait {

    public static void main(String[] args)throws Exception {
        final Object obj = new Object();
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                int sum = 0;
                for(int i=0;i<10;i++){
                    sum+=i;
                }
                try {
                    synchronized (obj){
                        obj.wait();
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
                System.out.println(sum);
            }
        });
        A.start();
        //睡眠一秒鐘,保證線程A已經計算完成,阻塞在wait方法
        Thread.sleep(1000);
        synchronized (obj){
            obj.notify();
        }
    }
}
複製代碼

 

那若是我們換成LockSupport呢?簡單得很,看代碼:學習

直接調用就能夠了,沒有說非得在同步代碼塊裏才能用。簡單吧。

2、靈活spa

若是隻是LockSupport在使用起來比Object的wait/notify簡單,那還真不必專門講解下LockSupport。最主要的是靈活性。線程

上邊的例子代碼中,主線程調用了Thread.sleep(1000)方法來等待線程A計算完成進入wait狀態。若是去掉Thread.sleep()調用,代碼以下:3d

複製代碼
public class TestObjWait {

    public static void main(String[] args)throws Exception {
        final Object obj = new Object();
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                int sum = 0;
                for(int i=0;i<10;i++){
                    sum+=i;
                }
                try {
                    synchronized (obj){
                        obj.wait();
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
                System.out.println(sum);
            }
        });
        A.start();
        //睡眠一秒鐘,保證線程A已經計算完成,阻塞在wait方法
        //Thread.sleep(1000);
        synchronized (obj){
            obj.notify();
        }
    }
}
複製代碼

 

多運行幾回上邊的代碼,有的時候可以正常打印結果並退出程序,但有的時候線程沒法打印結果阻塞住了。緣由就在於:主線程調用完notify後,線程A才進入wait方法,致使線程A一直阻塞住。因爲線程A不是後臺線程,因此整個程序沒法退出。

那若是換作LockSupport呢?LockSupport就支持主線程先調用unpark後,線程A再調用park而不被阻塞嗎?是的,沒錯。代碼以下:

複製代碼
public class TestObjWait {

    public static void main(String[] args)throws Exception {
        final Object obj = new Object();
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                int sum = 0;
                for(int i=0;i<10;i++){
                    sum+=i;
                }
                LockSupport.park();
                System.out.println(sum);
            }
        });
        A.start();
        //睡眠一秒鐘,保證線程A已經計算完成,阻塞在wait方法
        //Thread.sleep(1000);
        LockSupport.unpark(A);
    }
}
複製代碼

 

無論你執行多少次,這段代碼都能正常打印結果並退出。這就是LockSupport最大的靈活所在。

 

總結一下,LockSupport比Object的wait/notify有兩大優點

①LockSupport不須要在同步代碼塊裏 。因此線程間也不須要維護一個共享的同步對象了,實現了線程間的解耦。

②unpark函數能夠先於park調用,因此不須要擔憂線程間的執行的前後順序。

 

3、應用普遍

LockSupport在Java的工具類用應用很普遍,我們這裏找幾個例子感覺感覺。以Java裏最經常使用的類ThreadPoolExecutor爲例。先看以下代碼:

複製代碼
public class TestObjWait {

    public static void main(String[] args)throws Exception {
        ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<Runnable>(1000);
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,5,1000, TimeUnit.SECONDS,queue);

        Future<String> future = poolExecutor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                TimeUnit.SECONDS.sleep(5);
                return "hello";
            }
        });
        String result = future.get();
        System.out.println(result);
    }
}
複製代碼

 代碼中咱們向線程池中扔了一個任務,而後調用Future的get方法,同步阻塞等待線程池的執行結果。

這裏就要問了:get方法是如何組塞住當前線程?線程池執行完任務後又是如何喚醒線程的呢?

我們跟着源碼一步步分析,先看線程池的submit方法的實現:

在submit方法裏,線程池將咱們提交的基於Callable實現的任務,封裝爲基於RunnableFuture實現的任務,而後將任務提交到線程池執行,並向當前線程返回RunnableFutrue。

進入newTaskFor方法,就一句話:return new FutureTask<T>(callable);

因此,我們主線程調用future的get方法就是FutureTask的get方法,線程池執行的任務對象也是FutureTask的實例。

接下來看看FutureTask的get方法的實現:

比較簡單,就是判斷下當前任務是否執行完畢,若是執行完畢直接返回任務結果,不然進入awaitDone方法阻塞等待。

awaitDone方法裏,首先會用到上節講到的cas操做,將線程封裝爲WaitNode,保持下來,以供後續喚醒線程時用。再就是調用了LockSupport的park/parkNanos組塞住當前線程。

 

上邊已經說完了阻塞等待任務結果的邏輯,接下來再看看線程池執行完任務,喚醒等待線程的邏輯實現。

前邊說了,我們提交的基於Callable實現的任務,已經被封裝爲FutureTask任務提交給了線程池執行,任務的執行就是FutureTask的run方法執行。以下是FutureTask的run方法:

c.call()就是執行咱們提交的任務,任務執行完後調用了set方法,進入set方法發現set方法調用了finishCompletion方法,想必喚醒線程的工做就在這裏邊了,看看代碼實現吧:

沒錯就在這裏邊,先是經過cas操做將全部等待的線程拿出來,而後便使用LockSupport的unpark喚醒每一個線程。

 

在使用線程池的過程當中,不知道你有沒有這麼一個疑問:線程池裏沒有任務時,線程池裏的線程在幹嗎呢?

看過個人這篇文章《線程池的工做原理與源碼解讀》的讀者必定知道,線程會調用隊列的take方法阻塞等待新任務。那隊列的take方法是否是也跟Future的get方法實現同樣呢?我們來看看源碼實現。

以ArrayBlockingQueue爲例,take方法實現以下:

與想象的有點出入,他是使用了Lock的Condition的await方法實現線程阻塞。但當咱們繼續追下去進入await方法,發現仍是使用了LockSupport:

限於篇幅,jdk裏的更多應用就再也不追下去了。

 

4、LockSupport的實現

學習要知其然,還要知其因此然。接下來不妨看看LockSupport的實現。

進入LockSupport的park方法,能夠發現它是調用了Unsafe的park方法,這是一個本地native方法,只能經過openjdk的源碼看看其本地實現了。 

它調用了線程的Parker類型對象的park方法,以下是Parker類的定義:

類中定義了一個int類型的_counter變量,我們上文中講靈活性的那一節說,能夠先執行unpark後執行park,就是經過這個變量實現,看park方法的實現代碼(因爲方法比較長就不總體截圖了):

park方法會調用Atomic::xchg方法,這個方法會原子性的將_counter賦值爲0,並返回賦值前的值。若是調用park方法前,_counter大於0,則說明以前調用過unpark方法,因此park方法直接返回。

接着往下看:

實際上Parker類用Posix的mutex,condition來實現的阻塞喚醒。若是對mutex和condition不熟,能夠簡單理解爲mutex就是Java裏的synchronized,condition就是Object裏的wait/notify操做。

park方法裏調用pthread_mutex_trylock方法,就至關於Java線程進入Java的同步代碼塊,而後再次判斷_counter是否大於零,若是大於零則將_counter設置爲零。最後調用pthread_mutex_unlock解鎖,至關於Java執行完退出同步代碼塊。若是_counter不大於零,則繼續往下執行pthread_cond_wait方法,實現當前線程的阻塞。

 

最後再看看unpark方法的實現吧,這塊就簡單多了,直接上代碼:

圖中的1和4就至關於Java的進入synchronized和退出synchronized的加鎖解鎖操做,代碼2將_counter設置爲1,同時判斷先前_counter的值是否小於1,即這段代碼:if(s<1)。若是不小於1,則就不會有線程被park,因此方法直接執行完畢,不然就會執行代碼3,來喚醒被阻塞的線程。

 

經過閱讀LockSupport的本地實現,咱們不難發現這麼個問題:屢次調用unpark方法和調用一次unpark方法效果同樣,由於都是直接將_counter賦值爲1,而不是加1。簡單說就是:線程A連續調用兩次LockSupport.unpark(B)方法喚醒線程B,而後線程B調用兩次LockSupport.park()方法, 線程B依舊會被阻塞。由於兩次unpark調用效果跟一次調用同樣,只能讓線程B的第一次調用park方法不被阻塞,第二次調用依舊會阻塞。

相關文章
相關標籤/搜索