摘要:內存之間的交互關于主內存和工作內存之間的具體交互協議,內存模型定義了中操作來完成,虛擬機實現的時候必須保證每個操作都是原子的,不可分割的對于和有例外鎖定作用于主內存變量,代表一個變量是一條線程獨占。
并發不一定依賴多線程,但是在java里面談論并發,大多與線程脫不開關系。
線程是大多是面試都會問到的問題。我們都知道,線程是比進程更輕量級的調度單位,線程之間可以共享內存。之前面試的時候,也是這樣回答,迷迷糊糊,沒有一個清晰的概念。
大學的學習的時候,寫C和C++,自己都沒有用過多線程,看過一個Windows編程的書,里面講多線程的時候,一大堆大寫的字母,看著一點都不爽,也是慚愧。后來的實習,寫unity,unity的C#使用的是協程。只有在做了java后端之后,才知道線程到底是怎么用的。了解了java內存模型之后,仔細看了一些資料,對java線程有了更深入的認識,整理寫成這篇文章,用來以后參考。
1 Java內存模型Java虛擬機規范試圖定義一種java內存模型來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓java程序在各種平臺下都能達到一致性內存訪問的效果。
java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量的底層細節。(這里所說的變量包括了實例字段、靜態字段和數組等,但不包括局部變量與方法參數,因為這些是線程私有的,不被共享。)
1.1 主內存和工作內存java規定所有的變量都存儲在主內存。每條線程有自己的工作內存。
線程的工作內存中的變量是主內存中該變量的副本,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同線程間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞需要通過主內存來完成。
1.2 內存之間的交互關于主內存和工作內存之間的具體交互協議,java內存模型定義了8中操作來完成,虛擬機實現的時候必須保證每個操作都是原子的,不可分割的(對于long和double有例外)
lock鎖定:作用于主內存變量,代表一個變量是一條線程獨占。
unlock解鎖:作用于主內存變量,把鎖定的變量解鎖。
read讀取:作用于主內存變量,把變量值從主內存傳到線程的工作內存中,供load使用。
load載入:作用工作內存變量,把上一個read到的值放入到工作內存中的變量中。
use使用:作用于工作內存變量,把工作內存中的一個變量的值傳遞給執行引擎。
assign:作用于工作內存變量,把執行引擎執行過的值賦給工作內存中的變量。
store存儲:作用于工作內存變量,把工作內存中的變量值傳給主內存,供write使用。
這些操作要滿足一定的規則。
1.3 volatilevolatile可以說是java的最輕量級的同步機制。
當一個變量被定義為volatile之后,他就具備兩種特性:
保證此變量對所有線程都是可見的
這里的可見性是指當一個線程修改了某變量的值,新值對于其他線程來講是立即得知的。而普通變量做不到,因為普通變量需要傳遞到主內存中才可以做到這點。
禁止指令重排
對于普通變量來說,僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執性順序一致。
若用volatile修飾變量,在編譯時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。
volatile對于單個的共享變量的讀/寫具有原子性,但是像num++這種復合操作,volatile無法保證其原子性。
1.4 long和doublelong和double是一個64位的數據類型。
虛擬機允許將沒有被volatile修飾的64位變量的讀寫操作分為兩次32位的操作來進行。因此當多個線程操作一個沒有聲明為volatile的long或者double變量,可能出現操作半個變量的情況。
但是這種情況是罕見的,一般商用的虛擬機都是講long和double的讀寫當成原子操作進行的,所以在寫代碼時不需要將long和double專門聲明為volatile。
1.5 原子性、可見性和有序性java的內存模型是圍繞著在并發過程中如何處理原子性、可見性和有序性。
原子性
基本數據類型的訪問讀寫是劇本原子性的。
如果需要一個更大范圍的原子性保證,java提供了lock和unlock操作,對應于寫代碼時就是synchronized關鍵字,因此在synchronized塊之間的操作也是具備原子性的。
可見性
可見性是指當一個線程修改到了一個共享變量的值,其他的線程能夠立即得知這個修改。共享變量的讀寫都是通過主內存作為媒介來處理可見性的。
volatile的特殊規則保證了新值可以立即同步到主內存,每次使用前立即從主內存刷新。
synchronized同步塊的可見性是由”對于一個變量unlock操作之前,必須先把此變量同步回內存中“來實現的。
final的可見性是指被final修飾的字段在構造器中一旦初始化完成,并且構造器沒有把this的引用傳遞出去,那么在其他線程中就能看見final字段的值。
有序性
如果在本線程內觀察,所有的操作都是有序的;如果在一個線程內觀察另一個線程,所有的操作都是無序的。
volatile關鍵字本身就包含了禁止指令重排的語義,而synchronized則是由“一個變量在同一時刻只允許一條線程對其進行lock操作”這條規則來實現有序性的。
如果java內存模型中的所有有序性都是靠著volatile和synchronized來完成,那有些操作將會變得很繁瑣,但是我們在寫java并發代碼的時候沒有感受到這一點,都是因為java有一個“先行發生”原則。
先行發生是java內存模型中定義的兩項操作之間的偏序關系,如果說操作A先發生于操作B,其實就是說在發生B之前,A產生的影響都能被B觀察到,這里的影響包括修改了內存中共享變量的值、發送了消息、調用了方法等等。
程序次序規則
在一個線程內,按程序代碼控制流順序執行。
管程鎖定規則
unlock發生在后面時間同一個鎖的lock操作。
volatile變量規則
volatile變量的寫操作發生在后面時間的讀操作。
線程啟動規則
線程終止規則
線程中斷規則
對象終結規則
一個對象的初始化完成在finalize方法之前。
傳遞性
如果A先行發生B,B先行發生C,那么A先行發生C。
由于指令重排的原因,所以一個操作的時間上的先發生,不代表這個操作就是先行發生;同樣一個操作的先行發生,也不代表這個操作必定在時間上先發生。
2 Java線程 2.1 線程的實現主流的操作系統都提供了線程的實現,java則是在不同的硬件和操作系統的平臺下,對線程的操作提供了統一的處理,一個Thread類的實例就代表了一個線程。Thread類的關鍵方法都是native的,所以java的線程實現也都是依賴于平臺相關的技術手段來實現的。
實現線程主要有3種方式:使用內核線程實現,使用用戶線程實現和使用用戶線程加輕量級進程實現。
2.1.1 使用內核線程實現內核線程就是直接由操作系統內核支持的線程,這種線程由內核來完成線程的切換,內核通過操縱調度器對線程進行調度,并負責將線程的任務映射到各個處理器上。
程序一般不會直接去調用內核線程,而是使用內核線程的一個高級接口——輕量級進程(Light Weigh Process),LWP就是我們通常意義上所說的線程。
由于每個輕量級進程都由一個內核線程支持,這種輕量級進程與內核線程之間1:1的關系成為一對一線程模型。
局限性
雖然由于內核線程的支持,每個輕量級進程都成為了一個獨立的調度單元,即使有一個阻塞,也不影響整個進程的工作,但是還是有一定的局限性:
系統調用代價較高
由于基于內核線程實現,所以各種線程的操作都要進行系統調用。而系統調用的代價比較高,需要在用戶態和內核態來回切換。
系統支持數量有限
每個輕量級進程都需要一個內核線程支持,需要消耗一定的內核資源,所以支持的線程數量是有限的。
2.1.2 使用用戶線程實現指的是完全建立在用戶空間的線程庫上,系統內核不能感知線程存在的實現。用戶線程的建立、同布、銷毀和調度完全在用戶態中完成,不需要內核幫助。
如果程序實現得當,則這些線程都不需要切換到內核態,操作非常快速消耗低,可以支持大規模線程數量。這種進程和用戶線程之間1:N的關系成為一對多線程模型。
局限性
不需要系統內核的,既是優勢也是劣勢。由于沒有系統內核支援,所有的操作都需要程序去處理,由于操作系統只是把處理器資源分給進程,那“阻塞如何處理”、“多處理器系統如何將線程映射到其他處理器上”這類問題的解決十分困難,所以現在使用用戶線程的越來越少了。
2.1.3 使用用戶線程加輕量級進程混合實現在這種混合模式下,既存在用戶線程,也存在輕量級進程。
用戶線程還是完全建立在用戶空間中,因此用戶線程的創建、切換、析構等操作依然廉價,而且支持大規模用戶線程并發、而操作系統提供支持的輕量級進程則作為用戶線程和內核線程之間的橋梁,這樣可以使用內核提供的線程調度和處理器映射,并且用戶線程的系統調用要通過輕量級進程來完成,大大降低了整個進程被完全阻塞的風險。
在這種模式下,用戶線程和輕量級進程數量比不固定N:M,這種模式就是多對多線程模型。
2.1.4 java線程的實現目前的jdk版本中,操作系統支持怎樣的線程模型,很大程度上就決定了jvm的線程是怎么映射的,這點在不同的平臺沒辦法打成一致。線程模型只對線程的并發規模和操作成本產生影響,對編碼和運行都沒什么差異。
windows和linux都是一對一的線程模型。
2.2 線程調度線程的調度是指系統為線程分配處理器使用權的過程,主要的調度方式有兩種:協同式線程調度和搶占式線程調度。
2.2.1 協同式線程調度線程的執性時間由線程本身來控制,線程把自己的工作執性完了之后,要主動通知系統切換到另外一個線程上。Lua的協程就是這樣。
好處
協同式多線程最大的好處就是實現簡單。
由于線程要把自己的事情干完之后才進行線程切換,切換操作對線程是克制的,所以沒有什么線程同步的問題。
壞處
壞處也很明顯,線程執行時間不可控。甚至如果一個線程寫的問題,一直不告訴系統切換,那程序就會一直阻塞。
2.2.2 搶占式線程調度每個線程由系統分配執行時間,線程的切換不是又線程本身來決定。
使用yield方法是可以讓出執行時間,但是要獲取執行時間,線程本身是沒有什么辦法的。
在這種調度模式下,線程的執行時間是系統可控的,也就不會出現一個線程導致整個進程阻塞。
2.2.3 java線程調度java使用的是搶占式線程調度。
雖然java的線程調度是系統來控制的,但是可以通過設置線程優先級的方式,讓某些線程多分配一些時間,某些線程少分配一些時間。
不過線程優先級還是不太靠譜,原因就是java的線程是通過映射到系統的原生線程來實現的,所以線程的調度還是取決于操作系統,操作系統的線程優先級不一定和java的線程優先級一一對應。而且優先級還可能被系統自行改變。所以我們不能在程序中通過優先級來準確的判斷先執行哪一個線程。
2.3 線程的狀態轉換看到網上有好多種說法,不過大致也都是說5種狀態:新建(new)、可運行(runnable)、運行(running)、阻塞(blocked)和死亡(dead)。
而深入理解jvm虛擬機中說java定義了5種線程狀態,在任一時間點,一個線程只能有其中的一種狀態:
新建new
運行runnable
包括了操作系統線程狀態的running和ready,也就是說處于此狀態的線程可能正在執行,也可能正在等待cpu給分配執行時間。
無限期等待waiting
處于這種狀態的線程不會被cpu分配執行時間,需要被其他線程顯示喚醒,能夠導致線程陷入無限期等待的方法有:
沒有設置timeout參數的wait方法。
沒有設置timeout參數的join方法。
LockSupport.park方法。
限期等待timed waiting
處于這種狀態的線程也不會被cpu分配執行時間,不過不需要被其他線程顯示喚醒,是經過一段時間之后,被操作系統自動喚醒。能夠導致線程陷入限期等待的方法有:
sleep方法。
設置timeout參數的wait方法。
設置參數的join方法。
LockSupport.parkNanos方法。
LockSupport.parkUntil方法。
阻塞blocked
線程被阻塞了。在線程等待進入同步區域的時候是這個狀態。
阻塞和等待的區別是:阻塞是排隊等待獲取一個排他鎖,而等待是指等一段時間或者一個喚醒動作。
結束terminated
已經終止的線程。
3 寫在最后并發處理的廣泛應用是使得Amdahl定律代替摩爾定律成為計算機性能發展源動力的根本原因,也是人類壓榨計算機運算能力的最有力武器。有些問題使用越多的資源就能越快地解決——越多的工人參與收割莊稼,那么就能越快地完成收獲。但是另一些任務根本就是串行化的——增加更多的工人根本不可能提高收割速度。
我們使用線程的重要原因之一是為了支配多處理器的能力,我們必須保證問題被恰當地進行了并行化的分解,并且我們的程序有效地使用了這種并行的潛能。有時候良好的設計原則不得不向現實做出一些讓步,我們必須讓計算機正確無誤的運行,首先保證并發的正確性,才能夠在此基礎上談高效,所以線程的安全問題是一個很值得考慮的問題。
雖然一直說java不好,但是java帶給我的影響確實最大的,從java這個平臺里學到了很多有用的東西。現在golang,nodejs,python等語言,每個都是在一方面能秒java,可是java生態和java對軟件行業的影響,是無法被超越的,java這種語言,從出生到現在幾十年了,基本上每次軟件技術的革命都沒有落下,每次都覺得要死的時候,忽然間柳暗花明,枯木逢春。咳咳,扯遠了。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70694.html
摘要:內存模型即,簡稱,其規范了虛擬機與計算機內存時如何協同工作的,規定了一個線程如何和何時看到其他線程修改過的值,以及在必須時,如何同步訪問共享變量。內存模型要求調用棧和本地變量存放在線程棧上,對象存放在堆上。 Java內存模型即Java Memory Model,簡稱JMM,其規范了Java虛擬機與計算機內存時如何協同工作的,規定了一個線程如何和何時看到其他線程修改過的值,以及在必須時,...
摘要:因為管理人員是了解手下的人員以及自己負責的事情的。處理器優化和指令重排上面提到在在和主存之間增加緩存,在多線程場景下會存在緩存一致性問題。有沒有發現,緩存一致性問題其實就是可見性問題。 網上有很多關于Java內存模型的文章,在《深入理解Java虛擬機》和《Java并發編程的藝術》等書中也都有關于這個知識點的介紹。但是,很多人讀完之后還是搞不清楚,甚至有的人說自己更懵了。本文,就來整體的...
摘要:因為管理人員是了解手下的人員以及自己負責的事情的。處理器優化和指令重排上面提到在在和主存之間增加緩存,在多線程場景下會存在緩存一致性問題。有沒有發現,緩存一致性問題其實就是可見性問題。 網上有很多關于Java內存模型的文章,在《深入理解Java虛擬機》和《Java并發編程的藝術》等書中也都有關于這個知識點的介紹。但是,很多人讀完之后還是搞不清楚,甚至有的人說自己更懵了。本文,就來整體的...
閱讀 3157·2023-04-25 18:22
閱讀 2390·2021-11-17 09:33
閱讀 3307·2021-10-11 10:59
閱讀 3238·2021-09-22 15:50
閱讀 2810·2021-09-10 10:50
閱讀 860·2019-08-30 15:53
閱讀 449·2019-08-29 11:21
閱讀 2909·2019-08-26 13:58