面試官問:數據的強一致性與弱一致性!直接把這篇甩給他

來源於:https://mp.weixin.qq.com/s/qxYvzUkwdEgch5LrqrGW2w

什麼是一致性?

在併發編程中,Java 是通過共享內存來實現共享變量操作的,所以在多線程編程中就會涉及到數據一致性的問題。

我先通過一個經典的案例來說明下多線程操作共享變量可能出現的問題,假設我們有兩個線程(線程 1 和線程 2)分別執行下面的方法,x 是共享變量:

<code>//代碼1
public

class

Demo

{

int
x
=

0
;

public

void
count
()

{
x
++;


System
.
out
.
println

(
x
)

}
}/<code>

如果兩個線程同時運行,兩個線程的變量的值可能會出現以下三種結果:

  • 1,1
  • 2,1
  • 1,2

2,1 和 1,2 的結果我們很好理解,那為什麼會出現以上 1,1 的結果呢?

在解釋為什麼會出現這樣的結果之前,我們先通過下圖來簡單瞭解下 Java 的內存模型。

Java內存模型

Java 採用共享內存模型來實現多線程之間的信息交換和數據同步。程序在運行時,局部變量將會存放在虛擬機棧中,而共享變量將會被保存在堆內存中。

面試官問:數據的強一致性與弱一致性!直接把這篇甩給他

由於局部變量是跟隨線程的創建而創建,線程的銷燬而銷燬,所以存放在棧中,由上圖我們可知,Java 棧數據不是所有線程共享的,所以不需要關心其數據的一致性。

共享變量存儲在堆內存或方法區中,由上圖可知,堆內存和方法區的數據是線程共享的。而堆內存中的共享變量在被不同線程操作時,會被加載到自己的工作內存中,也就是 CPU 中的高速緩存。

面試官問:數據的強一致性與弱一致性!直接把這篇甩給他

CPU 緩存可以分為一級緩存(L1)、二級緩存(L2)和三級緩存(L3),每一級緩存中所儲存的全部數據都是下一級緩存的一部分。當 CPU 要讀取一個緩存數據時,首先會從一級緩存中查找;如果沒有找到,再從二級緩存中查找;如果還是沒有找到,就從三級緩存或內存中查找。

如果是單核 CPU 運行多線程,多個線程同時訪問進程中的共享數據,CPU 將共享變量加載到高速緩存後,不同線程在訪問緩存數據的時候,都會映射到相同的緩存位置,這樣即使發生線程的切換,緩存仍然不會失效。

如果是多核 CPU 運行多線程,每個核都有一個 L1 緩存,如果多個線程運行在不同的內核上訪問共享變量時,每個內核的 L1 緩存將會緩存一份共享變量。

假設線程 A 操作 CPU 從堆內存中獲取一個緩存數據,此時堆內存中的緩存數據值為 0,該緩存數據會被加載到 L1 緩存中,在操作後,緩存數據的值變為 1,然後刷新到堆內存中。

在正好刷新到堆內存中之前,又有另外一個線程 B 將堆內存中為 0 的緩存數據加載到了另外一個內核的 L1 緩存中,此時線程 A 將堆內存中的數據刷新到了 1,而線程 B 實際拿到的緩存數據的值為 0。

此時,內核緩存中的數據和堆內存中的數據就不一致了,且線程 B 在刷新緩存到堆內存中的時候也將覆蓋線程 A 中修改的數據。這時就產生了數據不一致的問題。

解釋1,1的結果

瞭解完內存模型之後,結合以下圖,我就可以理解 1,1 的運行結果了。

面試官問:數據的強一致性與弱一致性!直接把這篇甩給他

重排序

在 Java 內存模型中,還存在重排序的問題。請看以下代碼:

<code>//代碼1
public class Demo {
int x = 0;
boolean flag = false;
public void writer() {
x = 1;
flag = true;
}
public void reader() {
if (flag) {
int r1 = x;
System.out.println(r1==x)
}
}
}/<code>

如果兩個線程同時運行,線程1調用writer方法,線程2調用reader方法,線程 2 中的r1變量的值可能會出現以下兩種可能:

r1=0或者r1=1

現在一起來看看 r1=1 的運行結果,如下圖所示:

面試官問:數據的強一致性與弱一致性!直接把這篇甩給他

那 r1=0 又是得到的?我們再來看一下圖:

面試官問:數據的強一致性與弱一致性!直接把這篇甩給他

所以在 JVM 中,重排序是十分重要的一環,特別是在併發編程中。可是JVM 要是能對它們進行任意排序的話,也可能會給併發編程帶來一系列的問題,其中就包括了一致性的問題。

為了解決這個問題,Java 提出了 Happens-before 規則來規範線程的執行順序:

Happens-before 規則

  • 程序次序規則:在單線程中,代碼的執行是有序的,雖然可能會存在運行指令的重排序,但最終執行的結果和順序執行的結果是一致的;
  • 鎖定規則:一個鎖處於被一個線程鎖定佔用狀態,那麼只有當這個線程釋放鎖之後,其它線程才能再次獲取鎖操作;
  • volatile 變量規則:如果一個線程正在寫 volatile 變量,其它線程讀取該變量會發生在寫入之後;
  • 線程啟動規則:Thread 對象的 start() 方法先行發生於此線程的其它每一個動作;
  • 線程終結規則:線程中的所有操作都先行發生於對此線程的終止檢測;
  • 對象終結規則:一個對象的初始化完成先行發生於它的 finalize() 方法的開始;
  • 傳遞性:如果操作 A happens-before 操作 B,操作 B happens-before 操作 C,那麼操作 A happens-before 操作 C;
  • 線程中斷規則:對線程 interrupt() 方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。

強一致性與弱一致性

結合這些規則,我們可以將一致性分為以下幾個級別:

嚴格一致性(強一致性):所有的讀寫操作都按照全局時鐘下的順序執行,且任何時刻線程讀取到的緩存數據都是一樣的,Hashtable 就是嚴格一致性;

面試官問:數據的強一致性與弱一致性!直接把這篇甩給他

順序一致性:多個線程的整體執行可能是無序的,但對於單個線程而言執行是有序的,要保證任何一次讀都能讀到最近一次寫入的數據,volatile 可以阻止指令重排序,所以修飾的變量的程序屬於順序一致性;

面試官問:數據的強一致性與弱一致性!直接把這篇甩給他

弱一致性:不能保證任何一次讀都能讀到最近一次寫入的數據,但能保證最終可以讀到寫入的數據,所以像concurrentHashMap就是弱一致性的一種實現。


分享到:


相關文章: