Synchronized關鍵字
Java語言的關鍵字,當它用來修飾一個方法或者一個代碼塊的時候,能夠保證在同一時刻最多只有一個線程執行該段代碼。
當兩個并發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以后才能執行該代碼塊。
然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。
尤其關鍵的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。
第三個例子同樣適用其它同步代碼塊。也就是說,當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就獲得了這個object的對象鎖。結果,其它線程對該object對象所有同步代碼部分的訪問都被暫時阻塞。
以上規則對其它對象鎖同樣適用.
代碼示例
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
|
package test160118; public class TestSynchronized { public static void main(String[] args) { Sy sy = new Sy( 0 ); Sy sy2 = new Sy( 1 ); sy.start(); sy2.start(); } } class Sy extends Thread { private int flag ; static Object x1 = new Object(); static Object x2 = new Object(); public Sy( int flag) { this .flag = flag; } @Override public void run() { System.out.println(flag); try { if (flag == 0 ) { synchronized (x1) { System.out.println(flag+ "鎖住了x1" ); Thread.sleep( 1000 ); synchronized (x2) { System.out.println(flag+ "鎖住了x2" ); } System.out.println(flag+ "釋放了x1和x2" ); } } if (flag == 1 ) { synchronized (x2) { System.out.println(flag+ "鎖住了x2" ); Thread.sleep( 1000 ); synchronized (x1) { System.out.println(flag+ "鎖住了x1" ); } System.out.println(flag+ "釋放了x1和x2" ); } } } catch (InterruptedException e) { e.printStackTrace(); } } } |
ThreadLocal無鎖化線程封閉實現原理
ThreadLocal能做什么呢?
這個一句話不好說,我們不如來看看實際項目中遇到的一些困解:當你在項目中根據一些參數調用進入一些方法,然后方法再調用方法,進而跨對象調用方法,很多層次,這些方法可能都會用到一些相似的參數,例如,A中需要參數a、b、c,A調用B后,B中需要b、c參數,而B調用C方法需要a、b參數,此時不得不將所有的參數全部傳遞給B,以此類推,若有很多方法的調用,此時的參數就會越來越繁雜,另外,當程序需要增加參數的時候,此時需要對相關的方法逐個增加參數,是的,很麻煩,相信你也遇到過,這也是在C語言面向對象過來的一些常見處理手段,不過我們簡單的處理方法是將它包裝成對象傳遞進去,通過增加對象的屬性就可以解決這個問題,不過對象通常是有意義的,所以有些時候簡單的對象包裝增加一些擴展不相關的屬性會使得我們class的定義變得十分的奇怪,所以在這些情況下我們在架構這類復雜的程序的時候,我們通過使用一些類似于Scope的作用域的類來處理,名稱和使用起來都會比較通用,類似web應用中會有context、session、request、page等級別的scope,而ThreadLocal也可以解決這類問題,只是他并不是很適合解決這類問題,它面對這些問題通常是初期并沒有按照scope以及對象的方式傳遞,認為不會增加參數,當增加參數時,發現要改很多地方的地方,為了不破壞代碼的結構,也有可能參數已經太多,已經使得方法的代碼可讀性降低,增加ThreadLocal來處理,例如,一個方法調用另一個方法時傳入了8個參數,通過逐層調用到第N個方法,傳入了其中一個參數,此時最后一個方法需要增加一個參數,第一個方法變成9個參數是自然的,但是這個時候,相關的方法都會受到牽連,使得代碼變得臃腫不堪。
上面提及到了ThreadLocal一種亡羊補牢的用途,不過也不是特別推薦使用的方式,它還有一些類似的方式用來使用,就是在框架級別有很多動態調用,調用過程中需要滿足一些協議,雖然協議我們會盡量的通用,而很多擴展的參數在定義協議時是不容易考慮完全的以及版本也是隨時在升級的,但是在框架擴展時也需要滿足接口的通用性和向下兼容,而一些擴展的內容我們就需要ThreadLocal來做方便簡單的支持。
簡單來說,ThreadLocal是將一些復雜的系統擴展變成了簡單定義,使得相關參數牽連的部分變得非常容易,以下是我們例子說明:
Spring的事務管理器中,對數據源獲取的Connection放入了ThreadLocal中,程序執行完后由ThreadLocal中獲取connection然后做commit和rollback,使用中,要保證程序通過DataSource獲取的connection就是從spring中獲取的,為什么要做這樣的操作呢,因為業務代碼完全由應用程序來決定,而框架不能要求業務代碼如何去編寫,否則就失去了框架不讓業務代碼去管理connection的好處了,此時業務代碼被切入后,spring不會向業務代碼區傳入一個connection,它必須保存在一個地方,當底層通過ibatis、spring jdbc等框架獲取同一個datasource的connection的時候,就會調用按照spring約定的規則去獲取,由于執行過程都是在同一個線程中處理,從而獲取到相同的connection,以保證commit、rollback以及業務操作過程中,使用的connection是同一個,因為只有同一個conneciton才能保證事務,否則數據庫本身也是不支持的。
其實在很多并發編程的應用中,ThreadLocal起著很重要的重要,它不加鎖,非常輕松的將線程封閉做得天衣無縫,又不會像局部變量那樣每次需要從新分配空間,很多空間由于是線程安全,所以,可以反復利用線程私有的緩沖區。
如何使用ThreadLocal?
在系統中任意一個適合的位置定義個 ThreadLocal 變量,可以定義為 public static 類型(直接new出來一個ThreadLocal對象),要向里面放入數據就使用set(Object),要獲取數據就用get()操作,刪除元素就用remove(),其余的方法是非 public 的方法,不推薦使用。
下面是一個簡單例子(代碼片段1):
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
|
public class ThreadLocalTest2 { public final static ThreadLocal <String>TEST_THREAD_NAME_LOCAL = new ThreadLocal<String>(); public final static ThreadLocal <String>TEST_THREAD_VALUE_LOCAL = new ThreadLocal<String>(); public static void main(String[]args) { for ( int i = 0 ; i < 100 ; i++) { final String name = "線程-【" + i + "】" ; final String value = String.valueOf(i); new Thread() { public void run() { try { TEST_THREAD_NAME_LOCAL.set(name); TEST_THREAD_VALUE_LOCAL.set(value); callA(); } finally { TEST_THREAD_NAME_LOCAL.remove(); TEST_THREAD_VALUE_LOCAL.remove(); } } }.start(); } } public static void callA() { callB(); } public static void callB() { new ThreadLocalTest2().callC(); } public void callC() { callD(); } public void callD() { System.out.println(TEST_THREAD_NAME_LOCAL.get() + "/t=/t" + TEST_THREAD_VALUE_LOCAL.get()); } } |
這里模擬了100個線程去訪問分別設置 name 和 value ,中間故意將 name 和 value 的值設置成一樣,看是否會存在并發的問題,通過輸出可以看出,線程輸出并不是按照順序輸出,說明是并行執行的,而線程 name 和 value 是可以對應起來的,中間通過多個方法的調用,以模實際的調用中參數不傳遞,如何獲取到對應的變量的過程,不過實際的系統中往往會跨類,這里僅僅在一個類中模擬,其實跨類也是一樣的結果,大家可以自己去模擬就可以。
相信看到這里,很多程序員都對 ThreadLocal 的原理深有興趣,看看它是如何做到的,盡然參數不傳遞,又可以像局部變量一樣使用它,的確是蠻神奇的,其實看看就知道是一種設置方式,看到名稱應該是是和Thread相關,那么廢話少說,來看看它的源碼吧,既然我們用得最多的是set、get和remove,那么就從set下手:
set(T obj)方法為(代碼片段2):
1
2
3
4
5
6
7
8
|
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null ) map.set( this , value); else createMap(t, value); } |
首先獲取了當前的線程,和猜測一樣,然后有個 getMap 方法,傳入了當前線程,我們先可以理解這個map是和線程相關的map,接下來如果 不為空,就做set操作,你跟蹤進去會發現,這個和HashMap的put操作類似,也就是向map中寫入了一條數據,如果為空,則調用createMap方法,進去后,看看( 代碼片段3 ):
1
2
3
|
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap( this , firstValue); } |
返現創建了一個ThreadLocalMap,并且將傳入的參數和當前ThreadLocal作為K-V結構寫入進去( 代碼片段4 ):
1
2
3
4
5
6
7
|
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1 ); table[i] = new Entry(firstKey, firstValue); size = 1 ; setThreshold(INITIAL_CAPACITY); } |
這里就不說明ThreadLocalMap的結構細節,只需要知道它的實現和HashMap類似,只是很多方法沒有,也沒有implements Map,因為它并不想讓你通過某些方式(例如反射)獲取到一個Map對他進一步操作,它是一個ThreadLocal里面的一個static內部類,default類型,僅僅在java.lang下面的類可以引用到它,所以你可以想到Thread可以引用到它。
我們再回過頭來看看getMap方法,因為上面我僅僅知道獲取的Map是和線程相關的,而通過 代碼片段3 ,有一個t.threadLocalMap = new ThreadLocalMap(this, firstValue)的時候,相信你應該大概有點明白,這個變量應該來自Thread里面,我們根據getMap方法進去看看:
1
2
3
|
ThreadLocalMap getMap(Thread t) { return t.threadLocals; } |
是的,是來自于Thread,而這個Thread正好又是當前線程,那么進去看看定義就是:
1
|
ThreadLocal.ThreadLocalMap threadLocals = null ; |
這個屬性就是在Thread類中,也就是每個Thread默認都有一個ThreadLocalMap,用于存放線程級別的局部變量,通常你無法為他賦值,因為這樣的賦值通常是不安全的。
好像是不是有點亂,不著急,我們回頭先摸索下思路:
1、Thread里面有個屬性是一個類似于HashMap一樣的東西,只是它的名字叫ThreadLocalMap,這個屬性是default類型的,因此同一個package下面所有的類都可以引用到,因為是Thread的局部變量,所以每個線程都有一個自己單獨的Map,相互之間是不沖突的,所以即使將ThreadLocal定義為static線程之間也不會沖突。
2、ThreadLocal和Thread是在同一個package下面,可以引用到這個類,可以對他做操作,此時ThreadLocal每定義一個,用this作為Key,你傳入的值作為value,而this就是你定義的ThreadLocal,所以不同的ThreadLocal變量,都使用set,相互之間的數據不會沖突,因為他們的Key是不同的,當然同一個ThreadLocal做兩次set操作后,會以最后一次為準。
3、綜上所述,在線程之間并行,ThreadLocal可以像局部變量一樣使用,且線程安全,且不同的ThreadLocal變量之間的數據毫無沖突。
我們繼續看看get方法和remove方法,其實就簡單了:
1
2
3
4
5
6
7
8
9
10
|
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null ) { ThreadLocalMap.Entry e = map.getEntry( this ); if (e != null ) return (T)e.value; } return setInitialValue(); } |
通過根據當前線程調用getMap方法,也就是調用了t.threadLocalMap,然后在map中查找,注意Map中找到的是Entry,也就是K-V基本結構,因為你set寫入的僅僅有值,所以,它會設置一個e.value來返回你寫入的值,因為Key就是ThreadLocal本身。你可以看到map.getEntry也是通過this來獲取的。
同樣remove方法為:
1
2
3
4
5
|
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null ) m.remove( this ); } |
同樣根據當前線程獲取map,如果不為空,則remove,通過this來remove。
補充下(2013-6-29),搞忘寫有什么坑了,這個ThreadLocal有啥坑呢,大家從前面應該可以看出來,這個ThreadLocal相關的對象是被綁定到一個Map中的,而這個Map是Thread線程的中的一個屬性,那么就有一個問題是,如果你不自己remove的話或者說如果你自己的程序中不知道什么時候去remove的話,那么線程不注銷,這些被set進去的數據也不會被注銷。
反過來說,寫代碼中除非你清晰的認識到這個對象應該在哪里set,哪里remove,如果是模糊的,很可能你的代碼中不會走remove的位置去,或導致一些邏輯問題,另外,如果不remove的話,就要等線程注銷,我們在很多應用服務器中,線程是被復用的,因為在內核分配線程還是有開銷的,因此在這些應用中線程很難會被注銷掉,那么向ThreadLocal寫入的數據自然很不容易被注銷掉,這些可能在我們使用某些開源框架的時候無意中被隱藏用到,都有可能會導致問題,最后發現OOM得時候數據竟然來自ThreadLocalMap中,還不知道這些數據是從哪里設置進去的,所以你應當注意這個坑,可能不止一個人掉進這個坑里去過。