您好,登錄后才能下訂單哦!
這篇文章主要介紹“Java并發代碼設計的步驟是什么”,在日常操作中,相信很多人在Java并發代碼設計的步驟是什么問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Java并發代碼設計的步驟是什么”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
發布和逸出
在此之前 我們先來了解一下發布和逸出的概念。
發布是指讓對象在當前作用域之外使用,例如將對象的引用傳遞到其他類的方法,在一個方法中返回其引用等。
在許多情況下我們要保證內部對象不被發布,發布一些內部狀態可能會破壞封裝性,讓使用者可以隨意改變其狀態,從而破壞線程安全。
而在某些情況下,我們又需要發布某些內部對象,如果需要線程安全的情況下,則需要正確的同步。
當一個對象在不應該被發布的時候發布了,這種情況就叫逸出。
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已經逸出了它的作用域,這個私有變量被發布了,因為任何調用者都可能修改數組。
同時發布users的時候也間接發布了User對象的引用。
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(); } }
在內部類中保存了一個指向創建該內部類的外圍類的引用,所以內部類中可以使用創建該內部類的外圍類的私有屬性、方法。
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的發布將導致ConstructorEscape對象的發布,由于ConstructorEscape對象被發布時還未構造完成,這將導致ConstructorEscape對象逸出
總結一下如何安全發布的步驟
找出構成對象狀態的所有變量
找出約束狀態變量的不變性條件
建立對象狀態的并發訪問策略
線程封閉
線程封閉的思想很簡單,既然線程安全問題是由于多線程對共享變量的訪問造成的,那么如果我們可以避免操作共享變量,每個線程訪問自己的變量,就不會有線程安全的問題,這是實現線程安全最簡單的方法。
通過線程控制逃逸規則可以幫助你判斷代碼中對某些資源的訪問是否是線程安全的,如果一個資源的創建,使用,銷毀都在同一個線程內完成,且永遠不會脫離該線程的控制,則該資源的使用就是線程安全的。
資源可以是對象,數組,文件,數據庫連接,套接字等等。Java中你無需主動銷毀對象,所以“銷毀”指不再有引用指向對象。即使對象本身線程安全,但如果該對象中包含其他資源(文件,數據庫連接),整個應用也許就不再是線程安全的了。比如2個線程都創建了各自的數據庫連接,每個連接自身是線程安全的,但它們所連接到的同一個數據庫也許不是線程安全的
我們再來看線程封閉的幾種實現方式:
棧封閉
棧封閉是線程封閉的一個特例,在棧封閉中只能通過局部變量來訪問對象,局部變量存儲在線程自己的棧中。也就是說,局部變量永遠也不會被多個線程共享。所以,基礎類型的局部變量是線程安全的。
對象的局部引用和基礎類型的局部變量不太一樣。盡管引用本身沒有被共享,但引用所指的對象并沒有存儲在線程的棧內。所有的對象都存在共享堆中。如果在某個方法中創建的對象不會逸出該方法,那么它就是線程安全的。實際上,哪怕將這個對象作為參數傳給其它方法,只要別的線程獲取不到這個對象,那它仍是線程安全的。
public void someMethod(){ LocalObject localObject = new LocalObject(); localObject.callMethod(); method2(localObject); } public void method2(LocalObject localObject){ localObject.setValue("value"); }
如上,LocalObject對象沒有被方法返回,也沒有被傳遞給someMethod()方法外的對象。每個執行someMethod()的線程都會創建自己的LocalObject對象,并賦值給localObject引用。因此,這里的LocalObject是線程安全的。事實上,整個someMethod()都是線程安全的。即使將LocalObject作為參數傳給同一個類的其它方法或其它類的方法時,它仍然是線程安全的。當然,如果LocalObject通過某些方法被傳給了別的線程,那它就不再是線程安全的了
程序控制線程封閉
通過程序實現來進行線程封閉,也就是說我們無法利用語言特性將對象封閉到特定的線程上,這一點導致這種方式顯得不那么可靠假設我們保證只有一個線程可以對某個共享的對象進行寫入操作,那么這個對象的"讀取-修改-寫入"在任何情況下都不會出現竟態條件。如果我們為這個對象加上volatile修飾則可以保證該對象的可見性,任何線程都可以讀取該對象,但只有一個線程可以對其進行寫入。這樣,僅僅通過volatile修飾就適當地保證了其安全性,相比直接使用synchoronized修飾,雖然更適合,但實現起來稍微復雜。
程序控制線程封閉,這個不是一種具體的技術,而是一種設計思路,從設計上把處理一個對象狀態的代碼都放到一個線程中去,從而避免線程安全的問題。
ThreadLocal
ThreadLocal機制本質上是程序控制線程封閉,只不過是Java本身幫忙處理了 。來看Java的Thread類和ThreadLocal類:
Thread線程類維護了一個ThreadLocalMap的實例變量
ThreadLocalMap就是一個Map結構
ThreadLocal的set方法取到當前線程,拿到當前線程的threadLocalMap對象,然后把ThreadLocal對象作為key,把要放入的值作為value,放到Map
ThreadLocal的get方法取到當前線程,拿到當前線程的threadLocalMap對象,然后把ThreadLocal對象作為key,拿到對應的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的設計很簡單,就是給線程對象設置了一個內部的Map,可以放置一些數據。JVM從底層保證了Thread對象之間不會看到對方的數據。
使用ThreadLocal前提是給每個ThreadLocal保存一個單獨的對象,這個對象不能是在多個ThreadLocal共享的,否則這個對象也是線程不安全的
ThreadLocal 內存泄漏
ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用來引用它,那么系統 GC 的時候,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現key為null的Entry,就沒有辦法訪問這些key為null的Entry的value,如果當前線程再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠無法回收,造成內存泄漏。
其實,ThreadLocalMap的設計中已經考慮到這種情況,也加上了一些防護措施:在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap里所有key為null的value。
所以每次使用完ThreadLocal,都調用它的remove()方法,清除數據就可以避免這個問題
不可變對象
一個對象如果在創建后不能被修改,那么就稱為不可變對象。在并發編程中,一種被普遍認可的原則就是:盡可能的使用不可變對象來創建簡單、可靠的代碼
在并發編程中,不可變對象特別有用。由于創建后不能被修改,所以不會出現操作共享變量導致的內存一致性錯誤
但是程序員們通常并不熱衷于使用不可變對象,因為他們擔心每次創建新對象的開銷。實際上這種開銷常常被過分高估,而且使用不可變對象所帶來的一些效率提升也抵消了這種開銷
我們先來看一個使用同步來解決線程安全的例子
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執行后調用set方法 就會導致 getName 跟getRGB的值不匹配 synchronized (color) { int myColorInt = color.getRGB(); String myColorName = color.getName(); } //必需使這2個語句同步執行
創建不可變對象的幾條原則
不提供修改可變對象的方法。(包括修改字段的方法和修改字段引用對象的方法)
將類的所有字段定義為final、private的。
不允許子類重寫方法。簡單的辦法是將類聲明為final,更好的方法是將構造函數聲明為私有的,通過工廠方法創建對象。
如果類的字段是對可變對象的引用,不允許修改被引用對象。
不共享可變對象的引用。當一個引用被當做參數傳遞給構造函數,而這個引用指向的是一個外部的可變對象時,一定不要保存這個引用。如果必須要保存,那么創建可變對象的拷貝,然后保存拷貝對象的引用。同樣如果需要返回內部的可變對象時,不要返回可變對象本身,而是返回其拷貝
修改后的例子
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; } }
事實不可變對象
如果對象本事是可變的,但是程序運行過程中,不存在改變的可能,那么就稱為事實不可變對象,這樣也不需要額外的線程安全的保護
同步
當我們不得不使用共享變量,而且需要經常修改的時候我們就需要使用同步來實現線程安全了。
Java我們可以使用 Synchronized/Lock volatite CAS 來實現同步。
synchronized是一種獨占鎖,它假設最壞的情況,并且只有在確保其它線程不會造成干擾的情況下執行,會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。
與鎖相比,volatile變量是一和更輕量級的同步機制,因為在使用這些變量時不會發生上下文切換和線程調度等操作,但是volatile變量也存在一些局限:不能用于構建原子的復合操作,因此當一個變量依賴舊值時就不能使用volatile變量。
CAS是一種樂觀鎖,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。
同步解決了三個相互關聯的問題:
原子性:哪些指令必須是不可分割的
可見性:一個線程執行的結果對另一個線程是可見的
有序性:某個線程的操作結果對其它線程來看是無序的
到此,關于“Java并發代碼設計的步驟是什么”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。