天天看點

java-并發-線程安全

多線程的可見性和有序性

———–多個線程之間是不能互相傳遞資料通信的,它們之間的溝通隻能通過共享變量來進行。JMM規定了jvm有主記憶體,主記憶體是多個線程共享的。當new一個對象的時候,也是被配置設定在主記憶體中,每個線程都有自己的工作記憶體,工作記憶體存儲了主存的某些對象的副本,當然線程的工作記憶體大小是有限制的。

當線程操作某個對象時,執行順序如下:

(1) 從主存複制變量到目前工作記憶體 (read and load)

(2) 執行代碼,改變共享變量值 (use and assign)

(3) 用工作記憶體資料重新整理主存相關内容 (store and write)

當一個共享變量在多個線程的工作記憶體中都有副本時,如果一個線程修改了這個共享變量,那麼其他線程應該能夠看到這個被修改後的值,這就是多線程的可見性問題。

線程在引用變量時不能直接從主記憶體中引用,如果線程工作記憶體中沒有該變量,則會從主記憶體中拷貝一個副本到工作記憶體中,這個過程為read-load,完成後線程會引用該副本。當同一線程再度引用該字段時,有可能重新從主存中擷取變量副本(read-load-use),也有可能直接引用原來的副本 (use),也就是說 read,load,use順序可以由JVM實作系統決定。線程不能直接為主存中中字段指派,它會将值指定給工作記憶體中的變量副本(assign),完成後這個變量副本會同步到主存儲區(store- write),至于何時同步過去,根據JVM實作系統決定.有該字段,則會從主記憶體中将該字段指派到工作記憶體中,這個過程為read-load,完成後線程會引用該變量副本,當同一線程多次重複對字段指派時

線程有可能隻對工作記憶體中的副本進行指派,隻到最後一次指派後才同步到主存儲區,是以assign,store,weite順序可以由JVM實作系統決定

第一次執行結果為10200,第二次執行結果為1060,每次執行的結果都是不确定的,因為線程的執行順序是不可預見的。這是java同步産生的根源,synchronized關鍵字保證了多個線程對于同步塊是互斥的,synchronized作為一種同步手段,解決java多線程的執行有序性和記憶體可見性,而volatile關鍵字之解決多線程的記憶體可見性問題。

java用synchronized關鍵字做為多線程并發環境的執行有序性的保證手段之一。當一段代碼會修改共享變量,這一段代碼成為互斥區或臨界區,為了保證共享變量的正确性,synchronized标示了臨界區

一個線程執行臨界區代碼過程如下:

1 獲得同步鎖

2 清空工作記憶體

3 從主存拷貝變量副本到工作記憶體

4 對這些變量計算

5 将變量從工作記憶體寫回到主存

6 釋放鎖

可見,synchronized既保證了多線程的并發有序性,又保證了多線程的記憶體可見性。

經典的線程同步模型

聲明一個Plate對象為plate,被線程A和線程B共享,A專門放雞蛋,B專門拿雞蛋。假設

1 開始,A調用plate.putEgg方法,此時eggs.size()為0,是以順利将雞蛋放到盤子,還執行了notify()方法,喚醒鎖的阻塞隊列的線程,此時阻塞隊列還沒有線程。

2 又有一個A線程對象調用plate.putEgg方法,此時eggs.size()不為0,調用wait()方法,自己進入了鎖對象的阻塞隊列。

3 此時,來了一個B線程對象,調用plate.getEgg方法,eggs.size()不為0,順利的拿到了一個雞蛋,還執行了notify()方法,喚醒鎖的阻塞隊列的線程,此時阻塞隊列有一個A線程對象,喚醒後,它進入到就緒隊列,就緒隊列也就它一個,是以馬上得到鎖,開始往盤子裡放雞蛋,此時盤子是空的,是以放雞蛋成功。

4 假設接着來了線程A,就重複2;假設來料線程B,就重複3。

整個過程都保證了放雞蛋,拿雞蛋,放雞蛋,拿雞蛋。

olatile是java提供的一種同步手段,隻不過它是輕量級的同步,為什麼這麼說,因為volatile隻能保證多線程的記憶體可見性,不能保證多線程的執行有序性。而最徹底的同步要保證有序性和可見性,例如synchronized。任何被volatile修飾的變量,都不拷貝副本到工作記憶體,任何修改都及時寫在主存。是以對于Valatile修飾的變量的修改,所有線程馬上就能看到,但是volatile不能保證對變量的修改是有序的。

當一個VolatileTest對象被多個線程共享,a的值不一定是正确的,因為a=a+count包含了好幾步操作,而此時多個線程的執行是無序的,因為沒有任何機制來保證多個線程的執行有序性和原子性。volatile存在的意義是,任何線程對a的修改,都會馬上被其他線程讀取到,因為直接操作主存,沒有線程對工作記憶體和主存的同步。是以,volatile的使用場景是有限的,在有限的一些情形下可以使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:

1)對變量的寫操作不依賴于目前值。

2)該變量沒有包含在具有其他變量的不變式中

在沒有volatile聲明時,多線程環境下,a的最終值不一定是正确的,因為this.a=a;涉及到給a指派和将a同步回主存的步驟,這個順序可能被打亂。如果用volatile聲明了,讀取主存副本到工作記憶體和同步a到主存的步驟,相當于是一個原子操作。是以簡單來說,volatile适合這種場景:一個變量被多個線程共享,線程直接給這個變量指派。這是一種很簡單的同步場景,這時候使用volatile的開銷将會非常小。

線程的working memory隻是cpu的寄存器和高速緩存的抽象描述,,cpu在計算的時候,并不總是從記憶體讀取資料,它的資料讀取順序優先級是:寄存器-高速緩存-記憶體。線程耗費的是CPU,線程計算的時候,原始的資料來自記憶體,在計算過程中,有些資料可能被頻繁讀取,這些資料被存儲在寄存器和高速緩存中,當線程計算完後,這些緩存的資料在适當的時候應該寫回記憶體。當個多個線程同時讀寫某個記憶體資料時,就會産生多線程并發問題,涉及到三個特性:原子性,有序性,可見性。動态的記憶體模型,甚至已經超越了JVM的範圍。

1.程式計數器

是一個較小的記憶體空間,每一個Java線程都有一個程式計數器來用于儲存程式執行到目前方法的哪一個指令,看成是位元組碼的行号訓示器。分支、循環、跳轉、異常處理、線程回複等。每個線程私有一個計數器,獨立存儲,注意計數器記憶體區域是唯一沒有規定沒有任何oom的區域。

2.線程棧 -位元組碼服務

生命周期和線程一樣,是線程私有的。線程的每個方法被執行的時候,都會同時建立一個幀(Frame)用于存儲本地變量表、操作棧、動态連結、方法出入口等資訊。每一個方法的調用至完成,就意味着一個幀在VM棧中的入棧至出棧的過程。如果線程請求的棧深度大于虛拟機所允許的深度,将抛出StackOverflowError異常;如果VM棧可以動态擴充(VM Spec中允許固定長度的VM棧),當擴充時無法申請到足夠記憶體則抛出OutOfMemoryError異常。

3.本地方法棧-NATIVE 方法服務 sun spothot将以上兩個棧合二為一了。

4.堆

每個線程的棧都是該線程私有的,堆則是所有線程共享的。當我們new一個對象時,該對象就被配置設定到了堆中。但是堆,并不是一個簡單的概念,堆區又劃分了很多區域,為什麼堆劃分成這麼多區域,這是為了JVM的記憶體垃圾收集,似乎越扯越遠了,扯到垃圾收集了,現在的jvm的gc都是按代收集,堆區大緻被分為三大塊:新生代,舊生代,持久代(虛拟的);新生代又分為eden區,s0區(From survivor),s1區(to survivor)。建立一個對象時,基本小的對象,生命周期短的對象都會放在新生代的eden區中,eden區滿時,有一個小範圍的gc(minor gc),整個新生代滿時,會有一個大範圍的gc(major gc),将新生代裡的部分對象轉到舊生代裡。

JIT編譯器的發展,和逃逸分析技術的成熟,不是所有的執行個體對象都在這兒了。

5.方法區

其實就是永久代(Permanent Generation),方法區中存放了每個Class的結構資訊,包括常量池、字段描述、方法描述等等。VM Space描述中對這個區域的限制非常寬松,除了和Java堆一樣不需要連續的記憶體,也可以選擇固定大小或者可擴充外,甚至可以選擇不實作垃圾收集。相對來說,垃圾收集行為在這個區域是相對比較少發生的,但并不是某些描述那樣永久代不會發生GC(至 少對目前主流的商業JVM實作來說是如此),這裡的GC主要是對常量池的回收和對類的解除安裝,雖然回收的“成績”一般也比較差強人意,尤其是類解除安裝,條件相當苛刻。

6.常量池

Class檔案中除了有類的版本、字段、方法、接口等描述等資訊外,還有一項資訊是常量表(constant_pool table),用于存放編譯期已可知的常量,這部分内容将在類加載後進入方法區(永久代)存放。但是Java語言并不要求常量一定隻有編譯期預置入Class的常量表的内容才能進入方法區常量池,運作期間也可将新内容放入常量池(最典型的String.intern()方法)

關于垃圾收集,在此不多說,流到垃圾收集那一章再詳細說吧。關于java的同步,其實還有基于CPU原語的比較并交換的非阻塞算法(CAS),不過這個在java的并發包裡已經實作了很多。

7、直接記憶體

不屬于JMM,在nio引入中,基于channel和buffer的I/O模式,可以使用native函數直接配置設定堆外的記憶體,然後在一個存在于堆中directByteBuffer作為這快記憶體引用進行操作,會提升性能,避免了java堆和native堆的來回複制資料。