從volatile說到,i++原子操作,執行緒安全問題
1、可見性(Visibility)
可見性是指,當一個執行緒修改了某一個全域性共享變數的數值,其他執行緒是否能夠知道這個修改。
顯然,在序列程式來說可見性的問題是不存在的。因為你在任何一個地方操作修改了某個變數,那麼在後續的程式裡面,讀取這個變數的數值,一定是修改後的數值。
但是,這個問題在並行程式裡面就不見得了。在並行程式裡面,如果一個執行緒修改了某一個全域性變數,那麼其他執行緒未必可以馬上知道這個變動。下面的圖1展示了可見性問題的一種。如果在CPU1和CPU2上各執行一個執行緒,他們共享變數t,由於編譯器優化或者應該優化的緣故,在CPU1上的執行緒將變數t進行了優化,將其快取在Cache中或者暫存器裡面。這種情況下,如果在CPU2上的那個執行緒修改了執行緒t 的實際數值,那麼CPU1上的執行緒可能並無法意識到這個改動,依然會讀取cache中或者暫存器裡面的資料。因此,就產生了可見性問題。外在表現就是,變數t 的數值被CPU2上的執行緒修改,但是CPU1上的執行緒依然會讀到一箇舊的資料。
2、原子性(Atomicity)
原子性,指的是一個操作是不可中斷的。即使是在多個執行緒一起執行的時候,一個操作一旦開始,就不會被其他執行緒打斷。
3、Java記憶體模型的抽象結構( JMM )
在Java中,所有例項域、靜態域和陣列元素都儲存在堆記憶體中,堆記憶體線上程之間共享(本章用“共享變數”這個術語代指例項域,靜態域和陣列元素)。區域性變數(Local Variables),方法定義引數(Java語言規範稱之為Formal Method Parameters)和異常處理器引數(ExceptionHandler Parameters)不會線上程之間共享,它們不會有記憶體可見性問題,也不受記憶體模型的影響。
Java執行緒之間的通訊由Java記憶體模型(本文簡稱為JMM)控制,JMM決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。從抽象的角度來看,JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數儲存在主記憶體(Main Memory)中,每個執行緒都有一個私有的本地記憶體(Local Memory),本地記憶體中儲存了該執行緒以讀/寫共享變數的副本。本地記憶體是JMM的一個抽象概念,並不真實存在。它涵蓋了快取、寫緩衝區、暫存器以及其他的硬體和編譯器優化。Java記憶體模型的抽象示意如圖所示。
圖3-1 Java記憶體模型的抽象結構示意圖
從圖3-1來看,如果執行緒A與執行緒B之間要通訊的話,必須要經歷下面2個步驟。
1)執行緒A把本地記憶體A中更新過的共享變數重新整理到主記憶體中去。
2)執行緒B到主記憶體中去讀取執行緒A之前已更新過的共享變數。
下面通過示意圖(見圖3-2)來說明這兩個步驟。
圖3-2 執行緒之間的通訊圖
如圖3-2所示,本地記憶體A和本地記憶體B由主記憶體中共享變數x的副本。假設初始時,這3個記憶體中的x值都為0。執行緒A在執行時,把更新後的x值(假設值為1)臨時存放在自己的本地記憶體A中。當執行緒A和執行緒B需要通訊時,執行緒A首先會把自己本地記憶體中修改後的x值重新整理到主記憶體中,此時主記憶體中的x值變為了1。隨後,執行緒B到主記憶體中去讀取執行緒A更新後的x值,此時執行緒B的本地記憶體的x值也變為了1。
從整體來看,這兩個步驟實質上是執行緒A在向執行緒B傳送訊息,而且這個通訊過程必須要經過主記憶體。JMM通過控制主記憶體與每個執行緒的本地記憶體之間的互動,來為Java程式設計師提供記憶體可見性保證。
4、volatile
4.1使用volatile以後,做了如下事情
- 每次修改volatile變數都會同步到主存中。
- 每次讀取volatile變數的值都強制從主存讀取最新的值(強制JVM不可優化volatile變數,如JVM優化後變數讀取會使用cpu快取而不從主存中讀取)
4.2 volatile解決的是多執行緒間共享變數的可見性問題,而保證不了多執行緒間共享變數原子性問題。對於多執行緒的i++,++i,依然還是會存在多執行緒問題,volatile是無法解決的.如下:使用一個執行緒i++,另一個i--,最終得到的結果不為0。
4.2.1 多執行緒下的i++問題
一個執行緒對count進行times次的加操作,一個執行緒對count進行times次的減操作。count最後的結果,不為0.
- publicclass VolatileTest {
- privatestaticvolatileint count = 0;
- privatestaticfinalint times = 10000;
- publicstaticvoid main(String[] args) {
- long curTime = System.nanoTime();
- Thread decThread = new DecThread();
- decThread.start();
- System.out.println("Start thread: " + Thread.currentThread() + " i++");
- for (int i = 0; i < times; i++) {
- count++;
- }
- System.out.println("End thread: " + Thread.currentThread() + " i--");
- // 等待decThread結束
- while (decThread.isAlive())
- ;
- long duration = System.nanoTime() - curTime;
- System.out.println("Result: " + count);
- System.out.format("Duration: %.2fs\n", duration / 1.0e9);
- }
- privatestaticclass DecThread extends Thread {
- @Override
- publicvoid run() {
- System.out.println("Start thread: " + Thread.currentThread()
- + " i--");
- for (int i = 0; i < times; i++) {
- count--;
- }
- System.out
- .println("End thread: " + Thread.currentThread() + " i--");
- }
- }
- }
4.2.2 程式的執行結果
[javascript] view plain copy- Start thread: Thread[Thread-0,5,main] i--
- Start thread: Thread[main,5,main] i++
- End thread: Thread[main,5,main] i++
- End thread: Thread[Thread-0,5,main] i--
- Result: -6240
- Duration: 0.00s
4.2.3 i++和++i並非原子操作
原因是i++和++i並非原子操作,我們若檢視位元組碼,會發現
[javascript] view plain copy- void f1() { i++; }
的位元組碼如下
[javascript] view plain copy- void f1();
- Code:
- 0: aload_0
- 1: dup
- 2: getfield #2; //Field i:I
- 5: iconst_1
- 6: iadd
- 7: putfield #2; //Field i:I
- 10: return
可見i++執行了多部操作,從變數i中讀取讀取i的值->值+1 ->將+1後的值寫回i中,這樣在多執行緒的時候執行情況就類似如下了
[javascript] view plain copy- Thread1 Thread2
- r1 = i; r3 = i;
- r2 = r1 + 1; r4 = r3 + 1;
- i = r2; i = r4;
這樣會造成的問題就是 r1, r3讀到的值都是 0,最後兩個執行緒都將 1 寫入 i, 最後 i等於 1,但是卻進行了兩次自增操作。
可知加了volatile和沒加volatile都無法解決非原子操作的執行緒同步問題。
5、使用迴圈CAS,實現i++原子操作
5.1 關於Java併發包的介紹
Java提供了java.util.concurrent.atomic包來提供執行緒安全的基本型別包裝類。這些包裝類都是是用CAS來實現,i++的原子性操作。以AtomicInteger為例子,講一下 public final int getAndIncrement(){} 方法的實現。
- publicfinalint getAndIncrement() {
- for (;;) {
- int current = get();
- int next = current + 1;
- if (compareAndSet(current, next))
- return current;
- }
- }
5.2 使用迴圈CAS,來實現i++的原子性操作
- publicclass AtomicIntegerTest {
- privatestatic AtomicInteger count = new AtomicInteger(0);
- privatestaticfinalint times = 10000;
- AtomicInteger atomicInteger;
- publicstaticvoid main(String[] args) {
- long curTime = System.nanoTime();
- Thread decThread = new DecThread();
- decThread.start();
- System.out.println("Start thread: " + Thread.currentThread() + " i++");
- for (int i = 0; i < times; i++) {
- // 進行自加的操作
- count.getAndIncrement();
- }
- System.out.println("End thread: " + Thread.currentThread() + " i++");
-
相關推薦
從volatile說到,i++原子操作,執行緒安全問題
1、可見性(Visibility) 可見性是指,當一個執行緒修改了某一個全域性共享變數的數值,其他執行緒是否能夠知道這個修改。 顯然,在序列程式來說可見性的問題是不存在的。因為你在任何一個地方操作修改了某個變數,那麼在後續的程式裡面,讀取這個變數的
四十八、從JVM記憶體模型談執行緒安全
作為一個三個多月沒有去工作的獨立開發者而言,今天去小米麵試了一把.怎麼說呢,無論你水平如何,請確保在面試之前要做準備,就像其中一位面試官說的一樣,我知道你水平不錯,但是無論如何也是要準備下的,不然你怎麼會連這個方法也忘記了? 此刻,我突然覺得我是一個假程式設計師.為什麼這麼說呢,作為一個從12年
java單例模式 原子類 執行緒安全
public class Singleton {private static AtomicReference<Singleton> singleton = new AtomicReference<>();private Singleton() {}p
為什麼volatile也無法保證執行緒安全
Java記憶體模型 java使用的是共享變數模型,如下圖所示 執行緒1要讀取執行緒2修改後的值必須要執行緒2寫回到記憶體,執行緒1再讀取。 Jvm又是如何讀取主存變數到執行緒中的呢? 記憶體間的相互操作 lock 將物件變成執行緒獨佔的狀態 unlock 將執行緒獨佔
c/c++語言中的volatile 保證讀寫執行緒安全的條件
背景: 關於這個問題之前討論了很多, 比如: C++多執行緒有必要加volatile麼? 多執行緒程式設計
記一次愚蠢的操作--執行緒安全問題
前言 只有光頭才能變強。 文字已收錄至我的GitHub倉庫,歡迎Star:https://github.com/ZhongFuCheng3y/3y 記一次在工作中愚蠢的操作,本文關鍵字:執行緒安全 (我怎麼天天在寫Bug啊) 一、交代背景 我這邊有一個系統,提供一個RPC介面去傳送各種資訊(比如簡訊、
使用AtomicInteger原子類代替i++執行緒安全操作
Java中自增自減操作不具原子性,在多執行緒環境下是執行緒不安全的,可以使用使用AtomicInteger原子類代替i++,i--操作完成多執行緒執行緒安全操作。 下面是等於i++多執行緒的自增操作程式碼: public class AtomicIntegerTest { private s
從volatile說到i++的執行緒安全問題
簡介volatile關鍵字保證了在多執行緒環境下,被修飾的變數在別修改後會馬上同步到主存,這樣該執行緒對這個變數的修改就是對所有其他執行緒可見的,其他執行緒能夠馬上讀到這個修改後值.Thread的本地記憶體每個Thread都擁有自己的執行緒儲存空間Thread何時同步本地儲存
併發程式設計——為什麼volatile變數++操作執行緒不安全
學習volatile的時候也許我們會看到下面這句話: 對於volatile變數來說,自增操作執行緒不安全。 那為什麼不安全呢?本帥博主看的是《併發程式設計的藝術》這本書,這本書對這一事件也沒有做出很讓人易懂的解釋。那麼我們自己用例子
java 執行緒安全的全域性計數器-AtomicInteger原子操作類
首先 , 測試一下正常程式碼 public class Test1 { public static int count = 0; public static void main(String[] args) { for (int i =
volatile不能保證原子性,也就不能保證執行緒安全
volatile只能保證變數的可見性,無法保證對變數的操作的原子性。 還是以最常用的i++來說吧,包含3個步驟 1,從記憶體讀取i當前的值 2,加1 3,把修改後的值重新整理到記憶體 對於普通變數來說多執行緒下1,2之間被中斷,其
操作的原子性與執行緒安全
本案例來源於java zone社群,由於原始碼裡面存在一些自己開發的註解,我暫時沒找到相關的文件,所以我做了一些修改。用的都是ja
執行緒安全之原子操作
原子操作 原子性就是指該操作是不可再分的。不論是多核還是單核,具有原子性的量,同一時刻只能有一個執行緒來對它進行操作。原子操作可以是一個步驟,也可以是多個步驟,但是其順序不可以被打亂,也不可以被切割而只執行其中的一部分(不可中斷性)。將操作視作一個整體,資源在該次操作中保持一致,這是原子性的核心特徵。
原子操作組合與執行緒安全
public class TestTwoAtomicMethods { private final ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<Integer,Integer>(); @Inte
怎麼進行執行緒安全的日期格式化操作?
由於 DateFormat 是非執行緒安全的,因此在多執行緒併發情況下日期格式化時需要特別注意。下面記錄幾種格式化的方式: 執行緒不安全的處理方式 private static final DateFormat DATE_FORMA
2017.10.20 C#跨執行緒操作控制元件的執行緒安全方法
C#跨執行緒操作控制元件的執行緒安全方法 在C#中,經常用到這樣一個場景,Windows Form程式啟動一個工作者執行緒執行一部分工作,這樣做是為了避免速度慢的工作如果直接呼叫會使得主Form停止響應一段時間。 既然啟動了執行緒,就避免不了執行緒之間資料傳遞的事情,相信你有很多種辦法
執行緒安全(上)--徹底搞懂synchronized(從偏向鎖到重量級鎖)
接觸過執行緒安全的同學想必都使用過synchronized這個關鍵字,在java同步程式碼快中,synchronized的使用方式無非有兩個: 通過對一個物件進行加鎖來實現同步,如下面程式碼。 synchronized(lockObject){ &nb
從一道題看執行緒安全--牛客網Java基礎題
從一道題看執行緒安全 Java中的執行緒安全是什麼: 就是執行緒同步的意思,就是當一個程式對一個執行緒安全的方法或者語句進行訪問的時候,其他的不能再對他進行操作了,必須等到這次訪問結束以後才能對這個執行緒安全的方法進行訪問。 什麼叫執行緒安全: 如果你的程式碼所在的程序中有多個執行緒在同時
執行緒安全的懶漢模式為什麼要使用volatile關鍵字
來看這樣一段程式碼,為什麼一定要用volatile關鍵字修飾instance變數呢? public class Singleton{ private static volatile Singleton instance; private Singleton(){ }
JAVA多執行緒安全的三大特性 + synchronized和volatile
文章目錄 執行緒安全的三大特性 原子性 可見性 有序性 保證執行緒安全的兩個關鍵字 Synchronized Volatile 執行緒安全的三大特性 在多執行緒程式設計中