1. 程式人生 > >java高併發系列-第1天:必須知道的幾個概念

java高併發系列-第1天:必須知道的幾個概念

java高併發系列-第1天:必須知道的幾個概念

同步(Synchronous)和非同步(Asynchronous)

同步和非同步通常來形容一次方法呼叫,同步方法呼叫一旦開始,呼叫者必須等到方法呼叫返回後,才能繼續後續的行為。非同步方法呼叫更像一個訊息傳遞,一旦開始,方法呼叫就會立即返回,呼叫者就可以繼續後續的操作。而非同步方法通常會在另外一個執行緒中“真實”地執行。整個過程,不會阻礙呼叫者的工作。

如圖:

上圖中顯示了同步方法呼叫和非同步方法呼叫的區別。對於呼叫者來說,非同步呼叫似乎是一瞬間就完成的。如果非同步呼叫需要返回結果,那麼當這個非同步呼叫真實完成時,則會通知呼叫者。

打個比方,比如購物,如果你去商場買空調,當你到了商場看重了一款空調,你就向售貨員下單。售貨員去倉庫幫你調配物品。這天你熱的是在不行了,就催著商家趕緊給你送貨,於是你就在商店裡面候著他們,直到商家把你和空調一起送回家,一次愉快的購物就結束了。這就是同步呼叫。

不過,如果我們趕時髦,就坐在家裡開啟電腦,在電腦上訂購了一臺空調。當你完成網上支付的時候,對你來說購物過程已經結束了。雖然空調還沒有送到家,但是你的任務已經完成了。商家接到你的訂單後,就會加緊安平送貨,當然這一切已經跟你無關了。你已經支付完成,想幹什麼就能去幹什麼,出去溜幾圈都不成問題,等送貨上門的時候,接到商家的電話,回家一趟簽收就完事了。這就是非同步呼叫。

併發(Concurrency)和並行(Parallelism)

併發和並行是兩個非常容易被混淆的概念。他們都可以表示兩個或者多個任務一起執行,但是側重點有所不同。併發偏重於多個任務交替執行,而多個任務之間有可能還是序列的,而並行是真正意義上的“同時執行”,下圖很好地詮釋了這點。

大家排隊在一個咖啡機上接咖啡,交替執行,是併發;兩臺咖啡機上面接咖啡,是並行。

從嚴格意義上來說,並行的多工是真的同時執行,而對於併發來說,這個過程只是交替的,一會執行任務A,一會執行任務B,系統會不停地在兩者之間切換。但對於外部觀察者來說,即使多個任務之間是序列併發的,也會造成多工間並行執行的錯覺。

併發說的是在一個時間段內,多件事情在這個時間段內交替執行。

並行說的是多件事情在同一個時刻同事發生。

實際上,如果系統內只有一個CPU,而使用多程序或者多執行緒任務,那麼真實環境中這些任務不可能是真實並行的,畢竟一個CPU一次只能執行一條指令,在這種情況下多程序或者多執行緒就是併發的,而不是並行的(作業系統會不停地切換多工)。真實的並行也只可能出現在擁有多個CPU的系統中(比如多核CPU)。

臨界區

臨界區用來表示一種公共資源或者說共享資料,可以被多個執行緒使用,但是每一次只能有一個執行緒使用它,一旦臨界區資源被佔用,其他執行緒要想使用這個資源就必須等待。

比如,一個辦公室裡有一臺印表機,印表機一次只能執行一個任務。如果小王和小明同時需要列印檔案,很明顯,如果小王先發了列印任務,印表機就開始列印小王的檔案,小明的任務就只能等待小王列印結束後才能列印,這裡的印表機就是一個臨界區的例子。

在並行程式中,臨界區資源是保護的物件,如果意外出現印表機同時執行兩個任務的情況,那麼最有可能的結果就是打印出來的檔案是損壞的檔案,它既不是小王想要的,也不是小明想要的。

阻塞(Blocking)和非阻塞(Non-Blocking)

阻塞和非阻塞通常用來形容很多執行緒間的相互影響。比如一個執行緒佔用了臨界區資源,那麼其他所有需要這個資源的執行緒就必須在這個臨界區中等待。等待會導致執行緒掛起,這種情況就是阻塞。此時,如果佔用資源的執行緒一直不願意釋放資源,那麼其他執行緒阻塞在這個臨界區上的執行緒都不能工作。

非阻塞的意思與之相反,它強調沒有一個執行緒可以妨礙其他執行緒執行,所有的執行緒都會嘗試不斷向前執行。

死鎖(Deadlock)、飢餓(Starvation)和活鎖(Livelock)

死鎖、飢餓和活鎖都屬於多執行緒的活躍性問題。如果發現上述幾種情況,那麼相關執行緒就不再活躍,也就是說它可能很難再繼續往下執行了。

死鎖應該是最糟糕的一種情況了(當然,其他幾種情況也好不到哪裡去),如下圖顯示了一個死鎖的發生:

A、B、C、D四輛小車都在這種情況下都無法繼續行駛了。他們彼此之間相互佔用了其他車輛的車道,如果大家都不願意釋放自己的車道,那麼這個狀況將永遠持續下去,誰都不可能通過,死鎖是一個很嚴重的並且應該避免和實時小心的問題,後面的文章中會做更詳細的討論。

飢餓是指某一個或者多個執行緒因為種種原因無法獲得所要的資源,導致一直無法執行。比如它的優先順序可能太低,而高優先順序的執行緒不斷搶佔它需要的資源,導致低優先順序執行緒無法工作。在自然界中,母雞給雛鳥餵食很容易出現這種情況:由於雛鳥很多,食物有限,雛鳥之間的事務競爭可能非常厲害,經常搶不到事務的雛鳥有可能被餓死。執行緒的飢餓非常類似這種情況。此外,某一個執行緒一直佔著關鍵資源不放,導致其他需要這個資源的執行緒無法正常執行,這種情況也是飢餓的一種。於死鎖想必,飢餓還是有可能在未來一段時間內解決的(比如,高優先順序的執行緒已經完成任務,不再瘋狂執行)。

活鎖是一種非常有趣的情況。不知道大家是否遇到過這麼一種場景,當你要做電梯下樓時,電梯到了,門開了,這是你正準備出去。但很不巧的是,門外一個人當著你的去路,他想進來。於是,你很禮貌地靠左走,禮讓對方。同時,對方也非常禮貌的靠右走,希望禮讓你。結果,你們倆就又撞上了。於是乎,你們都意識到了問題,希望儘快避讓對方,你立即向右邊走,同時,他立即向左邊走。結果,又撞上了!不過介於人類的智慧,我相信這個動作重複兩三次後,你應該可以順利解決這個問題。因為這個時候,大家都會本能地對視,進行交流,保證這種情況不再發生。但如果這種情況發生在兩個執行緒之間可能就不那麼幸運了。如果執行緒智力不夠。且都秉承著“謙讓”的原則,主動將資源釋放給他人使用,那麼久會導致資源不斷地在兩個執行緒間跳動,而沒有一個執行緒可以同時拿到所有資源正常執行。這種情況就是活鎖。

死鎖的例子

package com.jvm.visualvm;

/**
 * <a href="http://www.itsoku.com/archives">Java乾貨鋪子,只生產乾貨,公眾號:javacode2018</a>
 */
public class Demo4 {

    public static void main(String[] args) {
        Obj1 obj1 = new Obj1();
        Obj2 obj2 = new Obj2();
        Thread thread1 = new Thread(new SynAddRunalbe(obj1, obj2, 1, 2, true));
        thread1.setName("thread1");
        thread1.start();
        Thread thread2 = new Thread(new SynAddRunalbe(obj1, obj2, 2, 1, false));
        thread2.setName("thread2");
        thread2.start();
    }

    /**
     * 執行緒死鎖等待演示
     */
    public static class SynAddRunalbe implements Runnable {
        Obj1 obj1;
        Obj2 obj2;
        int a, b;
        boolean flag;

        public SynAddRunalbe(Obj1 obj1, Obj2 obj2, int a, int b, boolean flag) {
            this.obj1 = obj1;
            this.obj2 = obj2;
            this.a = a;
            this.b = b;
            this.flag = flag;
        }

        @Override
        public void run() {
            try {
                if (flag) {
                    synchronized (obj1) {
                        Thread.sleep(100);
                        synchronized (obj2) {
                            System.out.println(a + b);
                        }
                    }
                } else {
                    synchronized (obj2) {
                        Thread.sleep(100);
                        synchronized (obj1) {
                            System.out.println(a + b);
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static class Obj1 {
    }

    public static class Obj2 {
    }
}

執行上面程式碼,可以通過jstack檢視到死鎖資訊:

"thread2" #13 prio=5 os_prio=0 tid=0x0000000029225000 nid=0x3c94 waiting for monitor entry [0x0000000029c9f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at com.jvm.visualvm.Demo4$SynAddRunalbe.run(Demo4.java:50)
    - waiting to lock <0x00000007173d40f0> (a com.jvm.visualvm.Demo4$Obj1)
    - locked <0x00000007173d6310> (a com.jvm.visualvm.Demo4$Obj2)
    at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
    - None

"thread1" #12 prio=5 os_prio=0 tid=0x0000000029224800 nid=0x6874 waiting for monitor entry [0x0000000029b9f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at com.jvm.visualvm.Demo4$SynAddRunalbe.run(Demo4.java:43)
    - waiting to lock <0x00000007173d6310> (a com.jvm.visualvm.Demo4$Obj2)
    - locked <0x00000007173d40f0> (a com.jvm.visualvm.Demo4$Obj1)
    at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
    - None

thread1持有com.jvm.visualvm.Demo4$Obj1的鎖,等待獲取com.jvm.visualvm.Demo4$Obj2的鎖
thread2持有com.jvm.visualvm.Demo4$Obj2的鎖,等待獲取com.jvm.visualvm.Demo4$Obj1的鎖,兩個執行緒相互等待獲取對方持有的鎖,出現死鎖。

飢餓死鎖的例子

package com.jvm.jconsole;

import java.util.concurrent.*;

/**
 * <a href="http://www.itsoku.com/archives">Java乾貨鋪子,只生產乾貨,公眾號:javacode2018</a>
 */
public class ExecutorLock {
    private static ExecutorService single = Executors.newSingleThreadExecutor();

    public static class AnotherCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            System.out.println("in AnotherCallable");
            return "annother success";
        }
    }


    public static class MyCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            System.out.println("in MyCallable");
            Future<String> submit = single.submit(new AnotherCallable());
            return "success:" + submit.get();
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable task = new MyCallable();
        Future<String> submit = single.submit(task);
        System.out.println(submit.get());
        System.out.println("over");
        single.shutdown();
    }
}

執行程式碼,輸出:

in MyCallable

使用jstack命令檢視執行緒堆疊資訊:

"pool-1-thread-1" #12 prio=5 os_prio=0 tid=0x0000000028e3d000 nid=0x58a4 waiting on condition [0x00000000297ff000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x0000000717921bf0> (a java.util.concurrent.FutureTask)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429)
    at java.util.concurrent.FutureTask.get(FutureTask.java:191)
    at com.jvm.jconsole.ExecutorLock$MyCallable.call(ExecutorLock.java:25)
    at com.jvm.jconsole.ExecutorLock$MyCallable.call(ExecutorLock.java:20)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
    - <0x00000007173f2690> (a java.util.concurrent.ThreadPoolExecutor$Worker)

"main" #1 prio=5 os_prio=0 tid=0x00000000033e4000 nid=0x5f94 waiting on condition [0x00000000031fe000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x00000007173f1d48> (a java.util.concurrent.FutureTask)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429)
    at java.util.concurrent.FutureTask.get(FutureTask.java:191)
    at com.jvm.jconsole.ExecutorLock.main(ExecutorLock.java:32)

   Locked ownable synchronizers:
    - None

堆疊資訊結合圖中的程式碼,可以看出主執行緒在32行處於等待中,執行緒池中的工作執行緒在25行處於等待中,等待獲取結果。由於執行緒池是一個執行緒,AnotherCallable得不到執行,而被餓死,最終導致了程式死鎖的現象。

java高併發系列交流群:

相關推薦

java併發系列-1:必須知道概念

java高併發系列-第1天:必須知道的幾個概念 同步(Synchronous)和非同步(Asynchronous) 同步和非同步通常來形容一次方法呼叫,同步方法呼叫一旦開始,呼叫者必須等到方法呼叫返回後,才能繼續後續的行為。非同步方法呼叫更像一個訊息傳遞,一旦開始,方法呼叫就會立即返回,呼叫者就可以繼續後續的

java併發系列 - 22java中底層工具類Unsafe,高手必須要了解

這是java高併發系列第22篇文章,文章基於jdk1.8環境。 本文主要內容 基本介紹 通過反射獲取Unsafe例項 Unsafe中的CAS操作 Unsafe中原子操作相關方法介紹 Unsafe中執行緒排程相關方法 park和unpark示例 Unsafe鎖示例 Unsafe中保證變數的可見性 Unsafe

java併發系列 - 6:執行緒的基本操作

新建執行緒 新建執行緒很簡單。只需要使用new關鍵字建立一個執行緒物件,然後呼叫它的start()啟動執行緒即可。 Thread thread1 = new Thread1(); t1.start(); 那麼執行緒start()之後,會幹什麼呢?執行緒有個run()方法,start()會建立一個新的執行緒並讓

java併發系列 - 12JUC:ReentrantLock重入鎖

java高併發系列 - 第12天JUC:ReentrantLock重入鎖 本篇文章開始將juc中常用的一些類,估計會有十來篇。 synchronized的侷限性 synchronized是java內建的關鍵字,它提供了一種獨佔的加鎖方式。synchronized的獲取和釋放鎖由jvm實現,使用者不需要顯示的釋

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

這是java高併發系列第14篇文章。 本文主要內容: 講解3種讓執行緒等待和喚醒的方法,每種方法配合具體的示例 介紹LockSupport主要用法 對比3種方式,瞭解他們之間的區別 LockSupport位於java.util.concurrent(簡稱juc)包中,算是juc中一個基礎類,juc中很多地

java併發系列 - 15:JUC中的Semaphore,最簡單的限流工具類,必備技能

這是java高併發系列第15篇文章 Semaphore(訊號量)為多執行緒協作提供了更為強大的控制方法,前面的文章中我們學了synchronized和重入鎖ReentrantLock,這2種鎖一次都只能允許一個執行緒訪問一個資源,而訊號量可以控制有多少個執行緒可以同時訪問特定的資源。 Semaphore常用

java併發系列 - 16:JUC中等待多執行緒完成的工具類CountDownLatch,必備技能

這是java高併發系列第16篇文章。 本篇內容 介紹CountDownLatch及使用場景 提供幾個示例介紹CountDownLatch的使用 手寫一個並行處理任務的工具類 假如有這樣一個需求,當我們需要解析一個Excel裡多個sheet的資料時,可以考慮使用多執行緒,每個執行緒解析一個sheet裡的資料

java併發系列 - 17:JUC中的迴圈柵欄CyclicBarrier常見的6種使用場景及程式碼示例

這是java高併發系列第17篇。 本文主要內容: 介紹CyclicBarrier 6個示例介紹CyclicBarrier的使用 對比CyclicBarrier和CountDownLatch CyclicBarrier簡介 CyclicBarrier通常稱為迴圈屏障。它和CountDownLatch很相似,

java併發系列 - 21java中的CAS操作,java併發的基石

這是java高併發系列第21篇文章。 本文主要內容 從網站計數器實現中一步步引出CAS操作 介紹java中的CAS及CAS可能存在的問題 悲觀鎖和樂觀鎖的一些介紹及資料庫樂觀鎖的一個常見示例 使用java中的原子操作實現網站計數器功能 我們需要解決的問題 需求:我們開發了一個網站,需要對訪問量進行統計,使

java併發系列 - 23:JUC中原子類,一篇就夠了

這是java高併發系列第23篇文章,環境:jdk1.8。 本文主要內容 JUC中的原子類介紹 介紹基本型別原子類 介紹陣列型別原子類 介紹引用型別原子類 介紹物件屬性修改相關原子類 預備知識 JUC中的原子類都是都是依靠volatile、CAS、Unsafe類配合來實現的,需要了解的請移步: volati

java併發系列 - 24:ThreadLocal、InheritableThreadLocal(通俗易懂)

java高併發系列第24篇文章。 環境:jdk1.8。 本文內容 需要解決的問題 介紹ThreadLocal 介紹InheritableThreadLocal 需要解決的問題 我們還是以解決問題的方式來引出ThreadLocal、InheritableThreadLocal,這樣印象會深刻一些。 目前

java併發系列 - 25:掌握JUC中的阻塞佇列

這是java高併發系列第25篇文章。 環境:jdk1.8。 本文內容 掌握Queue、BlockingQueue介面中常用的方法 介紹6中阻塞佇列,及相關場景示例 重點掌握4種常用的阻塞佇列 Queue介面 佇列是一種先進先出(FIFO)的資料結構,java中用Queue介面來表示佇列。 Queue介面中

java併發系列 - 27:實戰篇,介面效能成倍提升,讓同事刮目相看,現學現用

這是java高併發系列第27篇文章。 開發環境:jdk1.8。 案例講解 電商app都有用過吧,商品詳情頁,需要給他們提供一個介面獲取商品相關資訊: 商品基本資訊(名稱、價格、庫存、會員價格等) 商品圖片列表 商品描述資訊(描述資訊一般是由富文字編輯的大文字資訊) 資料庫中我們用了3張表儲存上面的資訊:

java併發系列 - 31:獲取執行緒執行結果,這6種方法你都知道

這是java高併發系列第31篇。 環境:jdk1.8。 java高併發系列已經學了不少東西了,本篇文章,我們用前面學的知識來實現一個需求: 在一個執行緒中需要獲取其他執行緒的執行結果,能想到幾種方式?各有什麼優缺點? 結合這個需求,我們使用6種方式,來對之前學過的知識點做一個回顧,加深記憶。 方式1:Thre

java併發系列 - 32併發中計數器的實現方式有哪些?

這是java高併發系列第32篇文章。 java環境:jdk1.8。 本文主要內容 4種方式實現計數器功能,對比其效能 介紹LongAdder 介紹LongAccumulator 需求:一個jvm中實現一個計數器功能,需保證多執行緒情況下資料正確性。 我們來模擬50個執行緒,每個執行緒對計數器遞增100萬次

java併發系列 - 29併發中常見的限流方式

這是java高併發系列第29篇。 環境:jdk1.8。 本文內容 介紹常見的限流演算法 通過控制最大併發數來進行限流 通過漏桶演算法來進行限流 通過令牌桶演算法來進行限流 限流工具類RateLimiter 常見的限流的場景 秒殺活動,數量有限,訪問量巨大,為了防止系統宕機,需要做限流處理 國慶期間,一般

Java級特性 1節 集合框架和泛型

eric 如果 語法 提高 需要 his testin fir hashset   Java中,存儲多個同類型的數據,可以用數組來實現,但數組有一些缺陷: 數組長度固定不變,布恩那個很好的適應元素數量動態變化的情況 可以通過數組.length獲取數組長度,卻無法直接

Java併發系列——檢視閱讀

Java高併發系列——檢視閱讀 參考 java高併發系列 liaoxuefeng Java教程 CompletableFuture AQS原理沒講,需要找資料補充。 JUC中常見的集合原來沒講,比如ConcurrentHashMap最常用的,後面的都很泛,沒有深入,虎頭蛇尾。 阻塞佇列講得不夠深入

java併發核心要點|系列1

在java高併發程式設計,有幾個很重要的內容: 1.CAS演算法 2.CPU重排序 3.快取行偽共享 我們先來說說高併發世界中的主要關鍵問題是什麼? 是資料共享。 因為多執行緒之間要共享資料,就會遇到各種問題。如下圖: 如果兩個執行緒同時寫入,那怎麼保證資料的一致性?是執行緒1先寫,還是執

跟著阿里p7一起學java併發 - 18:玩轉java執行緒池,這一篇就夠了

java中的執行緒池,這一篇就夠了 java高併發系列第18篇文章。 本文主要內容 什麼是執行緒池 執行緒池實現原理 執行緒池中常見的各種佇列 自定義執行緒建立的工廠 常見的飽和策略 自定義飽和策略 執行緒池中兩種關閉方法有何不同 擴充套件執行緒池 合理地配置執行緒池 執行緒池中執行緒數量的配置 什麼是執