Java多线程——不变性与安全发布
1、不變性
某個(gè)對(duì)象在被創(chuàng)建后其狀態(tài)就不能被修改,那么這個(gè)對(duì)象就稱為不可變對(duì)象,不可變對(duì)象一定是線程安全的。不可變對(duì)象很簡單。他們只有一種狀態(tài),并且該狀態(tài)由構(gòu)造函數(shù)來控制。
當(dāng)滿足以下條件時(shí),對(duì)象才是不可變的:(1)、對(duì)象創(chuàng)建以后其狀態(tài)就不能改變;(2)、對(duì)象的所有域都是final類型;(3)、對(duì)象是正確創(chuàng)造的(在對(duì)象創(chuàng)建期間,this引用沒有溢出)。
1.1 final域
關(guān)鍵字final用于構(gòu)造不可變對(duì)象,final類型的域是不能修改的(但是final域所引用的對(duì)象是可變的,那么這些引用的對(duì)象是可以修改的),即使對(duì)象是可變的,通過將可變對(duì)象的某些域聲明為final類型,相當(dāng)于告訴維護(hù)人員這些域是不可變化的。
?
2、正確發(fā)布一個(gè)對(duì)象
正確發(fā)布一個(gè)對(duì)象遇到的兩個(gè)問題:(1)引用本身要被其他線程看到;(2)對(duì)象的狀態(tài)要被其他線程看到。
在多線程編程中,首要的原則,就是要避免對(duì)象的共享,因?yàn)槿绻麤]有對(duì)象的共享,那么多線程編寫要輕松得多,但是,如果要共享對(duì)象,那么除了能夠正確的將構(gòu)造函數(shù)書寫正確外,如何正確的發(fā)布也是一個(gè)很重要的問題。
我們看下面的代碼:
1 public class Client { 2 public Holder holder; 3 4 public void initialize(){ 5 holder = new Holder(42); 6 } 7 } 8 9 10 public class Holder { 11 int n; 12 public Holder(int n) { 13 this.n = n; 14 } 15 public void assertSanity() { 16 if(n != n) 17 throw new AssertionError("This statement is false."); 18 } 19 } View Code在Client類中,Holder對(duì)象被發(fā)布了,但是這是一個(gè)不正確的發(fā)布。由于可見性問題,其他線程看到的Holder對(duì)象將處于不一致的狀態(tài),即使在該對(duì)象的構(gòu)成構(gòu)函數(shù)中已經(jīng)正確的該構(gòu)建了不變性條件,這種不正確的發(fā)布導(dǎo)致其他線程看到尚未創(chuàng)建完成的對(duì)象。主要是Holder對(duì)象的創(chuàng)建不是原子性的,可能還未構(gòu)造完成,其他線程就開始調(diào)用Holder對(duì)象。
由于沒有使用同步的方法來卻確保Holder對(duì)象(包含引用和對(duì)象狀態(tài)都沒有)對(duì)其他線程可見,因此將Holder成為未正確發(fā)布。問題不在于Holder本身,而是其沒有正確的發(fā)布。上面沒有正確發(fā)布的可能導(dǎo)致的問題:
- 別的線程對(duì)于holder字段,可能會(huì)看到過時(shí)的值,這樣就會(huì)導(dǎo)致空引用,或者是過時(shí)的值(即使holder已經(jīng)被設(shè)置了)(引用本身沒有被別的線程看到)
- 更可怕的是,對(duì)于已經(jīng)更新holder,及時(shí)能夠看到引用的更新,但是對(duì)于對(duì)象的狀態(tài),看到的卻可能是舊值,對(duì)于上面的代碼,可能會(huì)拋出AssertionError異常
主要是holder = new Holder(42);這個(gè)代碼不是原子性的,可能在構(gòu)造未完成時(shí),其他線程就會(huì)調(diào)用holder對(duì)象引用,從而導(dǎo)致不可預(yù)測的結(jié)果。
2.1安全發(fā)布常用模式
要安全的發(fā)布一個(gè)對(duì)象,對(duì)象的引用和對(duì)象的狀態(tài)必須同時(shí)對(duì)其他線程可見。一般一個(gè)正確構(gòu)造的對(duì)象(構(gòu)造函數(shù)不發(fā)生this逃逸),可以通過如下方式來正確發(fā)布:
(1)、在靜態(tài)初始化函數(shù)中初始化一個(gè)對(duì)象引用
(2)、將一個(gè)對(duì)象引用保存在volatile類型的域或者是AtomicReference對(duì)象中
(3)、將對(duì)象的引用保存到某個(gè)正確構(gòu)造對(duì)象的final類型的域中。
(4)、將對(duì)象的引用保存到一個(gè)由鎖保護(hù)的域。
在線程安全容器內(nèi)部同步意味著,在將對(duì)象放到某個(gè)容器中,比如Vector中,將滿足上面的最后一條需求。如果線程A將對(duì)象X放到一個(gè)線程安全的容器中,隨后線程B讀取這個(gè)對(duì)象,那么可以確保可以確保B看到A設(shè)置的X狀態(tài),即便是這段讀/寫X的應(yīng)用程序代碼沒有包含顯示的同步。下面容器內(nèi)提供了安全發(fā)布的保證:
(1)、通過將一個(gè)鍵或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全將它發(fā)布給任何從這些容器中訪問它的線程。
(2)、通過將某個(gè)元素放到Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchroizedList,可以將該元素安全的發(fā)布到任何從這些容器中訪問該元素的線程。
(3)、通過將元素放到BlockingQueue或者是ConcrrentLinkedQueue中,可以將該元素安全的發(fā)布到任何從這些訪問隊(duì)列中訪問該元素的線程。
通常,要發(fā)布一個(gè)靜態(tài)構(gòu)造的對(duì)象,最簡單和最安全的方式是使用靜態(tài)初始化器:?public static Holder = new Holder(42);
靜態(tài)初始化器由JVM在類的初始化階段執(zhí)行,由于JVM內(nèi)部存在同步機(jī)制,所以這種方式初始化對(duì)象都可以被安全的發(fā)布。對(duì)于可變對(duì)象,安全的發(fā)布之時(shí)確保在發(fā)布當(dāng)時(shí)狀態(tài)的可見性,而在隨后的每次對(duì)象的訪問時(shí),同樣需要使用同步來確保修改操作的可見性。
轉(zhuǎn)載于:https://www.cnblogs.com/Hxinguan/p/7471461.html
總結(jié)
以上是生活随笔為你收集整理的Java多线程——不变性与安全发布的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [Linux] 权限与指令间的关系
- 下一篇: 2017-2018-1 Java演绎法