當前位置:
首頁 > 知識 > Java內存模型與線程安全

Java內存模型與線程安全

計算機cpu的運算能力強大,但是數據的存儲相對於cpu運算能力需要消耗大量時間,為了充分利用運算能力引入了緩存,但是也為計算機系統帶來了更高的複雜度,因為它引入了一個新的問題:緩存一致性。

線程的工作內存中保存了被該線程使用到的變數的主內存副本拷貝,線程對變數的所有操作(讀取、賦值等)都必須在工作內存中進行。

Java內存模型規定了所有的變數都存儲在主內存(Main Memory)中。每條線程還有自己的工作內存(Working Memory),線程的工作內存中保存了被該線程使用到的變數的主內存副本拷貝,線程對變數的所有操作(讀取,賦值等)都必須是工作內存中進行,而不能直接讀寫主內存中的變數。線程之間無法直接訪問對方工作內存中的變數,線程間變數值的傳遞均需要通過主內存來完成。交互關係如下圖:

Java內存模型與線程安全

從更底層來說,主內存就是硬體的內存,而為了獲取更好的運行速度,虛擬機及硬體系統可能會讓工作內存優先存儲於寄存器和高速緩存。

線程安全

cpu計算時數據讀取順序優先順序:寄存器-高速緩存-內存。線程耗費的是CPU,線程計算的時候,原始的數據來自內存,在計算過程中,有些數據可能被頻繁讀取,這些數據被存儲在寄存器和高速緩存中,當線程計算完後,這些緩存的數據在適當的時候應該寫回內存。

當多個線程同時讀寫某個內存數據時,各個線程都從主內存中獲取數據,線程之間數據是不可見的,就可能會產生寄存器/高速緩存/內存之間的同步問題。線程安全的前提是該變數是否被多個線程訪問,只要有多於一個的線程操作給定的狀態變數,此時就可能產生多線程問題。比如,主內存變數A原始值為1,線程1從主內存取出變數A,修改A的值為2,在線程1未將變數A寫回主內存的時候,線程2拿到變數A的值仍然為1,這就是多線程的可見性問題。


原子性、可見性與有序性

避免線程安全問題主要圍繞著並發過程中的原子性、可見性、有序性這三個特徵。

原子性(Atomicity)

原子性就是操作不能被線程調度機制中斷,要麼全部執行完畢,要麼不執行。java內存模型確保基本類型數據的訪問大都是原子操作,即多個線程在並發訪問的時候是線程非安全的。比如」a = 2」、 「return a;」都具有原子性。但是類似」a += b」、」i++」的操作不具有原子性,所以如果add方法不是同步的就會出現難以預料的結果。在某些JVM中」a += b」可能要經過(取出a和b; 計算a+b; 將計算結果寫入內存)步驟,如果有兩個線程t1,t2在進行這樣的操作。t1在第二步做完之後還沒來得及把數據寫回內存就被線程調度器中斷了,於是t2開始執行,t2執行完畢後t1又把沒有完成的第三步做完。這個時候就出現了錯誤,相當於t2的計算結果被無視掉了。再如,請分析以下哪些操作是原子性操作:

Java內存模型與線程安全

注意:在32位平台下,對64位數據的讀取和賦值是需要通過兩個操作來完成的,不能保證其原子性,這就導致了long、double類型的變數在32位虛擬機中是非原子操作。

可以使用AtomicXXX、synchronized和Lock保證原子性。synchronized和Lock能夠保證任一時刻只有一個線程執行該代碼塊,自然就不存在原子性問題了。

可見性

可見性就是一個線程對共享變數做了修改之後,其他的線程立即能夠看到修改後的值。Java內存模型將工作內存中的變數修改後的值同步到主內存,在讀取變數前從主內存刷新最新值到工作內存中,這種依賴主內存的方式來實現可見性的。

看下面這段代碼:

Java內存模型與線程安全

上面的代碼可能出現下面情形,當線程1執行 i = 10時,會先把i的初始值載入到高速緩存中,然後賦值為10,那麼高速緩存當中i的值變為10了,如果此時沒有立即寫入到主存當中,此時線程2執行 j = i,它會先去主存讀取i的值並載入到緩存當中,注意此時內存當中i的值還是0,那麼就會使得j的值為0,而不是10。

Java提供了volatile關鍵字來保證可見性。當對非volatile變數進行讀寫的時候,每個線程先從內存拷貝變數到CPU緩存中,非volatile變數被修改之後,什麼時候被寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值,因此無法保證可見性。而聲明變數是volatile的,會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會跳過CPU cache這一步去內存中讀取新值。volatile只確保了可見性,並不能確保原子性。

synchronized也可以保證可見性。當ThreadA釋放鎖M時,它所寫過的變數(比如,x和y,存在它工作內存中的)都會同步到主存中,而當ThreadB在申請同一個鎖M時,ThreadB的工作內存會被設置為無效,然後ThreadB會重新從主存中載入它要訪問的變數到它的工作內存中(這時x=1,y=1,是ThreadA中修改過的最新的值)。通過這樣的方式來實現ThreadA到ThreadB的線程間的通信。

有序性(Ordering)

有序性:即程序執行的順序按照代碼的先後順序執行。為了提高性能,編譯器和處理器常常會對指令做重排序。CPU雖然並不保證完全按照代碼順序執行,但它會保證程序最終的執行結果和代碼順序執行時的結果一致。重排序過程不會影響到單線程程序的執行,但會影響到多線程並發執行的正確性。

下面看一個例子:

Java內存模型與線程安全

代碼中,由於語句1和語句2沒有數據依賴性,可能會被重排序。假如發生了重排序,在線程1執行過程中先執行語句2,而此是線程2會以為初始化工作已經完成,那麼就會跳出while循環,去執行doSomethingwithconfig(context)方法,而此時context並沒有被初始化,就會導致程序出錯。

可以通過volatile關鍵字來保證一定的「有序性」,volatile關鍵字本身就包含了禁止指令重排序的語義,volatile前的代碼還會在voaltile前,其後的代碼還會在其後。另外可以通過synchronized和Lock來保證有序性,synchronized和Lock保證每個時刻是有一個線程執行同步代碼,相當於是讓線程順序執行同步代碼,自然就保證了有序性,synchronized標記的變數可以被編譯器優化。

也就是說,要想並發程序正確地執行,必須要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會導致程序運行不正確。


volatile/synchronized/atomic

上面介紹中可以得知,synchronized是通過同一時刻只有一個線程執行共享代碼來保證多線程三個特徵的;volatile 變數具有 synchronized 的可見性特性,禁止指令重排,但是不具備原子特性。使用volatile變數,必須同時滿足下面兩個條件:

  • 對變數的寫操作不依賴於當前值。

  • 該變數沒有包含在具有其他變數的不變式中。

在目前大多數的處理器架構上,volatile 讀操作開銷非常低,幾乎和非 volatile 讀操作一樣。而 volatile 寫操作的開銷要比非 volatile 寫操作多很多,因為要保證可見性需要實現內存界定(Memory Fence),即便如此,volatile 的總開銷仍然要比鎖獲取低,volatile 操作不會像鎖一樣造成阻塞。

AtomicXXX具有原子性和可見性,就拿AtomicLong來說,它既解決了volatile的原子性沒有保證的問題,又具有可見性。它的實現使用了CAS(比較並交換)指令保證了原子性,AtomicLong的源碼里也用到了volatile。

總結,當前常用的多線程同步機制可以分為下面三種類型:

  1. volatile 變數:輕量級多線程同步機制,不會引起上下文切換和線程調度。僅提供內存可見性保證,不提供原子性。

  2. CAS 原子指令:輕量級多線程同步機制,不會引起上下文切換和線程調度。它同時提供內存可見性和原子化更新保證。

  3. 內部鎖和顯式鎖:重量級多線程同步機制,可能會引起上下文切換和線程調度,它同時提供內存可見性和原子性。

學習Java的同學注意了!!!

學習過程中遇到什麼問題或者想獲取學習資源的話,歡迎加入Java學習交流群495273252,我們一起學Java!

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Java團長 的精彩文章:

Java編程之反射中的註解詳解
Java中線程總結
全面掌握Java內部類
程序員的未來在哪裡?

TAG:Java團長 |

您可能感興趣

Intel CPU 根本性的安全風險:超線程
聊一聊 Spring 中的線程安全性
OpenBSD出於安全考慮:禁用Intel處理器的超線程技術
muduo——EventLoop處理線程安全的問題
Spring 中獲取 request 的幾種方法,及其線程安全性分析
Linux中進程與線程的概念以及區別
Android 進程和線程
android 多線程編程
談談servlet線程不安全的問題
SqlSessionTemplate是如何保證MyBatis中SqlSession的線程安全的?
深入理解Flutter引擎線程模式
Envoy為什麼能戰勝Ngnix——線程模型分析篇
Python學習之進程和線程
python threading中處理主進程和子線程的關係
高性能的 PHP 封裝的 HTTP Restful 多線程並發請求庫-MultiHttp
Python的Socket知識6:線程、線程鎖、線程池、上下文管理
入門Python多線程/多進程編程
Synchronized鎖在Spring事務管理下,為啥還線程不安全?
數據科學愛用程序語言Julia將加入多線程平行運算功能
同步容器(如Vector)並不是所有操作都線程安全