【讀書會第十二期】 《深入理解Java虛擬機》第5章Java內存模型與線程
Java內存模型與線程
Java存儲器模型的首要目標就是為程序中的各種變數設定一個存取規則,也就是將變數存入虛擬機上,以及從記憶體中提取變數。這里的變量不同于 Java編程中的變量,包含實例字段,靜態字段,以及組成陣列對象的元素,但不包含本地變量和方法參數。為了提高性能, Java存儲器模型不會對執行引擎使用特定的寄存器或緩存與主機存儲器進行交互,也不會對技術編譯器進行修改的指令進行限制。
Java的記憶體模式將所有的變量都儲存在主要記憶體(這里的主要記憶體就像是實體硬件中的主要記憶體,兩者可以相仿,但是實際上,這只是虛擬機記憶體的一部分)。每個線程都有自己的工作記憶體(工作記憶,類似于處理器快取),線程的工作記憶體存儲著執行緒所用的變數的主要記憶體,執行緒對變數的一切動作(讀取、賦值等)都必須在工作記憶體中完成,無法直接寫入主記憶體。線程間的可變數據傳輸必須經過主內存,線程、主內存和工作內存三者的相互關系如下圖:
原子性、可見性與有序性
1、原子性(Atomicity)
由Java內存模型來直接保證原子性的變量操作包包括read、load、assign、use、store和write這六個,我們大致認為,基本數據類型的訪問、讀寫都是具備原子性的。如果應用場景需要一個更大范圍的原子性保證,Java內存模型還提供了lock和unlock操作來滿足這種需求,盡管虛擬機未把lock和unlock操作直接開放給用戶使用,但是卻提供了更高層次的字節碼指令monitorenter和monitorexit來隱式地使用這兩個操作。這兩個字節碼指令反映到Java代碼中就是同步塊——synchronized關鍵字,因此在synchronized塊之間的操作也具備原子性。
2、可見性(Visibility)
可見性就是值當一個線程修改了共享變量的值時,其他線程能夠立即得知這個修改。除了volatile關鍵字之外,Java還有兩個關鍵字能實現可見性,他們是synchronized和final。同步塊的可見性是由“對一個變量執行unlock之前,必須先把此變量同步回主內存中(執行store、write操作)”這條規則獲得的。而final關鍵字的可見性是指:被final修飾的字段在構造器中一旦被初始化完成,并且構造器沒有把“this”的引用傳遞出去(this引用逃逸是一件很危險的事情,其他線程有可能通過這個引用訪問到“初始化了一半”的對象),那么在其他線程中就能看見final字段的值。
3、有序性
Java程序中天然的有序性可以總結為一句話:如果在本線程內觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。前半句是指“線程內似表現為串行的語義”(Within-Thread As-If-Serial Semantic),后半句是指“指令重排序”現象和“工作內存與主內存同步延遲”現象。
Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性,volatile關鍵字本身包含了禁止指令重排序的語義,而synchronized則是由“一個變量在同一個時刻只允許一條線程對其進行lock操作”這條規則獲得的,這個規則決定了只有同步一個鎖的兩個同步塊只能串行進入。
線程的實現
我們知道,線程是比進程更輕量級的調度執行單位,線程的引入,可以把一個進程的資源分配和執行調度分開,各個線程既可以共享進程資源(內存地址、文件I/O等),又可以獨立調度。目前線程是Java里面進行處理器資源調度的最基本單位,不過如果日后Loom項目能成功為Java引入纖程 (Fiber)的話,可能就會改變這一點。
主流的操作系統都提供了線程實現,Java語言則提供了在不同硬件和操作系統平臺下對線程操作的統一處理,每個已經調用過start()方法且還未結束的java.lang.Thread類的實例就代表著一個線程。我們注意到Thread類與大部分的Java類庫API有著顯著差別,它的所有關鍵方法都被聲明為Native。在 Java類庫API中,一個Native方法往往就意味著這個方法沒有使用或無法使用平臺無關的手段來實現(當然也可能是為了執行效率而使用Native方法,不過通常最高效率的手段也就是平臺相關的手段)。正因為這個原因,本節的標題被定為“線程的實現”而不是“Java線程的實現”,在稍后介紹的實現方式中,我們也先把Java的技術背景放下,以一個通用的應用程序的角度來看看線程是如何實現的。
實現線程主要有三種方式:使用內核線程實現(1:1實現),使用用戶線程實現(1:N實現), 使用用戶線程加輕量級進程混合實現(N:M實現)。
使用內核線程實現的方式也被稱為1:1實現。內核線程(Kernel-Level Thread,KLT)就是直接由操作系統內核(Kernel,下稱內核)支持的線程,這種線程由內核來完成線程切換,內核通過操縱調度器(Scheduler)對線程進行調度,并負責將線程的任務映射到各個處理器上。每個內核線程可以視為內核的一個分身,這樣操作系統就有能力同時處理多件事情,支持多線程的內核就稱為多線程內核 (Multi-Threads Kernel)。
程序一般不會直接使用內核線程,而是使用內核線程的一種高級接口——輕量級進程(Light Weight Process,LWP),輕量級進程就是我們通常意義上所講的線程,由于每個輕量級進程都由一個內核線程支持,因此只有先支持內核線程,才能有輕量級進程。這種輕量級進程與內核線程之間1:1 的關系稱為一對一的線程模型,如下圖所示。
由于內核線程的支持,每個輕量級進程都成為一個獨立的調度單元,即使其中某一個輕量級進程在系統調用中被阻塞了,也不會影響整個進程繼續工作。輕量級進程也具有它的局限性:首先,由于是基于內核線程實現的,所以各種線程操作,如創建、析構及同步,都需要進行系統調用。而系統調用的代價相對較高,需要在用戶態(User Mode)和內核態(Kernel Mode)中來回切換。其次,每個輕量級進程都需要有一個內核線程的支持,因此輕量級進程要消耗一定的內核資源(如內核線程的??臻g),因此一個系統支持輕量級進程的數量是有限的。
使用用戶線程實現的方式被稱為1:N實現。廣義上來講,一個線程只要不是內核線程,都可以認為是用戶線程(User Thread,UT)的一種,因此從這個定義上看,輕量級進程也屬于用戶線程,但輕量級進程的實現始終是建立在內核之上的,許多操作都要進行系統調用,因此效率會受到限制,并不具備通常意義上的用戶線程的優點。
而狹義上的用戶線程指的是完全建立在用戶空間的線程庫上,系統內核不能感知到用戶線程的存在及如何實現的。用戶線程的建立、同步、銷毀和調度完全在用戶態中完成,不需要內核的幫助。如果程序實現得當,這種線程不需要切換到內核態,因此操作可以是非??焖偾业拖牡?,也能夠支持規模更大的線程數量,部分高性能數據庫中的多線程就是由用戶線程實現的。這種進程與用戶線程之間1:N的關系稱為一對多的線程模型,如上圖所示。
用戶線程的優勢在于不需要系統內核支援,劣勢也在于沒有系統內核的支援,所有的線程操作都需要由用戶程序自己去處理。線程的創建、銷毀、切換和調度都是用戶必須考慮的問題,而且由于操作系統只把處理器資源分配到進程,那諸如“阻塞如何處理”“多處理器系統中如何將線程映射到其他處理器上”這類問題解決起來將會異常困難,甚至有些是不可能實現的。因為使用用戶線程實現的程序通常都比較復雜[1],除了有明確的需求外(譬如以前在不支持多線程的操作系統中的多線程程序、需要支持大規模線程數量的應用),一般的應用程序都不傾向使用用戶線程。Java、Ruby等語言都曾經使用過用戶線程,最終又都放棄了使用它。但是近年來許多新的、以高并發為賣點的編程語言又普遍支持了用戶線程,譬如Golang、Erlang等,使得用戶線程的使用率有所回升。
線程除了依賴內核線程實現和完全由用戶程序自己實現之外,還有一種將內核線程與用戶線程一起使用的實現方式,被稱為N:M實現。在這種混合實現下,既存在用戶線程,也存在輕量級進程。用戶線程還是完全建立在用戶空間中,因此用戶線程的創建、切換、析構等操作依然廉價,并且可以支持大規模的用戶線程并發。而操作系統支持的輕量級進程則作為用戶線程和內核線程之間的橋梁,這樣可以使用內核提供的線程調度功能及處理器映射,并且用戶線程的系統調用要通過輕量級進程來完成,這大大降低了整個進程被完全阻塞的風險。在這種混合模式中,用戶線程與輕量級進程的數量比是不定的,是N:M的關系,如下圖所示,這種就是多對多的線程模型。
許多UNIX系列的操作系統,如Solaris、HP-UX等都提供了M:N的線程模型實現。在這些操作系統上的應用也相對更容易應用M:N的線程模型。
Java線程調度
協同式線程調度
主動切換
搶占式線程調度系統
分配時間片
狀態轉換
Java語言定義了6種線程狀態,在任意一個時間點中,一個線程只能有且只有其中的一種狀態,并且可以通過特定的方法在不同狀態之間轉換。這6種狀態分別是:
新建(New):創建后尚未啟動的線程處于這種狀態。
運行(Runnable):包括操作系統線程狀態中的Running和Ready,也就是處于此狀態的線程有可能正在執行,也有可能正在等待著操作系統為它分配執行時間。
無限期等待(Waiting):處于這種狀態的線程不會被分配處理器執行時間,它們要等待被其他線程顯式喚醒。以下方法會讓線程陷入無限期的等待狀態:
■沒有設置Timeout參數的Object::wait()方法;
■沒有設置Timeout參數的Thread::join()方法;
■LockSupport::park()方法。
限期等待(TimedWaiting):處于這種狀態的線程也不會被分配處理器執行時間,不過無須等待被其他線程顯式喚醒,在一定時間之后它們會由系統自動喚醒。以下方法會讓線程進入限期等待狀態:
■Thread::sleep()方法;
■設置了Timeout參數的Object::wait()方法;
■設置了Timeout參數的Thread::join()方法;
■LockSupport::parkNanos()方法;
■LockSupport::parkUntil()方法。
阻塞(Blocked):線程被阻塞了,“阻塞狀態”與“等待狀態”的區別是“阻塞狀態”在等待著獲取到一個排它鎖,這個事件將在另外一個線程放棄這個鎖的時候發生;而“等待狀態”則是在等待一段時間,或者喚醒動作的發生。在程序等待進入同步區域的時候,線程將進入這種狀態。
結束(Terminated):已終止線程的線程狀態,線程已經結束執行。
上述6種狀態在遇到特定事件發生的時候將會互相轉換,它們的轉換關系如下圖所示。
Java 任務調度 虛擬化
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。