初涉Java內存模型


最近在繼續加緊準備春招,加強一下Java多線程這一塊,這篇文章是對於Java內存模型的一些總結,只是簡單地談談

Java內存模型

Java實現會帶來不同的“翻譯”,不同CPU平臺的機器指令又千差萬別,無法保證併發安全的效果一致。

JVM內存結構 VS Java內存模型 VS Java對象模型

三個截然不同的概念,容易混淆

JVM內存結構

和Java虛擬機的運行時區域有關

  • 組成:堆,虛擬機棧,方法區,本地方法棧,程序計數器

Java對象模型

和Java對象在虛擬機中的表現形式有關

  • 是Java對象自身的存儲模型
  • JVM會給這個類創建一個instanceKlass,保存在方法區,用來在JVM層表示該Java類
  • 當我們在Java代碼中,使用new創建一個對象的時候,JVM會創建一個instanceOopDesc對象,這個對象中包含了對象頭以及實例數據

Java內存模型(JMM)

爲什麼需要JMM(Java Memory Model)

  • C語言不存在內存模型的概念
  • 依賴處理器,不同處理器結果不一樣,可能一個程序在不同處理器運行結果不同
  • 無法保障併發安全
  • 需要一個標準,讓多線程運行的結果可以預期

JMM是一種規範

即這是一組規範,需要各個JVM的實現來遵循JMM規範,以便於開發者可以利用這些規範,更加方便地開發多線程程序。如沒有這樣的規範,那麼可能經過不同JVM的不同規則的重排序之後,導致不同的虛擬機上運行的結果不一樣,就會產生問題。

JMM是工具類和關鍵字的原理

  • volatile、synchronized、Lock等的原理都是JMM
  • 若沒有JMM,就需要我們自己制定什麼時候用內存柵欄,即什麼時候同步,很麻煩

最重要的3點內容:重排序、可見性、原子性

重排序

package jmm;


import java.util.concurrent.CountDownLatch;


/**
* @Auther: Bob
* @Date: 2020/2/15 15:41
* @Description: 重排序的演示
* 直到達到某個條件才停止,用來測試小概率時間
*/
public class OutOfOrderExecution {
	private static int x = 0, y = 0;
	private static int a = 0, b = 0;
	public static void main(String[] args) throws InterruptedException {
		int i = 0;
		for (; ; ) {
			i++;
			x = 0;
			y = 0;
			a = 0;
			b = 0;
			CountDownLatch latch = new CountDownLatch(1);
			Thread one = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					latch.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
					}
				a = 1;
				x = b;
				}
			});
		Thread two = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					latch.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				b = 1;
				y = a;
				}
			});
			one.start();
			two.start();
			latch.countDown();
			one.join();
			two.join();
			String result = "第" + i + "次 (" + x + "," + y + ")";
			if (x == 1 && y == 1) {
				System.out.println(result);
				break;
			} else {
				System.out.println(result);
			}
		}
	}
}

執行結果:第453次纔出現了1,1

什麼是重排序

在線程1內部的兩行代碼的實際執行順序和代碼在Java文件中的順序不一致,代碼指令並不是嚴格按照代碼語句順序執行的,它們的順序被改變了,這就是重排序,這裏被顛倒的是y = a 和 b = 1 這兩行語句

重排序的好處:提高處理速度
重排序的3種情況
  • 編譯器優化:包括JVM,JIT編譯器等
  • CPU指令重排:就算編譯器不發生重拍,CPU也可能對指令進行重拍
  • 內存的“重排序”:線程A的修改線程B卻看不到,引出可見性問題(不是真正的重排序)

可見性

什麼是可見性
package jmm;

/**
 * @Auther: Bob
 * @Date: 2020/2/15 16:37
 * @Description: 演示可見性帶來的問題
 */
public class FieldVisibility {
    volatile int a = 1;
    volatile int b = 2;
    private void change() {
        a = 3;
        b = a;
    }

    private void print() {
        System.out.println("b = " + b + ", a = " + a);
    }
    public static void main(String[] args) {
        while (true) {
            FieldVisibility test = new FieldVisibility();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.change();
                }
            }).start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.print();
                }
            }).start();
        }
    }

}

有可見性問題的原因:

  • 高速緩存的容量比主內存小,但是速度僅次於寄存器,所以在CPU和主內存之間就多了Cache層
  • 線程間的對於共享變量的可見性問題不是直接由多核引起的,而是由多緩存引起的
  • 如果所有核心都只用一個緩存,那麼就不存在內存可見性問題了
  • 每個核心都會將自己需要的數據讀到獨佔緩存中,數據修改後也是寫入到緩存中,然後等待輸入到主存中。所以會導致讀取的值是一個已經過期的值

“利用volatile關鍵字可以解決問題”

volatile關鍵字
volatile是什麼
  • voltile是一種同步機制,比synchronized或者Lock相關類更輕量,因爲適用vilatile並不會發生上下文切換等開銷很大的行爲。
  • 如果一個變量修改成volatile,那麼JVM就知道了這個變量可能被併發修改
  • 開銷小,相應能力也小,volatile做不到synchronized那樣的原子保護,volatile只在有限場景下才能發揮作用
volatile適用場景與不適用場景
  • 不適用:a++
  • 適用場景1:boolean flag,若一哥共享變量在程序中只是被各個線程賦值,而無其他操作,那麼可以用volatile來代替synchronized或者代替原子變量,因爲賦值自身具有原子性,而volatile又保證了可見性,所以足以保證線程安全。
  • 適用場合2:作爲刷新之前變量的觸發器
volatile的兩點作用
  1. 可見性:讀一個volatile變量之前,需要先使相應的本地緩存失效,這樣就必須讀取到主內存讀取最新值,寫一個volatile屬性會立即刷入到主內存。
  2. 禁止指令重排序優化:解決單利雙重鎖亂序問題
volatile和synchronized的關係

如果一個共享變量自始至終只被各個線程賦值,而沒有其他的操作,那麼久可以用volatile來代替synchronized或者代替原子變量,因爲賦值自身是有原子性的,而volatile又保證了可見性,所以就足以保證線程安全,此時volatile可以看做輕量版的synchronized。

用volatile可以修正重排序問題
volatile小結

在這裏插入圖片描述

在這裏插入圖片描述

除了volatile可以讓變量保證可見性外,synchronized、Lock、併發集合、Thread.join()和Thread.start()等都可以保證的可見性

對synchronized可見性的正確理解
  • synchronized不僅保證了原子性,還保證了可見性
  • synchronized不僅讓被保護的代碼安全,還近朱者赤(解鎖之前的所有操作另一個線程都能看到)

原子性

什麼是原子性

一系列的操作,要麼全部執行成功,要麼全部不執行,是不可分割的

Java中的原子操作有哪些
  • 除了long和double之外的基本類型(int,byte,boolean,short,char,float)的賦值操作,根據Oracle的官方文檔,在32位的JVM上,long和double的操作不是原子的,但是在64位的JVM上是原子的,對於64位的值的寫入 ,可以分爲兩個32位的操作進行寫入,讀取錯誤,使用volatile解決。(在實際開發中,商用虛擬機中不會出現)
  • 所有引用reference的賦值操作,不論是32位還是64位的操作系統
  • java.concurrent.Atomic.*包中所有類的原子操作
原子操作 + 原子操作 != 原子操作
  • 簡單地把原子操作組合在一起,並不能保證整體依然具有原子性
  • 全同步的HashMap也不完全安全

總結得不太好,多請見諒,多數都只是涉及概念層面的東西。。。

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