首頁  >  文章  >  Java  >  Java之JMM高並發程式設計實例分析

Java之JMM高並發程式設計實例分析

WBOY
WBOY轉載
2023-05-02 18:52:07687瀏覽

一、什麼是JMM

JMM就是Java記憶體模型(java memory model)。因為在不同的硬體生產商和不同的作業系統下,記憶體的存取有一定的差異,所以會造成相同的程式碼運作在不同的系統上會出現各種問題。所以java記憶體模型(JMM)屏蔽掉各種硬體和作業系統的記憶體存取差異,以實現讓java程式在各種平台下都能達到一致的並發效果。

Java記憶體模型規定所有的變數都儲存在主記憶體中,包括實例變量,靜態變量,但不包含局部變數和方法參數。每個線程都有自己的工作內存,線程的工作內存保存了該線程用到的變量和主內存的副本拷貝,線程對變量的操作都在工作內存中進行。線程不能直接讀寫主記憶體中的變數。

不同的執行緒之間也無法存取對方工作記憶體中的變數。線程之間變數值的傳遞均需要透過主記憶體來完成。

Java之JMM高並發程式設計實例分析

每個執行緒的工作記憶體都是獨立的,執行緒操作資料只能在工作記憶體中進行,然後刷回到主記憶體。這是 Java 記憶體模型定義的線程基本工作方式。

溫馨提醒一下,這裡有些人會把Java記憶體模型誤解為Java記憶體結構,然後答到堆,棧,GC垃圾回收,最後和麵試官想問的問題相差甚遠。其實一般問到Java記憶體模型都是想問多線程,Java並發相關的問題。

二、JMM定義了什麼

這個簡單,整個Java記憶體模型其實是圍繞著三個特徵建立起來的。分別是:原子性,可見性,有序性。這三個特徵可謂是整個Java並發的基礎。

原子性

原子性指的是一個操作是不可分割,不可中斷的,一個執行緒在執行時不會被其他執行緒幹擾。

面試官拿筆寫了段程式碼,下面這幾句程式碼能保證原子性嗎?

int i = 2;
int j = i;
i++;
i = i + 1;

第一句是基本型別賦值運算,必定是原子性運算。

第二句先讀取i的值,再賦值到j,兩步驟操作,不能保證原子性。

第三和第四句其實是等效的,先讀取i的值,再 1,最後賦值到i,三步操作了,不能保證原子性。

JMM只能保證基本的原子性,如果要保證一個程式碼區塊的原子性,提供了monitorenter 和 moniterexit 兩個字節碼指令,也就是 synchronized 關鍵字。因此在 synchronized 區塊之間的操作都是原子性的。

可見性

可見性指當一個執行緒修改共享變數的值,其他執行緒能夠立即知道被修改了。 Java是利用volatile關鍵字來提供可見性的。當變數被volatile修飾時,這個變數被修改後會立刻刷新到主內存,當其它線程需要讀取該變數時,就會去主內存中讀取新值。而普通變數則不能保證這一點。

除了volatile關鍵字之外,final和synchronized也能實現可見性。

synchronized的原理是,在執行完,進入unlock之前,必須將共享變數同步到主記憶體中。

final修飾的字段,一旦初始化完成,如果沒有物件逸出(指物件為初始化完成就可以被別的執行緒使用),那麼對於其他執行緒都是可見的。

有序性

在Java中,可以使用synchronized或volatile保證多執行緒之間操作的有序性。實作原理有些差異:

volatile關鍵字是使用記憶體屏障達到禁止指令重排序,以確保有序性。

synchronized的原理是,一個執行緒lock之後,必須unlock後,其他執行緒才可以重新lock,使得被synchronized包住的程式碼區塊在多執行緒之間是串列執行的。

三、八種記憶體互動操作

記憶體互動操作有8種:

  • lock(鎖定),作用於主記憶體中的變量,把變數標識為執行緒獨佔的狀態。

  • read(讀取),作用於主記憶體的變量,把變數的值從主記憶體傳送到執行緒的工作記憶體中,以便下一步的load操作使用。

  • load(已載入),作用於工作記憶體的變量,把read操作主存的變數放入工作記憶體的變數副本中。

  • use(使用),作用於工作記憶體的變量,把工作記憶體中的變數傳送到執行引擎,每當虛擬機器遇到一個需要使用到變數的值的位元組碼指令時將會執行這個操作。

  • assign(賦值),作用於工作記憶體的變量,它把一個從執行引擎中接受到的值賦值給工作記憶體的變數副本中,每當虛擬機器遇到一個給變數賦值的字節碼指令時將會執行這個操作。

  • store(儲存),作用於工作記憶體的變量,它把一個從工作記憶體中一個變數的值傳送到主記憶體中,以便後續的write使用。

  • write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

我再补充一下JMM对8种内存交互操作制定的规则吧:

  • 不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。

  • 不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。

  • 不允许线程将没有assign的数据从工作内存同步到主内存。

  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。

  • 一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。

  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。

  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。

  • 一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。

四、volatile关键字

很多并发编程都使用了volatile关键字,主要的作用包括两点:

  • 保证线程间变量的可见性。

  • 禁止CPU进行指令重排序。

可见性

volatile修饰的变量,当一个线程改变了该变量的值,其他线程是立即可见的。普通变量则需要重新读取才能获得最新值。

volatile保证可见性的流程大概就是这个一个过程:

Java之JMM高並發程式設計實例分析

volatile一定能保证线程安全吗

先说结论吧,volatile不能一定能保证线程安全。

怎么证明呢,我们看下面一段代码的运行结果就知道了:

public class VolatileTest extends Thread {
private static volatile int count = 0;
public static void main(String[] args) throws Exception {
Vector<Thread> threads = new Vector<>();
for (int i = 0; i < 100; i++) {
VolatileTest thread = new VolatileTest();
threads.add(thread);
thread.start();
}
//等待子线程全部完成
for (Thread thread : threads) {
thread.join();
}
//输出结果,正确结果应该是1000,实际却是984
System.out.println(count);//984
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
//休眠500毫秒
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
count++;
}
}
}

为什么volatile不能保证线程安全?

很简单呀,可见性不能保证操作的原子性,前面说过了count++不是原子性操作,会当做三步,先读取count的值,然后+1,最后赋值回去count变量。需要保证线程安全的话,需要使用synchronized关键字或者lock锁,给count++这段代码上锁:

private static synchronized void add() {
count++;
}

禁止指令重排序

首先要讲一下as-if-serial语义,不管怎么重排序,(单线程)程序的执行结果不能被改变。

为了使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率,只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。

重排序的种类分为三种,分别是:编译器重排序,指令级并行的重排序,内存系统重排序。整个过程如下所示:

Java之JMM高並發程式設計實例分析

指令重排序在单线程是没有问题的,不会影响执行结果,而且还提高了性能。但是在多线程的环境下就不能保证一定不会影响执行结果了。

所以在多线程环境下,就需要禁止指令重排序。

volatile关键字禁止指令重排序有两层意思:

  • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行。

  • 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

下面举个例子:

private static int a;//非volatile修饰变量
private static int b;//非volatile修饰变量
private static volatile int k;//volatile修饰变量
private void hello() {
a = 1; //语句1
b = 2; //语句2
k = 3; //语句3
a = 4; //语句4
b = 5; //语句5
//...
}

变量a,b是非volatile修饰的变量,k则使用volatile修饰。所以语句3不能放在语句1、2前,也不能放在语句4、5后。但是语句1、2的顺序是不能保证的,同理,语句4、5也不能保证顺序。

并且,执行到语句3的时候,语句1,2是肯定执行完毕的,而且语句1,2的执行结果对于语句3,4,5是可见的。

volatile禁止指令重排序的原理

首先要讲一下内存屏障,内存屏障可以分为以下几类:

  • LoadLoad 屏障:對於這樣的語句Load1,LoadLoad,Load2。在Load2及後續讀取操作要讀取的資料被存取前,保證Load1要讀取的資料已讀取完畢。

  • StoreStore屏障:對於這樣的語句Store1, StoreStore, Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。

  • LoadStore 屏障:對於這樣的語句Load1, LoadStore,Store2,在Store2及後續寫入作業被刷出前,保證Load1要讀取的資料被讀取完畢。

  • StoreLoad 屏障:對於這樣的語句Store1, StoreLoad,Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。

在每個volatile讀取作業後插入LoadLoad屏障,在讀取作業後插入LoadStore屏障。

Java之JMM高並發程式設計實例分析

在每個volatile寫入作業的前面插入一個StoreStore屏障,後面插入一個SotreLoad屏障。

Java之JMM高並發程式設計實例分析

以上是Java之JMM高並發程式設計實例分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除