摘要:發(fā)布的對象內(nèi)部狀態(tài)可能會破壞封裝性,使程序難以維持不變性條件。不變性線程安全性是不可變對象的固有屬性之一??勺儗ο蟊仨毻ㄟ^安全方式來發(fā)布,并且必須是線程安全的或者有某個鎖保護起來。
線程的優(yōu)缺點
線程是系統(tǒng)調(diào)度的基本單位。線程如果使用得當,可以有效地降低程序的開發(fā)和維護等成本,同時提升復(fù)雜應(yīng)用程序的性能。多線程程序可以通過提高處理器資源的利用率來提升系統(tǒng)的吞吐率。與此同時,在線程的使用開發(fā)過程中,也存在著諸多需要考慮的風險。
安全性:有合理的同步下,多線程的并發(fā)隨機執(zhí)行使線程安全性變得復(fù)雜,如++i。
活躍性:在多線程中,常因為缺少資源而處于阻塞狀態(tài),當某個操作不幸造成無限循環(huán),無法繼續(xù)執(zhí)行下去的時候,就會發(fā)生活躍性問題。
性能:線程總會帶來程序的運行時開銷,多線程中,當頻繁地出現(xiàn)上下文切換操作時,將會帶來極大的開銷。
線程安全性線程安全的問題著重于解決如何對狀態(tài)訪問操作進行管理,特別是對共享和可變的狀態(tài)。共享意味著可多個線程同時訪問;可變即在變量在其生命周期內(nèi)可以被改變;狀態(tài)就是由某個類中的成員變量(Field)。
一個無狀態(tài)的對象一定是線程安全的。因為它沒有可被改變的東西。
public class LoginServlet implements Servlet { public void service(ServletRequest req, ServletResponse resp) { System.out.println("無狀態(tài)Servlet,安全的類,沒有字段可操作"); } }原子性
正如我們熟知的 ++i操作,它包含了三個獨立的“讀取-修改-寫入”操作序列,顯然是一個復(fù)合操作。為此java提供了原子變量來解決 ++i這類問題。當狀態(tài)只是一個的時候,完全可以勝任所有的情況,但當一個對象擁有兩個及以上的狀態(tài)時,仍然存在著需要思考的復(fù)合操作,盡管狀態(tài)都使用原子變量。如下:
public class UnsafeCachingFactorizer implements Servlet { private final AtomicReferencelastNumber = new AtomicReference (); private final AtomicReference lastFactors = new AtomicReference (); public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); if (i.equals(lastNumber.get())) { encodeIntoResponse(resp, lastFactors.get()); } else { BigInteger[] factors = factor(i); lastNumber.set(i); lastFactors.set(factors); encodeIntoResponse(resp, factors); } } } // lastNumber lastFactors 雖然都是原子的,但是 if-else 是復(fù)合操作,屬“先驗條件”
既然是復(fù)合操作,最直接,簡單的方式就是使用synchronized將這個方法同步起來。這種方式能到達預(yù)期效果,但效率十分低下。
既然提到synchronized加鎖同步,那么就必須知道 鎖的特點:
鎖是可以重入的。即子類的同步方法可以調(diào)用本類或父類的同步方法。
同一時刻,只有一個線程能夠訪問對象中的同步方法。
靜態(tài)方法的鎖是 類;普通方法的鎖是 對象本身。
回顧上面的代碼,一個方法體中,只要涉及了多個狀態(tài)的時候,就一定需要同步整個方法嗎?答案是否定的,同步只是為了讓多步操作為原子性,即對復(fù)合操作同步即可,因此需要明確的便是哪些操作是復(fù)合操作。如下:
public class CachedFactorizer implements Servlet { private BigInteger lastNumber; private BigInteger[] lastFactors; private long hits; private long cacheHits; public synchronized long getHits() { return hits; } public synchronized double getCacheHitRatio() { return (double) cacheHits / (double) hits; } public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = null; synchronized (this) { ++hits; if (i.equals(lastNumber)) { ++cacheHits; factors = lastFactors.clone(); } } if (factors == null) { factors = factor(i); synchronized (this) { lastNumber = 1; lastFactors = factors.clone(); } } encodeIntoResponse(reqsp, factors); } }// 兩個synchronized分別同步獨立的復(fù)合操作。對象共享
重排序:當一個線程修改對象狀態(tài)后,其他線程沒有看見修改后的狀態(tài),這種現(xiàn)象稱為“重排序”。
java內(nèi)存模型允許編譯器對操作順序進行重排序,并將數(shù)據(jù)緩存在寄存器中。當缺乏同步的情況下,每一個線程在獨立的緩存中使用緩存的數(shù)據(jù),并不知道主存中的數(shù)據(jù)已被更改。這就涉及到內(nèi)存可見性的問題。
可見性內(nèi)存可見性:同步的另一個重要的方面。我們不僅希望防止多個線程同時操作對象狀態(tài),而且還希望確保某一個線程修改了狀態(tài)后,能被其他線程看見變化。
volatile:使用 synchronized可以實現(xiàn)內(nèi)存可見,但java提供了一種稍弱的更輕量級得同步機制volatile變量。在訪問volatile變量時不會執(zhí)行加鎖操作,因此不會產(chǎn)生線程阻塞。即便如此還是不能過度使用volatile,當且僅當能簡化代碼的實現(xiàn)以及對同步策略的驗證時,才考慮使用它。
發(fā)布與逸出發(fā)布指:使對象能夠在當前作用于之外的代碼中使用。即對象引用能被其他對象持有。發(fā)布的對象內(nèi)部狀態(tài)可能會破壞封裝性,使程序難以維持不變性條件。
逸出指:當某個不應(yīng)該發(fā)布的對象被發(fā)布時,這種情況被稱為逸出。
// 正確發(fā)布:對象引用放置公有靜態(tài)域中,所有類和線程都可見 class CarFactory { public static Setcars; private CarFactory() { cars = new HashSet (); } // 私有,外部無法獲取 CarFactory的引用 public static Car void newInstance() { Car car = new Car("碰碰車"); cars.put(car); return car; } // 使用方法來獲取 car }
// 逸出 class Person { private String[] foods = new String[] {"土豆"}; public Person(Event event) { person.registListener { new EventListener() { public void onEvent(Event e) { doSomething(e); } } } }// 隱式逸出了this,外界得到了Person的引用 并且 EventListener也獲取了Person的引用。 public String[] getFoods() { return foods; }// 對發(fā)布的私有 foods,外界還是可以修改foods內(nèi)部值 }線程封閉
將可變的數(shù)據(jù)僅放置在單線程中操作的技術(shù),稱之為發(fā)線程封閉。
棧封閉:只能通過局部變量才能訪問對象。局部變量的固有屬性之一就是封裝在執(zhí)行線程中,它們位于執(zhí)行線程的棧中,其他線程無法訪問這個棧,即只在一個方法內(nèi)創(chuàng)建和使用對象。
public int test(Person p) { int num = 0; PersonHolder holder = new PersonHolder(); Person newPerson = deepCopy(p); Person woman = holder.getLove(newPerson); newPerson.setWomen(person); num++; return num; // 基本類型沒有引用,對象創(chuàng)建和修改都沒有逸出本方法 }
ThreadLocal類:ThreadLocal能夠使線程中的某個值與保存值的對象關(guān)聯(lián)起來。ThreadLocal提供了 get、set等訪問接口的方法,這些方法為每一個使用該變量的線程都存有一份獨立的副本,因get總是返回由當前執(zhí)行線程在調(diào)用set時設(shè)置的最新值。
private static ThreadLocalconnectionHolder = new ThreadLocal () { public Connection initialValue() { return DriverManager.getConnection(DB_URL); } }; public static Connection getConnection() { return connectionHolder.get(); }
當某個頻繁執(zhí)行的操作需要一個臨時對象,例如一個緩沖區(qū),而同時又希望避免在每次執(zhí)行時都重新分配該臨時對象,就可以使用ThreadLocal。不變性
線程安全性是不可變對象的固有屬性之一。不可變對象一定是線程安全的,它們的不變性條件是由構(gòu)造函數(shù)創(chuàng)建的,只要它們的狀態(tài)不可變。
// 在可變對象基礎(chǔ)上構(gòu)建不可變類 public final class ThreadStooges { private final Setstooges = new HashSet (); public ThreadStooges() { stooges.add("Moe"); stooges.add("Larry"); } public boolean isStooge(String name) { return stooges.contains(name); } }// 沒有提供可修改狀態(tài)的方式,盡管使用了Set可變集合,但被private final修飾著
對象不可變的條件
安全發(fā)布對象創(chuàng)建以后其狀態(tài)就不能修改。
對象的所有域都是final類型。
對象是正確創(chuàng)建的(在對象的創(chuàng)建期間,this引用沒有逸出)
任何線程都可以在不需要額外同步的情況下安全地訪問不可變對象,即使在發(fā)布這些對象時沒有使用同步。
// 安全的 Holder類 class Holder { private int n; public Holder(int n) { this.n = n; } } public class SessionHolder { // 錯誤的發(fā)布,導(dǎo)致 Holder不安全 public Holder holder; public void init() { holder = new Holder(10); } }// 當初始化 holder的時候,holder.n會被先默認初始化為 0,然后構(gòu)造函數(shù)才初始化為 10;在并發(fā)情況下,可能會有線程在默認初始化 與 構(gòu)造初始化中,獲取到 n 值為 0, 而不是 10;
要安全的發(fā)布一個對象,對象的引用以及對象的狀態(tài)必須同時對其他線程可見。一個正確構(gòu)造的對象可以通過以下方式安全發(fā)布:
在靜態(tài)初始化函數(shù)中初始化一個對象引用。
將對象的引用保存到 volatitle 類型的域或者 AtomicReferance 對象中。
將對象的引用保存到某個正確構(gòu)造對象的 final 類型域中。
將對象的引用保存到一個由鎖保護的域中。
在線程并發(fā)容器中的安全發(fā)布:
通過將一個鍵或者值放入 Hashtable、synchronizedMap 或者 ConsurrentMap中,可以安全地將它發(fā)布給任何從這些容器中訪問它的線程(無論是直接訪問還是通過迭代器訪問)。
通過將某個元素放入 Vector、 CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList 或 synchronizedSet中,可以將元素安全地發(fā)布到任何從這些容器中訪問該元素的線程。
通過將某個元素放入 BlockingQueue或者ConcurrentLinkedQueue中,可以將該元素安全地發(fā)布到任何從這些隊列中訪問該元素的線程。
通常,要發(fā)布一個靜態(tài)構(gòu)造的對象,最簡單、安全的方式就是使用靜態(tài)的初始化器。如public static Holder holder = new Holder(10)。如果對象在發(fā)布后狀態(tài)不會被修改(則稱為事實不可變對象),那么在沒有額外的同步情況下,任何線程都可以安全地使用被安全發(fā)布的不可變對象。
對象的發(fā)布需求取決于它的可變性:
不可變對象可以通過任意機制來發(fā)布。
事實不可變對象必須通過安全方式來發(fā)布。
可變對象必須通過安全方式來發(fā)布,并且必須是線程安全的或者有某個鎖保護起來。
在并發(fā)程序中使用和共享對象時可采用的策略:
線程封閉。將對象封閉在線程中,如在方法中創(chuàng)建和修改局部對象。
只讀共享。
線程安全共享。對象內(nèi)部實現(xiàn)同步,使用公有接口來訪問。
保護對象。使用特定的鎖來保護對象。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/73777.html
摘要:當我們希望能界定這二者之間的區(qū)別時,我們將第一種稱為純粹的函數(shù)式編程,后者稱為函數(shù)式編程。函數(shù)式編程我們的準則是,被稱為函數(shù)式的函數(shù)或方法都只能修改本地變量。另一種觀點支持引用透明的函數(shù)式編程,認為方法不應(yīng)該有對外部可見的對象修改。 一、實現(xiàn)和維護系統(tǒng) 1.共享的可變數(shù)據(jù) 如果一個方法既不修改它內(nèi)嵌類的狀態(tài),也不修改其他對象的狀態(tài),使用return返回所有的計算結(jié)果,那么我們稱其為純粹...
摘要:之前,使用匿名類給蘋果排序的代碼是的,這段代碼看上去并不是那么的清晰明了,使用表達式改進后或者是不得不承認,代碼看起來跟清晰了。這是由泛型接口內(nèi)部實現(xiàn)方式造成的。 # Lambda表達式在《Java8實戰(zhàn)》中第三章主要講的是Lambda表達式,在上一章節(jié)的筆記中我們利用了行為參數(shù)化來因?qū)Σ粩嘧兓男枨?,最后我們也使用到了Lambda,通過表達式為我們簡化了很多代碼從而極大地提高了我們的...
摘要:利用前面所述的方法,這個例子可以用方法引用改寫成下面的樣子構(gòu)造函數(shù)引用對于一個現(xiàn)有構(gòu)造函數(shù),你可以利用它的名稱和關(guān)鍵字來創(chuàng)建它的一個引用。 第三章 Lambda表達式 函數(shù)式接口 函數(shù)式接口就是只定義一個抽象方法的接口,哪怕有很多默認方法,只要接口只定義了一個抽象方法,它就仍然是一個函數(shù)式接口。 常用函數(shù)式接口 showImg(https://segmentfault.com/img...
摘要:上下文比如,接受它傳遞的方法的參數(shù),或者接受它的值得局部變量中表達式需要類型稱為目標類型。但局部變量必須顯示的聲明,或?qū)嶋H上就算。換句話說,表達式只能捕獲指派給它們的局部變量一次。注捕獲實例變量可以被看作捕獲最終局部變量。 由于第三章的內(nèi)容比較多,而且為了讓大家更好的了解Lambda表達式的使用,也寫了一些相關(guān)的實例,可以在Github或者碼云上拉取讀書筆記的代碼進行參考。 類型檢查、...
摘要:線程允許同一個進程中同時存在多個程序控制流。線程也被稱為輕量級進程?,F(xiàn)代操作系統(tǒng)中,都是以線程為基本的調(diào)度單位,而不是進程。 并發(fā)簡史 在早期的計算機中不包含操作系統(tǒng),從頭至尾都只執(zhí)行一個程序,并且這個程序能訪問計算機所有資源。操作系統(tǒng)的出現(xiàn)使得計算機每次能運行多個程序,并且不同的程序都在單獨的進程中運行:操作系統(tǒng)為各個獨立執(zhí)行的進程分配內(nèi)存、文件句柄、安全證書等。不同進程之間通過一些...
閱讀 1130·2021-11-24 09:39
閱讀 1431·2021-11-18 13:18
閱讀 2578·2021-11-15 11:38
閱讀 1896·2021-09-26 09:47
閱讀 1715·2021-09-22 15:09
閱讀 1692·2021-09-03 10:29
閱讀 1594·2019-08-29 17:28
閱讀 3013·2019-08-29 16:30