java內存模型與線程《Java虛擬機》要點精煉

Java內存模型

物理計算的內存模型與亂序排序

物理計算機中的併發問題:大多數的計算任務不能只由處理器完成,而需要通過處理器與內存交互,而處理器處理的速度與內存讀取的速度之間差了幾個數據集,因此引入了高速緩存來作爲內存與處理器之間的緩衝。

將運算需要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存之中。
但這樣會涉及到一個重要問題:如果多個處理器處理的是同一塊內存,就可能導致緩存不一致,進而影響在主內存的存儲。
在這裏插入圖片描述
除了高速緩存外,還引入了亂序優化。爲了使得處理器內部的運算單元能儘量被充分利用,處理器可能會對輸入代碼進行亂序執行(Out-Of-Order Execution)優化,最後再進行重組。Java中也有對指令的重排序優化。

java中的內存模型

基本模型

Java內存模型的主要目標是定義程序中各個變量的訪問規則。這裏的變量不包括局部變量與方法參數,因爲它們是被線程所私有的,不涉及共用。這裏的變量是指對象實例,靜態變量,和構成數組對象的元素。

java的內存模型包括主內存與工作內存,主內存用於存儲所有的變量,而工作內存是每個線程獨有的,工作內存中儲存了該線程所用到的變量的主內存副本拷貝(不會拷貝整個對象,而是拷貝對象中的部分字段),一個線程對變量進行操作(讀取或更改)都需要對工作內存進行操作,而不是直接向主內存操作。
在這裏插入圖片描述

工作內存與主內存之間的交互

  • lock(鎖定):作用於主內存的變量,將變量標識爲線程獨佔的狀態
  • unlock(解鎖):作用於主內存的變量,把一個處於鎖定狀態的內存釋放出來,釋放後的變量纔可以被其他內存使用。
  • read(讀取):作用於主內存的變量,它把主內存的變量的值傳輸到線程的工作內存。
  • load(加載):作用於工作內存的變量,將read讀取的內存存儲至工作內存的變量副本中。
  • use(使用):作用於工作內存的變量,他將一個工作內存的變量的值交給執行引擎,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作
  • assign(賦值):作用於工作內存的變量,將執行引擎中的值賦給工作內存的變量,每當虛擬機遇到一個賦值給變量的字節碼指令會執行這個操作。
  • store(存儲):作用於工作內存的變量,將該變量的值傳輸到主內存,以便後續的write使用
  • write(寫入):作用於主內存的變量,將store傳輸的變量的值存儲到主內存中的變量裏。

基本規則:

  1. 不允許主內存向工作內存傳輸而工作內存不接受(不允許只有read而沒有load),也不允許工作內存向主內存存儲而主內存不接受(不允許只有store沒有write)。
  2. 不允許一個線程捨棄assign操作。(不允許在工作內存賦值之後,而不同步到主內存)
  3. 不允許一個線程無原因地,即沒有發生過任何assign操作就把數據從線程的工作內存同步回主內存中。
  4. 一個新的變量只能在主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量,即對一個變量實施use、store操作之前必須先執行過了load和assign操作。
  5. 對一個變量執行unlock操作之前,必須先把此變量同步回主內存中。
  6. 如果對一個變量執行lock操作,那將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。

volatile

1.保證變量對所有線程都是可見的,當一個線程更改了這個變量的值,那麼當前變量的值對於其他線程是立刻可見的。普通變量是無法做到的,因爲需要先在工作內存中賦值(assign),然後在發送回主內存修改。(store->write)
2.禁止指令重排序。指令重排序是指CPU採用了允許將多條指令不按程序規定的順序分開發送給各相應電路單元處,但要求重排序不會影響結果。例如指令一要求地址a中的值+10,指令二要求地址a中的值*2,而指令三要求地址b中的值+10。這樣的話我們可以將指令三放到指令一和指令二之間,因爲指令三和指令一指令三之間沒有關係,但指令一不能在指令二後面因爲(a+10)*2(a*2)+10的結果不同。

而volatile的思想是設置了內存屏障,不允許將內存屏障後面的指令排序到內存屏障之前,只有一個線程時不需要內存屏障,多個線程纔會需要,因爲單一線程的字節碼是串行執行的。

原子性

原子性:原子性就是指對數據的操作是一個獨立的、不可分割的整體。也就是說當數據進行一個操作時,不要切換線程,要在執行結束後纔可以切換線程。

以圖中爲例,我們要想執行i++這個操作, 首先就要i=i+1,之後存儲i=2,不能再這個過程中切換到其他線程。
在這裏插入圖片描述
一般解決互斥性和原子性的方式是Synchronized方法,或Lock方法。

Synchronized方法通過monitorenter和monitorexit來隱式的操作lock和unlock操作。

可見性

可見性是指當一個線程更改了變量的值後,其他線程能立刻得知該變量的值更改。java內存中是通過變量修改後再同步到主內存,在變量讀取前從主內存中刷新這種依賴主內存作爲媒介的方式來保證的可見性。

除volatile外,java中還有final和synchronized可以實現可見性

同步塊的可見性是當我們assign賦值後,必須先store並write進主內存後纔可以調用unlock方法。

而被final修飾的變量,只要在初始化結束後,且沒有this賦值逃逸,就可以被所有線程可見。

有序性

線程內字節碼以串行執行,線程外禁止指令重排序以保證有序性。

volatile是使用內存屏障組織指令重排序,而synchronized是同一時刻只允許一個線程對對象進行lock操作。

以單例模式爲例,new Singleton()其實是三步:(1)爲對象分配內存;(2)執行構造函數,初始化成員變量(3)將對象指向分配的內存(此時instance就不是null了),但jvm中允許亂序執行,有可能出現(1)(3)(2)的順序,那麼假如A線程執行了(1)(3),而此時B線程調用getInstance就會返回一個instance對象,但調用上就會出錯。
Java 中也可通過Synchronized或Volatile來保證順序性。

java與線程

實現線程的方式

1.使用內核線程實現

內核線程(KLT)就是直接由系統內核操作的線程,這種線程由內核完成切換,通過調度器(Scheduler)調度對線程進行調度,並負責將線程的任務分配給各個CPU去執行。

而實際中我們不會直接使用內核線程,而是使用輕量級進程(LWP),一個輕量級進程要對應一個內核線程。輕量級進程與內核線程是1:1的對應關係。
在這裏插入圖片描述
優點:每個輕量級進程都是一個獨立的調度單元,即使一個進程被堵塞了,也不影響其他進程正常工作。
缺點:需要在內核態與用戶態來回切換。同時每個輕量級進程需要內核線程的支持,因此一個系統所能有的輕量級線程是有限的。

2.使用用戶線程實現

指完全建立在用戶的線程庫上,而系統內核不能感知線程存在的實現。

優點:由於用戶線程的建立、同步、銷燬和調度完全在用戶態中完成,不需要內核的幫助,甚至可以不需要切換到內核態,所以操作非常快速且低消耗的,且可以支持規模更大的線程數量。

缺點:由於沒有系統內核的支援,所有的線程操作都需要用戶程序自己處理,線程的創建、切換和調度都是需要考慮的問題,實現較複雜。

一對多的線程模型進程:進程與用戶線程之間1:N的關係,如圖所示
在這裏插入圖片描述

java線程調度

協同式線程調度線程的執行時間由自己決定,當線程執行結束後會主動通知系統切換線程。只有當前執行線程執行結束後纔會繼續執行下一個線程。這樣做的後果是可能由於錯誤操作導致其他線程一直處於阻塞狀態。

搶佔式線程調度:每個線程的執行時間由系統決定,線程執行時間是系統可控的,不存在一個線程導致整個進程阻塞的問題。可以通過設置線程優先級,優先級越高的線程越容易被系統選擇執行。

線程間的協作(wait/notify/sleep/yield/join)

  1. 新建(New):線程創建後尚未啓動(沒有調用start方法)
  2. 運行(Runable):包括正在執行(Running)和等待着CPU爲它分配執行時間(Ready)兩種
    3.** 無限期等待(Waiting)**:該線程不會被分配CPU執行時間,要通過notify和notifyAll來通知停止等待。以下方法會 讓線程陷入無限期等待狀態:
    • 沒有設置Timeout參數的Object.wait()
    • 沒有設置Timeout參數的Thread.join()
    • LockSupport.park()
  3. 限期等待(Timed Waiting):該線程不會被分配CPU執行時間,但在一定時間後會被系統自動喚醒。以下方法會讓線程進入限期等待狀態:
    • Thread.sleep()
    • 設置了Timeout參數的Object.wait()
    • 設置了Timeout參數的Thread.join()
    • LockSupport.parkNanos()
    • LockSupport.parkUntil()
  4. 阻塞(Blocked):線程被阻塞。和等待狀態不同的是,阻塞狀態表示在等待獲取到一個排他鎖,在另外一個線程放棄這個鎖的時候發生;而等待狀態表示在等待一段時間或者喚醒動作的發生,在程序等待進入同步區域的時候發生。
  5. 結束(Terminated)線程已經結束執行
    在這裏插入圖片描述

wait

wait方法的作用就是阻塞當前線程等待notify/notifyAll方法的喚醒,或等待超時後自動喚醒。調用wait方法後,線程是會釋放對monitor對象的所有權的

notify

既然wait方式是通過對象的monitor對象來實現的,所以只要在同一對象上去調用notify/notifyAll方法,就可以喚醒對應對象monitor上等待的線程了。notify和notifyAll的區別在於前者只能喚醒monitor上的一個線程,對其他線程沒有影響,而notifyAll則喚醒所有的線程。

sleep

這個結果的區別很明顯,通過sleep方法實現的暫停,程序是順序進入同步塊的,只有當上一個線程執行完成的時候,下一個線程才能進入同步方法,sleep暫停期間一直持有monitor對象鎖,其他線程是不能進入的,而wait方法則不同,當調用wait方法後,當前線程會釋放持有的monitor對象鎖,因此,其他線程還可以進入到同步方法,線程被喚醒後,需要競爭鎖,獲取到鎖之後再繼續執行。

java線程安全

Syncronized

Synchronized的使用原理:每個對象有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:

monitorenter
1.如果monitor的進入數爲0,則該線程進入monitor,然後將進入數設置爲1,該線程即爲monitor的所有者。
2.如果線程已經佔有該monitor,只是重新進入,則進入monitor的進入數加1.
3.如果其他線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數爲0,再重新嘗試獲取monitor的所有權。

monitorexit
執行monitorexit的線程必須是object所對應的monitor的所有者。
指令執行時,monitor的進入數減1,如果減1後進入數爲0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去獲取這個 monitor 的所有權。

對於普通同步方法,鎖是當前實例對象。因此在類Test中對兩個方法進行上鎖,同時在另一個類中創建一個Test實例,並創建兩個線程分別執行Test實例的A、B兩個方法,兩個線程會按順序執行。線程2需要等待線程1執行完才能執行。

**對於靜態同步方法,鎖是當前類的Class對象。**當我們在類Test中創建了兩個靜態方法,同時對靜態方法上鎖。在另一個類中即使創建兩個對象,並創建兩個線程分別執行對象1的A方法和對象2的B方法,線程2也會等線程1結束之後才能執行。

對於同步方法塊,鎖是Synchonized括號裏配置的對象。對於代碼塊的同步實質上需要獲取Synchronized關鍵字後面括號中對象的monitor,由於這段代碼中括號的內容都是this,而method1和method2又是通過同一的對象去調用的,所以進入同步塊之前需要去競爭同一個對象上的鎖,因此只能順序執行同步塊。

Lock接口

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章