## 02 絕對不僅僅是為了面試—我們為什么需要學習多線程
> 智慧,不是死的默念,而是生的沉思。
> ——斯賓諾莎
此時此刻,是何種原因促使你打開了本篇關于 Java 并發的專欄?實事求是地講,對于絕大多數研發人員,平時用到多線程的場景并不多。但多線程在我們的日常開發中卻無處不在,只不過很多時候,框架已經幫你實現了。比如 web 開發,容器已經幫你實現了多線程;再比如大數據開發,框架也已幫你實現了多線程,甚至分布式計算。那促使你學習多線程的原因是什么呢?我想很大可能你是為了面試打基礎、做準備。沒錯,這真的很現實!從我最近換工作的經歷來看,多線程在面試題中出現的概率幾乎是 100%。如果你想升職加薪!加入一線大廠!成為互聯網精英!多線程的知識儲備是必備的。但你想過嗎,為什么面試官熱衷于問一個平時用到并不多的技術問題?
## 1\. 面試官考查多線程的原因
我想除了工作確實需要之外,面試官考察多線程可能有如下原因:
1. **考察你的工作技術深度。**
多線程雖然很少用到。但是如果你做底層開發,或者負責基礎設施(例如消息隊列)研發,肯定會用到多線程。通過面試多線程,可以考察你的工作在技術方面的深度。
2. **考察你的學習、理解能力。**
面試大概率會考多線程問題,這已經是公開的秘密了。這其實是一個開卷考試,對所有候選人是公平的。比拼的是候選人的學習能力、理解能力、做事的態度。你可以沒用過,但你要有快速掌握的能力,和穩扎穩打的學習態度。
我認為第二點是主要原因。求職者都知道面試官會考查多線程,但為什么還是有的人答非所問,有的人卻對答如流,有的人甚至可以深入底層原理?這無外乎兩個原因:
1. 對面試的準備和態度。明知道要考察多線程,候選人卻不認真準備,這種態度帶到工作中是何其的可怕?
2. 學習的能力。短時間內掌握平時不常用到的多線程并不容易。徹底理解多線程,還需要 JVM 的知識。這除了自身的學習能力外,如果配合一本好的教材、幾篇好的博客,能夠大大加快你的學習速度、提升你的學習深度。
生活在知識爆炸的時代,怕的不是沒有選擇,而是不知道怎么選。其實市面上關于多線程編程的書籍太多了,那為什么我還要花時間寫這個專欄呢?我在準備這個專欄前,買了 7 本多線程相關書籍。全部通讀下來后,感覺質量參差不齊。有的講得比較淺,有的講得夠深入卻晦澀難懂,而且每本書的寫法和側重點都不一樣。我寫這個專欄的目的,是想站在巨人的肩膀上,以更為通俗易懂的方式,把多線程的知識講出來。讓從來沒接觸過多線程的開發人員也能有興趣讀、能夠讀懂。并且能夠深入到底層原理,而不是蜻蜓點水。
## 2\. 軟件世界即現實世界
再回到題目上,雖然可能絕大多數讀者是抱著提升自身實力,為面試做準備的初衷來學習多線程。但我想告訴大家,多線程真的很強大,有很多使用場景,能幫你解決很多問題。在學習完多線程后,你手中便多了一樣武器,你解決問題的思路也更為寬廣。在你以后漫漫的編程生涯中,從此多了一種選擇。所以學習多線程,絕對不是僅僅為了面試。
其實多線程并不復雜,其實和現實世界中多人協作是一樣的。編程初學者,會覺得軟件是無形的,看不見、摸不到,只有冰冷冷的邏輯,學習起來晦澀難懂。其實從面向對象出現開始,軟件已經成為現實世界的對等映射。這不光體現在語言本身,其實在軟件領域無處不在,例如:
1. 設計模式
23 種經典設計模式,沒有哪一種不是從現實世界得來的靈感。如果你看過設計模式的文章,你一定對設計模式中生動有趣的例子所吸引。
2. 軟件設計
絕大多數軟件的設計,都參考了工業設計或者參考了生活中解決問題的方式,汲取其中的設計思想。其實不管軟件還是硬件或者生活中遇到的難題,在解決問題的思路上是一致的。無形的軟件設計,可以借助有形世界里的案例來幫助你思考。我最近在看 kafka 的源代碼,其中 producer 的設計思想和快遞公司發快遞的過程很類似。還有 Java NIO,也是類似的原理。可以說軟件設計的思想都發源于現實世界。
3. 軟件架構
我做個類比,軟件架構可以看作現實世界工廠里的機器設計和布置。我們需要考慮很多,比如需要哪些機器,不同機器如何配比、不同工序之間如何銜接、機器出問題如何應對、機器操作日志如何記錄、安全如何保障。工廠里遇到的問題在軟件架構上也都會遇到。
以上舉例,足以說明軟件和現實世界之間的相似程度。軟件其實就是現實世界的映射。我們在學習軟件的過程中,要善于找到生活中常見的例子類比,這樣理解起來就沒有困難了,而且便于記憶。
## 3\. 多線程典型應用場景
啰嗦了這么多,主要是為了介紹我在編程上面的學習心得,希望能對大家有些幫助。下面我們就來看看多線程的幾種典型應用場景,以下例子都由現實世界的場景切入講解。在現實世界中,我們可以認為每個人都是一個線程,當多個人一起完成一項工作,這其實就是多線程。我們來看下面的多線程場景:
1. 工作量太大,需要多人一塊干,以縮短工期
這種場景在現實生活中比比皆是,比如要完成書稿校對工作。顯然一個人校對太慢了,那就多叫幾個人吧!每個人分一個章節,同時進行校對,速度一下就上來了。如下圖所示:
編程上,如果程序需要重復執行一段邏輯,每次執行又互不影響,那么你可以考慮采用多線程,每個線程執行任務總量的一部分,最后再把每個線程執行的結果合并。通過并行處理,能夠大大減少執行時間。Java 8 開始出現的 lambda 并行流,就是采用的這種思想,只不過它是 JVM 去實現,而不需要我們做額外的處理。另外在大數據領域,對于海量數據的處理,也可以采用多線程,縮短執行時間。
2. 實現分工
這個場景的例子也很多,而且很貼近我們的生活。例如,我們每天中午都會去吃工作餐,飯館的工作流程大同小異,如下圖所示:飯店會有這么幾類員工,收銀員、廚師、傳菜員、清潔員。每個人各司其職,大家配合工作。飯店的工作流程如下:
1、顧客在收款臺點單;
2、后廚接到系統傳過來的訂單后開始加工;
3、做好飯菜后傳菜員取飯菜;
4、傳菜員找到客戶所在位置上菜;
5、顧客用餐后,清潔員進行打掃。
每種角色的員工只關心自己的輸入和輸出。比如廚師的輸入就是客戶的點菜單,輸出就是飯菜。而廚師的輸入則是上個環節收銀員的輸出。這樣做的好處是每個人專注于自己的工作,有助于效率的提升。其實好處還很多,我總結如下:
* 每種角色對應一個環節,每個環節在執行上獨立分開。這樣每個環節的工作就解耦了;
* 每個環節之間有了緩沖。收銀員一直在收銀,她不需要知道廚師是否空閑,她在不停輸出訂單。而廚師接到訂單就去加工,而不關心積累了多少訂單,只要一份菜接一份菜的去加工。訂單的列表就是一個緩沖,調節兩個環節速率的不匹配及不穩定。如果一個人干所有的事情,那么問題就來了。舉個反面的例子,某著名連鎖便利店,在早餐時段,收銀員即收銀又負責做咖啡并配餐,結果導致整個收銀的隊伍相當的長。我即使只買個面包,也要排隊很久;
* 每種角色只做自己的事情,省去了上下文切換的時間。如果你一個人干所有的事情,當你為顧客下單完成后,要跑去后廚炒菜,再端給客戶,然后再回到收銀臺為下一位顧客下單。單單是浪費在路上的時間就會有多少啊!而且每次切換工作,你都要在腦海里想一下接下來的這個工作需要怎么做;
* 我們看下清潔員這個角色。他看到有人吃完飯離開就會去收拾桌子。假如沒有分工,而是一個人干所有的工作,那么餐廳員工給客人端上飯菜后,還要一直等到客戶吃完飯,才能收拾桌子,效率何其低下。我想沒有老板傻到會讓自己的員工如此工作;
* 便于對原有流程進行改變。假如老板想在點餐前,增加向客戶推銷關注店鋪公眾號,并注冊會員的環節。如果沒有分工,老板要向所有員工通知這個事情,并且組織所有人學習。但是有了分工后,只有收銀員需要進行學習。而其他角色的員工完全不需要知道這件事情。老板是不是輕松多了?
通過分工,多人協作,餐廳的工作才能高效運轉起來。我們開發的程序也是如此,如果你所有的工作都在一個線程里,那么首先這段主邏輯會相當復雜,而且難于維護和擴展,另外相信效率也會相對低下。如果我們的程序通過多線程 + 緩沖的方式,把不同步驟解耦,那么將大大提高效率。
還是拿 kafka 舉例,Kafka 的 producer 發送消息的機制就是如此,首先不同的發消息線程會往緩存中累積消息,此時消息沒有被真正發送出去,只是累積在本地緩存中。Kafka 有專門負責網絡 IO 的 sender 線程,當緩存滿了,sender 線程被喚醒,它真正把消息發送出去,而此時新的消息還會被累積進來。
Kafka 的這種多線程設計,使得收集消息和 IO 發送消息解耦。sender 線程可以根據消息發往主機的不同,把消息分類打包,一次網絡 IO 可以發送出多條消息,從而大大減少了網絡 IO 的消耗。
我們再想想清潔員所做的工作,是不是很熟悉?沒錯,其實 JVM 中的 GC 線程就相當于清潔員。
3. 分頭行動,最后匯合
這也是分工協作的一種,只不過是分頭行動后,大家要把行動的結果匯總,才能執行接下來的任務。接下來這個例子,作為研發同學再熟悉不過了。現在 BS 軟件開發,前后端分離已經成為了趨勢,在這種開發方式下,一般分為三步:
1、前、后端研發定義接口;
2、前、后端開發分頭開發;
3、前、后端聯調。
第 3 步的前提是第 2 步。在第 2 步中,前后端程序員分頭進行開發,誰先開發完都沒有用,只有二者都開發完了,才能進入第三步。
在微服務大行其道的時代,類似上面這種場景的多線程應用很常見。例如,你的一個業務接口中,可能會調用數個微服務接口獲取數據。如果你沒有采用多線程,那么每次請求時,主線程都會被阻塞。但是假如你采用了多線程開發,對微服務的幾個請求可以同時發出,主線程阻塞時間只取決于幾個請求中最長的那個,而不是所有請求阻塞時間之和,這樣會極大地提高響應速度。
4. 排隊的同時,不耽誤做其他事情
我平時很討厭排隊,所以看到做什么事情要排隊,我就放棄了,因為排隊給我的感覺就是在浪費時間,什么都做不了。當然,在移動互聯網時代,排隊時刷刷手機還是可以的。有沒有一種方式,能讓我把隊排了,但不耽誤我做別的事情呢?當然有,我舉個例子。我們都應該做過體檢,檢查項目中最慢的就是 B 超。我說過我最不愛排隊,所以我都是最后才去做 B 超,但每次還是要排隊半個小時以上。近幾年我去體檢時,發現流程優化了。你先去 B 超排個號,然后可以先去做其他項目的檢查,當你聽到叫號你所在的區間時,再去做 B 超。這個流程改進只需要添加一個要素,就是發你一個號碼。拿到你的序號牌后,你可以去做其他事情。等叫到你的號,你可以憑號進行 B 超檢查。
這其實就是 java 多線程中的 Future 模式。這種模式下,主線程不會因為一個耗時的業務操作而被阻塞住,主線程可以單起一個線程去處理耗時的操作,主線程邏輯繼續執行,等用到另外線程返回的數據時,再通過 Futrue 對象獲取。Future 就是你的一張舊船票,你憑借這張舊船票,還能登上那艘客船。
## 4\. 總結
本節我們了解了多線程的應用場景。其實除了文中列舉的,還有許多其它使用多線程的場景。現實世界中幾乎所有的工作都需要多人協作,而計算機的世界亦是如此。了解完多線程各種應用場景,下面就讓我們開啟 Java 多線程的學習之旅吧!
- 前言
- 第1章 Java并發簡介
- 01 開篇詞:多線程為什么是你必需要掌握的知識
- 02 絕對不僅僅是為了面試—我們為什么需要學習多線程
- 03 多線程開發如此簡單—Java中如何編寫多線程程序
- 04 人多力量未必大—并發可能會遇到的問題
- 第2章 Java中如何編寫多線程
- 05 看若兄弟,實如父子—Thread和Runnable詳解
- 06 線程什么時候開始真正執行?—線程的狀態詳解
- 07 深入Thread類—線程API精講
- 08 集體協作,什么最重要?溝通!—線程的等待和通知
- 09 使用多線程實現分工、解耦、緩沖—生產者、消費者實戰
- 第3章 并發的問題和原因詳解
- 10 有福同享,有難同當—原子性
- 11 眼見不實—可見性
- 12 什么?還有這種操作!—有序性
- 13 問題的根源—Java內存模型簡介
- 14 僵持不下—死鎖詳解
- 第4章 如何解決并發問題
- 15 原子性輕量級實現—深入理解Atomic與CAS
- 16 讓你眼見為實—volatile詳解
- 17 資源有限,請排隊等候—Synchronized使用、原理及缺陷
- 18 線程作用域內共享變量—深入解析ThreadLocal
- 第5章 線程池
- 19 自己動手豐衣足食—簡單線程池實現
- 20 其實不用造輪子—Executor框架詳解
- 第6章 主要并發工具類
- 21 更高級的鎖—深入解析Lock
- 22 到底哪把鎖更適合你?—synchronized與ReentrantLock對比
- 23 按需上鎖—ReadWriteLock詳解
- 24 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap上
- 25 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap下
- 26不讓我進門,我就在門口一直等!—BlockingQueue和ArrayBlockingQueue
- 27 倒數計時開始,三、二、一—CountDownLatch詳解
- 28 人齊了,一起行動—CyclicBarrier詳解
- 29 一手交錢,一手交貨—Exchanger詳解
- 30 限量供應,不好意思您來晚了—Semaphore詳解
- 第7章 高級并發工具類及并發設計模式
- 31 憑票取餐—Future模式詳解
- 32 請按到場順序發言—Completion Service詳解
- 33 分階段執行你的任務-學習使用Phaser運行多階段任務
- 34 誰都不能偷懶-通過 CompletableFuture 組裝你的異步計算單元
- 35拆分你的任務—學習使用Fork/Join框架
- 36 為多線程們安排一位經理—Master/Slave模式詳解
- 第8章 總結
- 37 結束語