【操作系統(tǒng)】2.4 線程同步

線程的同步就是保證多個(gè)線程的共同資源在同一時(shí)刻只有一個(gè)線程在使用和修改,保證數(shù)據(jù)是唯一的和準(zhǔn)確的

1.線程間的同步方式

1.1 互斥量

采用互斥對(duì)象機(jī)制,只有擁有互斥對(duì)象的線程才有訪問公共資源的權(quán)限。因?yàn)榛コ鈱?duì)象只有一個(gè),所以可以保證公共資源不會(huì)被多個(gè)線程同時(shí)訪問。例如,Java 中的 synchronized 關(guān)鍵詞和各種 Lock 都是采用互斥對(duì)象機(jī)制。

1.2 信號(hào)量

信號(hào)量允許同一時(shí)刻多個(gè)線程訪問同一資源,但是需要控制同一時(shí)刻訪問此資源的最大線程數(shù)量。

1.3 事件

事件機(jī)制通過通知的方式保持多線程同步,還可以實(shí)現(xiàn)多線程優(yōu)先級(jí)。

2.Java的線程同步

① 線程同步的目的是保證數(shù)據(jù)的唯一性和正確性。

Java的內(nèi)存模型JMM決定一個(gè)線程對(duì)共享變量的寫入何時(shí)對(duì)另一個(gè)線程可見,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:共享變量存儲(chǔ)在主內(nèi)存(Main Memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(Local Memory),本地內(nèi)存保存了被該線程使用到的主內(nèi)存的副本拷貝,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量。

Java的內(nèi)存模型 JMM

② 線程同步的主要解決原子性、可見性和有序性。

A.原子性 : 一次操作或者多次操作,要么所有的操作全部都得到執(zhí)行并且不會(huì)受到任何因素的干擾而中斷,要么都不執(zhí)行。synchronized 可以保證代碼片段的原子性

B.可見性?:當(dāng)一個(gè)線程修改了共享變量,那么其余線程都能立即可以看到修改后的最新數(shù)據(jù)。volatile?關(guān)鍵字可以保證共享變量的可見性。

C.有序性?:代碼的執(zhí)行是有先后順序的。然而 Java 在編譯器以及運(yùn)行期間的優(yōu)化,導(dǎo)致代碼的執(zhí)行順序未必就是編寫代碼時(shí)候的順序。volatile?關(guān)鍵字可以禁止指令進(jìn)行重排序優(yōu)化。

2.1 volatile

Java語言提供volatile關(guān)鍵字,用于修飾變量,在多線程并發(fā)中保證了共享變量的“ 可見性”。

volatile關(guān)鍵字的作用:

① 保證變量的可見性,即告訴Java內(nèi)存模型,volatile修飾的變量是不穩(wěn)定的,每次使用都要到主內(nèi)存中讀取。

②?防止指令重排序。

volatile保證變量的可見性

2.2 synchronized

synchronized 關(guān)鍵字底層原理屬于 JVM 層面。

(1)synchronized 修飾代碼塊

public class SynchronizedDemo {

? ? ? public void method() {

? ? ? ? ? ? ? synchronized (this) {

? ? ? ? ? ? ? ? ? ? ? ? ?System.out.println("synchronized 代碼塊");

? ? ? ? ? ? ? }

? ? ? ?}

}

通過 JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關(guān)字節(jié)碼信息:首先切換到類的對(duì)應(yīng)目錄執(zhí)行?javac SynchronizedDemo.java?命令生成編譯后的 .class 文件,然后執(zhí)行javap -c -s -v -l SynchronizedDemo.class

SynchronizedDemo.class

synchronized 同步語句塊的實(shí)現(xiàn)使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結(jié)束位置。?當(dāng)執(zhí)行 monitorenter 指令時(shí),線程試圖獲取鎖也就是獲取 monitor(monitor對(duì)象存在于每個(gè)Java對(duì)象的對(duì)象頭中,synchronized 鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對(duì)象可以作為鎖的原因) 的持有權(quán)。當(dāng)計(jì)數(shù)器為0則可以成功獲取,獲取后將鎖計(jì)數(shù)器設(shè)為1也就是加1。相應(yīng)的在執(zhí)行 monitorexit 指令后,將鎖計(jì)數(shù)器設(shè)為0,表明鎖被釋放。如果獲取對(duì)象鎖失敗,那當(dāng)前線程就要阻塞等待,直到鎖被另外一個(gè)線程釋放為止。

(2)synchronized 修飾方法

public class SynchronizedDemo2 {

? ? ? ?public synchronized void method() {

? ? ? ? ? ? ? System.out.println("synchronized 方法");

? ? ? ?}

}

SynchronizedDemo2.class

synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實(shí)是 ACC_SYNCHRONIZED 標(biāo)識(shí),該標(biāo)識(shí)指明了該方法是一個(gè)同步方法,JVM 通過該 ACC_SYNCHRONIZED 訪問標(biāo)志來辨別一個(gè)方法是否聲明為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用。

參考:https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/Multithread/synchronized.md

2.3 CAS?

CAS在Java并發(fā)應(yīng)用中通常指CompareAndSwap或CompareAndSet操作,即比較并交換。

① CAS是一個(gè)原子操作,比較一個(gè)內(nèi)存位置的值并且只有相等時(shí)修改這個(gè)內(nèi)存位置的值為新的值,保證了新的值總是基于最新的信息計(jì)算的,如果有其他線程在這期間修改了這個(gè)值則CAS失敗。CAS返回是否成功或者內(nèi)存位置原來的值用于判斷是否CAS成功。

② JVM中的CAS操作是利用了 CPU 的CMPXCHG指令實(shí)現(xiàn)的。

?Java 提供了 CAS 的原子類:AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference等。使用樣例如下:

public?class CASTest{

????static?AtomicInteger?i?=?new?AtomicInteger(0);

????public static void increment() {

????????//?自增?1并返回之后的結(jié)果

? ? ? ? ? ?i.incrementAndGet();

? ? }

}

問題1:ABA問題

當(dāng)線程A即將要執(zhí)行第三步的時(shí)候,線程 B 把 i 的值加1,之后又馬上把 i 的值減 1,然后,線程 A 執(zhí)行第三步,這個(gè)時(shí)候線程 A 是認(rèn)為并沒有人修改過 i 的值,因?yàn)?i 的值并沒有發(fā)生改變。這就是ABA問題。?為了解決 ABA 問題。Java 中提供了 AtomicStampedReference 類,可以進(jìn)行版本控制。

問題2:CAS 沒有加鎖導(dǎo)致所有的線程都可以進(jìn)入 increment() 這個(gè)方法,假如進(jìn)入這個(gè)方法的線程太多,就會(huì)出現(xiàn)一個(gè)問題:每次有線程要執(zhí)行第三個(gè)步驟的時(shí)候,i 的值老是被修改了,所以線程又到回到第一步繼續(xù)重頭再來。如果線程密集,就會(huì)循環(huán)消耗資源。

Java8 引入了一個(gè) cell[] 數(shù)組,它的工作機(jī)制是這樣的:假如有 5 個(gè)線程要對(duì) i ?進(jìn)行自增操作,由于 5 個(gè)線程的話,不是很多,起沖突的幾率較小,那就讓他們按照以往正常的那樣,采用 CAS 來自增吧。如果有 100 個(gè)線程要對(duì) i 進(jìn)行自增操作的話,這個(gè)時(shí)候,沖突就會(huì)大大增加,系統(tǒng)就會(huì)把這些線程分配到不同的 cell 數(shù)組元素去,假如 cell[10] 有 10 個(gè)元素吧,且元素的初始化值為 0,那么系統(tǒng)就會(huì)把 100 個(gè)線程分成 10 組,每一組對(duì) cell 數(shù)組其中的一個(gè)元素做自增操作,這樣到最后,cell 數(shù)組 10 個(gè)元素的值都為 10,系統(tǒng)在把這 10 個(gè)元素的值進(jìn)行匯總,進(jìn)而得到 100,等價(jià)于 100 個(gè)線程對(duì) i 進(jìn)行了 100 次自增操作。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容