日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
Java并發(fā)編程之并發(fā)代碼設(shè)計

引子

成都創(chuàng)新互聯(lián)是一家成都網(wǎng)站建設(shè)、成都網(wǎng)站設(shè)計,提供網(wǎng)頁設(shè)計,網(wǎng)站設(shè)計,網(wǎng)站制作,建網(wǎng)站,按需求定制設(shè)計,網(wǎng)站開發(fā)公司,從2013年創(chuàng)立是互聯(lián)行業(yè)建設(shè)者,服務(wù)者。以提升客戶品牌價值為核心業(yè)務(wù),全程參與項目的網(wǎng)站策劃設(shè)計制作,前端開發(fā),后臺程序制作以及后期項目運營并提出專業(yè)建議和思路。

之前的文章我們探討了引發(fā)線程安全的原因主要是由于多線程的對共享內(nèi)存的操作導(dǎo)致的可見性或有序性被破壞,從而導(dǎo)致內(nèi)存一致性的錯誤。

那么如何設(shè)計并發(fā)代碼解決這個問題吶?

我們一般使用這幾種方式:

  • 線程封閉
  • 不可變對象
  • 同步

發(fā)布和逸出

在此之前 我們先來了解一下發(fā)布和逸出的概念。

發(fā)布是指讓對象在當(dāng)前作用域之外使用,例如將對象的引用傳遞到其他類的方法,在一個方法中返回其引用等。

在許多情況下我們要保證內(nèi)部對象不被發(fā)布,發(fā)布一些內(nèi)部狀態(tài)可能會破壞封裝性,讓使用者可以隨意改變其狀態(tài),從而破壞線程安全。

而在某些情況下,我們又需要發(fā)布某些內(nèi)部對象,如果需要線程安全的情況下,則需要正確的同步。

當(dāng)一個對象在不應(yīng)該被發(fā)布的時候發(fā)布了,這種情況就叫逸出。

 
 
 
  1. public class Escape {
  2.   
  3.    private List users = Lists.newArrayList();
  4.     public List getUsers() {
  5.         return users;
  6.     }
  7.     public void setUsers(List users) {
  8.         this.users = users;
  9.     }

getUsers已經(jīng)逸出了它的作用域,這個私有變量被發(fā)布了,因為任何調(diào)用者都可能修改數(shù)組。

同時發(fā)布users的時候也間接發(fā)布了User對象的引用。

 
 
 
  1. public class OuterEscape {
  2.     private String str = "Outer's string";
  3.     public class Inner {
  4.         public void write() {
  5.             System.out.println(OuterEscape.this.str);
  6.         }
  7.     }
  8.     public static void main(String[] args) {
  9.         OuterEscape out = new OuterEscape();
  10.         OuterEscape.Inner in = out.new Inner();
  11.         in.write();
  12.     }
  13. }   

在內(nèi)部類中保存了一個指向創(chuàng)建該內(nèi)部類的外圍類的引用,所以內(nèi)部類中可以使用創(chuàng)建該內(nèi)部類的外圍類的私有屬性、方法。

 
 
 
  1. public class ConstructorEscape {
  2.     private Thread t;
  3.     public ConstructorEscape() {
  4.         System.out.println(this);
  5.         t = new Thread() {
  6.             public void run() {
  7.                 System.out.println(ConstructorEscape.this);
  8.             }
  9.         };
  10.         t.start();
  11.     }
  12.     public static void main(String[] args) {
  13.         ConstructorEscape a = new ConstructorEscape();
  14.     }
  15. }   

this引用被線程t共享,故線程t的發(fā)布將導(dǎo)致ConstructorEscape對象的發(fā)布,由于ConstructorEscape對象被發(fā)布時還未構(gòu)造完成,這將導(dǎo)致ConstructorEscape對象逸出

總結(jié)一下如何安全發(fā)布的步驟

  • 找出構(gòu)成對象狀態(tài)的所有變量
  • 找出約束狀態(tài)變量的不變性條件
  • 建立對象狀態(tài)的并發(fā)訪問策略

線程封閉

線程封閉的思想很簡單,既然線程安全問題是由于多線程對共享變量的訪問造成的,那么如果我們可以避免操作共享變量,每個線程訪問自己的變量,就不會有線程安全的問題,這是實現(xiàn)線程安全最簡單的方法。

通過線程控制逃逸規(guī)則可以幫助你判斷代碼中對某些資源的訪問是否是線程安全的,如果一個資源的創(chuàng)建,使用,銷毀都在同一個線程內(nèi)完成,且永遠不會脫離該線程的控制,則該資源的使用就是線程安全的。

資源可以是對象,數(shù)組,文件,數(shù)據(jù)庫連接,套接字等等。Java中你無需主動銷毀對象,所以“銷毀”指不再有引用指向?qū)ο?。即使對象本身線程安全,但如果該對象中包含其他資源(文件,數(shù)據(jù)庫連接),整個應(yīng)用也許就不再是線程安全的了。比如2個線程都創(chuàng)建了各自的數(shù)據(jù)庫連接,每個連接自身是線程安全的,但它們所連接到的同一個數(shù)據(jù)庫也許不是線程安全的

我們再來看線程封閉的幾種實現(xiàn)方式:

棧封閉

棧封閉是線程封閉的一個特例,在棧封閉中只能通過局部變量來訪問對象,局部變量存儲在線程自己的棧中。也就是說,局部變量永遠也不會被多個線程共享。所以,基礎(chǔ)類型的局部變量是線程安全的。

對象的局部引用和基礎(chǔ)類型的局部變量不太一樣。盡管引用本身沒有被共享,但引用所指的對象并沒有存儲在線程的棧內(nèi)。所有的對象都存在共享堆中。如果在某個方法中創(chuàng)建的對象不會逸出該方法,那么它就是線程安全的。實際上,哪怕將這個對象作為參數(shù)傳給其它方法,只要別的線程獲取不到這個對象,那它仍是線程安全的。

 
 
 
  1. public void someMethod(){
  2.   
  3.   LocalObject localObject = new LocalObject();
  4.   localObject.callMethod();
  5.   method2(localObject);
  6. }
  7. public void method2(LocalObject localObject){
  8.   localObject.setValue("value");

如上,LocalObject對象沒有被方法返回,也沒有被傳遞給someMethod()方法外的對象。每個執(zhí)行someMethod()的線程都會創(chuàng)建自己的LocalObject對象,并賦值給localObject引用。因此,這里的LocalObject是線程安全的。事實上,整個someMethod()都是線程安全的。即使將LocalObject作為參數(shù)傳給同一個類的其它方法或其它類的方法時,它仍然是線程安全的。當(dāng)然,如果LocalObject通過某些方法被傳給了別的線程,那它就不再是線程安全的了

程序控制線程封閉

通過程序?qū)崿F(xiàn)來進行線程封閉,也就是說我們無法利用語言特性將對象封閉到特定的線程上,這一點導(dǎo)致這種方式顯得不那么可靠假設(shè)我們保證只有一個線程可以對某個共享的對象進行寫入操作,那么這個對象的"讀取-修改-寫入"在任何情況下都不會出現(xiàn)竟態(tài)條件。如果我們?yōu)檫@個對象加上volatile修飾則可以保證該對象的可見性,任何線程都可以讀取該對象,但只有一個線程可以對其進行寫入。這樣,僅僅通過volatile修飾就適當(dāng)?shù)乇WC了其安全性,相比直接使用synchoronized修飾,雖然更適合,但實現(xiàn)起來稍微復(fù)雜。

程序控制線程封閉,這個不是一種具體的技術(shù),而是一種設(shè)計思路,從設(shè)計上把處理一個對象狀態(tài)的代碼都放到一個線程中去,從而避免線程安全的問題。

ThreadLocal

ThreadLocal機制本質(zhì)上是程序控制線程封閉,只不過是Java本身幫忙處理了 。來看Java的Thread類和ThreadLocal類:

  1. Thread線程類維護了一個ThreadLocalMap的實例變量
  2. ThreadLocalMap就是一個Map結(jié)構(gòu)
  3. ThreadLocal的set方法取到當(dāng)前線程,拿到當(dāng)前線程的threadLocalMap對象,然后把ThreadLocal對象作為key,把要放入的值作為value,放到Map
  4. ThreadLocal的get方法取到當(dāng)前線程,拿到當(dāng)前線程的threadLocalMap對象,然后把ThreadLocal對象作為key,拿到對應(yīng)的value
 
 
 
  1. public class Thread implements Runnable {
  2.      ThreadLocal.ThreadLocalMap threadLocals = null;
  3. }
  4. public class ThreadLocal {
  5.     public T get() {
  6.         Thread t = Thread.currentThread();
  7.         ThreadLocalMap map = getMap(t);
  8.         if (map != null) {
  9.             ThreadLocalMap.Entry e = map.getEntry(this);
  10.             if (e != null)
  11.                 return (T)e.value;
  12.         }
  13.         return setInitialValue();
  14.     }
  15.     ThreadLocalMap getMap(Thread t) {
  16.         return t.threadLocals;
  17.     }
  18.     public void set(T value) {
  19.         Thread t = Thread.currentThread();
  20.         ThreadLocalMap map = getMap(t);
  21.         if (map != null)
  22.             map.set(this, value);
  23.         else
  24.             createMap(t, value);
  25.     }

ThreadLocal的設(shè)計很簡單,就是給線程對象設(shè)置了一個內(nèi)部的Map,可以放置一些數(shù)據(jù)。JVM從底層保證了Thread對象之間不會看到對方的數(shù)據(jù)。

使用ThreadLocal前提是給每個ThreadLocal保存一個單獨的對象,這個對象不能是在多個ThreadLocal共享的,否則這個對象也是線程不安全的

ThreadLocal 內(nèi)存泄漏

ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用來引用它,那么系統(tǒng) GC 的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現(xiàn)key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當(dāng)前線程再遲遲不結(jié)束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠無法回收,造成內(nèi)存泄漏。

其實,ThreadLocalMap的設(shè)計中已經(jīng)考慮到這種情況,也加上了一些防護措施:在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap里所有key為null的value。

所以每次使用完ThreadLocal,都調(diào)用它的remove()方法,清除數(shù)據(jù)就可以避免這個問題

不可變對象

一個對象如果在創(chuàng)建后不能被修改,那么就稱為不可變對象。在并發(fā)編程中,一種被普遍認(rèn)可的原則就是:盡可能的使用不可變對象來創(chuàng)建簡單、可靠的代碼

在并發(fā)編程中,不可變對象特別有用。由于創(chuàng)建后不能被修改,所以不會出現(xiàn)操作共享變量導(dǎo)致的內(nèi)存一致性錯誤

但是程序員們通常并不熱衷于使用不可變對象,因為他們擔(dān)心每次創(chuàng)建新對象的開銷。實際上這種開銷常常被過分高估,而且使用不可變對象所帶來的一些效率提升也抵消了這種開銷

我們先來看一個使用同步來解決線程安全的例子

 
 
 
  1. public class SynchronizedRGB {
  2.     // Values must be between 0 and 255.
  3.     private int red;
  4.     private int green;
  5.     private int blue;
  6.     private String name;
  7.     private void check(int red,
  8.                        int green,
  9.                        int blue) {
  10.         if (red < 0 || red > 255
  11.             || green < 0 || green > 255
  12.             || blue < 0 || blue > 255) {
  13.             throw new IllegalArgumentException();
  14.         }
  15.     }
  16.     public SynchronizedRGB(int red,
  17.                            int green,
  18.                            int blue,
  19.                            String name) {
  20.         check(red, green, blue);
  21.         this.red = red;
  22.         this.green = green;
  23.         this.blue = blue;
  24.         this.name = name;
  25.     }
  26.     public void set(int red,
  27.                     int green,
  28.                     int blue,
  29.                     String name) {
  30.         check(red, green, blue);
  31.         synchronized (this) {
  32.             this.red = red;
  33.             this.green = green;
  34.             this.blue = blue;
  35.             this.name = name;
  36.         }
  37.     }
  38.     public synchronized int getRGB() {
  39.         return ((red << 16) | (green << 8) | blue);
  40.     }
  41.     public synchronized String getName() {
  42.         return name;
  43.     }
  44.  
 
 
 
  1. SynchronizedRGB color =
  2.     new SynchronizedRGB(0, 0, 0, "Pitch Black");
  3. ...
  4. int myColorInt = color.getRGB();      // 1
  5. String myColorName = color.getName(); // 2
  6. //如果其他線程在1執(zhí)行后調(diào)用set方法 就會導(dǎo)致 getName 跟getRGB的值不匹配
  7. synchronized (color) {
  8.     int myColorInt = color.getRGB();
  9.     String myColorName = color.getName();
  10. }
  11. //必需使這2個語句同步執(zhí)行 

創(chuàng)建不可變對象的幾條原則

  • 不提供修改可變對象的方法。(包括修改字段的方法和修改字段引用對象的方法)
  • 將類的所有字段定義為final、private的。
  • 不允許子類重寫方法。簡單的辦法是將類聲明為final,更好的方法是將構(gòu)造函數(shù)聲明為私有的,通過工廠方法創(chuàng)建對象。
  • 如果類的字段是對可變對象的引用,不允許修改被引用對象。
  • 不共享可變對象的引用。當(dāng)一個引用被當(dāng)做參數(shù)傳遞給構(gòu)造函數(shù),而這個引用指向的是一個外部的可變對象時,一定不要保存這個引用。如果必須要保存,那么創(chuàng)建可變對象的拷貝,然后保存拷貝對象的引用。同樣如果需要返回內(nèi)部的可變對象時,不要返回可變對象本身,而是返回其拷貝

修改后的例子

 
 
 
  1. final public class ImmutableRGB {
  2.     // Values must be between 0 and 255.
  3.     final private int red;
  4.     final private int green;
  5.     final private int blue;
  6.     final private String name;
  7.     private void check(int red,
  8.                        int green,
  9.                        int blue) {
  10.         if (red < 0 || red > 255
  11.             || green < 0 || green > 255
  12.             || blue < 0 || blue > 255) {
  13.             throw new IllegalArgumentException();
  14.         }
  15.     }
  16.     public ImmutableRGB(int red,
  17.                         int green,
  18.                         int blue,
  19.                         String name) {
  20.         check(red, green, blue);
  21.         this.red = red;
  22.         this.green = green;
  23.         this.blue = blue;
  24.         this.name = name;
  25.     }
  26.     public int getRGB() {
  27.         return ((red << 16) | (green << 8) | blue);
  28.     }
  29.     public String getName() {
  30.         return name;
  31.     }
  32.  

事實不可變對象

如果對象本事是可變的,但是程序運行過程中,不存在改變的可能,那么就稱為事實不可變對象,這樣也不需要額外的線程安全的保護

同步

當(dāng)我們不得不使用共享變量,而且需要經(jīng)常修改的時候我們就需要使用同步來實現(xiàn)線程安全了。

Java我們可以使用 Synchronized/Lock volatite CAS 來實現(xiàn)同步。

synchronized是一種獨占鎖,它假設(shè)最壞的情況,并且只有在確保其它線程不會造成干擾的情況下執(zhí)行,會導(dǎo)致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。

與鎖相比,volatile變量是一和更輕量級的同步機制,因為在使用這些變量時不會發(fā)生上下文切換和線程調(diào)度等操作,但是volatile變量也存在一些局限:不能用于構(gòu)建原子的復(fù)合操作,因此當(dāng)一個變量依賴舊值時就不能使用volatile變量。

CAS是一種樂觀鎖,每次不加鎖而是假設(shè)沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。

同步解決了三個相互關(guān)聯(lián)的問題:

  • 原子性:哪些指令必須是不可分割的
  • 可見性:一個線程執(zhí)行的結(jié)果對另一個線程是可見的
  • 有序性:某個線程的操作結(jié)果對其它線程來看是無序的

總結(jié)

理解線程安全的概念很重要, 所謂線程安全問題,就是處理對象狀態(tài)的問題 。如果要處理的對象是無狀態(tài)的(不變性),或者可以避免多個線程共享的(線程封閉),那么我們可以放心,這個對象可能是線程安全的。當(dāng)無法避免,必須要共享這個對象狀態(tài)給多線程訪問時,這時候才用到線程同步的一系列技術(shù)。

這個理解放大到架構(gòu)層面,我們來設(shè)計業(yè)務(wù)層代碼時,業(yè)務(wù)層***做到無狀態(tài),這樣就業(yè)務(wù)層就具備了可伸縮性,可以通過橫向擴展平滑應(yīng)對高并發(fā)。

所以我們處理線程安全可以有幾個層次:

  1. 能否做成無狀態(tài)的不變對象。無狀態(tài)是最安全的。
  2. 能否線程封閉
  3. 采用何種同步技術(shù) (Synchronized/Lock volatite CAS)

網(wǎng)頁標(biāo)題:Java并發(fā)編程之并發(fā)代碼設(shè)計
標(biāo)題鏈接:http://www.dlmjj.cn/article/djehjch.html