国产成人精品久久免费动漫-国产成人精品天堂-国产成人精品区在线观看-国产成人精品日本-a级毛片无码免费真人-a级毛片毛片免费观看久潮喷

您的位置:首頁(yè)技術(shù)文章
文章詳情頁(yè)

JAVA如何解決并發(fā)問(wèn)題

瀏覽:84日期:2022-08-30 18:05:45
并發(fā)問(wèn)題的根源在哪

首先,我們要知道并發(fā)要解決的是什么問(wèn)題?并發(fā)要解決的是單進(jìn)程情況下硬件資源無(wú)法充分利用的問(wèn)題。而造成這一問(wèn)題的主要原因是CPU-內(nèi)存-磁盤三者之間速度差異實(shí)在太大。如果將CPU的速度比作火箭的速度,那么內(nèi)存的速度就像火車,而最慘的磁盤,基本上就相當(dāng)于人雙腿走路。

這樣造成的一個(gè)問(wèn)題,就是CPU快速執(zhí)行完它的任務(wù)的時(shí)候,很長(zhǎng)時(shí)間都會(huì)在等待磁盤或是內(nèi)存的讀寫。

計(jì)算機(jī)的發(fā)展有一部分就是如何重復(fù)利用資源,解決硬件資源之間效率的不平衡,而后就有了多進(jìn)程,多線程的發(fā)展。并且演化出了各種為多進(jìn)程(線程)服務(wù)的東西:

CPU增加緩存機(jī)制,平衡與內(nèi)存的速度差異 增加了多個(gè)概念,CPU時(shí)間片,程序計(jì)數(shù)器,線程切換等,用以更好得服務(wù)并發(fā)場(chǎng)景 編譯器的指令優(yōu)化,希望在內(nèi)部充分利用硬件資源

但是這樣一來(lái),也會(huì)帶來(lái)新的并發(fā)問(wèn)題,歸結(jié)起來(lái)主要有三個(gè)。

由于緩存導(dǎo)致的可見性問(wèn)題 線程切換帶來(lái)的原子性問(wèn)題 編譯器優(yōu)化帶來(lái)的有序性問(wèn)題

我們分別介紹這幾個(gè):

緩存導(dǎo)致的可見性

CPU為了平衡與內(nèi)存之間的性能差異,引入了CPU緩存,這樣CPU執(zhí)行指令修改數(shù)據(jù)的時(shí)候就可以批量直接讀寫CPU緩存的內(nèi)存,一個(gè)階段后再將數(shù)據(jù)寫回到內(nèi)存。

但由于現(xiàn)在多核CPU技術(shù)的發(fā)展,各個(gè)線程可能運(yùn)行在不同CPU核上面,每個(gè)CPU核各有各自的CPU緩存。前面說(shuō)到對(duì)變量的修改通常都會(huì)先寫入CPU緩存,再寫回內(nèi)存。這就會(huì)出現(xiàn)這樣一種情況,線程1修改了變量A,但此時(shí)修改后的變量A只存儲(chǔ)在CPU緩存中。這時(shí)候線程B去內(nèi)存中讀取變量A,依舊只讀取到舊的值,這就是可見性問(wèn)題。

線程切換帶來(lái)的原子性

為了更充分得利用CPU,引入了CPU時(shí)間片時(shí)間片的概念。進(jìn)程或線程通過(guò)爭(zhēng)用CPU時(shí)間片,讓CPU可以更加充分得利用。

比如在進(jìn)行讀寫磁盤等耗時(shí)高的任務(wù)時(shí),就可以將寶貴的CPU資源讓出來(lái)讓其他線程去獲取CPU并執(zhí)行任務(wù)。

但這樣的切換也會(huì)導(dǎo)致問(wèn)題,那就是會(huì)破壞線程某些任務(wù)的原子性。比如java中簡(jiǎn)單的一條語(yǔ)句count += 1。

映射到CPU指令有三條,讀取count變量指令,變量加1指令,變量寫回指令。雖然在高級(jí)語(yǔ)言(java)看來(lái)它就是一條指令,但實(shí)際上確是三條CPU指令,并且這三條指令的原子性無(wú)法保證。也就是說(shuō),可能在執(zhí)行到任意一條指令的時(shí)候被打斷,CPU被其他線程搶占了。而這個(gè)期間變量值可能會(huì)被修改,這里就會(huì)引發(fā)數(shù)據(jù)不一致的情況了。所以高并發(fā)場(chǎng)景下,很多時(shí)候都會(huì)通過(guò)鎖實(shí)現(xiàn)原子性。而這個(gè)問(wèn)題也是很多并發(fā)問(wèn)題的源頭。

編譯器優(yōu)化帶來(lái)的有序性

因?yàn)楝F(xiàn)在程序員編寫的都是高級(jí)語(yǔ)言,編譯器需要將用戶的代碼轉(zhuǎn)成CPU可以執(zhí)行的指令。

同時(shí),由于計(jì)算機(jī)領(lǐng)域的不斷發(fā)展,編譯器也越來(lái)越智能,它會(huì)自動(dòng)對(duì)程序員編寫的代碼進(jìn)行優(yōu)化,而優(yōu)化中就有可能出現(xiàn)實(shí)際執(zhí)行代碼順序和編寫的代碼順序不一樣的情況。

而這種破壞程序有序性的行為,在有些時(shí)候會(huì)出現(xiàn)一些非常微妙且難以察覺的并發(fā)編程bug。

舉個(gè)簡(jiǎn)單的例子,我們常見的單例模式是這樣的:

public class Singleton { private Singleton() {} private static Singleton sInstance; public static Singleton getInstance() { if (sInstance == null) {//第一次驗(yàn)證是否為null synchronized (Singleton.class) { //加鎖 if (sInstance == null) { //第二次驗(yàn)證是否為null sInstance = new Singleton(); //創(chuàng)建對(duì)象 } } } return sInstance; }}

即通過(guò)兩段判斷加鎖來(lái)保證單例的成功生成,但在極小的概率下,可能會(huì)出現(xiàn)異常情況。原因就出現(xiàn)在sInstance = new Singleton();這一行代碼上。這行代碼,我們理解的執(zhí)行順序應(yīng)該是這樣:

為Singleton象分配一個(gè)內(nèi)存空間。 在分配的內(nèi)存空間實(shí)例化對(duì)象。 把Instance 引用地址指向內(nèi)存空間。

但在實(shí)際編譯的過(guò)程中,編譯器有可能會(huì)幫我們進(jìn)行優(yōu)化,優(yōu)化完它的順序可能變成如下:

為Singleton對(duì)象分配一個(gè)內(nèi)存空間。 把instance 引用地址指向內(nèi)存空間。 在分配的內(nèi)存空間實(shí)例化對(duì)象。

按照優(yōu)化完的順序,當(dāng)并發(fā)訪問(wèn)的時(shí)候,可能會(huì)出現(xiàn)這樣的情況

A線程進(jìn)入方法進(jìn)行第1次instance == null判斷。 此時(shí)A線程發(fā)現(xiàn)instance 為null 所以對(duì)Singleton.class加鎖。 然后A線程進(jìn)入方法進(jìn)行第2次instance == null判斷。 然后A線程發(fā)現(xiàn)instance 為null,開始進(jìn)行對(duì)象實(shí)例化。 為對(duì)象分配一個(gè)內(nèi)存空間。 .把Instance 引用地址指向內(nèi)存空間(而就在這個(gè)指令完成后,線程B進(jìn)入了方法)。 B線程首先進(jìn)入方法進(jìn)行第1次instance == null判斷。B線程此時(shí)發(fā)現(xiàn)instance 不為null ,所以它會(huì)直接返回instance (而此時(shí)返回的instance 是A線程還沒有初始化完成的對(duì)象)

最終線程B拿到的instance 是一個(gè)沒有實(shí)例化對(duì)象的空內(nèi)存地址,所以導(dǎo)致instance使用的過(guò)程中造成程序錯(cuò)誤。解決辦法很簡(jiǎn)單,可以給sInstance對(duì)象加上一個(gè)關(guān)鍵字,volatile,這樣編譯器就不會(huì)亂優(yōu)化,有關(guān)volatile的具體內(nèi)容后續(xù)再細(xì)說(shuō)。

主要解決辦法

通過(guò)上面的介紹,其實(shí)可以歸納無(wú)論是CPU緩存,線程切換還是編譯器優(yōu)化亂序,出現(xiàn)問(wèn)題的核心都是因?yàn)槎鄠€(gè)線程要并發(fā)讀寫某個(gè)變量或并發(fā)執(zhí)行某段代碼。那么我們可以控制,一次只讓一個(gè)線程執(zhí)行變量讀寫就可以了,這就是互斥。

而在某些時(shí)候,互斥還不夠,還需要一定的條件。比如一個(gè)生產(chǎn)者一個(gè)消費(fèi)者并發(fā),生產(chǎn)者向隊(duì)列存東西,消費(fèi)者向隊(duì)列拿東西。那么生產(chǎn)者寫的時(shí)候要保證存的時(shí)候隊(duì)列不是滿的,消費(fèi)者要保證拿的時(shí)候隊(duì)列非空。這種線程與線程間需要通信協(xié)作的情況,稱為同步,同步可以說(shuō)是更復(fù)雜的互斥。

既然知道了并發(fā)編程的根源以及同步和互斥,那我們來(lái)看看有哪些解決的思路。其實(shí)一共也就三種:

避免共享 Immutability(不變性) 管程及其他工具

下面我們分別說(shuō)說(shuō)這三種方案的優(yōu)缺點(diǎn)

避免共享

我們先來(lái)說(shuō)說(shuō)避免共享,其實(shí)避免共享說(shuō)是線程本地存儲(chǔ)技術(shù),在java中指的一般就是Threadlocal。ThreadLocal會(huì)為每個(gè)線程提供一個(gè)本地副本,每個(gè)線程都只會(huì)修改自己的ThreadLocal變量。這樣一來(lái)就不會(huì)出現(xiàn)共享變量,也就不會(huì)出現(xiàn)沖突了。

其實(shí)現(xiàn)原理是在ThreadLocal內(nèi)部維護(hù)一個(gè)ThreadLocalMap,每次有線程要獲取對(duì)應(yīng)變量的時(shí)候,先獲取當(dāng)前線程,然后根據(jù)不同線程取不同的值,典型的以空間換時(shí)間。

所以ThreadLocal還是比較適用于需要共享資源,且資源占用空間不大的情況。比如一些連接的session啊等等。但是這種模式應(yīng)用場(chǎng)景也較為有限,比如需要同步情況就難以勝任。

Immutability(不變性)

Immutability在函數(shù)式中用得比較多,函數(shù)式編程的一個(gè)主要目的是要寫出無(wú)副作用的代碼,有關(guān)什么是無(wú)副作用可以參考我以前的文章Scala函數(shù)式編程指南(一) 函數(shù)式思想介紹。而無(wú)副作用的一個(gè)主要特點(diǎn)就是變量都是Immutability即不可變的,即創(chuàng)建對(duì)象后不會(huì)再修改對(duì)象,比如scala默認(rèn)的變量和數(shù)據(jù)結(jié)構(gòu)都是不可變的。而在java中,不變性變量即通過(guò)final修飾的變量,如String,Long,Double等類型都是Immutability的,它們的內(nèi)部實(shí)現(xiàn)都是基于final關(guān)鍵字的。

那這又和并發(fā)編程有什么關(guān)系呢?其實(shí)啊,并發(fā)問(wèn)題很大部分原因就是因?yàn)榫€程切換破壞了原子性,這又導(dǎo)致線程隨意對(duì)變量的讀寫破壞了數(shù)據(jù)的一致性。而不變性就不必?fù)?dān)心這個(gè)問(wèn)題,因?yàn)樽兞慷际遣蛔?,不可寫只能讀的。在這種編程模式下,你要修改一個(gè)變量,那么只能新生成一個(gè)。這樣做的好處很明顯,但壞處也是顯而易見,那就是引入了額外的編程復(fù)雜度,喪失了代碼的可讀性和易用性。

因?yàn)槿绱?,不變性的并發(fā)解決方案其實(shí)相對(duì)而已沒那么廣泛,其中比較有代表性的算是Actor并發(fā)編程模型,我以前也有討論過(guò),有興趣可以看看Actor模型淺析 一致性和隔離性,這種編程模型和常規(guī)并發(fā)解決方案有很顯著的差異。按我的了解,Acctor模式多用在分布式系統(tǒng)的一些協(xié)調(diào)功能,比如維持集群中多個(gè)機(jī)器的心跳通信等等。如果在單機(jī)并發(fā)環(huán)境下,還是下面要介紹的管程類工具才是利器。

管程及其他工具

其實(shí)最早的操作系統(tǒng)中,解決并發(fā)問(wèn)題用的是信號(hào)量,信號(hào)量通過(guò)兩個(gè)原子操作wait(S),和signal(S)(俗稱P,V操作)來(lái)實(shí)現(xiàn)訪問(wèn)資源互斥和同步。比如下面這個(gè)小例子:

//整型信號(hào)量定義int S;//P操作wait(S){ while(S<=0); S--;}//V操作signal(S){ S++;}

雖然信號(hào)量方便有效,但信號(hào)量要對(duì)每個(gè)共享資源都實(shí)現(xiàn)對(duì)應(yīng)的P和V操作,這使得并發(fā)編程中可能要出現(xiàn)大量的P,V操作,并且這部分內(nèi)容難以抽象出來(lái)。

為了更好地實(shí)現(xiàn)同步互斥,于是就產(chǎn)生了管程(即Monitor,也有翻譯為監(jiān)視器),值得一提的是,管程也有幾種模型,分別是:Hasen模型,Hoare模型和MESA模型。其中MESA模型應(yīng)用最廣泛,java也是參考自MESA模型。這里簡(jiǎn)單介紹下管程的理論知識(shí),這部分內(nèi)容參考自進(jìn)程同步機(jī)制-----為進(jìn)程并發(fā)執(zhí)行保駕護(hù)航,希望了解更多管程理論知識(shí)的童鞋可以看看。

我們來(lái)通過(guò)一個(gè)經(jīng)典的生產(chǎn)-消費(fèi)隊(duì)列來(lái)解釋,如下圖

JAVA如何解決并發(fā)問(wèn)題

我們先解釋下圖中右半部分的內(nèi)容,右上角有一個(gè)等待調(diào)用的線程隊(duì)列,管程中每次只能有一個(gè)線程在執(zhí)行任務(wù),所以多個(gè)任務(wù)需要等待。然后是各個(gè)名詞的意思,生產(chǎn)-消費(fèi)需要往隊(duì)列寫入和取出東西,這里的隊(duì)列就是共享變量,對(duì)共享資源進(jìn)行操作稱之為過(guò)程(入隊(duì)和出隊(duì)兩個(gè)過(guò)程)。而向隊(duì)列寫入和取出是有條件的,寫入的時(shí)候隊(duì)列必須是非滿的,取出的時(shí)候隊(duì)列必須是非空的,這兩個(gè)條件被稱為條件變量。

然后再來(lái)看看左半部分的內(nèi)容,假設(shè)線程T1讀取共享變量(即隊(duì)列),此時(shí)發(fā)現(xiàn)隊(duì)列為空(條件變量之一),那么T1此時(shí)需要等待,去哪里等呢?去條件變量隊(duì)列不能為空對(duì)應(yīng)的隊(duì)列中去等待。此時(shí)另一個(gè)線程T2向共享變量隊(duì)列寫數(shù)據(jù),通過(guò)了條件變量隊(duì)列不能滿,那么寫完后就會(huì)通知線程T1。但因?yàn)楣艹痰南拗?,管程中只能有一個(gè)線程在執(zhí)行,所以T1線程不能立即執(zhí)行,它會(huì)回到右上角的線程等待隊(duì)列等待(不同的管程模型在這里是有分歧的,比如Hasen模型是立即中斷T2線程讓隊(duì)列中下一個(gè)線程執(zhí)行)。

解釋完這個(gè)圖,管程的概念也就呼之欲出了,

hansen對(duì)管程的定義如下:一個(gè)管程定義了一個(gè)數(shù)據(jù)結(jié)構(gòu)和能力為并發(fā)進(jìn)程所執(zhí)行(在該數(shù)據(jù)結(jié)構(gòu)上)的一組操作,這組操作能同步進(jìn)程和改變管程中的數(shù)據(jù)。

本質(zhì)上,管程是對(duì)共享資源以及對(duì)共享資源的操作抽象成變量和方法,要操作共享變量?jī)H能通過(guò)管程提供的方法(比如上面的入隊(duì)和出隊(duì))間接訪問(wèn)。所以你會(huì)發(fā)現(xiàn)管程其實(shí)和面向?qū)ο蟮睦砟钍鞘窒嘟?,在java中,主要提供了低層次了synchronized關(guān)鍵字和wait(),notify()等方法。同時(shí)還提供了高層次的ReenTrantLock和Condition來(lái)實(shí)現(xiàn)管程模型。

以上就是JAVA如何解決并發(fā)問(wèn)題的詳細(xì)內(nèi)容,更多關(guān)于JAVA 并發(fā)的資料請(qǐng)關(guān)注好吧啦網(wǎng)其它相關(guān)文章!

標(biāo)簽: Java
相關(guān)文章:
主站蜘蛛池模板: 波多野结衣在线观看高清免费资源 | 中文亚洲欧美 | 久久老司机波多野结衣 | 国产美女精品在线 | 香蕉久久久久久狠狠色 | 精品国产免费一区二区三区五区 | 悟空影视大全免费影视 | 国产一区二区三区免费大片天美 | 亚洲午夜片子大全精品 | 美女一级毛片视频 | 国产成人精品999在线观看 | 国产精品成人观看视频网站 | 美女喷水网站 | 日韩三级中文字幕 | 国产在线一区二区三区欧美 | 国产精品日本欧美一区二区 | 国产真实自拍 | 久久影院视频 | 欧美日本亚洲国产一区二区 | 亚洲国产精品日韩在线 | 国产精品免费视频一区二区三区 | 欧美成人一级视频 | 国产免费一区不卡在线 | 美女一级视频 | 久久久久久久久久毛片精品美女 | 色天使色婷婷在线影院亚洲 | 一级毛片免费在线观看网站 | 成人免费在线网站 | 欧美日韩精品在线播放 | 精品一区二区三区在线视频观看 | 久久国产免费一区二区三区 | 欧美色大成网站www永久男同 | 综合自拍亚洲综合图区美腿丝袜 | 国产精品黄色片 | 午夜精品一区二区三区在线观看 | 一二三中文乱码亚洲乱码 | 最新欧美一级视频 | 国产欧美一区二区三区在线 | 成人精品一区二区激情 | 亚洲福利国产精品17p | 国内精品久久久久久 |