內存模型與多線程設計-線程與虛擬機

RoadMap

1. 線程實現

    線程是比進程更輕量級的調度執行單位,線程的引入,能夠把一個進程的資源分配和執行調度分開,各個線程既能夠共享進程資源(內存地址、文件I/O等),又能夠獨立調度(線程是CPU調度的基本單位),主流的操做系統都提供了線程實現,Java語言則提供了在不一樣硬件和操做系統平臺下對線程操做的統一處理,實現線程主要有3種方式:使用內核線程實現、使用用戶線程實現和使用用戶線程加輕量級進程混合實現。java

1.1 內核線程實現

    內核線程(Kernel-Level Thread,KLT)就是直接由操做系統內核支持的線程,這種線程由內核來完成線程切換,內核經過操縱調度器(Scheduler)對線程進行調度,並負責將線程的任務映射到各個處理器上。web

    程序通常不會直接去使用內核線程,而是去使用內核線程的一種高級接口——輕量級進程(Light Weight Process,LWP),輕量級進程就是咱們一般意義上所講的線程,因爲每一個輕量級進程都由一個內核線程支持,所以只有先支持內核線程,纔能有輕量級進程。這種輕量級進程與內核線程之間1:1的關係稱爲一對一的線程模型算法

1.2 使用用戶線程實現

    從廣義上來說,一個線程只要不是內核線程,就能夠認爲是用戶線程(User Thread,UT),所以,從這個定義上來說,輕量級進程也屬於用戶線程,但輕量級進程的實現始終是創建在內核之上的,許多操做都要進行系統調用,效率會受到限制。 而狹義上的用戶線程指的是徹底創建在用戶空間的線程庫上,系統內核不能感知線程存在的實現。用戶線程的創建、同步、銷燬和調度徹底在用戶態中完成,不須要內核的幫助。這種進程與用戶線程之間1:N的關係稱爲一對多的線程模型。安全

1.3 使用用戶線程加輕量級進程混合實現

    線程除了依賴內核線程實現和徹底由用戶程序本身實現以外,還有一種將內核線程與用戶線程一塊兒使用的實現方式。在這種混合實現下,既存在用戶線程,也存在輕量級進程。用戶線程仍是徹底創建在用戶空間中,所以用戶線程的建立、切換、析構等操做依然廉價,而且能夠支持大規模的用戶線程併發。而操做系統提供支持的輕量級進程則做爲用戶線程和內核線程之間的橋樑,這樣可使用內核提供的線程調度功能及處理器映射,而且用戶線程的系統調用要經過輕量級線程來完成,大大下降了整個進程被徹底阻塞的風險。在這種混合模式中,用戶線程與輕量級進程的數量比是不定的,即爲N:M的關係。多線程

    對於Sun JDK來講,它的Windows版與Linux版都是使用一對一的線程模型實現的,一條Java線程就映射到一條輕量級進程之中,由於Windows和Linux系統提供的線程模型就是一對一的。併發

    在Solaris平臺中,因爲操做系統的線程特性能夠同時支持一對一及多對多的線程模型,所以在Solaris版的JDK中也對應提供了兩個平臺專有的虛擬機參數:工具

-XX:+UseLWPSynchronization(默認值)和oop

-XX:+UseBoundThreads來明確指定虛擬機使用哪一種線程模型。性能

 

2. 線程的調度

    線程調度是指系統爲線程分配處理器使用權的過程,主要調度方式有兩種,分別是協同式線程調度(Cooperative Threads-Scheduling)和搶佔式線程調度(Preemptive Threads-Scheduling).測試

Java使用的線程調度方式就是搶佔式調度。

2.1 調度方式

2.1.1 搶佔式(Preemptive Threads-Scheduling)

    搶佔式調度的多線程系統的每一個線程由系統來分配執行時間,線程的切換不禁線程自己來決定在在這種實現線程調度的方式下,線程的執行時間是系統可控的,也不會有一個線程致使整個進程阻塞的問題。缺點是實現成本高,程序對線程不可控。

2.1.2 協同式(Cooperative Threads-Scheduling)

    協同式調度的多線程系統的線程的執行時間由線程自己來控制,線程把本身的工做執行完了以後,要主動通知系統切換到另一個線程上。協同式多線程的最大好處是實現簡單,並且因爲線程要把本身的事情幹完後纔會進行線程切換,切換操做對線程本身是可知的,因此沒有什麼線程同步的問題。

2.1.3 建議

    雖然Java線程調度是系統自動完成的,可是咱們仍是能夠「建議」系統給某些線程多分配一點執行時間,另外的一些線程則能夠少分配一點(設置優先級)

    不過,線程優先級並非太靠譜,緣由是Java的線程是經過映射到系統的原生線程上來實現的,因此線程調度最終仍是取決於操做系統。雖然如今不少操做系統都提供線程優先級的概念,可是並不見得能與Java線程的優先級一一對應,不一樣的操做系統應對線程調度的方式也是有差別的。

    Java一共設置了10個優先級狀態

Java線程優先級 Windows 線程優先級
1(Thread.MIN_PRIORITY) TRHEAD_PRIORITY_LOWEST
2 TRHEAD_PRIORITY_LOWEST
3 TRHEAD_PRIORITY_BELOW_NORMAL
4 TRHEAD_PRIORITY_BELOW_NORMAL
5(Thread.NORMAL_PRIORITY) TRHEAD_PRIORITY_NORMAL
6 TRHEAD_PRIORITY_ABOVE_NORMAL
7 TRHEAD_PRIORITY_ABOVE_NORMAL
8 TRHEAD_PRIORITY_ABOVE_HIGHEST
9 TRHEAD_PRIORITY_ABOVE_HIGHEST
10(Thread.MAX_PRIORITY) TRHEAD_PRIORITY_ABOVE_CRITICAL

2.2 狀態轉換

Java語言定義了5種線程狀態,在任意一個時間點,一個線程只能有且只有其中的一種狀態,

3. 線程安全

3.1 線程安全類型

    Java語言中各類操做共享的數據的安全類型分爲如下5類:不可變、絕對線程安全、相對線程安全、線程兼容和線程對立

3.1.1 不可變

    不可變(Immutable),是指對象一旦被建立是不容許修改的,若是須要修改或者對象屬性等,則會從新生成一份新對象進行修改並返回給調度者,因此不可變對象必定是線程安全的。

    常見的不可變對象如String, replace方法,substring方法。基本類型的封裝類.經常使用的還有枚舉類型,以及java.lang.Number的部分子類,如Long和Double等數值包裝類型,BigInteger和BigDecimal等大數據類型.

3.1.2 絕對的線程安全

    絕對的線程安全是指 無論運行時環境如何,調用者都不須要任何額外的同步措施」一般須要付出很大的,甚至有時候是不切實際的代價(Brian Goetz給出的線程安全的定義)。在java語言中很難找到 絕對線程安全的類, 不僅僅是實現成本的問題,對系統運行的性能也有着很大影響。

3.1.3 相對線程安全

    相對的線程安全就是咱們一般意義上所講的線程安全,它須要保證對這個對象單獨的操做是線程安全的,咱們在調用的時候不須要作額外的保障措施,可是對於一些特定順序的連續調用,就可能須要在調用端使用額外的同步手段來保證調用的正確性,Java大部分的線程安全的集合容器都是相對線程安全的。

3.1.4 線程兼容

    線程兼容是指對象自己並非線程安全的,可是能夠經過在調用端正確地使用同步手段來保證對象在併發環境中能夠安全地使用,咱們平時說的這個類不是線程安全的, 應該就是對應這個安全級別(線程兼容)

3.1.5 線程對立

    線程對立是指不管調用端是否採起了同步措施,都沒法在多線程環境中併發使用的代碼,

3.2 實現線程安全

    緊接着的一個問題就是咱們應該如何實現線程安全,這與代碼編寫和虛擬機提供的鎖機制有着密不可分的聯繫,但虛擬機提供的同步和鎖機制也起到了很是重要的做用。由於這篇是虛擬機系列的博文,會更加偏重虛擬機的線程安全實現方面。

3.2.1 互斥同步(阻塞同步)

    互斥同步(Mutual Exclusion&Synchronization)是常見的一種併發正確性保障手段。同步是指在多個線程併發訪問共享數據時,保證共享數據在同一個時刻只被一個(或者是一些,使用信號量的時候)線程使用。而互斥是實現同步的一種手段,臨界區(Critical Section)、互斥量(Mutex)和信號量(Semaphore)都是主要的互斥實現方式。所以,在這4個字裏面,互斥是因,同步是果;互斥是方法,同步是目的    

    在Java中,最基本的互斥同步手段就是synchronized關鍵字,synchronized關鍵字通過編譯以後,會在同步塊的先後分別造成monitorenter和monitorexit這兩個字節碼指令,這兩個字節碼都須要一個reference類型的參數來指明要鎖定和解鎖的對象。若是Java程序中的synchronized明確指定了對象參數,那就是這個對象的reference;若是沒有明確指定,那就根據synchronized修飾的是實例方法仍是類方法,去取對應的對象實例或Class對象來做爲鎖對象。

    根據虛擬機規範的要求,在執行monitorenter指令時,首先要嘗試獲取對象的鎖。若是這個對象沒被鎖定,或者當前線程已經擁有了那個對象的鎖,把鎖的計數器加1,相應的,在執行monitorexit指令時會將鎖計數器減1,當計數器爲0時,鎖就被釋放。若是獲取對象鎖失敗,那當前線程就要阻塞等待,直到對象鎖被另一個線程釋放爲止。

    須要特別注意是,synchronized同步塊對同一條線程來講是可重入的,不會出現本身把本身鎖死的問題。另外,同步塊在已進入的線程執行完以前,會阻塞後面其餘線程的進入。

    除了synchronized以外,咱們還可使用java.util.concurren包中的相關併發工具類進行同步。

3.2.2 非阻塞同步

    互斥同步最主要的問題就是進行線程阻塞和喚醒所帶來的性能問題,互斥同步屬於一種悲觀的併發策略,不管共享數據是否真的會出現競爭,它都要進行加鎖,用戶態核心態轉換、維護鎖計數器和檢查是否有被阻塞的線程須要喚醒等操做。隨着硬件指令集的發展咱們能夠採起基於衝突檢測的樂觀併發策略,即就是先進行操做,若是沒有其餘線程爭用共享數據,那操做就成功了;若是共享數據有爭用,產生了衝突,那就再採起其餘的補償措施。由於不會把線程掛起因此是(Non-Blocking Synchronization)

    衝突檢測,是須要靠硬件指令來完成的,這類指令經常使用的有

    測試並設置(Test-and-Set)
    交換(Swap)
    比較並交換(Compare-and-Swap,CAS)
    加載連接/條件存儲(Load-Linked/Store-Conditional,LL/SC)

3.2.2.1 CAS 算法

    CAS的語義是「我認爲V的值應該爲A,若是是,那麼將V的值更新爲B,不然不修改並告訴V的值實際爲多少」,CAS是項 樂觀鎖 技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知此次競爭中失敗,並能夠再次嘗試。CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。

3.2.3 無需同步

    要保證線程安全,並非必定就要進行同步,二者沒有因果關係。同步只是保證共享數據爭用時的正確性的手段,若是自己不涉及數據共享,天然就不須要同步了,所以Java世界有兩類無需同步的狀況。

3.2.3.1 可重入代碼(Reentrant Code)

    能夠在代碼執行的任什麼時候刻中斷它,轉而去執行另一段代碼(包括遞歸調用它自己),而在控制權返回後,原來的程序不會出現任何錯誤。相對線程安全來講,可重入性是更基本的特性,它能夠保證線程安全,即全部的可重入的代碼都是線程安全的,可是並不是全部的線程安全的代碼都是可重入的。


3.2.3.2 線程本地存儲(Thread Local Storage)

    若是一段代碼中所須要的數據必須與其餘代碼共享,那就看看這些共享數據的代碼是否能保證在同一個線程中執行?若是能保證,咱們就能夠把共享數據的可見範圍限制在同一個線程以內,這樣,無須同步也能保證線程之間不出現數據爭用的問題。

    常見的有  生產者-消費者模式,web中的-request-response 模式。前者是將須要操做同一組數據的線程放到一個隊列裏面順序執行,消除資源競爭的狀況,後者則是,變量線程獨享(Thread-per-Request)。

     Java語言中,若是一個變量要被多線程訪問,可使用volatile關鍵字聲明它爲「易變的」,若是一個變量要被某個線程獨享,能夠經過java.lang.ThreadLocal類來實現線程本地存儲的功能。

相關文章
相關標籤/搜索