在併發編程中,基本上離不開這三個東西,如何實現多線程之間的數據共享,能夠用 volatile; 每一個線程維護本身的變量,則採用 ThreadLocal; 爲了保證方法or代碼塊的線程安全,就該 synchronized 上場。這裏將主要說明下這三個能夠怎麼用,以及內部的實現細節html
java編程語言容許線程訪問共享變量,爲了確保共享變量能被準確和一致的更新,線程應該確保經過排他鎖單獨得到這個變量。Java語言提供了volatile,在某些狀況下比鎖更加方便。若是一個字段被聲明成volatile,java線程內存模型確保全部線程看到這個變量的值是一致的。java
處理器爲了提升處理速度,不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存(L1,L2或其餘)後再進行操做,但操做完以後不知道什麼時候會寫到內存,若是對聲明瞭Volatile變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。可是就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會有問題,因此在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操做的時候,會強制從新從系統內存裏把數據讀處處理器緩存裏 用一個圖簡單的說明上面的過程web
圖畫的通常般,簡單說一下算法
咱們有兩個線程, 線程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
關鍵字幹掉,下面是輸出,從最終的結果來看好像並無什麼區別,那這個東西到底有什麼用,該怎麼用?緩存
輸出結果安全
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,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,in B--------- 135, out A------- out B--------
一篇參考連接: 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代碼塊中,或者爲常量時,沒必要使用。
synchronized
同步代碼塊 or 同步方法, 加鎖, 簡單來說,當一塊被這個關鍵詞修飾時,那麼這塊在統一時刻,只能有一個線程進行訪問
一般來說,有三種使用方法,用來修飾成員方法, 靜態方法, 和代碼快,下面分別來寫個測試case
public synchronized static 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 static 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) { 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--->
將上面的 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()
進行修飾,括號裏面就表示要加鎖的東西
public class SynchronizedTest { public void staticFunc() { System.out.println(Thread.currentThread().getName() + " in 1--->"); try { synchronized (this) { 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 void staticFunc2() { System.out.println(Thread.currentThread().getName() + " in 2--->"); try { synchronized (this) { 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(); } }
輸出以下, 對這個說明一點, 若是在靜態方法中, 使用了同步代碼塊, 那麼括號裏面的能夠寫什麼 ? xx.class
便可
Thread-0 in 1---> Thread-1 in 2---> Thread-0-->synch staticFunc print Thread-0 out 1---> Thread-1-->synch staticFunc2 print Thread-1 out 2--->
源碼以下
public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("Method 1 start"); } } }
在加鎖的代碼塊, 多了一個 monitorenter
, monitorexit
每一個對象有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的全部權,過程以下:
執行monitorexit的線程必須是objectref所對應的monitor的全部者。
談到
synchronized
就不可避免的要說到鎖這個東西,基本上在網上能夠搜索到一大批的關於偏向鎖,輕量鎖,重量鎖的講解文檔,對這個東西基本上我也不太理解,多看幾篇博文以後,簡單的記錄一下
先拋一個結論: 輕量級鎖是爲了在線程交替執行同步塊時提升性能,而偏向鎖則是在只有一個線程執行同步塊時進一步提升性能
獲取過程
釋放過程
「輕量級」是相對於使用操做系統互斥量來實現的傳統鎖而言的。可是,首先須要強調一點的是,輕量級鎖並非用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用產生的性能消耗。在解釋輕量級鎖的執行過程以前,先明白一點,輕量級鎖所適應的場景是線程交替執行同步塊的狀況,若是存在同一時間訪問同一鎖的狀況,就會致使輕量級鎖膨脹爲重量級鎖
簡單來說,單線程時,使用偏向鎖,若是這個時候,又來了一個線程訪問這個代碼塊,那麼就要升級爲輕量鎖,若是這個線程在訪問代碼塊同時,又來了一個線程來訪問這個代碼塊,那麼就要升級爲重量鎖了。下面更多的顯示了這些變更時,標記位的隨之改變

單例模式,懶加載的方式,就是一個典型的利用了 synchronized
的案例
public class SingleClz { private static final SingleClz instance; private SingleClz() {} public static SingleClz getINstance() { if(instance == null) { synchronized(SingleClz.class) { if(instance == null) { instance = new SingleClz(); } } } return instance; } }
線程本地變量,每一個線程保存變量的副本,對副本的改動,對其餘的線程而言是透明的(即隔離的)
先來瞅一下,這個東西通常的使用姿式。一般要獲取線程變量, 直接調用 ParamsHolder.get()
public class ParamsHolder { private static final ThreadLocal<Params> PARAMS_INFO = new ThreadLocal<>(); @ToString @Getter @Setter public static class Params { private String mk; } public static void setParams(Params params) { PARAMS_INFO.set(params); } public static void clear() { PARAMS_INFO.remove(); } public static Params get() { return PARAMS_INFO.get(); } public static void main(String[] args) { Thread child = new Thread(new Runnable() { @Override public void run() { System.out.println("child thread initial: " + ParamsHolder.get()); ParamsHolder.setParams(new ParamsHolder.Params("thread")); System.out.println("child thread final: " + ParamsHolder.get()); } }); child.start(); System.out.println("main thread initial: " + ParamsHolder.get()); ParamsHolder.setParams(new ParamsHolder.Params("main")); System.out.println("main thread final: " + ParamsHolder.get()); } }
輸出結果
child thread initial: null main thread initial: null child thread final: ParamsHolder.Params(mk=thread) main thread final: ParamsHolder.Params(mk=main)
直接看源碼中的兩個方法, 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
屬性, 這個屬性的解釋以下,簡單來說, 這個裏面的變量都是線程獨享的,徹底由線程本身hold住
ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class.
獲取的話主要是從 ThreadLocalMap
中,將存進去的參數撈出來,如今須要瞭解的就是這個對象的內部構造了, 裏面的有個table對象,維護了一個Entry的數組table
, Entry
的key爲ThreadLocal
對象, value爲具體的值。
聚焦在 int i = key.threadLocalHashCode & (table.length - 1);
這一行,這個就是獲取Entry對象在table
中索引值的主要邏輯,主要利用當前線程的hashCode值,假設出現兩個不一樣的線程,這個code值同樣,會如何?下面的getEntry()
邏輯中對key值進行了判斷是否爲當前線程
//ThreadLocalMap.java static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } /** * The table, resized as necessary. * table.length MUST always be a power of two. */ private Entry[] table; private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
針對上面的邏輯,有兩個點有必要繼續研究下, hashCode
的計算方式, 爲何要和數組的長度進行與計算
做爲ThreadLocal實例的變量只有 threadLocalHashCode 這一個,
nextHashCode
和HASH_INCREMENT
是ThreadLocal類的靜態變量,實際上HASH_INCREMENT
是一個常量,表示了連續分配的兩個ThreadLocal實例的threadLocalHashCode值的增量,而nextHashCode 的表示了即將分配的下一個ThreadLocal實例的threadLocalHashCode 的值
全部ThreadLocal對象共享一個AtomicInteger對象nextHashCode用於計算hashcode,一個新對象產生時它的hashcode就肯定了,算法是從0開始,以HASH_INCREMENT = 0x61c88647爲間隔遞增,這是ThreadLocal惟一須要同步的地方。根據hashcode定位桶的算法是將其與數組長度-1進行與操做
ThreadLocalMap的初始長度爲16,每次擴容都增加爲原來的2倍,即它的長度始終是2的n次方,上述算法中使用0x61c88647可讓hash的結果在2的n次方內儘量均勻分佈,減小衝突的機率
ThreadLocal
的注意事項這裏主要的一個問題是線程複用時, 若是不清楚掉ThreadLocal 中的值,就會有可怕的事情發生, 先簡單的演示一下
private static final ThreadLocal<AtomicInteger> threadLocal =new ThreadLocal<AtomicInteger>() { @Override protected AtomicInteger initialValue() { return new AtomicInteger(0); } }; static class Task implements Runnable { @Override public void run() { AtomicInteger s = threadLocal.get(); int initial = s.getAndIncrement(); // 指望初始爲0 System.out.println(initial); } } public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(2); executor.execute(new Task()); executor.execute(new Task()); executor.execute(new Task()); executor.shutdown(); }
輸出結果
0 0 1
說好的線程變量,這裏竟然沒有按照咱們預期的來玩,主要緣由就是線程複用了,而線程中的局部變量沒有清零,致使下一個使用這個線程的時候,這些局部變量也帶過來,致使沒有按照咱們的預期使用
這個最可能致使的一個超級嚴重的問題,就是web應用中的用戶串掉的問題,若是咱們將每一個用戶的信息保存在 ThreadLocal
中, 若是出現線程複用了,那麼問題就會致使明明是張三用戶,結果登陸顯示的是李四的賬號,這下就真的呵呵了
所以,強烈推薦,對於線程變量,一但不用了,就顯示的調用 remove()
方法進行清楚
SimpleDataFormate
是一個非線程安全的類,可使用 ThreadLocal 完成的線程安全的使用
public class ThreadLocalDateFormat { static ThreadLocal<DateFormat> sdf = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static String date2String(Date date) { return sdf.get().format(date); } public static Date string2Date(String str) throws ParseException { return sdf.get().parse(str); } }