天天看點

并發重要概念

在并發程式設計中,我們通常會遇到以下三個問題:原子性問題,可見性問題,有序性問題。我們先看具體看一下這三個概念:

1.原子性

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

  反映到并發程式設計中會出現什麼結果呢?

  舉個最簡單的例子,大家想一下假如為一個32位的變量指派過程不具備原子性的話,會發生什麼後果?

<code>  i = </code><code>9</code><code>;</code> 

  假若一個線程執行到這個語句時,我暫且假設為一個32位的變量指派包括兩個過程:為低16位指派,為高16位指派。

  那麼就可能發生一種情況:當将低16位數值寫入之後,突然被中斷,而此時又有一個線程去讀取i的值,那麼讀取到的就是錯誤的資料。

  Java記憶體模型隻保證了基本讀取和指派是原子性操作。即i=10指派;i讀取

2.可見性

  可見性是指當多個線程通路同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

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

<code>  //線程1執行的代碼</code>

<code>  int</code>  <code>i = </code> <code>0</code> <code>;</code>

<code>  i = </code> <code>10</code> <code>;</code>

   

<code>  //線程2執行的代碼</code>

<code>  j = i;</code>

假若執行線程1的是CPU1,執行線程2的是CPU2。由上面的分析可知,當線程1執行 i =10這句時,會先把i的初始值加載到CPU1的高速緩存中,然後指派為10,那麼在CPU1的高速緩存當中i的值變為10了,卻沒有立即寫入到主存當中。

  此時線程2執行 j = i,它會先去主存讀取i的值并加載到CPU2的緩存當中,注意此時記憶體當中i的值還是0,那麼就會使得j的值為0,而不是10.

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

3.有序性

  有序性:即程式執行的順序按照代碼的先後順序執行。舉個簡單的例子,看下面這段代碼:

<code>  int</code>  <code>i = </code> <code>0</code> <code>;              </code>

<code>  boolean</code>  <code>flag = </code> <code>false</code> <code>;</code>

<code>  i = </code> <code>1</code> <code>;                </code> <code>//語句1  </code>

<code>  flag = </code> <code>true</code> <code>;          </code> <code>//語句2</code>

  上面代碼定義了一個int型變量,定義了一個boolean類型變量,然後分别對兩個變量進行指派操作。從代碼順序上看,語句1是在語句2前面的,那麼JVM在真正執行這段代碼的時候會保證語句1一定會在語句2前面執行嗎?不一定,為什麼呢?這裡可能會發生指令重排序(Instruction Reorder)。

  下面解釋一下什麼是指令重排序,一般來說,處理器為了提高程式運作效率,可能會對輸入代碼進行優化,它不保證程式中各個語句的執行先後順序同代碼中的順序一緻,但是它會保證程式最終執行結果和代碼順序執行的結果是一緻的。

  比如上面的代碼中,語句1和語句2誰先執行對最終的程式結果并沒有影響,那麼就有可能在執行過程中,語句2先執行而語句1後執行。

  但是要注意,雖然處理器會對指令進行重排序,但是它會保證程式最終結果會和代碼順序執行結果相同,那麼它靠什麼保證的呢?再看下面一個例子:

<code>  int</code>  <code>a = </code> <code>10</code> <code>;    </code> <code>//語句1</code>

<code>  int</code>  <code>r = </code> <code>2</code> <code>;    </code> <code>//語句2</code>

<code>  a = a + </code> <code>3</code> <code>;    </code> <code>//語句3</code>

<code>  r = a*a;     </code> <code>//語句4</code>

這段代碼有4個語句,那麼可能的一個執行順序是:

  

并發重要概念

  那麼可不可能是這個執行順序呢: 語句2   語句1    語句4   語句3

  不可能,因為處理器在進行重排序時是會考慮指令之間的資料依賴性,如果一個指令Instruction 2必須用到Instruction 1的結果,那麼處理器會保證Instruction 1會在Instruction 2之前執行。

  雖然重排序不會影響單個線程内程式執行的結果,但是多線程呢?下面看一個例子:

<code>  //線程1:</code>

<code>  context = loadContext();   </code> <code>//語句1</code>

<code>  inited = </code> <code>true</code> <code>;             </code> <code>//語句2</code>

<code>  //線程2:</code>

<code>  while</code> <code>(!inited ){</code>

<code>  </code>   <code>sleep()</code>

<code>  }</code>

<code>  doSomethingwithconfig(context);</code>

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

   從上面可以看出,指令重排序不會影響單個線程的執行,但是會影響到線程并發執行的正确性。

  也就是說,要想并發程式正确地執行,必須要保證原子性、可見性以及有序性。隻要有一個沒有被保證,就有可能會導緻程式運作不正确。

   譬如<code>inited = </code> <code>true</code> <code>;加volatile,使之一定程度上有序,并且這是個指派操作,是原則操作,那就沒問題了。</code>