Synchronized關(guān)鍵字
Java語(yǔ)言的關(guān)鍵字,當(dāng)它用來(lái)修飾一個(gè)方法或者一個(gè)代碼塊的時(shí)候,能夠保證在同一時(shí)刻最多只有一個(gè)線程執(zhí)行該段代碼。
當(dāng)兩個(gè)并發(fā)線程訪問(wèn)同一個(gè)對(duì)象object中的這個(gè)synchronized(this)同步代碼塊時(shí),一個(gè)時(shí)間內(nèi)只能有一個(gè)線程得到執(zhí)行。另一個(gè)線程必須等待當(dāng)前線程執(zhí)行完這個(gè)代碼塊以后才能執(zhí)行該代碼塊。
然而,當(dāng)一個(gè)線程訪問(wèn)object的一個(gè)synchronized(this)同步代碼塊時(shí),另一個(gè)線程仍然可以訪問(wèn)該object中的非synchronized(this)同步代碼塊。
尤其關(guān)鍵的是,當(dāng)一個(gè)線程訪問(wèn)object的一個(gè)synchronized(this)同步代碼塊時(shí),其他線程對(duì)object中所有其它synchronized(this)同步代碼塊的訪問(wèn)將被阻塞。
第三個(gè)例子同樣適用其它同步代碼塊。也就是說(shuō),當(dāng)一個(gè)線程訪問(wèn)object的一個(gè)synchronized(this)同步代碼塊時(shí),它就獲得了這個(gè)object的對(duì)象鎖。結(jié)果,其它線程對(duì)該object對(duì)象所有同步代碼部分的訪問(wèn)都被暫時(shí)阻塞。
以上規(guī)則對(duì)其它對(duì)象鎖同樣適用.
代碼示例
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無(wú)鎖化線程封閉實(shí)現(xiàn)原理
ThreadLocal能做什么呢?
這個(gè)一句話不好說(shuō),我們不如來(lái)看看實(shí)際項(xiàng)目中遇到的一些困解:當(dāng)你在項(xiàng)目中根據(jù)一些參數(shù)調(diào)用進(jìn)入一些方法,然后方法再調(diào)用方法,進(jìn)而跨對(duì)象調(diào)用方法,很多層次,這些方法可能都會(huì)用到一些相似的參數(shù),例如,A中需要參數(shù)a、b、c,A調(diào)用B后,B中需要b、c參數(shù),而B調(diào)用C方法需要a、b參數(shù),此時(shí)不得不將所有的參數(shù)全部傳遞給B,以此類推,若有很多方法的調(diào)用,此時(shí)的參數(shù)就會(huì)越來(lái)越繁雜,另外,當(dāng)程序需要增加參數(shù)的時(shí)候,此時(shí)需要對(duì)相關(guān)的方法逐個(gè)增加參數(shù),是的,很麻煩,相信你也遇到過(guò),這也是在C語(yǔ)言面向?qū)ο筮^(guò)來(lái)的一些常見處理手段,不過(guò)我們簡(jiǎn)單的處理方法是將它包裝成對(duì)象傳遞進(jìn)去,通過(guò)增加對(duì)象的屬性就可以解決這個(gè)問(wèn)題,不過(guò)對(duì)象通常是有意義的,所以有些時(shí)候簡(jiǎn)單的對(duì)象包裝增加一些擴(kuò)展不相關(guān)的屬性會(huì)使得我們class的定義變得十分的奇怪,所以在這些情況下我們?cè)诩軜?gòu)這類復(fù)雜的程序的時(shí)候,我們通過(guò)使用一些類似于Scope的作用域的類來(lái)處理,名稱和使用起來(lái)都會(huì)比較通用,類似web應(yīng)用中會(huì)有context、session、request、page等級(jí)別的scope,而ThreadLocal也可以解決這類問(wèn)題,只是他并不是很適合解決這類問(wèn)題,它面對(duì)這些問(wèn)題通常是初期并沒有按照scope以及對(duì)象的方式傳遞,認(rèn)為不會(huì)增加參數(shù),當(dāng)增加參數(shù)時(shí),發(fā)現(xiàn)要改很多地方的地方,為了不破壞代碼的結(jié)構(gòu),也有可能參數(shù)已經(jīng)太多,已經(jīng)使得方法的代碼可讀性降低,增加ThreadLocal來(lái)處理,例如,一個(gè)方法調(diào)用另一個(gè)方法時(shí)傳入了8個(gè)參數(shù),通過(guò)逐層調(diào)用到第N個(gè)方法,傳入了其中一個(gè)參數(shù),此時(shí)最后一個(gè)方法需要增加一個(gè)參數(shù),第一個(gè)方法變成9個(gè)參數(shù)是自然的,但是這個(gè)時(shí)候,相關(guān)的方法都會(huì)受到牽連,使得代碼變得臃腫不堪。
上面提及到了ThreadLocal一種亡羊補(bǔ)牢的用途,不過(guò)也不是特別推薦使用的方式,它還有一些類似的方式用來(lái)使用,就是在框架級(jí)別有很多動(dòng)態(tài)調(diào)用,調(diào)用過(guò)程中需要滿足一些協(xié)議,雖然協(xié)議我們會(huì)盡量的通用,而很多擴(kuò)展的參數(shù)在定義協(xié)議時(shí)是不容易考慮完全的以及版本也是隨時(shí)在升級(jí)的,但是在框架擴(kuò)展時(shí)也需要滿足接口的通用性和向下兼容,而一些擴(kuò)展的內(nèi)容我們就需要ThreadLocal來(lái)做方便簡(jiǎn)單的支持。
簡(jiǎn)單來(lái)說(shuō),ThreadLocal是將一些復(fù)雜的系統(tǒng)擴(kuò)展變成了簡(jiǎn)單定義,使得相關(guān)參數(shù)牽連的部分變得非常容易,以下是我們例子說(shuō)明:
Spring的事務(wù)管理器中,對(duì)數(shù)據(jù)源獲取的Connection放入了ThreadLocal中,程序執(zhí)行完后由ThreadLocal中獲取connection然后做commit和rollback,使用中,要保證程序通過(guò)DataSource獲取的connection就是從spring中獲取的,為什么要做這樣的操作呢,因?yàn)闃I(yè)務(wù)代碼完全由應(yīng)用程序來(lái)決定,而框架不能要求業(yè)務(wù)代碼如何去編寫,否則就失去了框架不讓業(yè)務(wù)代碼去管理connection的好處了,此時(shí)業(yè)務(wù)代碼被切入后,spring不會(huì)向業(yè)務(wù)代碼區(qū)傳入一個(gè)connection,它必須保存在一個(gè)地方,當(dāng)?shù)讓油ㄟ^(guò)ibatis、spring jdbc等框架獲取同一個(gè)datasource的connection的時(shí)候,就會(huì)調(diào)用按照spring約定的規(guī)則去獲取,由于執(zhí)行過(guò)程都是在同一個(gè)線程中處理,從而獲取到相同的connection,以保證commit、rollback以及業(yè)務(wù)操作過(guò)程中,使用的connection是同一個(gè),因?yàn)橹挥型粋€(gè)conneciton才能保證事務(wù),否則數(shù)據(jù)庫(kù)本身也是不支持的。
其實(shí)在很多并發(fā)編程的應(yīng)用中,ThreadLocal起著很重要的重要,它不加鎖,非常輕松的將線程封閉做得天衣無(wú)縫,又不會(huì)像局部變量那樣每次需要從新分配空間,很多空間由于是線程安全,所以,可以反復(fù)利用線程私有的緩沖區(qū)。
如何使用ThreadLocal?
在系統(tǒng)中任意一個(gè)適合的位置定義個(gè) ThreadLocal 變量,可以定義為 public static 類型(直接new出來(lái)一個(gè)ThreadLocal對(duì)象),要向里面放入數(shù)據(jù)就使用set(Object),要獲取數(shù)據(jù)就用get()操作,刪除元素就用remove(),其余的方法是非 public 的方法,不推薦使用。
下面是一個(gè)簡(jiǎn)單例子(代碼片段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個(gè)線程去訪問(wèn)分別設(shè)置 name 和 value ,中間故意將 name 和 value 的值設(shè)置成一樣,看是否會(huì)存在并發(fā)的問(wèn)題,通過(guò)輸出可以看出,線程輸出并不是按照順序輸出,說(shuō)明是并行執(zhí)行的,而線程 name 和 value 是可以對(duì)應(yīng)起來(lái)的,中間通過(guò)多個(gè)方法的調(diào)用,以模實(shí)際的調(diào)用中參數(shù)不傳遞,如何獲取到對(duì)應(yīng)的變量的過(guò)程,不過(guò)實(shí)際的系統(tǒng)中往往會(huì)跨類,這里僅僅在一個(gè)類中模擬,其實(shí)跨類也是一樣的結(jié)果,大家可以自己去模擬就可以。
相信看到這里,很多程序員都對(duì) ThreadLocal 的原理深有興趣,看看它是如何做到的,盡然參數(shù)不傳遞,又可以像局部變量一樣使用它,的確是蠻神奇的,其實(shí)看看就知道是一種設(shè)置方式,看到名稱應(yīng)該是是和Thread相關(guān),那么廢話少說(shuō),來(lái)看看它的源碼吧,既然我們用得最多的是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); } |
首先獲取了當(dāng)前的線程,和猜測(cè)一樣,然后有個(gè) getMap 方法,傳入了當(dāng)前線程,我們先可以理解這個(gè)map是和線程相關(guān)的map,接下來(lái)如果 不為空,就做set操作,你跟蹤進(jìn)去會(huì)發(fā)現(xiàn),這個(gè)和HashMap的put操作類似,也就是向map中寫入了一條數(shù)據(jù),如果為空,則調(diào)用createMap方法,進(jìn)去后,看看( 代碼片段3 ):
1
2
3
|
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap( this , firstValue); } |
返現(xiàn)創(chuàng)建了一個(gè)ThreadLocalMap,并且將傳入的參數(shù)和當(dāng)前ThreadLocal作為K-V結(jié)構(gòu)寫入進(jìn)去( 代碼片段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); } |
這里就不說(shuō)明ThreadLocalMap的結(jié)構(gòu)細(xì)節(jié),只需要知道它的實(shí)現(xiàn)和HashMap類似,只是很多方法沒有,也沒有implements Map,因?yàn)樗⒉幌胱屇阃ㄟ^(guò)某些方式(例如反射)獲取到一個(gè)Map對(duì)他進(jìn)一步操作,它是一個(gè)ThreadLocal里面的一個(gè)static內(nèi)部類,default類型,僅僅在java.lang下面的類可以引用到它,所以你可以想到Thread可以引用到它。
我們?cè)倩剡^(guò)頭來(lái)看看getMap方法,因?yàn)樯厦嫖覂H僅知道獲取的Map是和線程相關(guān)的,而通過(guò) 代碼片段3 ,有一個(gè)t.threadLocalMap = new ThreadLocalMap(this, firstValue)的時(shí)候,相信你應(yīng)該大概有點(diǎn)明白,這個(gè)變量應(yīng)該來(lái)自Thread里面,我們根據(jù)getMap方法進(jìn)去看看:
1
2
3
|
ThreadLocalMap getMap(Thread t) { return t.threadLocals; } |
是的,是來(lái)自于Thread,而這個(gè)Thread正好又是當(dāng)前線程,那么進(jìn)去看看定義就是:
1
|
ThreadLocal.ThreadLocalMap threadLocals = null ; |
這個(gè)屬性就是在Thread類中,也就是每個(gè)Thread默認(rèn)都有一個(gè)ThreadLocalMap,用于存放線程級(jí)別的局部變量,通常你無(wú)法為他賦值,因?yàn)檫@樣的賦值通常是不安全的。
好像是不是有點(diǎn)亂,不著急,我們回頭先摸索下思路:
1、Thread里面有個(gè)屬性是一個(gè)類似于HashMap一樣的東西,只是它的名字叫ThreadLocalMap,這個(gè)屬性是default類型的,因此同一個(gè)package下面所有的類都可以引用到,因?yàn)槭荰hread的局部變量,所以每個(gè)線程都有一個(gè)自己?jiǎn)为?dú)的Map,相互之間是不沖突的,所以即使將ThreadLocal定義為static線程之間也不會(huì)沖突。
2、ThreadLocal和Thread是在同一個(gè)package下面,可以引用到這個(gè)類,可以對(duì)他做操作,此時(shí)ThreadLocal每定義一個(gè),用this作為Key,你傳入的值作為value,而this就是你定義的ThreadLocal,所以不同的ThreadLocal變量,都使用set,相互之間的數(shù)據(jù)不會(huì)沖突,因?yàn)樗麄兊腒ey是不同的,當(dāng)然同一個(gè)ThreadLocal做兩次set操作后,會(huì)以最后一次為準(zhǔn)。
3、綜上所述,在線程之間并行,ThreadLocal可以像局部變量一樣使用,且線程安全,且不同的ThreadLocal變量之間的數(shù)據(jù)毫無(wú)沖突。
我們繼續(xù)看看get方法和remove方法,其實(shí)就簡(jiǎn)單了:
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(); } |
通過(guò)根據(jù)當(dāng)前線程調(diào)用getMap方法,也就是調(diào)用了t.threadLocalMap,然后在map中查找,注意Map中找到的是Entry,也就是K-V基本結(jié)構(gòu),因?yàn)槟鉺et寫入的僅僅有值,所以,它會(huì)設(shè)置一個(gè)e.value來(lái)返回你寫入的值,因?yàn)镵ey就是ThreadLocal本身。你可以看到map.getEntry也是通過(guò)this來(lái)獲取的。
同樣remove方法為:
1
2
3
4
5
|
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null ) m.remove( this ); } |
同樣根據(jù)當(dāng)前線程獲取map,如果不為空,則remove,通過(guò)this來(lái)remove。
補(bǔ)充下(2013-6-29),搞忘寫有什么坑了,這個(gè)ThreadLocal有啥坑呢,大家從前面應(yīng)該可以看出來(lái),這個(gè)ThreadLocal相關(guān)的對(duì)象是被綁定到一個(gè)Map中的,而這個(gè)Map是Thread線程的中的一個(gè)屬性,那么就有一個(gè)問(wèn)題是,如果你不自己remove的話或者說(shuō)如果你自己的程序中不知道什么時(shí)候去remove的話,那么線程不注銷,這些被set進(jìn)去的數(shù)據(jù)也不會(huì)被注銷。
反過(guò)來(lái)說(shuō),寫代碼中除非你清晰的認(rèn)識(shí)到這個(gè)對(duì)象應(yīng)該在哪里set,哪里remove,如果是模糊的,很可能你的代碼中不會(huì)走remove的位置去,或?qū)е乱恍┻壿媶?wèn)題,另外,如果不remove的話,就要等線程注銷,我們?cè)诤芏鄳?yīng)用服務(wù)器中,線程是被復(fù)用的,因?yàn)樵趦?nèi)核分配線程還是有開銷的,因此在這些應(yīng)用中線程很難會(huì)被注銷掉,那么向ThreadLocal寫入的數(shù)據(jù)自然很不容易被注銷掉,這些可能在我們使用某些開源框架的時(shí)候無(wú)意中被隱藏用到,都有可能會(huì)導(dǎo)致問(wèn)題,最后發(fā)現(xiàn)OOM得時(shí)候數(shù)據(jù)竟然來(lái)自ThreadLocalMap中,還不知道這些數(shù)據(jù)是從哪里設(shè)置進(jìn)去的,所以你應(yīng)當(dāng)注意這個(gè)坑,可能不止一個(gè)人掉進(jìn)這個(gè)坑里去過(guò)。