1. 程式人生 > >java多執行緒系列(一)基礎概念

java多執行緒系列(一)基礎概念

前言

這一系列多執行緒的文章,一方面是個人對Java現有的多執行緒機制的學習和記錄,另一方面是希望能給不熟悉Java多執行緒機制、或有一定基礎但理解還不夠深的讀者一個比較全面的介紹,旨在使讀者對Java的多執行緒有一個遞增、全面和較深刻的理解,所以在第一部分就集中介紹一些概念和原理,表面看來這些對多執行緒的使用沒有太多關係,但理解這些概念/原理對理解多執行緒是至關重要的,因為Java的多執行緒並非是完全獨自實現的,它依賴於作業系統命令、CPU機制,並且隨著這些基礎軟硬體的發展而發展,所以請有意向對多執行緒全面理解的讀者,請耐心地一篇一篇地看完,我儘量在介紹的過程中給予足夠而又簡單的介紹,如果不能理解,請查閱作業系統及CPU方面的資料。

本系列文章的資料,都來源於官方文件以及相關人士/網友的論文、文章,是個人的學習總結,是對多執行緒機制的理解,不存在原創演算法/策略思想,畢竟原創/策略思想只有HotSpot作者及從事相關研究的大師才能提出。網路本身是個開放、免費的環境,如果本系列文章引用了其他作者的文字,還請作者多多理解,因為本身他們的文字大多也是來源於官方資料的。

總述

在JDK5之前,Java的多執行緒(包括它的效能)一直是個軟肋,只有synchronized、Thread.sleep()、Object.wait/notify這樣有限的方法,而synchronized的效率還特別地低(為什麼低,在後面的“核心態與使用者態”一節有詳細敘述),開銷比較大。JDK5相對於前面版本是重大改進,不僅在Java語法上有了很多改進(包括泛型、裝箱、for迴圈、變參等),在多執行緒上有了徹底的提高,其引進了併發程式設計大師Doug Lea的java.util.concurrent包(後面簡稱J.U.C),支援了現代CPU的CAS原語,不僅在效能上有了很大提升,在自由度上也有了更多的選擇,此時J.U.C的效率在高併發環境下的效率遠優於synchronized

。但JDK6(Mustang 野馬)中對synchronized的內在機制做了大量顯著的優化,加入了CAS的概念以及偏向鎖、輕量級鎖,使得synchronized的效率與J.U.C不相上下,並且官方說後面該關鍵字還有繼續優化的空間,所以在現在JDK7的時代,synchronized已經成為一般情況下的首選,在某些特殊場景——如可中斷的鎖、條件鎖、等待獲得鎖一段時間如果失敗則停止——下,J.U.C是適用的,所以對於多執行緒研究來說,瞭解其原理以及各自的適用場景是必要的。

這裡必須要指出的是,JDK5之前的版本對多執行緒的支援一直不佳,這並非是Sun的原因。Java是1995年誕生的(由Oak語言改名為Java,Oak語言當時是打算在電子消費品和嵌入式上建立統一平臺,沒想到後面卻發展成為主流的企業級應用語言),96年JDK1.0釋出,2002年JDK1.4釋出,這是java真正走向成熟的一個版本,但是當時的PC並不如今天這樣的普及,硬體整體是以單CPU和單核為主,也就是說既不普遍存在如今這樣高併發的使用場景、也不存在硬體多CPU、多核這樣的支援,而隨著時代的發展,高併發場景越來越多,多CPU由於是多核PC越來越普遍,相應的作業系統的指令集也跟隨這種形式出現瞭如CAS這樣的原語(什麼是CAS後面會重點闡述),也就是說,正是這些應用場景和基礎設施都具備了,Java這樣的高階語言自然也就需要有更多更好的對多執行緒的支援。技術總是跟隨著時代的發展而發展,又反過來推動著時代的前進,按照馬克思主義來說,是相輔相成。這也就是JDK5對多執行緒做了大量改進的歷史背景,而到了如今2013年,JDK8即將正式釋出,語言原生的多執行緒機制也未必能滿足時代要求了,於是很多天生適合多執行緒環境的(如ErLang這樣無狀態的函數語言程式設計語言)開始提供了Java版本,使得Java成為一個語言平臺——多執行緒、大計算的任務更多的是委派給適合多執行緒的語言來做,而Java專注於後臺業務處理,這也就是語言發展的脈絡。

基本概念

1.執行緒

執行緒是依附於程序的,程序是分配資源的最小單位,一個程序可以生成多個執行緒,這些執行緒擁有共享的程序資源。就每個執行緒而言,只有很少的獨有資源,如控制執行緒執行的執行緒控制塊,保留區域性變數和少數引數的棧空間等。執行緒有就緒、阻塞和執行三種狀態,並可以在這之間切換。也正因為多個執行緒會共享程序資源,所以當它們對同一個共享變數/物件進行操作的時候,執行緒的衝突和不一致性就產生了。執行緒這個概念在這裡就不詳述了,如果還不是很清楚地,可以查些相關資料。

多執行緒併發環境下,本質上要解決地是這兩個問題:

  • 執行緒之間如何通訊。
  • 執行緒之間如何同步。

概括起來說就是:執行緒之間如何正確通訊。這是本系列所需要講的主題。雖然說的是在Java層面如何保證,但會涉及到java虛擬機器、Java記憶體模型,以及Java這樣的高階語言最終是要對映到CPU來執行(關鍵原因是如今的CPU有快取、並且是多核的),所以本系列也會涉及一定的作業系統/硬體方面的知識,雖然有些難懂,但對於深刻把握多執行緒是至關重要的,所以需要多花一些時間。

2.鎖

當多個執行緒對同一個共享變數/物件進行操作,即使是最簡單的操作,如i++,在處理上實際也涉及到讀取、自增、賦值這三個操作,也就是說這中間存在時間差,導致多個執行緒沒有按照如程式編寫者所設想的去順序執行,出現錯位,從而導致最終結果與預期不一致。

Java中的多執行緒同步是通過鎖的概念來體現。鎖不是一個物件、不是一個具體的東西,而是一種機制的名稱。鎖機制需要保證如下兩種特性:

  • 互斥性:即在同一時間只允許一個執行緒持有某個物件鎖,通過這種特性來實現多執行緒中的協調機制,這樣在同一時間只有一個執行緒對需同步的程式碼塊(複合操作)進行訪問。互斥性我們也往往稱為操作的原子性。
  • 可見性:必須確保在鎖被釋放之前,對共享變數所做的修改,對於隨後獲得該鎖的另一個執行緒是可見的(即在獲得鎖時應獲得最新共享變數的值),否則另一個執行緒可能是在本地快取的某個副本上繼續操作從而引起不一致。

上面說的“持有某個物件鎖”這不太好理解,有些抽象。程式又不是人,怎麼能持有呢?什麼樣算持有呢?看完後文的同步機制內容,就會有一定理解,這裡暫且可以把它理解為:對物件的佔有權。持有某個物件鎖,就是告訴大家,這個物件現在歸我所用,在我沒釋放之前,別人不能佔用這個物件。網上大多文章說把鎖理解為房間鑰匙,拿到鎖的執行緒等於拿到房間鑰匙,可以進房間(也就是執行需同步的程式碼塊),而別的執行緒就不能拿這把鑰匙了,就是這個道理。

執行緒持有物件鎖(鑰匙)的目的,並不是僅僅拿著,而是表明擁有了程式碼段的執行權(拿鑰匙不是目的,進房間才是目的),別的執行緒沒拿到物件鎖,也就不能執行拿到鎖和釋放鎖之間的程式碼(如下例中的val++就是上面所說的“程式碼段”,也許表述不是那麼清晰,但相信大家還是好理解的)

public synchronized void synMethod(){
        val++;
    }

互斥性和可見性,這是鎖機制的兩個重要概念,在後面的文章中會多次提到,是理解Java多執行緒機制的基礎。

3.掛起、休眠、阻塞與非阻塞

這四個名次,在多執行緒裡是會頻繁提到的,所以有必要對它們解釋一下。

掛起(Suspend):當執行緒被掛起的時候,其會失去CPU的使用時間,直到被其他執行緒(使用者執行緒或排程執行緒)喚醒。

休眠(Sleep):同樣是會失去CPU的使用時間,但是在過了指定的休眠時間之後,它會自動啟用,無需喚醒(整個喚醒表面看是自動的,但實際上也得有守護執行緒去喚醒,只是不需程式設計者手動干預)。

阻塞(Block):線上程執行時,所需要的資源不能得到,則執行緒被掛起,直到滿足可操作的條件。

非阻塞(Block):線上程執行時,所需要的資源不能得到,則執行緒不是被掛起等待,而是繼續執行其餘事情,待條件滿足了之後,收到了通知(同樣是守護執行緒去做)再執行。

掛起和休眠是獨立的作業系統的概念,而阻塞與非阻塞則是在資源不能得到時的兩種處理方式,不限於作業系統,當資源申請不到時,要麼掛起執行緒等待、要麼繼續執行其他操作,資源被滿足後再通知該執行緒重新請求。顯然非阻塞的效率要高於阻塞,相應的實現的複雜度也要高一些。

在Java中顯式的掛起原先是通過Thread的suspend方法來體現,現在此概念已經消失,原因是suspend/resume方法已經被廢棄,它們容易產生死鎖,在suspend方法的註釋裡有這麼一段話:當suspend的執行緒持有某個物件鎖,而resume它的執行緒又正好需要使用此鎖的時候,死鎖就產生了。所以在現在的JDK版本中,掛起是JVM的系統行為,程式設計師無需干涉。休眠的過程中也不會釋放鎖,但它一定會在某個時間後被喚醒,所以不會死鎖。現在我們所說的掛起,往往並非指編寫者的程式裡主動掛起,而是由作業系統的執行緒排程器去控制。所以,我們常常說的“執行緒在申請鎖失敗後會被掛起、然後等待排程”這樣有一定歧義,因為這裡的“掛起”是作業系統級別的掛起,其實是在申請資源失敗時的阻塞,和Java中的執行緒的掛起(可能已經獲得鎖,也可能沒有鎖,總之和鎖無關)不是一個概念,很容易混淆,所以在後文中說的掛起,一般指的是作業系統的操作,而不是Thread中的suspend()。

相應地我們有必要提下java.lang.Object的wait/notify,這兩個方法同樣是等待/通知,但它們的前提是已經獲得了鎖,且在wait(等待)期間會釋放鎖。在wait方法的註釋裡明確提到:執行緒要呼叫wait方法,必須先獲得該物件的鎖,在呼叫wait之後,當前執行緒釋放該物件鎖並進入休眠(這裡到底是進入休眠還是掛起?文件沒有細說,從該方法能指定等待時間來看,我覺得更可能是休眠,沒有指定等待時間的,則可能是掛起,不管如何,在休眠/掛起之前,JVM都會從當前執行緒中把該物件的鎖釋放掉),只有以下幾種情況下會被喚醒:其他執行緒呼叫了該物件的notify或notifyAll、當前執行緒被中斷、呼叫wait時指定的時間已到。

4.核心態與使用者態

這是兩個作業系統的概念,但理解它們對我們理解Java的執行緒機制有著一定幫助。

有一些系統級的呼叫,比如清除時鐘、建立程序等這些系統指令,如果這些底層系統級指令能夠被應用程式任意訪問的話,那麼後果是危險的,系統隨時可能崩潰,所以CPU將所執行的指令設定為多個特權級別,在硬體執行每條指令時都會校驗指令的特權,比如Intel x86架構的CPU將特權分為0-3四個特權級,0級的許可權最高,3許可權最低。

而作業系統根據這系統呼叫的安全性分為兩種:核心態和使用者態。核心態執行的指令的特權是0,使用者態執行的指令的特權是3。當一個任務(程序)執行系統呼叫而進入核心指令執行時,我們就說程序處於核心執行態(或簡稱為核心態)。當任務(程序)執行自己的程式碼的時候,就處於使用者態。這就像我們Java的class,有很多的private方法,但對外公開的只有少量public方法一樣,這些private方法只有class本身可以呼叫的,不允許外界呼叫,否則會產生意料不到的問題。

那明白了核心態和使用者態的概念之後,我們來看在這兩種狀態之間切換會造成什麼樣的效率影響(這裡所說的切換就是執行一段使用者程式碼、再執行一段核心程式碼、再執行一段使用者程式碼這樣的交替行為,說交替執行更合適,說切換有些混淆)。在執行系統級呼叫時,需要將變數傳遞進去、可能要拷貝、計數、儲存一些上下文資訊,然後核心態執行完成之後需要再將引數傳遞到使用者程序中去,這個切換的代價相對來說是比較大的,所以應該是儘量避免頻繁地在核心態和使用者態之間切換。

好了,那作業系統的這兩種形態和我們的執行緒主題有什麼關係呢?這裡是關鍵。Java並沒有自己的執行緒模型,而是使用了作業系統的原生執行緒!如果要實現自己的執行緒模型,那麼有些問題就特別複雜,難以解決,比如如何處理阻塞、如何在多CPU之間合理地分配執行緒、如何鎖定,包括建立、銷燬執行緒這些,都需要Java自己來做,在JDK1.2之前Java曾經使用過自己實現的執行緒模型,後來放棄了,轉向使用作業系統的執行緒模型,因此建立、銷燬、排程、阻塞等這些事都交由作業系統來做,而執行緒方面的事在作業系統來說屬於系統級的呼叫,需要在核心態完成,所以如果頻繁地執行執行緒掛起、排程,就會頻繁造成在核心態和使用者態之間切換,影響效率(當然,作業系統的執行緒操作是不允許外界(包括Java虛擬機器)直接訪問的,而是開放了叫“輕量級程序”的介面供外界使用,其與核心執行緒在Window和Linux上是一對一的關係,這裡不多敘述)。

我們說JDK5之前的synchronized效率低下,是因為在阻塞時執行緒就會被掛起、然後等待重新排程,而執行緒操作屬於核心態,這頻繁的掛起、排程使得作業系統頻繁處於核心態和使用者態的轉換,造成頻繁的變數傳遞、上下文儲存等,從而效能較低。

如果需要更多地瞭解作業系統的使用者態和核心態的用處,以及作業系統的執行緒模型,請查閱相關資料。

本文出自:http://www.cnblogs.com/mengheng/p/3490693.html