10-Java中共享內存可見性以及synchronized和volatile關鍵字


Java中共享變量的內存可見性

  • 我們首先來看一下在多線程下處理共享變量時Java的內存模型,如圖所示

    image

    Java內存模型規定,將所有的變量都存放在主存中,當線程使用變量的時候,會把主內存里面的變量賦值到自己的工作區間或者叫工作內存,線程讀寫變量時操作的是自己的工作內存中的變量,Java內存模型是一個抽象的概念,那么在實際中線程的工作內存是什么呢?

    image

    圖中顯示的是一個雙核CPU系統架構,每一個核都有自己的控制器和運算器,其中控制器包含一組寄存器和操作控制器,運算器執行算術邏輯運算。每一個核都有自己的一級緩存。

    當一個線程操作共享變量的時,它首先從主存復制共享變量到自己的工作內存(私有內存)中,然后對工作內存的變量進行處理,處理完之后將變量值更新到主存中。假如線程A和線程B同時處理一個共享變量,會出現什么情況呢?我們使用上圖2-5所示的CPU架構,假設線程A和B使用不同的CPU執行,並且當前兩級cache都為空,那么由於這個時候cache的存在,將會導致內存不可見問題:

    1. 線程A首先獲取到共享變量X的值,由於兩級cache都沒有命中,所以加載主內存中X的值,假如為0。然后把X=0值緩存到兩級cache中,線程A修改X=1,然后將其寫入兩級cache中,並且刷新到主存中。線程A操作完畢后,線程A所在的CPU的兩級cache和主存中的X都為1。
    2. 線程B獲取到X的值,首選一級緩存沒有命中,然后看二級緩存,二級緩存命中了,所以返回了一個X=1;到這里一切都是正常的,因為這時候主內存中X=1,然后線程B修改X=2,並將其放到線程B所在的一級cache和二級cache中,最后更新主存中X=2。
    3. 線程A再次要修改X的值,獲取時一級緩存中命中,並且X=1,到這里問題就出現了,明明線程B已經把X修改為2了,為何線程A讀取X的值還是1呢?這就是共享變量的內存不可見問題。也就是線程B寫入的值對線程A不可見。那么如何解決共享變量線程不可見的問題呢?這里就需要使用java中的volatile關鍵字解決這個問題,下面會講到。

Java中Synchronized關鍵字

  • synchronized關鍵字介紹

    synchronized塊是Java提供的一種原子性內置鎖,Java中的每一個對象都可以看成一個同步鎖來使用。這些Java內置的使用者看不到的鎖被稱為內置鎖,也叫監視器鎖。線程的執行代碼塊在進入synchronized代碼塊前會自動的獲取到內部鎖,這時候其他線程訪問該同步代碼塊會被阻塞掛起。拿到內部鎖的線程會在正常退出同步代碼塊或者拋出異常后或者在同步代碼塊內調用了該內置鎖資源的wait系列方法時會釋放該內置鎖。內置鎖是排它鎖,也就是當一個線程獲取到這個鎖之后,其他線程必須等待該線程釋放鎖后才能獲得該鎖。

  • synchronized的內存語義

    前面介紹了共享變量內存可見性問題主要是由於線程當中工作內存所導致的。下面我們來講解synchronized的一個內存語義,這個內存語義就是解決共享變量內存可見性問題。進入synchronized塊的內存語義是把synchronized塊內使用到的變量從線程的工作內存中清除,這樣在synchronized塊內使用到該變量時候就不會從工作內存中取,而是直接從主存中取,退出synchronized塊的內存語義是把sunchronized塊對共享變量的修改刷新到主存中。其實這也是加鎖和釋放鎖的概念。當獲取鎖后會清空本地內存中將會用到的共享變量,在使用這些共享內存會從主存中加載,在釋放鎖時會將本地內存中修改的共享變量刷新到主存中。synchronized除了用來解決共享變量內存不可見問題,還可以用來實現原子性操作。另外注意的是,synchronized關鍵字會不會引起線程上下文切換並帶來線程調度開銷。

Java中volatile關鍵字

  • 上面介紹的是使用鎖的方式可以解決共享變量內存不可見問題。但是使用鎖太笨重,因此它會帶來線程上下文切換問題。對於解決內存可見性問題,Java還提供了一種弱形式的同步,也就是使用volatile關鍵字,該關鍵字確保一個變量的更新對其他線程馬上可見。當一個變量被聲明為volatile時,線程在寫入變量的時候不會把值緩存到寄存器或者其他地方,而是會把值刷新返回到主存中。當其他線程讀取該共享變量的時候,會直接從主存中重新獲取到最新值。而並不是使用工作內存中的值。voltile內存語義和synchronized語義有相似之處,當線程寫入volatile變量值的時候就等於線程退出synchronized同步塊(把寫入工作內存中共享變量的值同步到主內存),讀取volatile變量值時就相當於進入進入同步代碼塊(先清空本地內存中共享變量值,再從主存中獲取到最新值)。

  • 下面使用volatile關鍵字解決內存可見性問題的例子,如下代碼中的共享變量value就是不安全的,因為這里沒有適當的同步措施。

    public class ThreadNotSafeInteger {
        private int value;
    
        public int getValue() {
            return value;
        }
    
        public void setValue(int value) {
            this.value = value;
        }
    }
    
  • 首先來看使用synchronized關鍵字進行同步的方式

    public class ThreadNotSafeInteger {
        private int value;
    
        public synchronized int getValue() {
            return value;
        }
    
        public synchronized void setValue(int value) {
            this.value = value;
        }
    }
    
  • 然后使用volatile進行同步

    public class ThreadNotSafeInteger {
        private volatile int value;
    
        public int getValue() {
            return value;
        }
    
        public void setValue(int value) {
            this.value = value;
        }
    }
    
  • 在這里使用volatile和synchronized是等價的。都解決的共享內存變量value不可見問題。但是前者是獨占鎖,其他線程調用會被阻塞等待,同時還存在線程上下文切換個線程重現調度的開銷。這也是使用鎖方式不好的地方。后者使用的是非阻塞算法,不會造成線程上下文切換的開銷。

Java中原子性操作

  • 所謂原子操作,是指執行一系列操作要么一次性全部執行完,要么全部都不執行。如果不能保證操作室原子性操作,那么就會出現線程安全問題,如下:

    public class ThreadNotSafeCount {
        private Long value;
    
        public Long getValue() {
            return value;
        }
    
        public void setValue(Long value) {
            this.value = value;
        }
    
        private void inc() {
            ++value;
        }
    }
    

    首先執行javac ThreadNotSafeCount.java命令

    然后執行javap -c ThreadNotSafeCount.class命令

    Compiled from "ThreadNotSafeCount.java"
    public class com.heiye.learn2.ThreadNotSafeCount {
      public com.heiye.learn2.ThreadNotSafeCount();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
    
      public java.lang.Long getValue();
        Code:
           0: aload_0
           1: getfield      #2                  // Field value:Ljava/lang/Long;
           4: areturn
    
      public void setValue(java.lang.Long);
        Code:
           0: aload_0
           1: aload_1
           2: putfield      #2                  // Field value:Ljava/lang/Long;
           5: return
    }
    
  • 我們該如何保證多個操作的原子性呢?最簡單的辦法就是使用synchronized關鍵字進行同步,代碼如下

    public class ThreadNotSafeCount {
        private Long value;
    
        public synchronized Long getValue() {
            return value;
        }
    
        public synchronized void setValue(Long value) {
            this.value = value;
        }
    
        private synchronized void inc() {
            ++value;
        }
    }
    

    使用synchronized關鍵字的確可以實現線程安全性,即內存可見性和原子性,但是synchronized是獨占鎖,內有獲取到內部鎖的線程會被阻塞掉,但是getValue()只是讀操作,多個線程同時調用這個方法並不會引發線程安全問題,但是加了synchronized關鍵字后,同一時間只能有一個線程可以調用,這顯然是不合理的,沒有必要。也許會有這樣一個疑惑,可以不可把這個方法上的synchronized關鍵字去掉呢?答案是不能的,因為這里是靠synchronized來實現共享內存可見性的,那么有沒有什么更好的辦法呢?,答案是有的,下面講到的在內部使用非阻塞CAS算法實現的原子性操作類AtomicLong就是一個不錯的選擇。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM