Java 併發編程(一):摩拳擦掌

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

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

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

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

(戛然而止)

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

01、為什麼需要操作系統

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

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

Java 併發編程(一):摩拳擦掌

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

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

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

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

當然了,對於現在的操作系統來說,進程並不是最小的調度單位,而是線程。線程也被稱為輕量級進程。

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

02、多線程的優勢

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

Java 併發編程(一):摩拳擦掌

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

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

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

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

Java 併發編程(一):摩拳擦掌

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

03、多線程帶來的風險

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

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

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

Java 併發編程(一):摩拳擦掌

海倫

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

<code>public class Unsafe {
  private int chenmo;
  public int add() {
    return chenmo++;
  }
}/<code>

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

Java 併發編程(一):摩拳擦掌

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

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

04、單核 CPU 和多核 CPU

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

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

Java 併發編程(一):摩拳擦掌

來看答案吧。

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

那多核 CPU 呢?

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

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

05、最後

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

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

Java 併發編程(一):摩拳擦掌


分享到:


相關文章: