Java并發(fā)編程之并發(fā)代碼設(shè)計(jì)
引子
之前的文章我們探討了引發(fā)線程安全的原因主要是由于多線程的對(duì)共享內(nèi)存的操作導(dǎo)致的可見(jiàn)性或有序性被破壞,從而導(dǎo)致內(nèi)存一致性的錯(cuò)誤。
那么如何設(shè)計(jì)并發(fā)代碼解決這個(gè)問(wèn)題吶?
我們一般使用這幾種方式:
- 線程封閉
 - 不可變對(duì)象
 - 同步
 
發(fā)布和逸出
在此之前 我們先來(lái)了解一下發(fā)布和逸出的概念。
發(fā)布是指讓對(duì)象在當(dāng)前作用域之外使用,例如將對(duì)象的引用傳遞到其他類(lèi)的方法,在一個(gè)方法中返回其引用等。
在許多情況下我們要保證內(nèi)部對(duì)象不被發(fā)布,發(fā)布一些內(nèi)部狀態(tài)可能會(huì)破壞封裝性,讓使用者可以隨意改變其狀態(tài),從而破壞線程安全。
而在某些情況下,我們又需要發(fā)布某些內(nèi)部對(duì)象,如果需要線程安全的情況下,則需要正確的同步。
當(dāng)一個(gè)對(duì)象在不應(yīng)該被發(fā)布的時(shí)候發(fā)布了,這種情況就叫逸出。
- public class Escape {
 - private List<User> users = Lists.newArrayList();
 - public List<User> getUsers() {
 - return users;
 - }
 - public void setUsers(List<User> users) {
 - this.users = users;
 - }
 - }
 
getUsers已經(jīng)逸出了它的作用域,這個(gè)私有變量被發(fā)布了,因?yàn)槿魏握{(diào)用者都可能修改數(shù)組。
同時(shí)發(fā)布users的時(shí)候也間接發(fā)布了User對(duì)象的引用。
- public class OuterEscape {
 - private String str = "Outer's string";
 - public class Inner {
 - public void write() {
 - System.out.println(OuterEscape.this.str);
 - }
 - }
 - public static void main(String[] args) {
 - OuterEscape out = new OuterEscape();
 - OuterEscape.Inner in = out.new Inner();
 - in.write();
 - }
 - }
 
在內(nèi)部類(lèi)中保存了一個(gè)指向創(chuàng)建該內(nèi)部類(lèi)的外圍類(lèi)的引用,所以?xún)?nèi)部類(lèi)中可以使用創(chuàng)建該內(nèi)部類(lèi)的外圍類(lèi)的私有屬性、方法。
- public class ConstructorEscape {
 - private Thread t;
 - public ConstructorEscape() {
 - System.out.println(this);
 - t = new Thread() {
 - public void run() {
 - System.out.println(ConstructorEscape.this);
 - }
 - };
 - t.start();
 - }
 - public static void main(String[] args) {
 - ConstructorEscape a = new ConstructorEscape();
 - }
 - }
 
this引用被線程t共享,故線程t的發(fā)布將導(dǎo)致ConstructorEscape對(duì)象的發(fā)布,由于ConstructorEscape對(duì)象被發(fā)布時(shí)還未構(gòu)造完成,這將導(dǎo)致ConstructorEscape對(duì)象逸出
總結(jié)一下如何安全發(fā)布的步驟
- 找出構(gòu)成對(duì)象狀態(tài)的所有變量
 - 找出約束狀態(tài)變量的不變性條件
 - 建立對(duì)象狀態(tài)的并發(fā)訪問(wèn)策略
 
線程封閉
線程封閉的思想很簡(jiǎn)單,既然線程安全問(wèn)題是由于多線程對(duì)共享變量的訪問(wèn)造成的,那么如果我們可以避免操作共享變量,每個(gè)線程訪問(wèn)自己的變量,就不會(huì)有線程安全的問(wèn)題,這是實(shí)現(xiàn)線程安全最簡(jiǎn)單的方法。
通過(guò)線程控制逃逸規(guī)則可以幫助你判斷代碼中對(duì)某些資源的訪問(wèn)是否是線程安全的,如果一個(gè)資源的創(chuàng)建,使用,銷(xiāo)毀都在同一個(gè)線程內(nèi)完成,且永遠(yuǎn)不會(huì)脫離該線程的控制,則該資源的使用就是線程安全的。
資源可以是對(duì)象,數(shù)組,文件,數(shù)據(jù)庫(kù)連接,套接字等等。Java中你無(wú)需主動(dòng)銷(xiāo)毀對(duì)象,所以“銷(xiāo)毀”指不再有引用指向?qū)ο蟆<词箤?duì)象本身線程安全,但如果該對(duì)象中包含其他資源(文件,數(shù)據(jù)庫(kù)連接),整個(gè)應(yīng)用也許就不再是線程安全的了。比如2個(gè)線程都創(chuàng)建了各自的數(shù)據(jù)庫(kù)連接,每個(gè)連接自身是線程安全的,但它們所連接到的同一個(gè)數(shù)據(jù)庫(kù)也許不是線程安全的
我們?cè)賮?lái)看線程封閉的幾種實(shí)現(xiàn)方式:
棧封閉
棧封閉是線程封閉的一個(gè)特例,在棧封閉中只能通過(guò)局部變量來(lái)訪問(wèn)對(duì)象,局部變量存儲(chǔ)在線程自己的棧中。也就是說(shuō),局部變量永遠(yuǎn)也不會(huì)被多個(gè)線程共享。所以,基礎(chǔ)類(lèi)型的局部變量是線程安全的。
對(duì)象的局部引用和基礎(chǔ)類(lèi)型的局部變量不太一樣。盡管引用本身沒(méi)有被共享,但引用所指的對(duì)象并沒(méi)有存儲(chǔ)在線程的棧內(nèi)。所有的對(duì)象都存在共享堆中。如果在某個(gè)方法中創(chuàng)建的對(duì)象不會(huì)逸出該方法,那么它就是線程安全的。實(shí)際上,哪怕將這個(gè)對(duì)象作為參數(shù)傳給其它方法,只要?jiǎng)e的線程獲取不到這個(gè)對(duì)象,那它仍是線程安全的。
- public void someMethod(){
 - LocalObject localObject = new LocalObject();
 - localObject.callMethod();
 - method2(localObject);
 - }
 - public void method2(LocalObject localObject){
 - localObject.setValue("value");
 - }
 
如上,LocalObject對(duì)象沒(méi)有被方法返回,也沒(méi)有被傳遞給someMethod()方法外的對(duì)象。每個(gè)執(zhí)行someMethod()的線程都會(huì)創(chuàng)建自己的LocalObject對(duì)象,并賦值給localObject引用。因此,這里的LocalObject是線程安全的。事實(shí)上,整個(gè)someMethod()都是線程安全的。即使將LocalObject作為參數(shù)傳給同一個(gè)類(lèi)的其它方法或其它類(lèi)的方法時(shí),它仍然是線程安全的。當(dāng)然,如果LocalObject通過(guò)某些方法被傳給了別的線程,那它就不再是線程安全的了
程序控制線程封閉
通過(guò)程序?qū)崿F(xiàn)來(lái)進(jìn)行線程封閉,也就是說(shuō)我們無(wú)法利用語(yǔ)言特性將對(duì)象封閉到特定的線程上,這一點(diǎn)導(dǎo)致這種方式顯得不那么可靠假設(shè)我們保證只有一個(gè)線程可以對(duì)某個(gè)共享的對(duì)象進(jìn)行寫(xiě)入操作,那么這個(gè)對(duì)象的"讀取-修改-寫(xiě)入"在任何情況下都不會(huì)出現(xiàn)竟態(tài)條件。如果我們?yōu)檫@個(gè)對(duì)象加上volatile修飾則可以保證該對(duì)象的可見(jiàn)性,任何線程都可以讀取該對(duì)象,但只有一個(gè)線程可以對(duì)其進(jìn)行寫(xiě)入。這樣,僅僅通過(guò)volatile修飾就適當(dāng)?shù)乇WC了其安全性,相比直接使用synchoronized修飾,雖然更適合,但實(shí)現(xiàn)起來(lái)稍微復(fù)雜。
程序控制線程封閉,這個(gè)不是一種具體的技術(shù),而是一種設(shè)計(jì)思路,從設(shè)計(jì)上把處理一個(gè)對(duì)象狀態(tài)的代碼都放到一個(gè)線程中去,從而避免線程安全的問(wèn)題。
ThreadLocal
ThreadLocal機(jī)制本質(zhì)上是程序控制線程封閉,只不過(guò)是Java本身幫忙處理了 。來(lái)看Java的Thread類(lèi)和ThreadLocal類(lèi):
- Thread線程類(lèi)維護(hù)了一個(gè)ThreadLocalMap的實(shí)例變量
 - ThreadLocalMap就是一個(gè)Map結(jié)構(gòu)
 - ThreadLocal的set方法取到當(dāng)前線程,拿到當(dāng)前線程的threadLocalMap對(duì)象,然后把ThreadLocal對(duì)象作為key,把要放入的值作為value,放到Map
 - ThreadLocal的get方法取到當(dāng)前線程,拿到當(dāng)前線程的threadLocalMap對(duì)象,然后把ThreadLocal對(duì)象作為key,拿到對(duì)應(yīng)的value
 
- public class Thread implements Runnable {
 - ThreadLocal.ThreadLocalMap threadLocals = null;
 - }
 - public class ThreadLocal<T> {
 - 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();
 - }
 - ThreadLocalMap getMap(Thread t) {
 - return t.threadLocals;
 - }
 - public void set(T value) {
 - Thread t = Thread.currentThread();
 - ThreadLocalMap map = getMap(t);
 - if (map != null)
 - map.set(this, value);
 - else
 - createMap(t, value);
 - }
 - }
 
ThreadLocal的設(shè)計(jì)很簡(jiǎn)單,就是給線程對(duì)象設(shè)置了一個(gè)內(nèi)部的Map,可以放置一些數(shù)據(jù)。JVM從底層保證了Thread對(duì)象之間不會(huì)看到對(duì)方的數(shù)據(jù)。
使用ThreadLocal前提是給每個(gè)ThreadLocal保存一個(gè)單獨(dú)的對(duì)象,這個(gè)對(duì)象不能是在多個(gè)ThreadLocal共享的,否則這個(gè)對(duì)象也是線程不安全的
ThreadLocal 內(nèi)存泄漏
ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個(gè)ThreadLocal沒(méi)有外部強(qiáng)引用來(lái)引用它,那么系統(tǒng) GC 的時(shí)候,這個(gè)ThreadLocal勢(shì)必會(huì)被回收,這樣一來(lái),ThreadLocalMap中就會(huì)出現(xiàn)key為null的Entry,就沒(méi)有辦法訪問(wèn)這些key為null的Entry的value,如果當(dāng)前線程再遲遲不結(jié)束的話,這些key為null的Entry的value就會(huì)一直存在一條強(qiáng)引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠(yuǎn)無(wú)法回收,造成內(nèi)存泄漏。
其實(shí),ThreadLocalMap的設(shè)計(jì)中已經(jīng)考慮到這種情況,也加上了一些防護(hù)措施:在ThreadLocal的get(),set(),remove()的時(shí)候都會(huì)清除線程ThreadLocalMap里所有key為null的value。
所以每次使用完ThreadLocal,都調(diào)用它的remove()方法,清除數(shù)據(jù)就可以避免這個(gè)問(wèn)題
不可變對(duì)象
一個(gè)對(duì)象如果在創(chuàng)建后不能被修改,那么就稱(chēng)為不可變對(duì)象。在并發(fā)編程中,一種被普遍認(rèn)可的原則就是:盡可能的使用不可變對(duì)象來(lái)創(chuàng)建簡(jiǎn)單、可靠的代碼
在并發(fā)編程中,不可變對(duì)象特別有用。由于創(chuàng)建后不能被修改,所以不會(huì)出現(xiàn)操作共享變量導(dǎo)致的內(nèi)存一致性錯(cuò)誤
但是程序員們通常并不熱衷于使用不可變對(duì)象,因?yàn)樗麄儞?dān)心每次創(chuàng)建新對(duì)象的開(kāi)銷(xiāo)。實(shí)際上這種開(kāi)銷(xiāo)常常被過(guò)分高估,而且使用不可變對(duì)象所帶來(lái)的一些效率提升也抵消了這種開(kāi)銷(xiāo)
我們先來(lái)看一個(gè)使用同步來(lái)解決線程安全的例子
- public class SynchronizedRGB {
 - // Values must be between 0 and 255.
 - private int red;
 - private int green;
 - private int blue;
 - private String name;
 - private void check(int red,
 - int green,
 - int blue) {
 - if (red < 0 || red > 255
 - || green < 0 || green > 255
 - || blue < 0 || blue > 255) {
 - throw new IllegalArgumentException();
 - }
 - }
 - public SynchronizedRGB(int red,
 - int green,
 - int blue,
 - String name) {
 - check(red, green, blue);
 - this.red = red;
 - this.green = green;
 - this.blue = blue;
 - this.name = name;
 - }
 - public void set(int red,
 - int green,
 - int blue,
 - String name) {
 - check(red, green, blue);
 - synchronized (this) {
 - this.red = red;
 - this.green = green;
 - this.blue = blue;
 - this.name = name;
 - }
 - }
 - public synchronized int getRGB() {
 - return ((red << 16) | (green << 8) | blue);
 - }
 - public synchronized String getName() {
 - return name;
 - }
 - }
 
- SynchronizedRGB color =
 - new SynchronizedRGB(0, 0, 0, "Pitch Black");
 - ...
 - int myColorInt = color.getRGB(); // 1
 - String myColorName = color.getName(); // 2
 - //如果其他線程在1執(zhí)行后調(diào)用set方法 就會(huì)導(dǎo)致 getName 跟getRGB的值不匹配
 - synchronized (color) {
 - int myColorInt = color.getRGB();
 - String myColorName = color.getName();
 - }
 - //必需使這2個(gè)語(yǔ)句同步執(zhí)行
 
創(chuàng)建不可變對(duì)象的幾條原則
- 不提供修改可變對(duì)象的方法。(包括修改字段的方法和修改字段引用對(duì)象的方法)
 - 將類(lèi)的所有字段定義為final、private的。
 - 不允許子類(lèi)重寫(xiě)方法。簡(jiǎn)單的辦法是將類(lèi)聲明為final,更好的方法是將構(gòu)造函數(shù)聲明為私有的,通過(guò)工廠方法創(chuàng)建對(duì)象。
 - 如果類(lèi)的字段是對(duì)可變對(duì)象的引用,不允許修改被引用對(duì)象。
 - 不共享可變對(duì)象的引用。當(dāng)一個(gè)引用被當(dāng)做參數(shù)傳遞給構(gòu)造函數(shù),而這個(gè)引用指向的是一個(gè)外部的可變對(duì)象時(shí),一定不要保存這個(gè)引用。如果必須要保存,那么創(chuàng)建可變對(duì)象的拷貝,然后保存拷貝對(duì)象的引用。同樣如果需要返回內(nèi)部的可變對(duì)象時(shí),不要返回可變對(duì)象本身,而是返回其拷貝
 
修改后的例子
- final public class ImmutableRGB {
 - // Values must be between 0 and 255.
 - final private int red;
 - final private int green;
 - final private int blue;
 - final private String name;
 - private void check(int red,
 - int green,
 - int blue) {
 - if (red < 0 || red > 255
 - || green < 0 || green > 255
 - || blue < 0 || blue > 255) {
 - throw new IllegalArgumentException();
 - }
 - }
 - public ImmutableRGB(int red,
 - int green,
 - int blue,
 - String name) {
 - check(red, green, blue);
 - this.red = red;
 - this.green = green;
 - this.blue = blue;
 - this.name = name;
 - }
 - public int getRGB() {
 - return ((red << 16) | (green << 8) | blue);
 - }
 - public String getName() {
 - return name;
 - }
 - }
 
事實(shí)不可變對(duì)象
如果對(duì)象本事是可變的,但是程序運(yùn)行過(guò)程中,不存在改變的可能,那么就稱(chēng)為事實(shí)不可變對(duì)象,這樣也不需要額外的線程安全的保護(hù)
同步
當(dāng)我們不得不使用共享變量,而且需要經(jīng)常修改的時(shí)候我們就需要使用同步來(lái)實(shí)現(xiàn)線程安全了。
Java我們可以使用 Synchronized/Lock volatite CAS 來(lái)實(shí)現(xiàn)同步。
synchronized是一種獨(dú)占鎖,它假設(shè)最壞的情況,并且只有在確保其它線程不會(huì)造成干擾的情況下執(zhí)行,會(huì)導(dǎo)致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。
與鎖相比,volatile變量是一和更輕量級(jí)的同步機(jī)制,因?yàn)樵谑褂眠@些變量時(shí)不會(huì)發(fā)生上下文切換和線程調(diào)度等操作,但是volatile變量也存在一些局限:不能用于構(gòu)建原子的復(fù)合操作,因此當(dāng)一個(gè)變量依賴(lài)舊值時(shí)就不能使用volatile變量。
CAS是一種樂(lè)觀鎖,每次不加鎖而是假設(shè)沒(méi)有沖突而去完成某項(xiàng)操作,如果因?yàn)闆_突失敗就重試,直到成功為止。
同步解決了三個(gè)相互關(guān)聯(lián)的問(wèn)題:
- 原子性:哪些指令必須是不可分割的
 - 可見(jiàn)性:一個(gè)線程執(zhí)行的結(jié)果對(duì)另一個(gè)線程是可見(jiàn)的
 - 有序性:某個(gè)線程的操作結(jié)果對(duì)其它線程來(lái)看是無(wú)序的
 
總結(jié)
理解線程安全的概念很重要, 所謂線程安全問(wèn)題,就是處理對(duì)象狀態(tài)的問(wèn)題 。如果要處理的對(duì)象是無(wú)狀態(tài)的(不變性),或者可以避免多個(gè)線程共享的(線程封閉),那么我們可以放心,這個(gè)對(duì)象可能是線程安全的。當(dāng)無(wú)法避免,必須要共享這個(gè)對(duì)象狀態(tài)給多線程訪問(wèn)時(shí),這時(shí)候才用到線程同步的一系列技術(shù)。
這個(gè)理解放大到架構(gòu)層面,我們來(lái)設(shè)計(jì)業(yè)務(wù)層代碼時(shí),業(yè)務(wù)層***做到無(wú)狀態(tài),這樣就業(yè)務(wù)層就具備了可伸縮性,可以通過(guò)橫向擴(kuò)展平滑應(yīng)對(duì)高并發(fā)。
所以我們處理線程安全可以有幾個(gè)層次:
- 能否做成無(wú)狀態(tài)的不變對(duì)象。無(wú)狀態(tài)是最安全的。
 - 能否線程封閉
 - 采用何種同步技術(shù) (Synchronized/Lock volatite CAS)
 















 
 
 








 
 
 
 