從volatile分析i++和++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
使用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++是原子的並不對。