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