synchronized、volatile對於java程序員來說再熟悉不過了,但
是你知道這兩個關鍵字底層是如何實現的嗎(甚至在操作系層面是
通過什麼指令來實現的)?以及與其相關的術語:諸如用戶態與內核
態、cas、鎖升級、內存一致性協議、內存屏障都是什麼,下面我來一
一揭祕。本專題將分爲兩篇文章進行講解,此篇主要介紹關於
synchronized和volatile在kernel層面涉及的一些核心概念,下一篇
會詳細說明synchronized和volatile實現原理,包括內存屏障、
鎖升級過程(偏向、輕量、重量)、重入鎖、線程可見性、指令重排等
核心原理,其中也不乏DCL單例是否需要volatile修飾等有趣問題。
用戶態和內核態
一般的操作系統對操作指令進行了權限控制,在intel x86 cpu中將級別分爲0-3,0爲最高執行權限,3爲最低執行權限,0和3分別代表內核態和用戶態,簡單來說就是需要與操作系統,比如操作系統硬件打交道時,就需要調用內核態來完成,而jvm是運行在用戶態的,或者說運行在用戶空間的。
運行在用戶空間的是幹活的,權利小,內核空間纔是操作系統老大,做
大事需要向內核空間申請權限。
CAS
compare and swap(比較並交換),即有2個線程A和B,同時對int i = N進行加1操作,當線程A將i+1後的結果寫入主內存賦值給i前,首先會比較當前主內存中i的值是否爲N,如果爲N就執行賦值操作,否則有可能是B線程執行了對i的修改操作(如目前i=N+1),A線程就不執行賦值操作,A線程再次從主內存中讀取最新的i的值,然後再執行i+1操作,然後再次將結果賦值給i,在賦值之前同樣比較當前主內存中的i的值是否爲N+1,如果爲N+1,就將線程A修改的i的值賦值給i,否則再次執行上述操作,直至修改成功爲止。我們可以把這個操作成爲自旋操作。
AtomicInteger就是cas的實現:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
也就是最後會調用一個native方法compareAndSwapInt函數。這個方法依次會調用操作系統代碼unsafe.cpp,atomic.cpp和atomic_windows_x86.inline.hpp/atomic_linux_x86.inline.hpp:
// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0 \
__asm je L0 \
__asm _emit 0xF0 \
__asm L0:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
// alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
上面的代碼就是操作系統級別執行cas操作執行的指令代碼,簡單來說就是會執行lock_if_mp cmpxchg來完成cas操作的。其中cmpxchg就是執行cas操作的,lock_if_mp是指如果是多核cpu,就將cmpxchg指令進行加鎖執行,否則就不加鎖。從而可以看出cmpxchg並不是原子操作,需要加lock後纔是原子操作,其實鎖住的是北橋信號。上面就是cas在操作系統層面的執行原理。
緩存行
上面是最常見的緩存簡圖。main cache爲主內存,L1 cache、L2 cache、L3 cache分別爲1級緩存、2級緩存、3級緩存。cahe line爲緩存行。那麼緩存行是什麼?緩存行就是保存了我們需要執行的指令代碼,大多數一個緩存行爲64個字節,cpu一次也會讀取64個字節指令到cpu中進行使用。
下面我們來看一個比較有意思的兩段代碼片段:
代碼片段1:
public class TestCacheLingOne {
static volatile Long[] l = new Long[2];
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(){
@Override
public void run() {
for (long i=0;i<1000_000_000L;i++) {
l[0] = 1L;
}
}
};
Thread threadTwo = new Thread(){
@Override
public void run() {
for (long i=0;i<1000_000_000L;i++) {
l[1] = 2L;
}
}
};
long time = System.currentTimeMillis();
threadOne.start();
threadTwo.start();
threadOne.join();
threadTwo.join();
System.out.println((System.currentTimeMillis() - time) /1000d);
}
}
代碼片段2:
public class TestCacheLineTwo {
static volatile Long[] l = new Long[9];
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(){
@Override
public void run() {
for (long i=0;i<1000_000_000L;i++) {
l[0] = 1L;
}
}
};
Thread threadTwo = new Thread(){
@Override
public void run() {
for (long i=0;i<1000_000_000L;i++) {
l[8] = 2L;
}
}
};
long time = System.currentTimeMillis();
threadOne.start();
threadTwo.start();
threadOne.join();
threadTwo.join();
System.out.println((System.currentTimeMillis() - time) /1000d);
}
}
上面的兩段程序都是2個線程對同一個volatile修飾的Long類型數組進行10億次值修改操作,那麼執行用時差別還是很明顯的,這是爲什麼呢?這是由於cpu讀取緩存行字節數和下面我要說的緩存一致性有關,在下一篇內容中會進行具體說明。
緩存一致性協議
其實緩存一致性協議和synchronized、volatile本身沒有關係,緩存一致性是操作系統層面的概念。目前一致性協議有MESI、MSI、Dragon等等,我們常說的MESI其實是intnel x86的緩存一致性協議,MESI即:Modified(修改)、Exclusive(獨享)、Shared(共享)、Invalid(失效),指的就是緩存行的4種狀態。
上圖說明了2個線程對主內存中同一個緩存行的操作過程。
- cpu1即線程1,讀取主內存中的數據x,將x寫入當前線程緩存,緩存行狀態爲E(共享),同時cpu1監聽總線(嗅探)。
- 此時cpu2即線程2讀取主內存中的數據x,此時cpu1監聽器發現線程2讀取了主內存中的x緩存行,則將cpu1中的x緩存行置爲S(共享)
- 線程2將x緩存行從主內存讀取到線程2緩存中,將x緩存行狀態置爲S(共享),並監聽總線。
- 此時線程1修改了x值,通知總線,此時將線程2中的x緩存行置爲I(無效),將線程1中的x緩存行置爲E(獨享)。
- 此時線程2如果想操作x,那麼需要再次從主內存中讀取x緩存行。
CPU指令亂序
cpu指令亂序是指cpu爲了提高其執行效率,在操作系統層面會將指令亂序執行(不是按照程序編寫順序執行)。但是這種cpu指令亂序會導致在多線程情況下,產生問題。看如下代碼,證明cpu存在亂序執行:
public class Test {
static int a,b,c,d;
public static void main(String[] args) throws InterruptedException {
int x = 0;
for(;;) {
x++;
a = 0;
b = 0;
c = 0;
d = 0;
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
c = b;
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
d = a;
}
});
threadOne.start();
threadTwo.start();
threadOne.join();
threadTwo.join();
if (c == 0 && d == 0) {
System.out.println("執行第" + x + "次,證明cpu是可以亂序執行的");
break;
}
}
}
}
如果cpu沒有指令亂序執行存在,那麼不可能打印出同時滿足c=0,d=0,所以說cpu存在亂序問題。
最後
下一篇將接着本篇講解,會介紹內存屏障問題,及鎖升級過程、線程可見性、指令重排等相關技術。如果本篇對你有用,歡迎點贊、關注、轉載,由於水平有限,如有問題請留言。