1. 程式人生 > 程式設計 >Java記憶體模型和volatile、synchronized

Java記憶體模型和volatile、synchronized

前言

  1. 先說說計算機快取:計算機在執行程式的時候,都是通過CPU來執行指令,當然執行一串指令少不了需要某些資料,這些資料就在主記憶體中(實體記憶體)。隨著科技不斷髮展,CPU執行速度越來越快,但記憶體存取發展並沒有跟上CPU飛速發展的腳步,導致效能瓶頸出現在了記憶體存取上,所以這個時候出現了快取技術來加快資料的存取。
  2. 在程式真正執行時,會將運算需要的資料從主存複製一份到CPU的快取記憶體當中,那麼CPU進行計算時就可以直接從它的快取記憶體讀寫資料,當運算結束之後,再將快取記憶體中的資料重新整理到主存當中。
  3. 但是當出現多核CPU時,每個核上都存在快取記憶體,而且都可能執行著執行緒,執行緒又是併發的,它們都會修改自己所在核的快取記憶體中的變數,就會導致資料不一致的情況。

原子性、可見性、有序性

  1. 原子性是指在一個操作中就是cpu不可以在中途暫停然後再排程,既不被中斷操作,要不執行完成,要不就不執行。 -- 處理器優化會導致原子性問題
  2. 可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。 -- 快取一致性
  3. 有序性即程式執行的順序按照程式碼的先後順序執行。 -- 指令重排導致有序性問題
  4. 如何實現上面說到的三個特性呢?=>這就是Java的記憶體模型要解決的問題了

Java記憶體模型 -- JMM

  1. Java記憶體模型規定了所有的變數都儲存在主記憶體中,每條執行緒還有自己的工作記憶體,執行緒的工作記憶體中儲存了該執行緒中是使到的變數的主記憶體副本拷貝,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體。
  2. 不同的執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數的傳遞均需要自己的工作記憶體和主存之間進行資料同步進行。所以,就可能出現執行緒1改了某個變數的值,但是執行緒2不可見的情況。
  3. 而JMM就作用於工作記憶體和主存之間資料同步過程。他規定了如何做資料同步以及什麼時候做資料同步
  4. 所以,JMM就是一種規範,其用於解決由於多執行緒通過共享記憶體進行通訊時,存在的本地記憶體資料不一致、編譯器會對程式碼指令重排序、處理器會對程式碼亂序執行等帶來的問題。
  5. Java記憶體模型,除了定義了一套規範,還提供了一系列原語(volatile、synchronized、final、concurrent包),封裝了底層實現後,供開發者直接使用。

volatile

  1. 使用volatile能將被其修飾的變數在被修改後可以立即同步到主記憶體,被其修飾的變數在每次使用之前都從主記憶體重新整理。因此,可以使用volatile來保證多執行緒操作時變數的可見性。
  2. volatile只能保證有序性和可見性。

synchronized

  1. synchronized可以保證原子性、有序性和可見性。
  2. 作用域:
  3. 作用於普通方法
  4. 作用於靜態方法
  5. 作用於程式碼塊
  6. 只有在同步的塊或者方法中才能呼叫wait/notify等方法
  7. 作用範圍:
  8. 當修飾方法時候,其只作用於本物件例項
  9. 當修飾靜態方法時,作用於該類的物件(靜態方法本就屬於這個類,所以任何關於該類的都可)
  10. 當修飾程式碼塊時,還是該物件例項
  11. 這裡再延伸探討一下synchronized是如何保證原子性的呢?這就要先說說Java虛擬機器器如何執行執行緒同步了

Java虛擬機器器如何執行執行緒同步

  1. 在Java的記憶體結構中,有兩塊區域會被所有的執行緒共享 -- 方法區(儲存靜態變數)和堆(儲存所有物件例項),因為實現了共享所以同一個物件也應該能被多個執行緒改變。要實現這些改變那就需要Java虛擬機器器來管控。
  2. 所以虛擬機器器給每個物件和類都加了一個鎖,當某個執行緒要去改變這個物件的時候,就會去向虛擬機器器申請鎖(虛擬機器器可能會延遲給鎖),獲取到鎖的執行緒對物件進行修改,之後再將鎖還給虛擬機器器,虛擬機器器又為下個執行緒分配鎖。
  3. 其中給類加鎖,其實也是通過給物件加鎖實現的,因為每個類載入的時候,都會有一個java.lang.Class的例項,給這個例項加鎖來實現。
  4. 監視器(Monitors):JVM中鎖的是通過一個叫做監視器的東西來實現的,監視器用來監視一段程式碼,保證同一時間只有一個執行緒在執行它。
  5. 每一個監視器都與一個對段象相關聯,當開始執行到這段程式碼時,執行緒必須要獲取到它所引用物件的鎖。一旦獲得鎖,執行緒便可以進入“被保護”的程式碼開始執行。當執行緒離開程式碼塊的時候,無論如何離開,都會釋放所關聯物件的鎖。

synchronized實現原理

  1. 每個物件有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,執行緒執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:
    1. 如果monitor的進入數為0,則該執行緒進入monitor,然後將進入數設定為1,該執行緒即為monitor的所有者。
    2. 如果執行緒已經佔有該monitor,只是重新進入,則進入monitor的進入數加1。
    3. 如果其他執行緒已經佔用了monitor,則該執行緒進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權。
  2. 執行monitorexit的執行緒必須是objectref所對應的monitor的所有者。
    1. 指令執行時,monitor的進入數減1,如果減1後進入數為0,那執行緒退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的執行緒可以嘗試去獲取這個 monitor 的所有權。
  • 這是synchronized同步程式碼塊的原理,還有同步方法是通過ACC_SYNCHRONIZED標誌實現的,具體待補充...

最後回到synchronized是如何保證原子性問題上

比如執行緒1在執行monitorenter指令的時候,會對Monitor進行加鎖,加鎖後其他執行緒無法獲得鎖,除非執行緒1主動解鎖。即使在執行過程中,由於某種原因,比如CPU時間片用完,執行緒1放棄了CPU,但是,他並沒有進行解鎖。而由於synchronized的鎖是可重入的,下一個時間片還是隻能被他自己獲取到,還是會繼續執行程式碼。直到所有程式碼執行完。這就保證了原子性。

參考文章

how-the-java-virtual-machine-performs-thread-synchronization 再有人問你Java記憶體模型是什麼,就把這篇文章發給他。 Synchronized的實現原理