1. 程式人生 > >從volatile分析i++和++i非原子性問題

從volatile分析i++和++i非原子性問題

目錄

1、可見性(Visibility)

2、原子性(Atomicity)

3、Java記憶體模型的抽象結構( JMM )

4、volatile 

 5、 多執行緒下的i++問題

5、自定義實現i++原子操作

5.1   關於Java併發包的介紹

 5.2 使用迴圈CAS,來實現i++的原子性操作

5.3、使用鎖機制,實現i++原子操作

5.4 使用synchronized,實現i++原子操作

6 總結



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 

  使用volatile以後,做了如下事情:

        1、 每次修改volatile變數都會同步到主存中。
        2、每次讀取volatile變數的值都強制從主存讀取最新的值(強制JVM不可優化volatile變數,如JVM優化後變數讀取會使用cpu快取而不從主存中讀取)

         volatile解決的是多執行緒間共享變數的可見性問題,而保證不了多執行緒間共享變數原子性問題。對於多執行緒的i++,++i,依然還是會存在多執行緒問題,volatile是無法解決的.如下:使用一個執行緒i++,另一個i--,最終得到的結果不為0。

 5、 多執行緒下的i++問題

                   一個執行緒對count進行times次的加操作,一個執行緒對count進行times次的減操作。count最後的結果,不為0.
 

public class VolatileTest {
	private static volatile int count = 0;
	private static final int times = 10000;
 
	public static void 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);
	}
 
	private static class DecThread extends Thread {
 
		@Override
		public void 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--");
		}
	}
}

程式的執行結果:

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

   原因是i++和++i並非原子操作,我們若檢視“void f1() { i++; }”的位元組碼,會發現:

	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中,這樣在多執行緒的時候執行情況就類似如下了:

	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、自定義實現i++原子操作

5.1   關於Java併發包的介紹

        Java提供了java.util.concurrent.atomic包來提供執行緒安全的基本型別包裝類。這些包裝類都是是用CAS來實現i++的原子性操作。以AtomicInteger為例子,講一下 public final int getAndIncrement(){} 方法的實現。
 

public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }

 5.2 使用迴圈CAS,來實現i++的原子性操作

public class AtomicIntegerTest {
	private static AtomicInteger count = new AtomicInteger(0);
	private static final int times = 10000;
	AtomicInteger atomicInteger;
 
	public static void 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++");
 
		// 等待decThread結束
		while (decThread.isAlive());
 
		long duration = System.nanoTime() - curTime;
		System.out.println("Result: " + count);
		System.out.format("Duration: %.2fs\n", duration / 1.0e9);
	}
 
	private static class DecThread extends Thread {
 
		@Override
		public void run() {
			System.out.println("Start thread: " + Thread.currentThread()
					+ " i--");
			for (int i = 0; i < times; i++) {
				// 進行自減的操作
				count.getAndDecrement();
			}
			System.out.println("End thread: " + Thread.currentThread() + " i--");
		}
	}
}

程式的執行結果:

Start thread: Thread[main,5,main] i++
Start thread: Thread[Thread-0,5,main] i--
End thread: Thread[Thread-0,5,main] i--
End thread: Thread[main,5,main] i++
Result: 0
Duration: 0.00s

5.3、使用鎖機制,實現i++原子操作

package com.baowei.yuanzi;
 
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
 
public class LockTest {
	private static int count = 0;
	private static final int times = 10000;
 
	// 使用Lock實現,多執行緒的資料同步
	public static ReentrantLock lock = new ReentrantLock();
 
	public static void 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++) {
			// 進行自加的操作
			try {
				lock.lock();
				count++;
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				lock.unlock();
			}
		}
 
		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);
	}
 
	private static class DecThread extends Thread {
 
		@Override
		public void run() {
			System.out.println("Start thread: " + Thread.currentThread()
					+ " i--");
			for (int i = 0; i < times; i++) {
				// 進行自減的操作
				try {
					lock.lock();
					count--;
				} catch (Exception e) {
					e.printStackTrace();
				} finally {
					lock.unlock();
				}
			}
			System.out.println("End thread: " + Thread.currentThread() + " i--");
		}
	}
}

5.4 使用synchronized,實現i++原子操作

package com.baowei.yuanzi;
 
public class SynchronizedTest {
	private static int count = 0;
	private static final int times = 1000000;
 
	public static void 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++) {
			// 進行自加的操作
			synchronized (SynchronizedTest.class) {
				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);
	}
 
	private static class DecThread extends Thread {
 
		@Override
		public void run() {
			System.out.println("Start thread: " + Thread.currentThread()
					+ " i--");
			for (int i = 0; i < times; i++) {
				// 進行自減的操作
				synchronized (SynchronizedTest.class) {
					count--;
				}
			}
			System.out.println("End thread: " + Thread.currentThread() + " i--");
		}
	}
}

6 總結

一.i++

i++的操作分三步:

(1)棧中取出i

(2)i自增1

(3)將i存到棧

所以i++不是原子操作,上面的三個步驟中任何一個步驟同時操作,都可能導致i的值不正確自增

二.++i

在多核的機器上,cpu在讀取記憶體i時也會可能發生同時讀取到同一值,這就導致兩次自增,實際只增加了一次。

綜上,我認為i++和++i都不是原子操作。
 


1. 什麼是作業系統的“原子操作”
     原子操作是不可分割的,在執行完畢不會被任何其它任務或事件中斷,分為兩種情況(兩種都應該滿足)

     (1) 在單執行緒中, 能夠在單條指令中完成的操作都可以認為是" 原子操作",因為中斷只能發生於指令之間。

      (2) 在多執行緒中,不能被其它程序(執行緒)打斷的操作就叫原子操作。2.   面試的時候經常問的一道題目是i++在兩個執行緒裡邊分別執行100次,能得到的最大值和最小值分別是多少?(2 -200)

       i++只需要執行一條指令,並不能保證多個執行緒i++,操作同一個i,可以得到正確的結果。因為還有暫存器的因素,多個cpu對應多個暫存器。每次要先把i從記憶體複製到暫存器,然後++,然後再把i複製到記憶體中,這需要至少3步。從這個意義上講,說i++是原子的並不對。