Java併發之 volatile & synchronized & ThreadLocal 講解

Java併發之 volatile & synchronized & ThreadLocal 講解
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

Java 之 volatile & synchronized & ThreadLocal 講解

在併發程式設計中,基本上離不開這三個東西,如何實現多執行緒之間的資料共享,可以用 volatile; 每個執行緒維護自己的變數,則採用 ThreadLocal; 為了保證方法or程式碼塊的執行緒安全,就該 synchronized 上場。這裡將主要說明下這三個可以怎麼用,以及內部的實現細節

1. volatile

java程式語言允許執行緒訪問共享變數,為了確保共享變數能被準確和一致的更新,執行緒應該確保通過排他鎖單獨獲得這個變數。Java語言提供了volatile,在某些情況下比鎖更加方便。如果一個欄位被宣告成volatile,java執行緒記憶體模型確保所有執行緒看到這個變數的值是一致的。

實現原理

處理器為了提高處理速度,不直接和記憶體進行通訊,而是先將系統記憶體的資料讀到內部快取(L1,L2或其他)後再進行操作,但操作完之後不知道何時會寫到記憶體,如果對宣告瞭Volatile變數進行寫操作,JVM就會向處理器傳送一條Lock字首的指令,將這個變數所在快取行的資料寫回到系統記憶體。但是就算寫回到記憶體,如果其他處理器快取的值還是舊的,再執行計算操作就會有問題,所以在多處理器下,為了保證各個處理器的快取是一致的,就會實現快取一致性協議,每個處理器通過嗅探在匯流排上傳播的資料來檢查自己快取的值是不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行設定成無效狀態,當處理器要對這個資料進行修改操作的時候,會強制重新從系統記憶體裡把資料讀到處理器快取裡
用一個圖簡單的說明上面的過程

圖解

圖畫的一般般,簡單說一下

cpu與內部快取進行互動
volatile生命的變數,操作完之後寫入記憶體(data -> data’ 同時寫入記憶體)
其他cpu快取嗅探匯流排變動,並設定自己的data無效,使用時,從記憶體中獲取

輸入圖片說明

測試case

我們有兩個執行緒, 執行緒B修改一個共享變數tag, 執行緒A一直迴圈幹模式, 當發現 tag 設定為了 true 時, 則結束

private volatile boolean tag = false;
@Test
public void testVolatile() throws InterruptedException {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
System.out.println("in A-------");
while (!tag) {
System.out.print((i  )   ",");
}
System.out.println("\nout A-------");
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("in B---------");
tag = true;
System.out.println("out B--------");
}
});
threadA.start();
Thread.sleep(1);
threadB.start();;
}

輸出為:

in A-------
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,in B---------
96,
out A-------
out B--------

從上面的輸出可以看出,當進入執行緒B之後,將 tag設定為true, 對執行緒A而言,它很迅速的感知到了這個引數的變化, 並終止了迴圈; 如果將tag前面的volatile

一篇參考連結: http://blog.csdn.net/feier7501/article/details/20001083 (說明這篇博文中的case,本機jdk8並沒有復現….., 所以這是一個失敗的case)

再看一個case,

public class TestVolatile {
int a = 1;
int b = 2;
public void change(){
a = 3;
Thread.sleep(10);   // 人肉加長這個賦值的時間
b = a;
}
public void print(){
System.out.println("b=" b ";a=" a);
}
public static void main(String[] args) {
while (true){
final TestVolatile test = new TestVolatile();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
test.change();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
}

從上面的程式碼來看,正常來講,輸出1,2; 或者 3, 3, 而實際輸出卻並不是這樣

...... 
b=2;a=1
b=2;a=1
b=3;a=3
b=3;a=3
b=3;a=1           <--------------------- 看這裡
b=3;a=3
b=2;a=1
b=3;a=3
b=3;a=3
......

使用建議:在兩個或者更多的執行緒訪問的成員變數上使用volatile。當要訪問的變數已在synchronized程式碼塊中,或者為常量時,不必使用。

2. synchronized

synchronized

輸出如下, 兩個同步修飾的靜態方法, 第一個執行緒使用其中的方法時,第二個執行緒即便呼叫第二個靜態方法,依然會被阻塞

Thread-0 in 1--->
Thread-0-->synch staticFunc print
Thread-0 out 1--->
Thread-1 in 2--->
Thread-1-->synch staticFunc2 print
Thread-1 out 2--->

將上面的 synchronized 修飾去掉, 看下輸出如下,也就是說,兩者的呼叫是可以並行的

Thread-1 in 2--->
Thread-0 in 1--->
Thread-1-->synch staticFunc2 print
Thread-0-->synch staticFunc print
Thread-1 out 2--->
Thread-0 out 1--->

修飾成員方法

在上面的例子中,稍稍改動即可

public class SynchronizedTest {
public synchronized void staticFunc() {
System.out.println(Thread.currentThread().getName()   " in 1--->");
try {
System.out.println(Thread.currentThread().getName()   "-->synch staticFunc print");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()   " out 1--->");
}
public synchronized void staticFunc2() {
System.out.println(Thread.currentThread().getName()   " in 2--->");
try {
System.out.println(Thread.currentThread().getName()   "-->synch staticFunc2 print");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()   " out 2--->");
}
public static void main(String[] args) {
SynchronizedTest synchronizedTest = new SynchronizedTest();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronizedTest.staticFunc();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronizedTest.staticFunc2();
}
});
thread1.start();
thread2.start();
}
}

輸出如下:

Thread-0 in 1--->
Thread-0-->synch staticFunc print
Thread-0 out 1--->
Thread-1 in 2--->
Thread-1-->synch staticFunc2 print
Thread-1 out 2--->

成員方法和靜態方法的修飾區別是什麼 ?對上面的程式碼,做一個簡單的修改, Thread1呼叫物件1的方法1, Thread3 呼叫物件2的方法1

public static void main(String[] args) {
SynchronizedTest synchronizedTest = new SynchronizedTest();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronizedTest.staticFunc();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronizedTest.staticFunc2();
}
});
SynchronizedTest synchronizedTest2 = new SynchronizedTest();
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
synchronizedTest2.staticFunc2();
}
});
thread1.start();
thread2.start();
thread3.start();
}

輸出如下, 其中執行緒0 和執行緒1 保證有序, 但是與執行緒2就沒有什麼關係了;即這個鎖是針對物件的,這個也很容易理解,畢竟物件都不同了,物件的成員方法當然是相對獨立的

Thread-0 in 1--->
Thread-0-->synch staticFunc print
Thread-2 in 2--->
Thread-2-->synch staticFunc2 print
Thread-0 out 1--->
Thread-2 out 2--->
Thread-1 in 2--->
Thread-1-->synch staticFunc2 print
Thread-1 out 2--->

同步程式碼塊

同步程式碼塊的使用,就是將一塊程式碼用大括號圈起來, 外面用 synchronized()

輸出如下, 對這個說明一點, 如果在靜態方法中, 使用了同步程式碼塊, 那麼括號裡面的可以寫什麼 ? xx.class

實現原理

原始碼如下

public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("Method 1 start");
}
}
}

輸入圖片說明

在加鎖的程式碼塊, 多了一個 monitorenter

ThreadLocal

執行緒本地變數,每個執行緒儲存變數的副本,對副本的改動,對其他的執行緒而言是透明的(即隔離的)

1. 使用姿勢一覽

先來瞅一下,這個東西一般的使用姿勢。通常要獲取執行緒變數, 直接呼叫 ParamsHolder.get()

輸出結果

child thread initial: null
main thread initial: null
child thread final: ParamsHolder.Params(mk=thread)
main thread final: ParamsHolder.Params(mk=main)

2. 實現原理探究

直接看原始碼中的兩個方法, get/set, 看下到底是如何實現執行緒變數的

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

先看set方法, 邏輯是獲取當前執行緒物件, 獲取到執行緒物件中的 threadLocals

獲取的話主要是從 ThreadLocalMap

針對上面的邏輯,有兩個點有必要繼續研究下, hashCode

輸出結果

0
0
1

說好的執行緒變數,這裡居然沒有按照我們預期的來玩,主要原因就是執行緒複用了,而執行緒中的區域性變數沒有清零,導致下一個使用這個執行緒的時候,這些區域性變數也帶過來,導致沒有按照我們的預期使用

這個最可能導致的一個超級嚴重的問題,就是web應用中的使用者串掉的問題,如果我們將每個使用者的資訊儲存在 ThreadLocal

參考文件:

聊聊併發(一)深入分析Volatile的實現原理
Java 併發程式設計:volatile的使用及其原理
Synchronized及其實現原理
Java併發程式設計:Synchronized底層優化(偏向鎖、輕量級鎖)
聊聊併發(二)Java SE1.6中的Synchronized
理解ThreadLocal / 計算機程式的思維邏輯
【ThreadLocal】深入JDK原始碼之ThreadLocal類


(adsbygoogle = window.adsbygoogle || []).push({});

function googleAdJSAtOnload() {
var element = document.createElement(“script”);
element.src = “//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js”;
element.async = true;
document.body.appendChild(element);
}
if (window.addEventListener) {
window.addEventListener(“load”, googleAdJSAtOnload, false);
} else if (window.attachEvent) {
window.attachEvent(“onload”, googleAdJSAtOnload);
} else {
window.onload = googleAdJSAtOnload;
}

程式語言 最新文章