并發(fā)編程(一)—— volatile關(guān)鍵字和 atomic包

Java內(nèi)存模型

JMM(java內(nèi)存模型)

 java虛擬機(jī)有自己的內(nèi)存模型(Java Memory Model,JMM),JMM可以屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓java程序在各種平臺下都能達(dá)到一致的內(nèi)存訪問效果。

  JMM決定一個(gè)線程對共享變量的寫入何時(shí)對另一個(gè)線程可見,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:共享變量存儲在主內(nèi)存(Main Memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(Local Memory),本地內(nèi)存保存了被該線程使用到的主內(nèi)存的副本拷貝,線程對變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量。這三者之間的交互關(guān)系如下

計(jì)算機(jī)在執(zhí)行程序時(shí),每條指令都是在CPU中執(zhí)行的,而執(zhí)行指令過程中,勢必涉及到數(shù)據(jù)的讀取和寫入。由于程序運(yùn)行過程中的臨時(shí)數(shù)據(jù)是存放在主存(物理內(nèi)存)當(dāng)中的,這時(shí)就存在一個(gè)問題,由于CPU執(zhí)行速度很快,而從內(nèi)存讀取數(shù)據(jù)和向內(nèi)存寫入數(shù)據(jù)的過程跟CPU執(zhí)行指令的速度比起來要慢的多,因此如果任何時(shí)候?qū)?shù)據(jù)的操作都要通過和內(nèi)存的交互來進(jìn)行,會大大降低指令執(zhí)行的速度。因此在CPU里面就有了高速緩存。

  也就是,當(dāng)程序在運(yùn)行過程中,會將運(yùn)算需要的數(shù)據(jù)從主存復(fù)制一份到CPU的高速緩存當(dāng)中,那么CPU進(jìn)行計(jì)算時(shí)就可以直接從它的高速緩存讀取數(shù)據(jù)和向其中寫入數(shù)據(jù),當(dāng)運(yùn)算結(jié)束之后,再將高速緩存中的數(shù)據(jù)刷新到主存當(dāng)中。舉個(gè)簡單的例子,比如下面的這段代碼:

i = i + 1;


當(dāng)線程執(zhí)行這個(gè)語句時(shí),會先從主存當(dāng)中讀取i的值,然后復(fù)制一份到高速緩存當(dāng)中,然后CPU執(zhí)行指令對i進(jìn)行加1操作,然后將數(shù)據(jù)寫入高速緩存,最后將高速緩存中i最新的值刷新到主存當(dāng)中。

  這個(gè)代碼在單線程中運(yùn)行是沒有任何問題的,但是在多線程中運(yùn)行就會有問題了。在多核CPU中,每條線程可能運(yùn)行于不同的CPU中,因此每個(gè)線程運(yùn)行時(shí)有自己的高速緩存(對單核CPU來說,其實(shí)也會出現(xiàn)這種問題,只不過是以線程調(diào)度的形式來分別執(zhí)行的)。本文我們以多核CPU為例。

  比如同時(shí)有2個(gè)線程執(zhí)行這段代碼,假如初始時(shí)i的值為0,那么我們希望兩個(gè)線程執(zhí)行完之后i的值變?yōu)?。但是事實(shí)會是這樣嗎?

  可能存在下面一種情況:初始時(shí),兩個(gè)線程分別讀取i的值存入各自所在的CPU的高速緩存當(dāng)中,然后線程1進(jìn)行加1操作,然后把i的最新值1寫入到內(nèi)存。此時(shí)線程2的高速緩存當(dāng)中i的值還是0,進(jìn)行加1操作之后,i的值為1,然后線程2把i的值寫入內(nèi)存。

  最終結(jié)果i的值是1,而不是2。這就是著名的緩存一致性問題。通常稱這種被多個(gè)線程訪問的變量為共享變量。

并發(fā)編程中的三個(gè)概念

  在并發(fā)編程中,我們通常會遇到以下三個(gè)問題:原子性問題,可見性問題,有序性問題。我們先看具體看一下這三個(gè)概念:

1.原子性

  原子性:即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行。

  一個(gè)很經(jīng)典的例子就是銀行賬戶轉(zhuǎn)賬問題:

  比如從賬戶A向賬戶B轉(zhuǎn)1000元,那么必然包括2個(gè)操作:從賬戶A減去1000元,往賬戶B加上1000元。

  試想一下,如果這2個(gè)操作不具備原子性,會造成什么樣的后果。假如從賬戶A減去1000元之后,操作突然中止。然后又從B取出了500元,取出500元之后,再執(zhí)行 往賬戶B加上1000元 的操作。這樣就會導(dǎo)致賬戶A雖然減去了1000元,但是賬戶B沒有收到這個(gè)轉(zhuǎn)過來的1000元。

  所以這2個(gè)操作必須要具備原子性才能保證不出現(xiàn)一些意外的問題。

2.可見性

  可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。

  舉個(gè)簡單的例子,看下面這段代碼:

1 //線程1執(zhí)行的代碼

2 int i = 0;

3 i = 10;

4?

5 //線程2執(zhí)行的代碼

6 j = i;

假若執(zhí)行線程1的是CPU1,執(zhí)行線程2的是CPU2。由上面的分析可知,當(dāng)線程1執(zhí)行 i =10這句時(shí),會先把i的初始值加載到CPU1的高速緩存中,然后賦值為10,那么在CPU1的高速緩存當(dāng)中i的值變?yōu)?0了,卻沒有立即寫入到主存當(dāng)中。

此時(shí)線程2執(zhí)行 j = i,它會先去主存讀取i的值并加載到CPU2的緩存當(dāng)中,注意此時(shí)內(nèi)存當(dāng)中i的值還是0,那么就會使得j的值為0,而不是10。

這就是可見性問題,線程1對變量i修改了之后,線程2沒有立即看到線程1修改的值。

3.有序性

  有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。舉個(gè)簡單的例子,看下面這段代碼:

int i = 0;? ? ? ? ? ? ?

boolean flag = false;

i = 1;? ? ? ? ? ? ? ? //語句1?

flag = true;? ? ? ? ? //語句2

從代碼順序上看,語句1是在語句2前面的,那么JVM在真正執(zhí)行這段代碼的時(shí)候會保證語句1一定會在語句2前面執(zhí)行嗎?不一定,為什么呢?這里可能會發(fā)生指令重排序(Instruction Reorder)。

一般來說,處理器為了提高程序運(yùn)行效率,可能會對輸入代碼進(jìn)行優(yōu)化,它不保證程序中各個(gè)語句的執(zhí)行先后順序同代碼中的順序一致,但是它會保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流圈:609164807 ?幫助突破瓶頸 提升思維能力

比如上面的代碼中,語句1和語句2誰先執(zhí)行對最終的程序結(jié)果并沒有影響,那么就有可能在執(zhí)行過程中,語句2先執(zhí)行而語句1后執(zhí)行。

但是重排序也需要遵守一定規(guī)則:

  1.重排序操作不會對存在數(shù)據(jù)依賴關(guān)系的操作進(jìn)行重排序。

    比如:a=1;b=a; 這個(gè)指令序列,由于第二個(gè)操作依賴于第一個(gè)操作,所以在編譯時(shí)和處理器運(yùn)行時(shí)這兩個(gè)操作不會被重排序。

  2.重排序是為了優(yōu)化性能,但是不管怎么重排序,單線程下程序的執(zhí)行結(jié)果不能被改變

    比如:a=1;b=2;c=a+b這三個(gè)操作,第一步(a=1)和第二步(b=2)由于不存在數(shù)據(jù)依賴關(guān)系,所以可能會發(fā)生重排序,但是c=a+b這個(gè)操作是不會被重排序的,因?yàn)樾枰WC最終的結(jié)果一定是c=a+b=3。

volatile關(guān)鍵字

 ? volatile是Java提供的一種輕量級的同步機(jī)制。同synchronized相比(synchronized通常稱為重量級鎖),volatile更輕量級。

  一旦一個(gè)共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:

1)保證了不同線程對這個(gè)變量進(jìn)行操作時(shí)的可見性,即一個(gè)線程修改了某個(gè)變量的值,這新值對其他線程來說是立即可見的。

2)禁止進(jìn)行指令重排序。

1、共享變量的可見性

public class TestVolatile {


? ? public static void main(String[] args) {

? ? ? ? ThreadDemo td = new ThreadDemo();

? ? ? ? new Thread(td).start();

? ? ? ? while(true){

? ? ? ? ? ? if(td.isFlag()){

? ? ? ? ? ? ? ? System.out.println("------------------");

? ? ? ? ? ? ? ? break;

? ? ? ? ? ? }

? ? ? ? }

? ? }

}

class ThreadDemo implements Runnable {

? ? private? boolean flag = false;

? ? @Override

? ? public void run() {

? ? ? ? try {

? ? ? ? ? ? Thread.sleep(200);

? ? ? ? } catch (InterruptedException e) {

? ? ? ? }

? ? ? ? flag = true;

? ? ? ? System.out.println("flag=" + isFlag());

? ? }

? ? public boolean isFlag() {

? ? ? ? return flag;

? ? }

}

上面這個(gè)例子,開啟一個(gè)多線程去改變flag為true,main 主線程中可以輸出"------------------"嗎?

答案是NO!?

  這個(gè)結(jié)論會讓人有些疑惑,可以理解。開啟的線程雖然修改了flag 的值為true,但是還沒來得及寫入主存當(dāng)中,此時(shí)main里面的 td.isFlag()還是false,但是由于?while(true)? 是底層的指令來實(shí)現(xiàn),速度非常之快,一直循環(huán)都沒有時(shí)間去主存中更新td的值,所以這里會造成死循環(huán)!運(yùn)行結(jié)果如下:

此時(shí)線程是沒有停止的,一直在循環(huán)。

如何解決呢?只需將 flag?聲明為volatile,即可保證在開啟的線程A將其修改為true時(shí),main主線程可以立刻得知:

第一:使用volatile關(guān)鍵字會強(qiáng)制將修改的值立即寫入主存;

在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流圈:609164807 ?幫助突破瓶頸 提升思維能力

  第二:使用volatile關(guān)鍵字的話,當(dāng)開啟的線程進(jìn)行修改時(shí),會導(dǎo)致main線程的工作內(nèi)存中緩存變量flag的緩存行無效(反映到硬件層的話,就是CPU的L1緩存中對應(yīng)的緩存行無效);

  第三:由于線程main的工作內(nèi)存中緩存變量flag的緩存行無效,所以線程main再次讀取變量flag的值時(shí)會去主存讀取。

volatile具備兩種特性,第一就是保證共享變量對所有線程的可見性。將一個(gè)共享變量聲明為volatile后,會有以下效應(yīng):

  1.當(dāng)寫一個(gè)volatile變量時(shí),JMM會把該線程對應(yīng)的本地內(nèi)存中的變量強(qiáng)制刷新到主內(nèi)存中去;

  2.這個(gè)寫會操作會導(dǎo)致其他線程中的緩存無效。


2、禁止進(jìn)行指令重排序

這里我們引用上篇文章單例里面的例子

1 class Singleton{

2? ? private volatile static Singleton instance = null;

3

4? ? private Singleton() {

5? ? }

6? ? ?

7? ? public static Singleton getInstance() {

8? ? ? ? if(instance==null) {

9? ? ? ? ? ? synchronized (Singleton.class) {

10? ? ? ? ? ? ? ? if(instance==null)

11? ? ? ? ? ? ? ? ? ? instance = new Singleton();

12? ? ? ? ? ? }

13? ? ? ? }

14? ? ? ? return instance;

15? ? }

16 }

instance = new?Singleton(); 這段代碼可以分為三個(gè)步驟:

1、memory = allocate() 分配對象的內(nèi)存空間

2、ctorInstance() 初始化對象

3、instance = memory 設(shè)置instance指向剛分配的內(nèi)存

但是此時(shí)有可能發(fā)生指令重排,CPU 的執(zhí)行順序可能為:

1、memory = allocate() 分配對象的內(nèi)存空間

3、instance = memory 設(shè)置instance指向剛分配的內(nèi)存

2、ctorInstance() 初始化對象

在單線程的情況下,1->3->2這種順序執(zhí)行是沒有問題的,但是如果是多線程的情況則有可能出現(xiàn)問題,線程A執(zhí)行到11行代碼,執(zhí)行了指令1和3,此時(shí)instance已經(jīng)有值了,值為第一步分配的內(nèi)存空間地址,但是還沒有進(jìn)行對象的初始化;

此時(shí)線程B執(zhí)行到了第8行代碼處,此時(shí)instance已經(jīng)有值了則return instance,線程B 使用instance的時(shí)候,就會出現(xiàn)異常。

這里可以使用 volatile 來禁止指令重排序。

從上面知道volatile關(guān)鍵字保證了操作的可見性和有序性,但是volatile能保證對變量的操作是原子性嗎?

下面看一個(gè)例子:

package com.mmall.concurrency.example.count;

import java.util.concurrent.CountDownLatch;

/**

* @author: ChenHao

* @Description:

* @Date: Created in 15:05 2018/11/16

* @Modified by:

*/

public class CountTest {

? ? // 請求總數(shù)

? ? public static int clientTotal = 5000;

? ? public static volatile int count = 0;

? ? public static void main(String[] args) throws Exception {

? ? ? ? //使用CountDownLatch來等待計(jì)算線程執(zhí)行完

? ? ? ? final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);

? ? ? ? //開啟clientTotal個(gè)線程進(jìn)行累加操作

? ? ? ? for(int i=0;i<clientTotal;i++){

? ? ? ? ? ? new Thread(){

? ? ? ? ? ? ? ? public void run(){

? ? ? ? ? ? ? ? ? ? count++;//自加操作

? ? ? ? ? ? ? ? ? ? countDownLatch.countDown();

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }.start();

? ? ? ? }

? ? ? ? //等待計(jì)算線程執(zhí)行完

? ? ? ? countDownLatch.await();

? ? ? ? System.out.println(count);

? ? }

}

執(zhí)行結(jié)果:

針對這個(gè)示例,一些同學(xué)可能會覺得疑惑,如果用volatile修飾的共享變量可以保證可見性,那么結(jié)果不應(yīng)該是5000么?

問題就出在count++這個(gè)操作上,因?yàn)閏ount++不是個(gè)原子性的操作,而是個(gè)復(fù)合操作。我們可以簡單講這個(gè)操作理解為由這三步組成:

  1.讀取count

  2.count 加 1

  3.將count 寫到主存

  所以,在多線程環(huán)境下,有可能線程A將count讀取到本地內(nèi)存中,此時(shí)其他線程可能已經(jīng)將count增大了很多,線程A依然對過期的本地緩存count進(jìn)行自加,重新寫到主存中,最終導(dǎo)致了count的結(jié)果不合預(yù)期,而是小于5000。

那么如何來解決這個(gè)問題呢?下面我們來看看

Atomic包

在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作類,即對基本數(shù)據(jù)類型的 自增(加1操作),自減(減1操作)、以及加法操作(加一個(gè)數(shù)),減法操作(減一個(gè)數(shù))進(jìn)行了封裝,保證這些操作是原子性操作。atomic是利用CAS來實(shí)現(xiàn)原子性操作的(Compare And Swap)

package com.mmall.concurrency.example.count;

import java.util.concurrent.CountDownLatch;

import java.util.concurrent.atomic.AtomicInteger;

/**

* @author: ChenHao

* @Description:

* @Date: Created in 15:05 2018/11/16

* @Modified by:

*/

public class CountTest {

? ? // 請求總數(shù)

? ? public static int clientTotal = 5000;

? ? public static AtomicInteger count = new AtomicInteger(0);

? ? public static void main(String[] args) throws Exception {

? ? ? ? //使用CountDownLatch來等待計(jì)算線程執(zhí)行完

? ? ? ? final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);

?????? ?在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流圈:609164807 ?幫助突破瓶頸 提升思維能力

? ? ? ? //開啟clientTotal個(gè)線程進(jìn)行累加操作

? ? ? ? for(int i=0;i<clientTotal;i++){

? ? ? ? ? ? new Thread(){

? ? ? ? ? ? ? ? public void run(){

? ? ? ? ? ? ? ? ? ? count.incrementAndGet();//先加1,再get到值

? ? ? ? ? ? ? ? ? ? countDownLatch.countDown();

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }.start();

? ? ? ? }

? ? ? ? //等待計(jì)算線程執(zhí)行完

? ? ? ? countDownLatch.await();

? ? ? ? System.out.println(count);

? ? }

}

執(zhí)行結(jié)果:

下面我們來看看原子類操作的基本原理

1 public final int incrementAndGet() {

2? ? ? return unsafe.getAndAddInt(this, valueOffset, 1) + 1;

3 }

4

5 public final int getAndAddInt(Object var1, long var2, int var4) {

6? ? int var5;

7? ? do {

8? ? ? ? var5 = this.getIntVolatile(var1, var2);

9? ? } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

10

11? ? return var5;

12 }

13

14 /***

15 * 獲取obj對象中offset偏移地址對應(yīng)的整型field的值。

16 * @param obj 包含需要去讀取的field的對象

17 * @param obj中整型field的偏移量

18 */

19 public native int getIntVolatile(Object obj, long offset);

20

21 /**

22 * 比較obj的offset處內(nèi)存位置中的值和期望的值,如果相同則更新。此更新是不可中斷的。

23 *

24 * @param obj 需要更新的對象

25 * @param offset obj中整型field的偏移量

26 * @param expect 希望field中存在的值

27 * @param update 如果期望值expect與field的當(dāng)前值相同,設(shè)置filed的值為這個(gè)新值

28 * @return 如果field的值被更改返回true

29 */

30 public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);

首先介紹一下什么是Compare And Swap(CAS)?簡單的說就是比較并交換。

CAS 操作包含三個(gè)操作數(shù) —— 內(nèi)存位置(V)、預(yù)期原值(A)和新值(B)。如果內(nèi)存位置的值與預(yù)期原值相匹配,那么處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。CAS 有效地說明了“我認(rèn)為位置 V 應(yīng)該包含值 A;如果包含該值,則將 B 放到這個(gè)位置;否則,不要更改該位置,只告訴我這個(gè)位置現(xiàn)在的值即可?!?Java并發(fā)包(java.util.concurrent)中大量使用了CAS操作,涉及到并發(fā)的地方都調(diào)用了sun.misc.Unsafe類方法進(jìn)行CAS操作。

?我們來分析下incrementAndGet的邏輯:

  1.先獲取當(dāng)前的value值

  2.調(diào)用compareAndSet方法來來進(jìn)行原子更新操作,這個(gè)方法的語義是:

先檢查當(dāng)前value是否等于obj中整型field的偏移量處的值,如果相等,則意味著obj中整型field的偏移量處的值?沒被其他線程修改過,更新并返回true。如果不相等,compareAndSet則會返回false,然后循環(huán)繼續(xù)嘗試更新。

第一次count 為0時(shí)線程A調(diào)用incrementAndGet時(shí),傳參為?var1=AtomicInteger(0),var2為var1 里面 0?的偏移量,比如為8090,var4為需要加的數(shù)值1,var5為線程工作內(nèi)存值,在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流圈:609164807 ?幫助突破瓶頸 提升思維能力do里面會先執(zhí)行一次,通過getIntVolatile?獲取obj對象中offset偏移地址對應(yīng)的整型field的值此時(shí)var5=0;while 里面compareAndSwapInt?比較obj的8090處內(nèi)存位置中的值和期望的值var5,如果相同則更新obj的值為(var5+var4=1),此時(shí)更新成功,返回true,則?while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));結(jié)束循環(huán),return var5。

當(dāng)count 為0時(shí),線程B 和線程A 同時(shí)讀取到 count,進(jìn)入到第 8 行代碼處,線程B 也是取到的var5=0,當(dāng)線程B 執(zhí)行到compareAndSwapInt時(shí),線程A已經(jīng)執(zhí)行完compareAndSwapInt,已經(jīng)將內(nèi)存地址為8090處的值修改為1,此時(shí)線程B 執(zhí)行compareAndSwapInt返回false,則繼續(xù)循環(huán)執(zhí)行do里面的語句,再次取內(nèi)存地址偏移量為8090處的值為1,再去執(zhí)行compareAndSwapInt,更新obj的值為(var5+var4=2),返回為true,結(jié)束循環(huán),return var5。

CAS的ABA問題

  當(dāng)然CAS也并不完美,它存在"ABA"問題,假若一個(gè)變量初次讀取是A,在compare階段依然是A,但其實(shí)可能在此過程中,它先被改為B,再被改回A,而CAS是無法意識到這個(gè)問題的。CAS只關(guān)注了比較前后的值是否改變,而無法清楚在此過程中變量的變更明細(xì),這就是所謂的ABA漏洞。?

如果大家想學(xué)習(xí)以上路線內(nèi)容,在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流群。交流學(xué)習(xí)群號874811168 里面會分享一些資深架構(gòu)師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發(fā)、高性能、分布式、微服務(wù)架構(gòu)的原理,JVM性能優(yōu)化、分布式架構(gòu)等這些成為架構(gòu)師必備的知識體系。還能領(lǐng)取免費(fèi)的學(xué)習(xí)資源,目前受益良多

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容