真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

Java 并發(fā)編程解析 | 如何正確理解Java領(lǐng)域中的鎖機(jī)制,我們一般需要掌握哪些理論知識(shí)?

蒼穹之邊,浩瀚之摯,眰恦之美; 悟心悟性,善始善終,惟善惟道! —— 朝槿《朝槿兮年說(shuō)》

成都創(chuàng)新互聯(lián)-專業(yè)網(wǎng)站定制、快速模板網(wǎng)站建設(shè)、高性價(jià)比阿圖什網(wǎng)站開發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫(kù),直接使用。一站式阿圖什網(wǎng)站制作公司更省心,省錢,快速模板網(wǎng)站建設(shè)找我們,業(yè)務(wù)覆蓋阿圖什地區(qū)。費(fèi)用合理售后完善,十載實(shí)體公司更值得信賴。

寫在開頭

提起Java領(lǐng)域中的鎖,是否有種“道不盡紅塵奢戀,訴不完人間恩怨“的”感同身受“之感?細(xì)數(shù)那些個(gè)“玩意兒”,你對(duì)Java的熱情是否還如初戀般“人生若只如初見(jiàn)”?

Java中對(duì)于鎖的實(shí)現(xiàn)真可謂是“百花齊放”,按照編程友好程度來(lái)說(shuō),美其名曰是Java提供了種類豐富的鎖,每種鎖因其特性的不同,在適當(dāng)?shù)膱?chǎng)景下能夠展現(xiàn)出非常高的效率。

但是,從理解的難度上來(lái)講,其類型錯(cuò)中復(fù)雜,主要原因是Java是按照是否含有某一特性來(lái)定義鎖的實(shí)現(xiàn),如果不能正確理解其含義,了解其特性的話,往往都會(huì)深陷其中,難可自拔。

查詢過(guò)很多技術(shù)資料與相關(guān)書籍,對(duì)其介紹真可謂是“模棱兩可”,生怕我們搞懂了似的,但是這也是我們無(wú)法繞過(guò)去的一個(gè)“坎坎”,除非有其他的選擇。

作為一名Java Developer來(lái)說(shuō),正確了解和掌握這些鎖的機(jī)制和原理,需要我們帶著一些實(shí)際問(wèn)題,通過(guò)特性將鎖進(jìn)行分組歸類,才能真正意義上理解和掌握。

比如,在Java領(lǐng)域中,針對(duì)于不同場(chǎng)景提供的鎖,都用于解決什么問(wèn)題?其實(shí)現(xiàn)方式是什么?各自又有什么特點(diǎn),對(duì)應(yīng)的應(yīng)用有哪些?

帶著這些問(wèn)題,今天我們就一起來(lái)盤一盤,Java領(lǐng)域中的鎖機(jī)制,盤點(diǎn)一下相關(guān)知識(shí)點(diǎn),以及不同的鎖的適用場(chǎng)景,幫助我們更快捷的理解和掌握這項(xiàng)必備技術(shù)奧義。

關(guān)健術(shù)語(yǔ)

本文用到的一些關(guān)鍵詞語(yǔ)以及常用術(shù)語(yǔ),主要如下:

  • 線程調(diào)度(Thread Scheduling ):系統(tǒng)分配處理器使用權(quán)的過(guò)程,主要調(diào)度方式有兩種,分別是協(xié)同式線程調(diào)度(Cooperative Threads-Scheduling)和搶占式線程調(diào)度(Preemptive Threads-Scheduling)。
  • 線程切換(Thread Switch ):主要是指在并發(fā)過(guò)程中,多線程之間會(huì)對(duì)上下文進(jìn)行切換資源,并交叉執(zhí)行的一種并發(fā)機(jī)制。
  • 指令重排(Command Reorder ): 指編譯器或處理器為了優(yōu)化性能而采取的一種手段,在不存在數(shù)據(jù)依賴性情況下(如寫后讀,讀后寫,寫后寫),調(diào)整代碼執(zhí)行順序。
  • 內(nèi)存屏障(Memory Barrier): 也稱內(nèi)存柵欄,內(nèi)存柵障,屏障指令等, 是一類同步屏障指令,是CPU或編譯器在對(duì)內(nèi)存隨機(jī)訪問(wèn)的操作中的一個(gè)同步點(diǎn),使得此點(diǎn)之前的所有讀寫操作都執(zhí)行后才可以開始執(zhí)行此點(diǎn)之后的操作。

基本概述

縱觀Java領(lǐng)域中“五花八門”的鎖,我們可以依據(jù)Java內(nèi)存模型的工作機(jī)制,來(lái)具體分析一下對(duì)應(yīng)問(wèn)題的提出和表現(xiàn),這也不失為打開Java領(lǐng)域中鎖機(jī)制的“敲門磚”。

從本質(zhì)上講,鎖是一種協(xié)調(diào)多個(gè)進(jìn)程 或者多個(gè)線程對(duì)某一個(gè)資源的訪問(wèn)的控制機(jī)制。

一.計(jì)算機(jī)運(yùn)行模型

計(jì)算機(jī)運(yùn)行模型主要是描述計(jì)算機(jī)系統(tǒng)體系結(jié)構(gòu)的基本模型,一般主要是指CPU處理器結(jié)構(gòu)。

在計(jì)算機(jī)體系結(jié)構(gòu)中,中央處理器(CPU,Central Processing Unit)是一塊超大規(guī)模的集成電路,是一臺(tái)計(jì)算機(jī)的運(yùn)算核心(Core)和控制核心( Control Unit)。它的功能主要是解釋計(jì)算機(jī)指令以及處理計(jì)算機(jī)軟件中的數(shù)據(jù)。

一個(gè)計(jì)算能夠運(yùn)行起來(lái),主要是依靠CPU來(lái)負(fù)責(zé)執(zhí)行我們的輸入指令的,通常情況下,我們都把這些指令統(tǒng)稱為程序。

一般CPU決定著程序的運(yùn)行速度,可以看出CPU對(duì)程序的執(zhí)行有很重要的作用,但是一個(gè)計(jì)算機(jī)程序的運(yùn)行快慢并不是完全由CPU決定,除了CPU還有內(nèi)存、閃存等。

由此可見(jiàn),一個(gè)CPU主要由控制單元,算術(shù)邏輯單元和寄存器單元等3個(gè)部分組成。其中:

  • 控制單元( Control Unit): 屬于CPU的控制指揮中心,主要負(fù)責(zé)指揮CPU工作,通過(guò)向算術(shù)邏輯單元和寄存器單元來(lái)發(fā)送控制指令達(dá)到控制效果。
  • 算術(shù)邏輯單元(Arithmetic Logic Unit, ALU): 主要負(fù)責(zé)執(zhí)行運(yùn)算,一般是指算術(shù)運(yùn)算和邏輯運(yùn)算,主要是依據(jù)控制單元發(fā)送過(guò)來(lái)的指令進(jìn)行處理。
  • 寄存器單元(Register Unit): 主要用于存儲(chǔ)臨時(shí)數(shù)據(jù),保存著等待處理和已經(jīng)處理的數(shù)據(jù)。

一般來(lái)說(shuō),寄存器單元是為了減少CPU對(duì)內(nèi)存的訪問(wèn)次數(shù),提升數(shù)據(jù)讀取性能而提出的,CPU中的寄存器單元主要分為通用寄存器和專用寄存器兩個(gè)種,其中:

  • 通用寄存器:主要用于臨時(shí)存放CPU正在使用的數(shù)據(jù)。
  • 專用寄存器:主要用于臨時(shí)存放類似指令寄存器和程序計(jì)數(shù)器等CPU中專有用途的數(shù)據(jù)。其中:
    • 指令寄存器:用于存儲(chǔ)正在執(zhí)行的指令
    • 程序計(jì)數(shù)器: 保存等待執(zhí)行的指令地址

簡(jiǎn)單來(lái)說(shuō),CPU與主存儲(chǔ)器主要是通過(guò)總線來(lái)進(jìn)行通信,CPU通過(guò)控制單元來(lái)操作主存中的數(shù)據(jù)。而CPU與其他設(shè)備的通信都是由控制來(lái)實(shí)現(xiàn)。

綜上所述,我們便可以得到一個(gè)計(jì)算機(jī)內(nèi)存模型的大致雛形,接下來(lái),我們便來(lái)一起盤點(diǎn)解析是計(jì)算機(jī)內(nèi)存模型的基本奧義。

二.計(jì)算機(jī)內(nèi)存模型

計(jì)算機(jī)內(nèi)存模型一般是指計(jì)算系統(tǒng)底層與編程語(yǔ)言之間的約束規(guī)范,主要是描述計(jì)算機(jī)程序與共享存儲(chǔ)器訪問(wèn)的行為特征表現(xiàn)。

根據(jù)介紹計(jì)算機(jī)運(yùn)行模型來(lái)看,計(jì)算機(jī)內(nèi)存模型可以幫助以及指導(dǎo)我們理解Java內(nèi)存模型,主要在如下的兩個(gè)方面:

  • 首先,系統(tǒng)底層希望能夠?qū)Τ绦蜻M(jìn)行更多的優(yōu)化策略,一般主要是針對(duì)處理器和編譯器,從而提高運(yùn)行性能。
  • 其次,為編程語(yǔ)言帶來(lái)了更多的可編程性問(wèn)題,主要是復(fù)雜的內(nèi)存模型會(huì)有更多的約束,從而增加了程序設(shè)計(jì)的編程難度。

由此可見(jiàn),內(nèi)存模型用于定義處理器間的各層緩存與共享內(nèi)存的同步機(jī)制,以及線程與內(nèi)存之間交互的規(guī)則。

在操作系統(tǒng)層面,內(nèi)存主要可以分為物理內(nèi)存與虛擬內(nèi)存的概念,其中:

  • 物理內(nèi)存(Physical Memory): 通常指通過(guò)安裝內(nèi)存條而獲得的臨時(shí)儲(chǔ)存空間。主要作用是在計(jì)算機(jī)運(yùn)行時(shí)為操作系統(tǒng)和各種程序提供臨時(shí)儲(chǔ)存。常見(jiàn)的物理內(nèi)存規(guī)格有256M、512M、1G、2G等。
  • 虛擬內(nèi)存(Virtual Memory):計(jì)算機(jī)系統(tǒng)內(nèi)存管理的一種技術(shù)。它使得應(yīng)用程序認(rèn)為它擁有連續(xù)可用的內(nèi)存(一個(gè)連續(xù)完整的地址空間),它通常是被分隔成多個(gè)物理內(nèi)存碎片,還有部分暫時(shí)存儲(chǔ)在外部磁盤存儲(chǔ)器上,在需要時(shí)進(jìn)行數(shù)據(jù)交換。

一般情況下,當(dāng)物理內(nèi)存不足時(shí),可以用虛擬內(nèi)存代替, 在虛擬內(nèi)存出現(xiàn)之前,程序?qū)ぶ酚玫亩际俏锢淼刂贰?/p>

從常見(jiàn)的存儲(chǔ)介質(zhì)來(lái)看,主要有:寄存器(Register),高速緩存(Cache),隨機(jī)存取存儲(chǔ)器(RAM),只讀存儲(chǔ)器(ROM)等4種,按照讀取快慢的順序是:Register>Cache>RAM>ROM。其中:

  • 寄存器(Register): CPU處理器的一部分,主要分為通用寄存器和專用寄存器。
  • 高速緩存(Cache):用于減少 CPU 處理器訪問(wèn)內(nèi)存所需平均時(shí)間的部件,一般是指L1/L2/L3層高級(jí)緩存。
  • 隨機(jī)存取存儲(chǔ)器(Random Access Memory,RAM):與CPU直接交換數(shù)據(jù)的內(nèi)部存儲(chǔ)器,它可以隨時(shí)讀寫,而且速度很快,通常作為操作系統(tǒng)或其他正在運(yùn)行中的程序的臨時(shí)數(shù)據(jù)存儲(chǔ)媒介。
  • 只讀存儲(chǔ)器(Read-Only Memory,ROM):所存儲(chǔ)的數(shù)據(jù)通常都是裝入主機(jī)之前就寫好的,在工作的時(shí)候只能讀取而不能像隨機(jī)存儲(chǔ)器那樣隨便寫入。

由于CPU的運(yùn)算速度比主存(物理內(nèi)存)的存取速度快很多,為了提高處理速度,現(xiàn)代CPU不直接和主存進(jìn)行通信,而是在CPU和主存之間設(shè)計(jì)了多層的Cache(高速緩存),越靠近CPU的高速緩存越快,容
量也越小。

按照數(shù)據(jù)讀取順序和與CPU內(nèi)核結(jié)合的緊密程度來(lái)看,大多數(shù)采用多層緩存策略,最經(jīng)典的就三層高速緩存架構(gòu)。

也就是我們常說(shuō)的,CPU高速緩存有L1和L2高速緩存(即一級(jí)高速緩存和二級(jí)緩存高速),部分高端CPU還具有L3高速緩存(即三級(jí)高速緩存):

CPU內(nèi)核讀取數(shù)據(jù)時(shí),先從L1高速緩存中讀取,如果沒(méi)有命中,再到L2、L3高速緩存中讀取,假如這些高速緩存都沒(méi)有命中,它就會(huì)到主存中讀取所需要的數(shù)據(jù)。

每一級(jí)高速緩存中所存儲(chǔ)的數(shù)據(jù)都是下一級(jí)高速緩存的一部分,越靠近CPU的高速緩存讀取越快,容量也越小。

當(dāng)然,系統(tǒng)還擁有一塊主存(即主內(nèi)存),由系統(tǒng)中的所有CPU共享。擁有L3高速緩存的CPU,CPU存取數(shù)據(jù)的命中率可達(dá)95%,也就是說(shuō)只有不到5%的數(shù)據(jù)需要從主存中去存取。

因此,高速緩存大大縮小了高速CPU內(nèi)核與低速主存之間的速度差距,基本體現(xiàn)在如下:

  • L1高速緩存:最接近CPU,容量最小、存取速度最快,每個(gè)核上都有一個(gè)L1高速緩存。
  • L2高速緩存:容量更大、速度低些,在一般情況下,每個(gè)內(nèi)核上都有一個(gè)獨(dú)立的L2高速緩存。
  • L3高速緩存:最接近主存,容量最大、速度最低,由在同一個(gè)CPU芯片板上的不同CPU內(nèi)核共享。

總結(jié)來(lái)說(shuō),CPU通過(guò)高速緩存進(jìn)行數(shù)據(jù)讀取有以下優(yōu)勢(shì):

  • 寫緩沖區(qū)可以保證指令流水線持續(xù)運(yùn)行,可以避免由于CPU停頓下來(lái)等待向內(nèi)存寫入數(shù)據(jù)而產(chǎn)生的延遲。
  • 通過(guò)以批處理的方式刷新寫緩沖區(qū),以及合并寫緩沖區(qū)中對(duì)同一內(nèi)存地址的多次寫,減少對(duì)內(nèi)存總線的占用。

綜上所述,一般來(lái)說(shuō),對(duì)于單線程程序,編譯器和處理器的優(yōu)化可以對(duì)編程開發(fā)足夠透明,對(duì)其優(yōu)化的效果不會(huì)影響結(jié)果的準(zhǔn)確性。

而在多線程程序來(lái)說(shuō),為了提升性能優(yōu)化的同時(shí)又達(dá)到兼顧執(zhí)行結(jié)果的準(zhǔn)確性,需要一定程度上內(nèi)存模型規(guī)范。

由于經(jīng)常會(huì)采用多層緩存策略,這就導(dǎo)致了一個(gè)比較經(jīng)典的并發(fā)編程三大問(wèn)題之一的共享變量的可見(jiàn)性問(wèn)題,除了可見(jiàn)性問(wèn)題之外,當(dāng)然還有原子性問(wèn)題和有序性問(wèn)題。

由此來(lái)看,在計(jì)算機(jī)內(nèi)存模型中,主要可以提出主存和工作內(nèi)存的概念,其中:

  • 主存:一般指的物理內(nèi)存,主要是指RAM隨機(jī)存取存儲(chǔ)器和ROM只讀存儲(chǔ)器等
  • 工作內(nèi)存:一般指寄存器,還有以及我們說(shuō)的三層高速緩存策略中的L1/L2/L3層高級(jí)緩存Cache等

在Java領(lǐng)域中,為了解決這一系列問(wèn)題,特此提出了Java內(nèi)存模型,接下來(lái),我們就來(lái)一看看Java內(nèi)存模型的工作機(jī)制。

三.Java內(nèi)存模型

Java內(nèi)存模型主要是為了解決并發(fā)編程的可見(jiàn)性問(wèn)題,原子性問(wèn)題和有序性問(wèn)題等三大問(wèn)題,具有跨平臺(tái)性。

JMM最初由JSR-133(Java Memory Model and ThreadSpecification)文檔描述,JMM定義了一組規(guī)則或規(guī)范,該規(guī)范定義了一個(gè)線程對(duì)共享變量寫入時(shí),如何確保對(duì)另一個(gè)線程是可見(jiàn)的。

Java內(nèi)存模型(Java Memory Model JMM)指的是Java HotSpot(TM) VM 虛擬機(jī)定義的一種統(tǒng)一的內(nèi)存模型,將底層硬件以及操作系統(tǒng)的內(nèi)存訪問(wèn)差異進(jìn)行封裝,使得Java程序在不同硬件以及操作系統(tǒng)上執(zhí)行都能達(dá)到相同的并發(fā)效果。

Java內(nèi)存模型對(duì)于內(nèi)存的描述主要體現(xiàn)在三個(gè)方面:

  • 首先,描述程序各個(gè)變量之間關(guān)系,主要包括實(shí)例域,靜態(tài)域,數(shù)據(jù)元素等。
  • 其次,描述了在計(jì)算機(jī)系統(tǒng)中將變量存儲(chǔ)到內(nèi)存以及從內(nèi)存中獲取變量的底層細(xì)節(jié),主要包括針對(duì)某個(gè)線程對(duì)于共享變量的進(jìn)行操作時(shí),如何通知其他線程(涉及線程間如何通信)
  • 最后,描述了多個(gè)線程對(duì)于主存中的共享資源的安全訪問(wèn)問(wèn)題。

一般來(lái)說(shuō),Java內(nèi)存模型在對(duì)內(nèi)存的描述上,我們可以依據(jù)是編譯時(shí)分配還是運(yùn)行時(shí)分配,是靜態(tài)分配還是動(dòng)態(tài)分配,是堆上分配還是棧上分配等角度來(lái)進(jìn)行對(duì)比分析。

從Java HotSpot(TM) VM 虛擬機(jī)的整體結(jié)構(gòu)上來(lái)看,內(nèi)存區(qū)域可以分為線程私有區(qū),線程共享區(qū),直接內(nèi)存等內(nèi)容,其中:

  • 線程私有區(qū)(Thread Local):主要包括程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法區(qū),其中線程私有數(shù)據(jù)區(qū)域生命周期與線程相同, 依賴用戶線程的啟動(dòng)/結(jié)束 而 創(chuàng)建/銷毀。
  • 線程共享區(qū)(Thread Shared):主要包括JAVA 堆、方法區(qū),其中,線程共享區(qū)域隨虛擬機(jī)的啟動(dòng)/關(guān)閉而創(chuàng)建/銷毀。
  • 直接內(nèi)存(Driect Memory):不會(huì)受Java HotSpot(TM) VM 虛擬機(jī)中的GC影響,并不是JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)的成員。

根據(jù)線程私有區(qū)中包含的數(shù)據(jù)(程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法區(qū))來(lái)具體分析看,其中:

  • 程序計(jì)數(shù)器(Program Counter Register ):一塊較小的內(nèi)存空間, 是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器,每條線程都要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,而且是唯一一個(gè)在虛擬機(jī)中沒(méi)有規(guī)定任何OutOfMemoryError情況的區(qū)域。
  • 虛擬機(jī)棧(VM Stack):是描述Java方法執(zhí)行的內(nèi)存模型,在方法執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。
  • 本地方法區(qū)(Native Method Stack):和Java Stack作用類似, 區(qū)別是虛擬機(jī)棧為執(zhí)行Java方法服務(wù), 而本地方法棧則為Native方法服務(wù)。

根據(jù)線程共享區(qū)中包含的數(shù)據(jù)(JAVA 堆、方法區(qū))來(lái)具體分析看,其中:

  • JAVA 堆(Heap):是被線程共享的一塊內(nèi)存區(qū)域,創(chuàng)建的對(duì)象和數(shù)組都保存在Java堆內(nèi)存中,也是垃圾收集器進(jìn)行垃圾收集的最重要的內(nèi)存區(qū)域。
  • 方法區(qū)(Method Area):是指Java HotSpot(TM) VM 虛擬機(jī)把GC分代收集擴(kuò)展至方法區(qū),Java HotSpot(TM) VM 的垃圾收集器就可以像管理Java堆一樣管理這部分內(nèi)存, 而不必為方法區(qū)開發(fā)專門的內(nèi)存管理器,其中這里需要注意的是:
    • 在JDK1.8之前,使用永久代(Permanent Generation), 用于存儲(chǔ)被JVM加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù). , 即使用Java堆的永久代來(lái)實(shí)現(xiàn)方法區(qū), 主要是因?yàn)橛谰脦У膬?nèi)存回收的主要目標(biāo)是針對(duì)常量池的回收和類型的卸載, 其收益一般很小。
    • 在JDK1.8之后,永久代已經(jīng)被移除,被一個(gè)稱為“元數(shù)據(jù)區(qū)(Metadata Area)”的區(qū)域所取代。元空間(Metadata Space)的本質(zhì)和永久代類似,最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制。類的元數(shù)據(jù)放入 Native Memory, 字符串池和類的靜態(tài)變量放入Java堆中,這樣可以加載多少類的元數(shù)據(jù)由系統(tǒng)的實(shí)際可用空間來(lái)控制。

這里對(duì)線程共享區(qū)和程私有區(qū)其細(xì)節(jié),就暫時(shí)不做展開,但是我們可以簡(jiǎn)單地看出,對(duì)于Java領(lǐng)域中的內(nèi)存分配,這兩者之間已經(jīng)幫助我們做了具體區(qū)分。

在繼續(xù)后續(xù)問(wèn)題探索之前,我們一起來(lái)思考一個(gè)問(wèn)題:按照線性思維來(lái)看,一個(gè)Java程序從程序編寫到編譯,編譯到運(yùn)行,運(yùn)行到執(zhí)行等過(guò)程來(lái)說(shuō),究竟是先入堆還是先入棧呢 ?

這個(gè)問(wèn)題,其實(shí)我在看Java HotSpot(TM) VM 虛擬機(jī)相關(guān)知識(shí)的時(shí)候,一直有這樣的個(gè)疑慮,但是其實(shí)這樣的表述是不準(zhǔn)確的,這需要結(jié)合編譯原理相關(guān)的知識(shí)來(lái)具體分析。

按照編譯原理的觀點(diǎn),從Java內(nèi)存分配策略來(lái)看,程序運(yùn)行時(shí)的內(nèi)存分配有三種策略,其中:

  • 靜態(tài)存儲(chǔ)分配:靜態(tài)存儲(chǔ)分配要求在編譯時(shí)能知道所有變量的存儲(chǔ)要求,指在編譯時(shí),就能確定每個(gè)數(shù)據(jù)在運(yùn)行時(shí)的存儲(chǔ)空間,因而在編譯時(shí)就可以給他們分配固定的內(nèi)存空間。這種分配策略要求程序代碼中不允許有可變數(shù)據(jù)結(jié)構(gòu)的存在,也不允許有嵌套或者遞歸的結(jié)構(gòu)出現(xiàn),因?yàn)樗鼈兌紩?huì)導(dǎo)致編譯程序無(wú)法計(jì)算準(zhǔn)確的存儲(chǔ)空間需求。
  • 棧式存儲(chǔ)分配:棧式存儲(chǔ)分配要求在過(guò)程的入口處必須知道所有的存儲(chǔ)要求,也可稱為動(dòng)態(tài)存儲(chǔ)分配,是由一個(gè)類似于堆棧的運(yùn)行棧來(lái)實(shí)現(xiàn)的。和靜態(tài)存儲(chǔ)分配相反,在棧式存儲(chǔ)方案中,程序?qū)?shù)據(jù)區(qū)的需求在編譯時(shí)是完全未知的,只有到運(yùn)行的時(shí)候才能夠知道,也就是規(guī)定在運(yùn)行中進(jìn)入一個(gè)程序模塊時(shí),必須知道該程序模塊所需的數(shù)據(jù)區(qū)大小才能夠?yàn)槠浞峙鋬?nèi)存。棧式存儲(chǔ)分配按照先進(jìn)后出的原則進(jìn)行分配。
  • 堆式存儲(chǔ)分配:堆式存儲(chǔ)分配則專門負(fù)責(zé)在編譯時(shí)或運(yùn)行時(shí)模塊入口處都無(wú)法確定存儲(chǔ)要求的數(shù)據(jù)結(jié)構(gòu)的內(nèi)存分配,比如可變長(zhǎng)度串和對(duì)象實(shí)例。堆由大片的可利用塊或空閑塊組成,堆中的內(nèi)存可以按照任意順序分配和釋放。

也就是說(shuō),在Java領(lǐng)域中,一個(gè)Java程序從程序編寫到編譯,編譯到運(yùn)行,運(yùn)行到執(zhí)行等過(guò)程來(lái)說(shuō),單純考慮是先入堆還是入棧的問(wèn)題,在這里得到了答案。

從整體上來(lái)看,Java內(nèi)存模型主要考慮的事情基本與主存,線程本地內(nèi)存,共享變量,變量副本,線程等概念息息相關(guān),其中:

  • 從主存與線程本地內(nèi)存的關(guān)系來(lái)看 : 主存主要保存Java程序中的共享變量,其中主存不保存局部變量和方法參數(shù)列表;而線程本地內(nèi)存主要保存Java程序中的共享變量的變量副本。
  • 從線程與線程本地內(nèi)存的關(guān)系來(lái)看:每個(gè)線程都會(huì)維護(hù)一個(gè)自己專屬的本地內(nèi)存,不同線程之間互相不可直接通信,其線程之間的通信就會(huì)涉及共享變量可見(jiàn)性的問(wèn)題。

在Java內(nèi)存模型中,一般來(lái)說(shuō)主要提供volatile,synchronized,final以及鎖等4種方式來(lái)保證變量的可見(jiàn)性問(wèn)題,其中:

  • 通過(guò)volatile關(guān)鍵詞實(shí)現(xiàn): 利用volatile修飾聲明時(shí),變量一旦有更改都會(huì)被立即同步到主存中,當(dāng)線程需要使用這個(gè)變量時(shí),需要從主存中刷新到工作內(nèi)存中。
  • 通過(guò)synchronized關(guān)鍵詞實(shí)現(xiàn):利用synchronized修飾聲明時(shí),當(dāng)一個(gè)線程釋放一個(gè)鎖,強(qiáng)制刷新工作內(nèi)存中的變量到主存中,當(dāng)另外一個(gè)線程需要使用此鎖時(shí),會(huì)強(qiáng)制重新載入變量值。
  • 通過(guò)final關(guān)鍵詞實(shí)現(xiàn):利用final修飾聲明時(shí),變量一旦初始化完成,Java中的線程都可以看到這個(gè)變量。
  • 通過(guò)JDK中鎖實(shí)現(xiàn):當(dāng)一個(gè)線程釋放一個(gè)鎖,強(qiáng)制刷新工作內(nèi)存中的變量到主存中,當(dāng)另外一個(gè)線程需要使用此鎖時(shí),會(huì)強(qiáng)制重新載入變量值。

實(shí)際上,相比之下,Java內(nèi)存模型還引入了一個(gè)工作內(nèi)存的概念來(lái)幫助我們提升性能,而且JMM提供了合理的禁用緩存以及禁止重排序的方法,所以其核心的價(jià)值在于解決可見(jiàn)性和有序性。

其中,需要特別注意的是,其主存和工作內(nèi)存的區(qū)別:

  • 主存: 可以在計(jì)算機(jī)內(nèi)存模型說(shuō)是物理內(nèi)存,對(duì)應(yīng)到Java內(nèi)存模型來(lái)講,是Java HotSpot(TM) VM 虛擬機(jī)中虛擬內(nèi)存的一部分。
  • 工作內(nèi)存:在計(jì)算機(jī)內(nèi)存模型內(nèi)是指CPU緩存,一般是指寄存器,還有以及我們說(shuō)的三層高速緩存策略中的L1/L2/L3層高級(jí)緩存;對(duì)應(yīng)到Java內(nèi)存模型來(lái)講,主要是三層高速緩存Cache和寄存器。

綜上所述,我們對(duì)Java內(nèi)存模型的探討算是水到渠成了,但是Java內(nèi)存模型也提出了一些規(guī)范,接下來(lái),我們就來(lái)看看Happen-Before 關(guān)系原則。

四.Java一致性模型指導(dǎo)原則

Java一致性模型指導(dǎo)原則是指制定一些規(guī)范來(lái)將復(fù)雜的物理計(jì)算機(jī)的系統(tǒng)底層封裝到JVM中,從而向上提供一種統(tǒng)一的內(nèi)存模型語(yǔ)義規(guī)則,一般是指Happens-Before規(guī)則。

Happen-Before 關(guān)系原則,是 Java 內(nèi)存模型中保證多線程操作可見(jiàn)性的機(jī)制,也是對(duì)早期語(yǔ)言規(guī)范中含糊的可見(jiàn)性概念的一個(gè)精確定義,其行為依賴于處理器本身的內(nèi)存一致性模型。

Happen-Before 關(guān)系原則主要規(guī)定了Java內(nèi)存在多線程操作下的順序性,一般是指先發(fā)生操作的執(zhí)行結(jié)果對(duì)后續(xù)發(fā)生的操作可見(jiàn),因此稱其為Java一致性模型指導(dǎo)原則。

由于Happen-Before 關(guān)系原則是向上提供一種統(tǒng)一的內(nèi)存模型語(yǔ)義規(guī)則,它規(guī)范了Java HotSpot(TM) VM 虛擬機(jī)的實(shí)現(xiàn),也能為上層Java Developer描述多線程并發(fā)的可見(jiàn)性問(wèn)題。

在Java領(lǐng)域中,Happen-Before 關(guān)系原則主要有8種,具體如下:

  • 單線程原則:線程內(nèi)執(zhí)行的每個(gè)操作,都保證 happen-before 后面的操作,這就保證了基本的程序順序規(guī)則,這是開發(fā)者在書寫程序時(shí)的基本約定。
  • 鎖原則:對(duì)于一個(gè)鎖的解鎖操作,保證 happen-before 加鎖操作。
  • volatile原則:對(duì)于 volatile 變量,對(duì)它的寫操作,保證 happen-before 在隨后對(duì)該變量的讀取操作。
  • 線程Start原則:類似線程內(nèi)部操作的完成,保證 happen-before 其他 Thread.start() 的線程操作原則。
  • 線程Join原則:類似線程內(nèi)部操作的完成,保證 happen-before 其他 Thread.join() 的線程操作原則。
  • 線程Interrupt原則:類似線程內(nèi)部操作的完成,保證 happen-before 其他 Thread.interrupt() 的線程操作原則。
  • finalize原則: 對(duì)象構(gòu)建完成,保證 happen-before 于 finalizer 的開始動(dòng)作。
  • 傳遞原則: Happen-Before 關(guān)系是存在著傳遞性的,如果滿足 A happen-before B 和 B happen-before C,那么 A happen-before C 也成立。

對(duì)于Happen-Before 關(guān)系原則來(lái)說(shuō),而不是簡(jiǎn)單地線性思維的前后順序問(wèn)題,是因?yàn)樗粌H僅是對(duì)執(zhí)行時(shí)間的保證,也包括對(duì)內(nèi)存讀、寫操作順序的保證。僅僅是時(shí)鐘順序上的先后,并不能保證線程交互的可見(jiàn)性。

在Java HotSpot(TM) VM 虛擬機(jī)內(nèi)部的運(yùn)行時(shí)數(shù)據(jù)區(qū),但是真正程序執(zhí)行,實(shí)際是要跑在具體的處理器內(nèi)核上。簡(jiǎn)單來(lái)說(shuō),把本地變量等數(shù)據(jù)從內(nèi)存加載到緩存、寄存器,然后運(yùn)算結(jié)束寫回主內(nèi)存。

總的來(lái)說(shuō),JMM 內(nèi)部的實(shí)現(xiàn)通常是依賴于內(nèi)存屏障,通過(guò)禁止某些重排序的方式,提供內(nèi)存可見(jiàn)性保證,也就是實(shí)現(xiàn)了各種 happen-before 規(guī)則。與此同時(shí),更多復(fù)雜度在于,需要盡量確保各種編譯器、各種體系結(jié)構(gòu)的處理器,都能夠提供一致的行為。

五.Java指令重排

Java指令重排是指在執(zhí)行程序時(shí)為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排序的一種防護(hù)措施機(jī)制。

我們?cè)趯?shí)際開發(fā)工作中編寫代碼時(shí)候,是按照一定的代碼的思維和習(xí)慣去編排和組織代碼的,但是實(shí)際上,編譯器和CPU執(zhí)行的順序可能會(huì)代碼順序產(chǎn)生不一致的情況。

畢竟,編譯器和CPU會(huì)對(duì)我們編寫的程序代碼自身做一定程度上的優(yōu)化再去執(zhí)行,以此來(lái)提高執(zhí)行效率,因此提出了指令重排的機(jī)制。

一般來(lái)說(shuō),我們?cè)诔绦蛑芯帉懙拿恳粋€(gè)行代碼其實(shí)就是程序指令,按照線性思維方式來(lái)看,這些指令按道理是一行行代碼存在的順序去執(zhí)行的,只有上一行代碼執(zhí)行完畢,下一行代碼才會(huì)被執(zhí)行,這就說(shuō)明代碼的執(zhí)行有一定的順序。

但是這樣的順序,對(duì)于程序的執(zhí)行時(shí)間上來(lái)看是有一定的耗時(shí)的,為了加快代碼的執(zhí)行效率,一般會(huì)引入一種流水線技術(shù)的方式來(lái)解決這個(gè)問(wèn)題,就像Jenkins 流水線部署機(jī)制的編寫那樣。

但是流水線技術(shù)的本質(zhì)上,是把每一個(gè)指令拆成若干個(gè)部分,在同一個(gè)CPU的時(shí)間內(nèi)使其可以執(zhí)行多個(gè)指令的不同部分,從而達(dá)到提升執(zhí)行效率的目的,主要體現(xiàn)在:

  • 獲取指令階段: 主要使用指令通道和指令寄存器,一般是在CPU處理器主導(dǎo)
  • 編譯指令階段:主要使用指令編譯器,一般是在編譯器主導(dǎo)
  • 執(zhí)行指令階段:主要使用執(zhí)行單元和數(shù)據(jù)通道,相對(duì)來(lái)說(shuō)像是從內(nèi)存在主導(dǎo)

一般來(lái)說(shuō),指令從排會(huì)涉及到CPU,編譯器,以及內(nèi)存等,因此指令重排序的類型大致可以分為 編譯器指令重排,CPU指令重排,內(nèi)存指令重排,其中:

  • 編譯器指令重排:編譯器在不改變單線程程序語(yǔ)義的前提下,可以重新安排語(yǔ)句的執(zhí)行順序
  • CPU指令重排:現(xiàn)代處理器采用了指令級(jí)并行技術(shù)(Instruction-Level Parallelism, ILP)來(lái)將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
  • 內(nèi)存指令重排:由于處理器使用緩存和讀/寫緩沖區(qū),其加載和存儲(chǔ)操作看上去類似亂序執(zhí)行的情況。

在Java領(lǐng)域中,指令重排的原則是不能影響程序在單線程下的執(zhí)行的準(zhǔn)確性,但是在多線程的情況下,可能會(huì)導(dǎo)致程序執(zhí)行出現(xiàn)錯(cuò)誤的情況,主要是依據(jù)Happen-Before 關(guān)系原則來(lái)組織部重排序,其核心就是使用內(nèi)存屏障來(lái)實(shí)現(xiàn),通過(guò)內(nèi)存屏障可以堆內(nèi)存進(jìn)行順序約束,而且作用于線程。

由于Java有不同的編譯器和運(yùn)行時(shí)環(huán)境,對(duì)應(yīng)起來(lái)看,Java指令重排主要發(fā)生在編譯階段和運(yùn)行階段,而編譯階段對(duì)應(yīng)的是編譯器,運(yùn)行階段對(duì)應(yīng)著CPU,其中:

  • 編譯階段指令重排:
    • 1?? 通用描述:源代碼->機(jī)器碼的指令重排: 源代碼經(jīng)過(guò)編譯器變成機(jī)器碼,而機(jī)器碼可能被重排
    • 2?? Java描述:Java源文件->Java字節(jié)碼的指令重排: Java源文件被javac編譯后變成Java字節(jié)碼,其字節(jié)碼可能被重排
  • 運(yùn)行階段指令重排:
    • 1?? 通用描述:機(jī)器碼->CPU處理器的指令重排:機(jī)器碼經(jīng)過(guò)CPU處理時(shí),可能會(huì)被CPU重排才執(zhí)行
    • 2?? Java描述:Java字節(jié)碼->Java執(zhí)行器的指令重排: Java字節(jié)碼被Java執(zhí)行器執(zhí)行時(shí),可能會(huì)被CPU重排才執(zhí)行

既然設(shè)置內(nèi)存屏障,可以確保多CPU的高速緩存中的數(shù)據(jù)與內(nèi)存保持一致性, 不能確保內(nèi)存與CPU緩存數(shù)據(jù)一致性的指令也不能重排,內(nèi)存屏障正是通過(guò)阻止屏障兩邊的指令重排序來(lái)避免編譯器和硬件的不正確優(yōu)化而提出的一種解決辦法。

但是內(nèi)存屏障的是需要考慮CPU的架構(gòu)方式,不同硬件實(shí)現(xiàn)內(nèi)存屏障的方式不同,一般以常見(jiàn)Intel CPU來(lái)看,主要有:

  • 1?? lfence屏障: 是一種Load Barrier 讀屏障。
  • 2?? sfence屏障: 是一種Store Barrier 寫屏障 。
  • 3?? mfence屏障:是一種全能型的屏障,具備ifence和sfence的能力 。
  • 4?? Lock前綴,Lock不是一種內(nèi)存屏障,但是它能完成類似內(nèi)存屏障的功能。Lock會(huì)對(duì)CPU總線和高速緩存加鎖,可以理解為CPU指令級(jí)的一種鎖。

在Java領(lǐng)域中,Java內(nèi)存模型屏蔽了這種底層硬件平臺(tái)的差異,由JVM來(lái)為不同的平臺(tái)生成相應(yīng)的機(jī)器碼。

從廣義上的概念定義看,Java中的內(nèi)存屏障一般主要有Load和Store兩類:

  • 1?? 對(duì)Load Barrier來(lái)說(shuō),在讀指令前插入讀屏障,可以讓高速緩存中的數(shù)據(jù)失效,重新從主內(nèi)存加載數(shù)據(jù)
  • 2?? 對(duì)Store Barrier來(lái)說(shuō),在寫指令之后插入寫屏障,能讓寫入緩存的最新數(shù)據(jù)寫回到主內(nèi)存

從具體的使用方式來(lái)看,Java中的內(nèi)存屏障主要有以下幾種方式:

  • 1?? 通過(guò) synchronized關(guān)鍵字包住的代碼區(qū)域:當(dāng)線程進(jìn)入到該區(qū)域讀取變量信息時(shí),保證讀到的是最新的值。
    - a. 在同步區(qū)內(nèi)對(duì)變量的寫入操作,在離開同步區(qū)時(shí)就將當(dāng)前線程內(nèi)的數(shù)據(jù)刷新到內(nèi)存中。
    - b. 對(duì)數(shù)據(jù)的讀取也不能從緩存讀取,只能從內(nèi)存中讀取,保證了數(shù)據(jù)的讀有效性.這也是會(huì)插入StoreStore屏障的緣故。
  • 2?? 通過(guò)volatile關(guān)鍵字修飾變量:當(dāng)對(duì)變量的寫操作,會(huì)插入StoreLoad屏障。
  • 3?? 其他的設(shè)置方式,一般需要通過(guò)Unsafe這個(gè)類來(lái)執(zhí)行,主要是:
    - a. Unsafe.putOrderedObject():類似這樣的方法,會(huì)插入StoreStore內(nèi)存屏障
    - b. Unsafe.putVolatiObject() 類似這樣的方法,會(huì)插入StoreLoad屏障

綜上所述,一般來(lái)說(shuō)volatile關(guān)健字能保證可見(jiàn)性和防止指令重排序,也是我們最常見(jiàn)提到的方式。

六.Java并發(fā)編程的三宗罪

Java并發(fā)編程的三宗罪主要是指原子性問(wèn)題、可見(jiàn)性問(wèn)題和有序性問(wèn)題等三大問(wèn)題。

在介紹Java內(nèi)存模型時(shí),我們都說(shuō)其核心的價(jià)值在于解決可見(jiàn)性和有序性,以及還有原子性等,那么對(duì)其總結(jié)來(lái)說(shuō),就是Java并發(fā)編程的三宗罪,其中:

  • 原子性問(wèn)題:就是“不可中斷的一個(gè)或一系列操作”,是指不會(huì)被線程調(diào)度機(jī)制打斷的操作。這種操作一旦開始,就一直運(yùn)行到結(jié)束,中間不會(huì)有任何線程的切換。
  • 可見(jiàn)性問(wèn)題:一個(gè)線程對(duì)共享變量的修改,另一個(gè)線程能夠立刻可見(jiàn),我們稱該共享變量具備內(nèi)存可見(jiàn)性。
  • 有序性問(wèn)題:指程序按照代碼的先后順序執(zhí)行。如果程序執(zhí)行的順序與代碼的先后順序不同,并導(dǎo)致了錯(cuò)誤的結(jié)果,即發(fā)生了有序性問(wèn)題。

但是,這里我們需要知道,Java內(nèi)存模型是如何解決這些問(wèn)題的?主要體現(xiàn)如下幾個(gè)方面:

  • 解決原子性問(wèn)題:Java內(nèi)存模型通過(guò)read、load、assign、use、store、write來(lái)保證原子性操作,此外還有l(wèi)ock和unlock,直接對(duì)應(yīng)著synchronized關(guān)鍵字的monitorenter和monitorexit字節(jié)碼指令。
  • 解決可見(jiàn)性問(wèn)題:Java保證可見(jiàn)性通過(guò)volatile、final以及synchronized,鎖來(lái)實(shí)現(xiàn)。
  • 解決有序性問(wèn)題:由于處理器和編譯器的重排序?qū)е碌挠行蛐詥?wèn)題,Java主要可以通過(guò)volatile、synchronized來(lái)保證。

一定意義上來(lái)講,一般在Java并發(fā)編程中,其實(shí)加鎖可以解決一部分問(wèn)題,除此之外,我們還需要考慮線程饑餓問(wèn)題,數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題,競(jìng)爭(zhēng)條件問(wèn)題以及死鎖問(wèn)題,通過(guò)綜合分析才能得到意想不到的結(jié)果。

綜上所述,我們?cè)诶斫釰ava領(lǐng)域中的鎖時(shí),可以以此作為一個(gè)考量標(biāo)準(zhǔn)之一,來(lái)幫助和方便我們更快理解和掌握并發(fā)編程技術(shù)。

七.Java線程饑餓問(wèn)題

Java線程饑餓問(wèn)題是指長(zhǎng)期無(wú)法獲取共享資源或搶占CPU資源而導(dǎo)致線程無(wú)法執(zhí)行的現(xiàn)象。

在Java并發(fā)編程的過(guò)程中,特別是開啟線程數(shù)過(guò)多,會(huì)遇到某些線程貪婪地把CPU資源占滿,導(dǎo)致某些線程分配不到CPU而沒(méi)有辦法執(zhí)行。

在Java領(lǐng)域中,對(duì)于線程饑餓問(wèn)題,可以從以下幾個(gè)方面來(lái)看:

  • 互斥鎖synchronized饑餓問(wèn)題:在使用synchronized對(duì)資源進(jìn)行加鎖時(shí),不斷有大量的線程去競(jìng)爭(zhēng)獲取鎖,那么就可能會(huì)引發(fā)線程饑餓問(wèn)題,主要是synchronized只是加鎖,沒(méi)有要求公平性導(dǎo)致的。
  • 線程優(yōu)先級(jí)饑餓問(wèn)題:Java中每個(gè)線程都有自己的優(yōu)先級(jí),一般情況下使用默認(rèn)優(yōu)先級(jí),但是由于線程優(yōu)先級(jí)不同,也會(huì)引起線程饑餓問(wèn)題。
  • 線程自旋饑餓問(wèn)題: 主要是在Java并發(fā)操作中,會(huì)使用自旋鎖,由于鎖的核心的自旋操作,會(huì)導(dǎo)致大量線程自旋,也會(huì)引起線程饑餓問(wèn)題。
  • 等待喚醒饑餓問(wèn)題: 主要是因?yàn)镴VM中wait和notify實(shí)現(xiàn)不同,比方說(shuō)Java HotSpot(TM) VM 虛擬機(jī)是一種先入先出結(jié)構(gòu),也會(huì)引起線程饑餓問(wèn)題。

針對(duì)上述的饑餓問(wèn)題,為了解決它,JDK內(nèi)部實(shí)現(xiàn)一些具備公平性質(zhì)的鎖,可以直接使用。所以,解決線程饑餓問(wèn)題,一般是引入隊(duì)列,也就是排隊(duì)處理,最典型的有ReentrantLock。

綜上所述,這不就是為我們掌握和理解Java中的鎖機(jī)制時(shí),需要考慮Java線程饑餓問(wèn)題。

八.Java數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題

Java數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題是指至少存在兩個(gè)線程去讀寫某個(gè)共享內(nèi)存,其中至少一個(gè)線程對(duì)其共享內(nèi)存進(jìn)行寫操作。

對(duì)于數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題,最簡(jiǎn)單的理解就是,多個(gè)線程在同時(shí)對(duì)于共享內(nèi)存的進(jìn)行寫操作時(shí),在寫的過(guò)程中,其他的線程讀到數(shù)據(jù)是內(nèi)存數(shù)據(jù)中非正確預(yù)期的。

產(chǎn)生數(shù)據(jù)競(jìng)爭(zhēng)的原因,一個(gè)CPU在任意時(shí)刻只能執(zhí)行一條指令,但是對(duì)其某個(gè)內(nèi)存中的寫操作可能會(huì)用到若干條件機(jī)器指令,從而導(dǎo)致在寫的過(guò)程中還沒(méi)完全修改完內(nèi)存,其他線程去讀取數(shù)據(jù),從而導(dǎo)致結(jié)果不可預(yù)知。從而引發(fā)數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題,這個(gè)情況有點(diǎn)像MySQL數(shù)據(jù)中并發(fā)事務(wù)引起的臟讀情況。

在Java領(lǐng)域中,解決數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題的方式一般是把共享內(nèi)存的更新操作進(jìn)行原子化,同時(shí)也保證內(nèi)存的可見(jiàn)性。

針對(duì)上述的饑餓問(wèn)題,為了解決它,JDK內(nèi)部實(shí)現(xiàn)一系列的原子類,比如AtomicReference類等,但是主要可以采用CAS+自旋鎖的方式來(lái)實(shí)現(xiàn)。

綜上所述,這不就是為我們掌握和理解Java中的鎖機(jī)制時(shí),需要考慮Java數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題。

九.Java競(jìng)爭(zhēng)條件問(wèn)題

Java競(jìng)爭(zhēng)條件問(wèn)題是指代碼在執(zhí)行臨界區(qū)產(chǎn)生競(jìng)爭(zhēng)條件,主要是因?yàn)槎鄠€(gè)線程不同的執(zhí)行順序以及線程并發(fā)的交叉執(zhí)行導(dǎo)致執(zhí)行結(jié)果與預(yù)期不一致的情況。

對(duì)于競(jìng)爭(zhēng)條件問(wèn)題,其中臨界區(qū)是一塊代碼區(qū)域,其實(shí)說(shuō)白了就是我們自己寫的邏輯代碼,由于沒(méi)有考慮位,從而引發(fā)的多個(gè)線程不同的執(zhí)行順序以及線程并發(fā)的交叉執(zhí)行導(dǎo)致執(zhí)行結(jié)果與預(yù)期不一致的情況。

產(chǎn)生競(jìng)爭(zhēng)條件問(wèn)題的主要原因,一般主要有線程執(zhí)行順序的不確定性和并發(fā)機(jī)制導(dǎo)致上下文切換等兩個(gè)原因?qū)е赂?jìng)爭(zhēng)條件問(wèn)題,其中:

  • 線程執(zhí)行順序的不確定性:這個(gè)線程調(diào)度的工作方式有關(guān),現(xiàn)在大部分計(jì)算機(jī)的操作系統(tǒng)都是搶占方式的調(diào)度方式,所有的任務(wù)調(diào)度由操作系統(tǒng)來(lái)完全控制,線程的執(zhí)行順序不一定是按照編碼順序的,主要有操作系統(tǒng)調(diào)度算法決定。
  • 并發(fā)機(jī)制導(dǎo)致上下文切換:在并發(fā)的多線程的程序中,多個(gè)線程會(huì)導(dǎo)致進(jìn)行上下文的資源切換,并且交叉執(zhí)行,從而并發(fā)機(jī)制自身也會(huì)引起競(jìng)爭(zhēng)條件問(wèn)題。

在Java領(lǐng)域中,解決競(jìng)爭(zhēng)條件問(wèn)題的方式一般是把臨界區(qū)進(jìn)行原子化,保證臨界區(qū)的源自性,保證了臨界區(qū)捏只有一個(gè)線程,從而避免競(jìng)爭(zhēng)產(chǎn)生。

針對(duì)上述的饑餓問(wèn)題,為了解決它,JDK內(nèi)部實(shí)現(xiàn)一系列的原子類或者說(shuō)直接使用synchronized來(lái)聲明,均可實(shí)現(xiàn)。

綜上所述,這不就是為我們掌握和理解Java中的鎖機(jī)制時(shí),需要考慮Java競(jìng)爭(zhēng)條件問(wèn)題。

十.Java死鎖問(wèn)題

Java死鎖問(wèn)題主要是指一種有兩個(gè)或者兩個(gè)以上的線程或者進(jìn)程構(gòu)成一個(gè)無(wú)限互相等待的環(huán)形狀態(tài)的情況,不是一種鎖概念,而是一種線程狀態(tài)的表征描述。

一般為了保證線程安全問(wèn)題,我們都會(huì)想著給會(huì)使用加鎖機(jī)制來(lái)確保線程安全,但如果過(guò)度地使用加鎖,則可能導(dǎo)致鎖順序死鎖(Lock-Ordering Deadlock)。

或者有的場(chǎng)景我們使用線程池和信號(hào)量等來(lái)限制資源的使用,但這些被限制的行為可能會(huì)導(dǎo)致資源死鎖(Resource DeadLock)。

Java死鎖問(wèn)題的主要體現(xiàn)在以下幾個(gè)方面:

  • 1?? Java應(yīng)用程序不具備MySQL數(shù)據(jù)庫(kù)服務(wù)器的本地事務(wù),無(wú)法檢測(cè)一組事務(wù)中是否有死鎖的發(fā)生。
  • 2?? 在Java程序中,如果過(guò)度地使用加鎖,輕則導(dǎo)致程序響應(yīng)時(shí)間變長(zhǎng),系統(tǒng)吞吐量變小,重則導(dǎo)致應(yīng)用中的某一個(gè)功能直接失去響應(yīng)能力無(wú)法提供服務(wù)。

當(dāng)然,死鎖問(wèn)題的產(chǎn)生也必須具備以及同時(shí)滿足以下幾個(gè)條件:

  • 互斥條件:資源具有排他性,當(dāng)資源被一個(gè)線程占用時(shí),別的線程不能使用,只能等待。
  • 阻塞不釋放條件: 某個(gè)線程或者線程請(qǐng)求某個(gè)資源而進(jìn)入阻塞狀態(tài),不會(huì)釋放已經(jīng)獲取的資源。
  • 占有并等待條件: 某個(gè)線程或者線程應(yīng)該至少占有一個(gè)資源,等待獲取另外一個(gè)資源,該資源被其他線程或者線程霸占。
  • 非搶占條件: 不可搶占,資源請(qǐng)求者不能強(qiáng)制從資源占有者手中搶奪資源,資源只能由占有者主動(dòng)釋放。
  • 環(huán)形條件: 循環(huán)等待,多個(gè)線程存在環(huán)路的鎖依賴關(guān)系而永遠(yuǎn)等待下去。

對(duì)于死鎖問(wèn)題,一般都是需要編程開發(fā)人員人為去干預(yù)和防止的,只是需要一些措施區(qū)規(guī)范處理,主要可以分為事前預(yù)防和事后處理等2種方式,其中:

  • 事前預(yù)防: 一般是保證鎖的順序化,資源合并處理,以及避免嵌套鎖等。
  • 事后處理: 一般是對(duì)鎖設(shè)置超時(shí)機(jī)制,在死鎖發(fā)生時(shí)搶占鎖資源,以及撤銷線程機(jī)制等。

除了有死鎖的問(wèn)題,當(dāng)然還有活鎖問(wèn)題,主要是因?yàn)槟承┻壿媽?dǎo)致一直在做無(wú)用功,使得線程無(wú)法正確執(zhí)行的情況。

應(yīng)用分析

在Java領(lǐng)域中,我們可以將鎖大致分為基于Java語(yǔ)法層面(關(guān)鍵詞)實(shí)現(xiàn)的鎖和基于JDK層面實(shí)現(xiàn)的鎖。

單純從Java對(duì)其實(shí)現(xiàn)的方式上來(lái)看,我們大體上可以將其分為基于Java語(yǔ)法層面(關(guān)鍵詞)實(shí)現(xiàn)的鎖和基于JDK層面實(shí)現(xiàn)的鎖。其中:

  • 基于Java語(yǔ)法層面(關(guān)鍵詞)實(shí)現(xiàn)的鎖,主要是根據(jù)Java語(yǔ)義來(lái)實(shí)現(xiàn),最典型的應(yīng)用就是synchronized。
  • 基于JDK層面實(shí)現(xiàn)的鎖,主要是根據(jù)統(tǒng)一的AQS基礎(chǔ)同步器來(lái)實(shí)現(xiàn),最典型的有ReentrantLock。

需要特別注意的是,在Java領(lǐng)域中,基于JDK層面的鎖通過(guò)CAS操作解決了并發(fā)編程中的原子性問(wèn)題,而基于Java語(yǔ)法層面實(shí)現(xiàn)的鎖解決了并發(fā)編程中的原子性問(wèn)題和可見(jiàn)性問(wèn)題。

單純從Java對(duì)其實(shí)現(xiàn)的方式上來(lái)看,我們大體上可以將其分為基于Java語(yǔ)法層面(關(guān)鍵詞)實(shí)現(xiàn)的鎖和基于JDK層面實(shí)現(xiàn)的鎖。其中:

  • 基于Java語(yǔ)法層面(關(guān)鍵詞)實(shí)現(xiàn)的鎖,主要是根據(jù)Java語(yǔ)義來(lái)實(shí)現(xiàn),最典型的應(yīng)用就是synchronized。
  • 基于JDK層面實(shí)現(xiàn)的鎖,主要是根據(jù)統(tǒng)一的AQS基礎(chǔ)同步器來(lái)實(shí)現(xiàn),最典型的有ReentrantLock。

需要特別注意的是,在Java領(lǐng)域中,基于JDK層面的鎖通過(guò)CAS操作解決了并發(fā)編程中的原子性問(wèn)題,而基于Java語(yǔ)法層面實(shí)現(xiàn)的鎖解決了并發(fā)編程中的原子性問(wèn)題和可見(jiàn)性問(wèn)題。

而從具體到對(duì)應(yīng)的Java線程資源來(lái)說(shuō),我們按照是否含有某一特性來(lái)定義鎖,主要可以從如下幾個(gè)方面來(lái)看:

  • 從加鎖對(duì)象角度方面上來(lái)看,線程要不要鎖住同步資源 ? 如果是需要加鎖,鎖住同步資源的情況下,一般稱其為悲觀鎖;否則,如果是不需要加鎖,且不用鎖住同步資源的情況就屬于為樂(lè)觀鎖。
  • 從獲取鎖的處理方式上來(lái)看,假設(shè)鎖住同步資源,其對(duì)該線程是否進(jìn)入睡眠狀態(tài)或者阻塞狀態(tài)?如果會(huì)進(jìn)入睡眠狀態(tài)或者阻塞狀態(tài),一般稱其為互斥鎖,否則,不會(huì)進(jìn)入睡眠狀態(tài)或者阻塞狀態(tài)屬于一種非阻塞鎖,即就是自旋鎖。
  • 從鎖的變化狀態(tài)方面來(lái)看,多個(gè)線程在競(jìng)爭(zhēng)資源的流程細(xì)節(jié)上是否有差別?
  • 1?? 對(duì)于不會(huì)鎖住資源,多個(gè)線程只有一個(gè)線程能修改資源成功,其他線程會(huì)依據(jù)實(shí)際情況進(jìn)行重試,即就是不存在競(jìng)爭(zhēng)的情況,一般屬于無(wú)鎖。
  • 2?? 對(duì)于同一個(gè)線程執(zhí)行同步資源會(huì)自動(dòng)獲取鎖資源,一般屬于偏向鎖。
  • 3?? 對(duì)于多線程競(jìng)爭(zhēng)同步資源時(shí),沒(méi)有獲取到鎖資源的線程會(huì)自旋等待鎖釋放,一般屬于輕量級(jí)鎖。
  • 4?? 對(duì)于多線程競(jìng)爭(zhēng)同步資源時(shí),沒(méi)有獲取到鎖資源的線程會(huì)阻塞等待喚醒,一般屬于重量級(jí)鎖。
  • 從鎖競(jìng)爭(zhēng)時(shí)公平性上來(lái)看,多個(gè)線程在競(jìng)爭(zhēng)資源時(shí)是否需要排隊(duì)等待?如果是需要排隊(duì)等待的情況,一般屬于公平鎖;否則,先插隊(duì),然后再嘗試排隊(duì)的情況屬于非公平鎖。
  • 從獲取鎖的操作頻率次數(shù)來(lái)看,一個(gè)線程中的多個(gè)流程是否可以獲取同一把鎖?如果是可以多次進(jìn)行加鎖操作的情況,一般屬于可重入鎖,否則,可以多次進(jìn)行加鎖操作的情況屬于非可重入鎖。
  • 從獲取鎖的占有方式上來(lái)看,多個(gè)線程能不能共享一把鎖?如果是可以共享鎖資源的情況,一般屬于共享鎖;否則,獨(dú)占鎖資源的情況屬于排他鎖。

針對(duì)于上述描述的各種情況,這里就不做展開和贅述,看到這里只需要在腦中形成一個(gè)概念就行,后續(xù)會(huì)有專門的內(nèi)容來(lái)對(duì)其進(jìn)行分析和探討。

寫在最后

在上述的內(nèi)容中,一般常規(guī)的概念中,我們很難會(huì)依據(jù)上述這些問(wèn)題去認(rèn)識(shí)和看待Java中的鎖機(jī)制,主要是在學(xué)習(xí)和查閱資料的時(shí),大多數(shù)的論調(diào)都是零散和細(xì)分的,很難在我們的腦海中形成知識(shí)體系。

從本質(zhì)上講,我們對(duì)鎖應(yīng)該有一個(gè)認(rèn)識(shí),其主要是一種協(xié)調(diào)多個(gè)進(jìn)程 或者多個(gè)線程對(duì)某一個(gè)資源的訪問(wèn)的控制機(jī)制,是并發(fā)編程中最關(guān)鍵的一環(huán)。

接下來(lái),對(duì)于上述內(nèi)容做一個(gè)簡(jiǎn)單的總結(jié):

  • 1?? 計(jì)算機(jī)運(yùn)行模型主要是描述計(jì)算機(jī)系統(tǒng)體系結(jié)構(gòu)的基本模型,一般主要是指CPU處理器結(jié)構(gòu)。
  • 2?? 計(jì)算機(jī)內(nèi)存模型一般是指計(jì)算系統(tǒng)底層與編程語(yǔ)言之間的約束規(guī)范,主要是描述計(jì)算機(jī)程序與共享存儲(chǔ)器訪問(wèn)的行為特征表現(xiàn)。
  • 3?? Java內(nèi)存模型主要是為了解決并發(fā)編程的可見(jiàn)性問(wèn)題,原子性問(wèn)題和有序性問(wèn)題等三大問(wèn)題,具有跨平臺(tái)性。
  • 4?? Java一致性模型指導(dǎo)原則是指制定一些規(guī)范來(lái)將復(fù)雜的物理計(jì)算機(jī)的系統(tǒng)底層封裝到JVM中,從而向上提供一種統(tǒng)一的內(nèi)存模型語(yǔ)義規(guī)則,一般是指Happens-Before規(guī)則。
  • 5?? Java指令重排是指在執(zhí)行程序時(shí)為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排序的一種防護(hù)措施機(jī)制。
  • 6?? Java并發(fā)編程的三宗罪主要是指原子性問(wèn)題、可見(jiàn)性問(wèn)題和有序性問(wèn)題等三大問(wèn)題。
  • 7?? Java線程饑餓問(wèn)題是指長(zhǎng)期無(wú)法獲取共享資源或搶占CPU資源而導(dǎo)致線程無(wú)法執(zhí)行的現(xiàn)象。
  • 8?? Java數(shù)據(jù)競(jìng)爭(zhēng)問(wèn)題是指至少存在兩個(gè)線程去讀寫某個(gè)共享內(nèi)存,其中至少一個(gè)線程對(duì)其共享內(nèi)存進(jìn)行寫操作。
  • 9?? Java競(jìng)爭(zhēng)條件問(wèn)題是指代碼在執(zhí)行臨界區(qū)產(chǎn)生競(jìng)爭(zhēng)條件,主要是因?yàn)槎鄠€(gè)線程不同的執(zhí)行順序以及線程并發(fā)的交叉執(zhí)行導(dǎo)致執(zhí)行結(jié)果與預(yù)期不一致的情況。
  • ???? Java死鎖問(wèn)題主要是指一種有兩個(gè)或者兩個(gè)以上的線程或者進(jìn)程構(gòu)成一個(gè)無(wú)限互相等待的環(huán)形狀態(tài)的情況,不是一種鎖概念,而是一種線程狀態(tài)的表征描述。

單純從Java對(duì)其實(shí)現(xiàn)的方式上來(lái)看,我們大體上可以將其分為基于Java語(yǔ)法層面(關(guān)鍵詞)實(shí)現(xiàn)的鎖和基于JDK層面實(shí)現(xiàn)的鎖,其中:

  • 1?? 基于Java語(yǔ)法層面(關(guān)鍵詞)實(shí)現(xiàn)的鎖,主要是根據(jù)Java語(yǔ)義來(lái)實(shí)現(xiàn),最典型的應(yīng)用就是synchronized。
  • 2?? 基于JDK層面實(shí)現(xiàn)的鎖,主要是根據(jù)統(tǒng)一的AQS基礎(chǔ)同步器來(lái)實(shí)現(xiàn),最典型的有ReentrantLock。

綜上所述,我相信看到這里的時(shí)候,對(duì)Java領(lǐng)域中的鎖機(jī)制已經(jīng)有一個(gè)基本的輪廓,后面會(huì)專門寫一篇內(nèi)容來(lái)詳細(xì)介紹,敬請(qǐng)期待。

最后,技術(shù)研究之路任重而道遠(yuǎn),愿我們熬的每一個(gè)通宵,都撐得起我們想在這條路上走下去的勇氣,未來(lái)仍然可期,與君共勉!

版權(quán)聲明:本文為博主原創(chuàng)文章,遵循相關(guān)版權(quán)協(xié)議,如若轉(zhuǎn)載或者分享請(qǐng)附上原文出處鏈接和鏈接來(lái)源。


網(wǎng)頁(yè)題目:Java 并發(fā)編程解析 | 如何正確理解Java領(lǐng)域中的鎖機(jī)制,我們一般需要掌握哪些理論知識(shí)?
分享路徑:http://weahome.cn/article/dscgepg.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部