1. 程式人生 > >「阿里面試系列」搞懂併發程式設計,輕鬆應對80%的面試場景

「阿里面試系列」搞懂併發程式設計,輕鬆應對80%的面試場景

關注我的架構技術公眾號:“架構師修煉寶典”一週出產1-2篇技術文章,希望在你的架構技術路上有我的點滴陪伴!

作為一個合格的Java程式設計師,必須要對併發程式設計有一個深層次的瞭解,在很多網際網路企業都會重點考察這一塊。可能很多工作3年以上的Java程式設計師對於這一領域幾乎沒有太多研究。所以在接下來內容中,我會將併發程式設計整個領域由淺到深做非常全面的分析。

內容導航

  • 從作業系統的發展瞭解程序、執行緒模型
  • 執行緒的優勢
  • 執行緒的生命週期
  • 執行緒的應用場景

瞭解程序、執行緒模型

每次學習一個新技術,我會先去了解這個技術的背景,這個過程看似浪費時間,其實在後續的學習過程中,能夠促進理解很多問題。所以對於執行緒這個概念,我會先從作業系統講起。因為作業系統的發展帶來了軟體層面的變革。 從多執行緒的發展來看,可以作業系統的發展分為三個歷史階段:

  • 真空管和穿孔卡片
  • 電晶體和批處理系統
  • 積體電路和多道程式設計

最早的計算機只能解決簡單的數學運算問題,比如正弦、餘弦等。執行方式:程式設計師首先把程式寫到紙上,然後穿孔成卡票,再把卡片盒帶入到專門的輸入室。輸入室會有專門的操作員將卡片的程式輸入到計算機上。計算機執行完當前的任務以後,把計算結果從印表機上進行輸出,操作員再把打印出來的結果送入到輸出室,程式設計師就可以從輸出室取到結果。然後,操作員再繼續從已經送入到輸入室的卡片盒中讀入另一個任務重複上述的步驟。

操作員在機房裡面來回排程資源,造成計算機存在大量的空閒狀態 。而當時的計算機是非常昂貴的,人們為了減少這種資源的浪費。就採用了 批處理系統來解決

批處理作業系統的執行方式:在輸入室收集全部的作業,然後用一臺比較便宜的計算機把它們讀取到磁帶上。然後把磁帶輸入到計算機,計算機通過讀取磁帶的指令來進行運算,最後把結果輸出磁帶上。批處理作業系統的好處在於,計算機會一直處於運算狀態,合理的利用了計算機資源。(執行流程如下圖所示)

「阿里面試系列」搞懂併發程式設計,輕鬆應對80%的面試場景

(注:此圖來源於現代作業系統)

批處理作業系統雖然能夠解決計算機的空閒問題,但是當某一個作業因為等待磁碟或者其他I/O操作而暫停,那CPU就只能阻塞直到該I/O完成,對於CPU操作密集型的程式,I/O操作相對較少,因此浪費的時間也很少。但是對於I/O操作較多的場景來說,CPU的資源是屬於嚴重浪費的。

多道程式設計的出現解決了這個問題,就是把記憶體分為幾個部分,每一個部分放不同的程式。當一個程式需要等待I/O操作完成時。那麼CPU可以切換執行記憶體中的另外一個程式。如果記憶體中可以同時存放足夠多的程式,那CPU的利用率可以接近100%。 在這個時候,引入了第一個概念- 程序, 程序的本質是一個正在執行的程式,程式執行時系統會建立一個程序,並且給每個程序分配獨立的記憶體地址空間保證每個程序地址不會相互干擾。同時,在CPU對程序做時間片的切換時,保證程序切換過程中仍然要從程序切換之前執行的位置出開始執行。所以程序通常還會包括程式計數器、堆疊指標。

有了程序以後,可以讓作業系統從巨集觀層面實現多應用併發。而併發的實現是通過CPU時間片不端切換執行的。對於單核CPU來說,在任意一個時刻只會有一個程序在被CPU排程

有了程序以後,為什麼還會出現執行緒呢?

在一個應用程序中,會存在多個同時執行的任務,如果其中一個任務被阻塞,將會引起不依賴該任務的任務也被阻塞。舉個具體的例子來說,我們平常用word文件編輯內容的時候,都會有一個自動儲存的功能,這個功能的作用是,當計算機出現故障的情況下如果使用者未儲存文件,則能夠恢復到上一次自動儲存的點。假設word的自動儲存因為磁碟問題導致寫入較慢,勢必會影響到使用者的文件編輯功能,直到磁碟寫入完成使用者才可編輯,這種體驗是很差的。如果我們把一個程序中的多個任務通過執行緒的方式進行隔離,那麼按照前面提到的程序演進的理論來說,在單核心CPU架構中可以通過CPU的時間片切換實現執行緒的排程充分利用CPU資源以達到最大的效能。加Q群:725219329可獲取一份Java架構進階技術精品視訊。(高併發+Spring原始碼+JVM原理解析+分散式架構+微服務架構+多執行緒併發原理+BATJ面試寶典)

我們用了比較長的篇幅介紹了程序、執行緒發展的歷史。總的來說是人們對於計算機的要求越來越高;對於計算機本身的資源的利用率也在不斷提高。

執行緒的優勢

前面分析了執行緒的發展歷史,這裡簡單總結一下執行緒有的優勢如下

  • 執行緒可以認為是輕量級的程序,所以執行緒的建立、銷燬要比程序更快
  • 從效能上考慮,如果程序中存在大量的I/O處理,通過多執行緒能夠加快應用程式的執行速度(通過CPU時間片的快速切換)。
  • 由於執行緒是CPU的最小排程單元,所以在多CPU架構中能夠實現真正的並行執行。每一個CPU可以排程一個執行緒

這裡有兩個概念很多人沒有搞明白,就是並行和併發
並行:同時執行多個任務,在多核心CPU架構中,一個CPU核心執行一個執行緒,那麼4核心CPU,可以同時執行4個執行緒
併發:同處理多個任務的能力,通常我們會通過TPS或者QPS來表示某某系統支援的併發數是多少。

總的來說,並行是併發的子集。也就是說我們可以寫一個擁有多執行緒並行的程式,如果在沒有多核心CPU來執行這些執行緒,那就不能以並行的方式來執行程式中的多個執行緒。所以併發程式可以是並行的,也可以不是。Erlang之父Joe Armstrong通過一張圖型的方式來解釋併發和並行的區別,圖片如下

「阿里面試系列」搞懂併發程式設計,輕鬆應對80%的面試場景

執行緒的生命週期

執行緒是存在生命週期的,從執行緒的建立到銷燬,可能會經歷6種不同的狀態,但是在一個時刻執行緒只能處於其中一種狀態

  • NEW:初始狀態,執行緒被建立時候的狀態,還沒有呼叫start方法
  • RUNNABLE:執行狀態,執行狀態包含就緒和執行兩種狀態,因為執行緒啟動以後,並不是立即執行,而是需要通過排程去分配CPU時間片
  • BLOCKED:阻塞狀態,當執行緒去訪問一個加鎖的方法時,如果已經有其他執行緒獲得鎖,那麼當前執行緒會處於阻塞狀態
  • WAITING:等待狀態,設定執行緒進入等待狀態等待其他執行緒做一些特定的動作進行觸發
  • TIME_WAITING:超時等待狀態,和WAITING狀態的區別在於超時以後自動返回
  • TERMINATED:終止狀態,執行緒執行完畢

下圖整理了執行緒的狀態變更過程及變更的操作,每一個具體的操作原理,我會在後續的文章中進行詳細分析。

「阿里面試系列」搞懂併發程式設計,輕鬆應對80%的面試場景

這裡有一個問題大家可能搞不明白,BLOCKED和WAITING這兩個阻塞有什麼區別?

  • BLOCKED狀態是指當前執行緒在等待一個獲取鎖的操作時的狀態。
  • WAITING是通過Object.wait或者Thread.join、LockSupport.park等操作實現的
  • BLOCKED是被動的標記,而WAITING是主動操作
  • 如果說得再深入一點,處於WAITING狀態的執行緒,被喚醒以後,需要進入同步佇列去競爭鎖操作,而在同步佇列中,如果已經有其他執行緒持有鎖,則執行緒會處於BLOCKED狀態。所以可以說BLOCKED狀態是處於WAITING狀態的執行緒重新喚醒的必經的狀態

執行緒的應用場景

執行緒的出現,在多核心CPU架構下實現了真正意義上的並行執行。也就是說,一個程序內多個任務可以通過多執行緒並行執行來提高程式執行的效能。那執行緒的使用場景有哪些呢?

  • 執行後臺任務,在很多場景中,可能會有一些定時的批量任務,比如定時傳送簡訊、定時生成批量檔案。在這些場景中可以通過多執行緒的來執行
  • 非同步處理,比如在使用者註冊成功以後給使用者傳送優惠券或者簡訊,可以通過非同步的方式來執行,一方面提升主程式的執行效能;另一方面可以解耦核心功能,防止非核心功能對核心功能造成影響
  • 分散式處理,比如fork/join,將一個任務拆分成多個子任務分別執行
  • BIO模型中的執行緒任務分發,也是一種比較常見的使用場景,一個請求對應一個執行緒。加Q群:725219329可獲取一份Java架構進階技術精品視訊。(高併發+Spring原始碼+JVM原理解析+分散式架構+微服務架構+多執行緒併發原理+BATJ面試寶典)

合理的利用多執行緒,可以提升程式的吞吐量。同時,還可以通過增加CPU的核心數來提升程式的效能,這就體現了伸縮性的特點

關注我的架構技術公眾號:“架構師修煉寶典”一週出產1-2篇技術文章,希望在你的架構技術路上有我的點滴陪伴!