1. 程式人生 > >Java 併發程式設計(一):摩拳擦掌

Java 併發程式設計(一):摩拳擦掌


這篇文章的標題原本叫做——Java 併發程式設計(一):簡介,作者名叫小二。但我在接到投稿時覺得這標題不夠新穎,不夠吸引讀者的眼球,就在發文的時候強行修改了標題(也不咋滴)。

 

小二是一名 Java 程式設計師,就職於沉默公司,工齡是兩年零一個月零三天。和剛畢業那會相比,程式設計能力已經大有提升,但領導老王一直沒敢把併發程式設計的開發安排給小二,這讓小二心裡耿耿於懷。

這事不怪老王,小二心裡很清楚:編寫正確的程式很難,編寫正確的併發程式更是難上加難。自己功力還不到那個份上,萬一搞砸了,難免讓一向謹慎的老王面上無光。

小二想來想去,辦法只有一個,主動去學!就找老王要了一本《Java併發程式設計實戰》,據說這本書是併發程式設計中的經典之作。拿到書後,隨手翻了翻,竟然發現裡面藏著一封情書:小二激動壞了,想象著老王寫情話的樣子,不由得笑出來聲。

(戛然而止)

小二的背景就先介紹到這。接下來,我們來一起鑑賞下小二讀完這本書後寫下的第一篇文章。

Java 併發程式設計(一):簡介

01、為什麼需要作業系統

我喜歡在寫文章(不用紙和筆用電腦了)的時候聽音樂(不用 MP3 用電腦了),假如電腦只能做一件事情的話,我就只能在寫完文章的時候再聽音樂,或者聽完音樂的時候再開始寫作,這樣就很不爽——在沒有作業系統前,的確就是這麼不爽。

有了作業系統後,情況就變得大不一樣了,電腦可以同時執行多個程式。通過 TOP 命令可以檢視電腦上當前正在執行的程序(和程式有著密切的關係),見下圖。

通常情況下,一個程式會至少對應一個程序。上圖中,“Google Chrome”這三個程序意味著我的電腦上開啟著一個名叫谷歌瀏覽器的程式。

讓我們用一段專業的術語來描述一下程式和程序之間的關係:

程式是計算機為完成特定任務所執行的指令序列。 作業系統允許多道程式併發執行共享系統資源,而程式在併發執行時所產生的一系列特點使得傳統的程式概念已經不足以對其進行描述,因此,引入了“程序(Process)”:可以更好的描述計算機程式的執行過程,反映作業系統的併發執行、資源共享及使用者隨機訪問的特性,並以此作為資源分配的基本單位。

每當一個程式執行時,作業系統就為該程式建立了一個程序,併為它分配資源、排程其執行。程式執行結束後,程序也就消亡了。一個程式被同時執行多次,系統就會建立多個程序。因此,一個程式可以被多個程序執行,一個程序也可以同時執行多個程式。

當然了,對於現在的作業系統來說,程序並不是最小的排程單位,而是執行緒。執行緒也被稱為輕量級程序。

由於同一個程序中的所有執行緒會共享程序的記憶體地址空間,因此這些執行緒都能訪問相同的變數,如果沒有明確的同步機制來協同對共享資料的訪問,那麼當一個執行緒正在使用某個變數時,另外一個執行緒可能同時訪問這個變數,就會造成不可預測的結果。

02、多執行緒的優勢

查看了一下,我這臺電腦的物理 CPU(處理器)個數只有一個,但是核數(一塊 CPU 上面能處理資料的晶片組的數量)是 4 個。

這意味著,我這臺電腦能夠在同一時間處理一個程序內的四個執行緒任務:執行緒 A 正在讀取一個檔案,執行緒 B 正在寫入一個檔案,執行緒 C 正在計算一個數值,執行緒 D 正在進行網路傳輸。

我們知道,進行檔案讀寫或者網路傳輸通常會發生阻塞,這也是沒辦法的事。如果沒有多執行緒的幫助,程式會按照順序依次執行,也就意味著發生阻塞的時候其他任務只能乾巴巴的等著,什麼也做不了。

有了多執行緒,情況就完全不一樣了,執行緒之間可以互不干擾,從而發揮處理器的多核能力。

說個有點讓人難為情的事,我是 Eclipse 的(愚)忠實使用者,至今沒切換到 IDEA 陣營。在用 Eclipse 的時候經常會出現這樣的情況,一個進度被另外一個卡住,下一個必須等待上一個執行完畢才開始執行。等待的時候幾乎什麼也幹不成,點了取消也沒用!

假如 Eclipse 採用多執行緒的話,每個任務放在單獨的任務中執行,響應就會快很多。

03、多執行緒帶來的風險

曾有這樣一則耳熟能詳的故事。

特洛伊人在城外的海灘上發現了一隻巨大的木馬,他們把它拉進了城裡而不是把它燒掉或推到海里,以為這是天神給特洛伊人帶來的賜福。於是,特洛伊人歡天喜地,慶祝勝利,他們跳著唱著,喝光了一桶又一桶的酒,以為希臘人被他們戰敗了。

而故事的結局大家也都知道了。希臘人把特洛伊城掠奪成空,燒成一片灰燼。海倫(宙斯之女,被稱為“世上最美的女人”,她和特洛伊王子私奔,引發了特洛伊戰爭)也被墨涅依斯帶回了希臘。

海倫

多執行緒帶來了無與倫比的好處,但也潛藏了巨大的風險(就像那個木馬)。其中尤為突出的就是安全性問題。

public class Unsafe {
  private int chenmo;
  public int add() {
    return chenmo++;
  }
}

上面這段程式碼在單執行緒的環境中可以正確執行,但在多執行緒的環境中則不能。遞增運算 chenmo++ 可以拆分為三個操作:讀取 chenmo,將 chenmo 加 1,將計算結果賦值給 chenmo。兩個執行緒可能交替執行,發生下圖中的情況,於是兩個執行緒就會返回相同的結果。這也是最常見的一種安全性問題。

其次,多執行緒還會引發活躍性問題:執行緒 B 需要等待執行緒 A 釋放它們共有的資源,而執行緒 A 由於一些問題導致無法釋放資源,那麼執行緒 B 就只能苦苦地等下去。

再者,多執行緒還會引發效能問題(設計良好的多執行緒當然會提高效能):當執行緒排程器臨時掛起一個活躍中的執行緒轉而執行另外一個執行緒時,就會頻繁地出現上下文切換(Context Switch)——開銷很大(掙得多花的也多)。

04、單核 CPU 和多核 CPU

來思考一個問題吧。假如 CPU 只有一個,核數也只有一個,多執行緒還會有優勢嗎?

閉上眼,讓思維旋轉跳躍會。

來看答案吧。

單核 CPU 上執行的多執行緒程式,同一時間只有一個執行緒在跑,系統幫忙進行執行緒切換;系統給每個執行緒分配時間片(大概 10ms)來執行,看起來像是在同時跑,但實際上是每個執行緒跑一點點就換到其它執行緒繼續跑。所以效率不會有所提高,執行緒的切換反到增加了系統開銷。

那多核 CPU 呢?

當然有優勢了!多核需要多執行緒才能發揮優勢(不然巧婦難為無米之炊啊),同樣,多執行緒要在多核上才能有所發揮(好馬配好鞍啊)。

多核 CPU 多執行緒不僅善於處理 IO 密集型的任務(減少阻塞時間),還善於處理計算密集型的任務,比如加密解密、資料壓縮解壓縮(視訊、音訊、普通資料等),讓每個核心都物盡其用。

05、最後

親愛的讀者朋友們,小二投稿的第一篇文章到此就結束了。你對此感到滿意嗎?或者說你期待下一篇嗎?

(此時的小二正在翹首以盼)

&n