談到并發編程 ,必知曉并發三要素 :原子性、可見性、有序性。而 Volatile 涉及了可見性與有序性,是輕量級的 synchronized ,在并發編程中扮演著極其重要的角色。
本文將從理論知識到源碼實例,深入剖析 Volatile 的實現原理,幫助快速掌握并正確使用 Volatile 變量:
1.Volatile關鍵字
2.Java內存模型
3.Volatile內存模型可見性
4.Volatile的工作原理
5.Volatile的源碼案例
在談 Volatile 之前,我們先回顧下 Java 內存模型的三要素:原子性、可見性、有序性,也就是大家常提到的并發編程三要素。
并發編程的三要素
1.原子性
和數據庫事務中的原子性一樣,滿足原子性特性的操作是不可中斷的,要么全部執行成功要么全部執行失敗
只有簡單的讀取、賦值(而且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操作)才是原子操作。
比如:i = 2;j = i;i++;i = i + 1;
上面4個操作中,i=2是讀取操作,必定是原子性操作,j=i你以為是原子性操作,其實吧,分為兩步,一是讀取i的值,然后再賦值給j,這就是2步操作了,稱不上原子操作,i++和i = i + 1其實是等效的,讀取i的值,加1,再寫回主存,那就是3步操作了。
所以上面的舉例中,最后的值可能出現多種情況,就是因為滿足不了原子性。
非原子操作都會存在線程安全問題,需要我們使用同步技術(sychronized)來讓它變成一個原子操作,java的concurrent包下提供了一些原子類:比如:AtomicInteger、AtomicLong等。
2.可見性
多個線程訪問同一個共享變量時,其中一個線程對這個共享變量值的修改,其他線程能夠立刻獲得修改以后的值
3.有序性
編譯器和處理器為了優化程序性能而對指令序列進行重排序,也就是你編寫的代碼順序和最終執行的指令順序是不一致的。
但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程并發執行的正確性。
Volatile
Volatile 是一個Java語言的類型修飾符,一旦一個共享變量(類的成員變量、類的靜態成員變量)被Volatile修飾之后,那么就具備了兩層語義:
1、保證多線程下的可見性
2、禁止進行指令重排序(即保證有序性)
這里需要注意一個問題,Volatile只能讓被他修飾內容具有可見性、有序性。Volatile只能保證對單次讀/寫的原子性,i++ 這種操作不能保證原子性。
Volatile的內存模型
Java 內存模型(JMM)是一種抽象的概念,并不真實存在,它描述了一組規則或規范,通過這組規范定義了程序中各個變量(包括實例字段、靜態字段和構成數組對象的元素)的訪問方式。
試圖屏蔽各種硬件和操作系統的內存訪問差異,以實現讓 Java 程序在各種平臺下都能達到一致的內存訪問效果。
Java內存模型規定了所有的變量都存儲在主內存中,每條線程還有自己的工作內存,線程的工作內存中保存了該線程中是用到的變量的主內存副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量的傳遞均需要自己的工作內存和主存之間進行數據同步進行。
1)主內存主要存儲的是Java實例對象,所有線程創建的實例對象都存放在主內存中,不管該實例對象是成員變量還是方法中的本地變量(也稱局部變量),當然也包括了共享的類信息、常量、靜態變量。由于是共享數據區域,多條線程對同一個變量進行訪問可能會發現線程安全問題。
2)工作內存每條線程都有自己的工作內存(Working Memory,又稱本地內存,可與前面介紹的處理器高速緩存類比),線程的工作內存中保存了該線程使用到的變量的主內存中的共享變量的副本拷貝。工作內存是 JMM 的一個抽象概念,并不真實存在。它涵蓋了緩存,寫緩沖區,寄存器以及其他的硬件和編譯器優化。
主要存儲當前方法的所有本地變量信息(工作內存中存儲著主內存中的變量副本拷貝),每個線程只能訪問自己的工作內存,即線程中的本地變量對其它線程是不可見的,就算是兩個線程執行的是同一段代碼,它們也會各自在自己的工作內存中創建屬于當前線程的本地變量,當然也包括了字節碼行號指示器、相關Native方法的信息。
Volatile的實現原理
Volatile 保證內存可見性
主內存和工作內存之間的交互有具體的交互協議,JMM定義了八種操作來完成,這八種操作是原子的、不可再分的,它們分別是:lock,unlock,read,load,use,assign,store,write。
其中,lock , unlock , read , write 作用于主內存; load ,use , assign , store 作用于工作內存。
1) lock
將主內存中的變量鎖定,為一個線程所獨占
2) unclock
將lock加的鎖定解除,此時其它的線程可以有機會訪問此變量
3) read
將主內存中的變量值讀到工作內存當中
4) load
將read讀取的值保存到工作內存中的變量副本中。
5) use
將值傳遞給線程的代碼執行引擎。
6) assign
將執行引擎處理返回的值重新賦值給變量副本。
7) store
將變量副本的值存儲到主內存中。
8) write
將 store 存儲的值寫入到主內存的共享變量當中。
從主存復制變量到當前工作內存(read and load)
執行代碼,改變共享變量值 (use and assign)
用工作內存數據刷新主存相關內容 (store and write)
指令規則
1)read 和 load、store 和 write 必須成對出現。
2)assign 操作,工作內存變量改變后必須刷回主內存。
3)同一時間只能運行一個線程對變量進行 lock,當前線程 lock 可重入,unlock 次數必須等于 lock 的次數,該變量才能解鎖。
4)對一個變量 lock 后,會清空該線程工作內存變量的值,重新執行 load 或者 assign 操作初始化工作內存中變量的值。
5)unlock 前,必須將變量同步到主內存( store/write 操作)。
Volatile源碼案例
以上,是 Java 并發編程有關 Volatile 的實現原理。
關注『 mikechen的互聯網架構 』,回復“架構”,可獲取本篇文章的視頻詳解。
---END--
哈嘍,我是Mike,很高興認識你。
每篇深度技術文,都是我耗時2-5天用心創作的,如果大家看了覺得還行,謝謝【點贊+收藏+轉發】一鍵三連支持下。