溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Java 多線程安全(一) 不共享與不可變

發(fā)布時間:2020-06-05 10:02:18 來源:網(wǎng)絡(luò) 閱讀:606 作者:wx5c78c8b1dbb1b 欄目:編程語言
  1. 線程不安全

  • 當(dāng)一個類的狀態(tài)(指的存儲在狀態(tài)變量里面的數(shù)據(jù))是共享的和可變時,那么這個類就是線程不安全的."共享"意味著變量可以由多個線程同時訪問,而"可變"意味著變量的值在生命周期發(fā)生變化.

線程安全

  • 在線程安全的定義中,最核心的概念就是正確性,正確性的含義是指:某個類的行為與其規(guī)范完全一致.線程安全定義如下:當(dāng)多個線程訪問某個類時,不管運(yùn)行時采用何種調(diào)度方式或者這些線程將如何交替運(yùn)行,并且在主調(diào)代碼中不需要任何額外的同步或者協(xié)同,這個類都能表現(xiàn)出正確的行為,那么那么我們就稱這個類是線程安全的.

解決線程安全問題(一)

  • 不共享與不可變

    • 線程封閉(不共享數(shù)據(jù)):當(dāng)訪問的共享的可變數(shù)據(jù)時,通常需要使用同步.一種避免使用同步的方式就是不共享.如果僅在單線程里面訪問內(nèi)部數(shù)據(jù),就不需要同步.這種技術(shù)被稱為線程封閉技術(shù),它是實現(xiàn)線程安全性的最簡單方式之一.

  1. Ad-hoc 線程封閉

  • Ad-hoc 線程封閉是指,維護(hù)線程的封閉性的職責(zé)完全由程序來承擔(dān).例如volatile變量上存在一種特殊的線程封閉,只要你能確保只有單個線程對共享的volatile的變量執(zhí)行寫操作,那么就可以安全地在這些共享的volatile變量上執(zhí)行"讀取-修改-寫入"的操作.在這種情況下,相當(dāng)于變量封閉在單個線程中防止發(fā)生競態(tài)條件,并且volatile的變量的可見性保證還確保了其它線程能看見最新的值.由于Ad-hoc 線程封閉技術(shù)的脆弱性,沒有任何一種語言的特性是能將對象封閉到目標(biāo)線程上,因此盡量少用,在可能的情況下,使用更強(qiáng)的封閉技術(shù)(棧封閉和ThreadLocal).

棧封閉(常用)

  • 棧封閉是線程封閉的一種特例,在棧封閉中,只能通過局部變量才能訪問對象.正如封裝能使得代碼更容易維護(hù)不變性條件那樣,同步變量也能使對象更易于封閉在線程中.局部變量的固有屬性之一就是封閉在執(zhí)行程序中.他們位于執(zhí)行線程的棧中,其它線程無法訪問這個棧.棧封閉(也被稱為線程內(nèi)部使用或者線程局部使用)比 Ad-hoc 線程更易于維護(hù),也更加健壯.

  • 如下代碼實例:

/**
?*?獲取user總數(shù)
?*?@param?userList
?*?@return
?*/
public?int?getTotalUser?(List<User>?userList)?{
????List<User>?userLists?=?null;
????int??totalUser?=?0;
????userLists?=?userList;
????for?(User?user?:?userList)?{
????????totalUser?++;
????}
????return?totalUser;
}

該方法userLists是一個局部變量,存在于每個線程的棧中,是每一個線程私有的,別的線程獲取不到,只要不把這個對象的發(fā)布出去,也就是返回,這樣這個userLists 閉在了這個線程棧中,就是線程安全的.而對于totalUser 這個基本類型來說,發(fā)布出去也沒有關(guān)系,因為由于任何線程都無法獲取對基本類型的引用,因此Java語言

的這種機(jī)制就確保了基本類型的局部變量始終封閉在線程內(nèi),也是線程安全的.

ThreadLocal類

  • 維持線程封閉的一種更規(guī)范方法是使用ThreadLocal,這個類能使線程中的某個值與保存值的對象關(guān)聯(lián)起來.ThreadLocal類為每一個線程都維護(hù)了自己獨(dú)有的變量拷貝,競爭條件被徹底消除了,那就沒有任何必要對這些線程同步,他們也能最大限度的cpu調(diào)度,并發(fā)執(zhí)行,并且有每個線程在訪問變量時,讀取和修改,都是自己獨(dú)有的那一份變量拷貝,變量就徹底封閉在了每個線程中,也就是線程安全的了,此方案是空間(內(nèi)存)來換取線程安全的策略.

  • 代碼示例:多線程獲取數(shù)據(jù)庫連接.

public?class?ConnectionUtils?{

????private?static?ThreadLocal<Connection>?connectionThreadLocal
????????????=?new?ThreadLocal<Connection>(){
????????protected??Connection?initialValue?()?{
????????????Connection?connection?=?null;
????????????try?{
????????????????Class.forName("org.postgresql.Driver").newInstance();
????????????????connection?=?DriverManager.getConnection
????????????????("jdbc:postgresql://localhost:5432/postgres",
????????????????????????"postgres",?"test");
????????????}?catch?(Exception?e)?{
????????????????e.printStackTrace();
????????????}
????????????return?connection;
????????}

????};

????public?static?Connection?getConnection?()?{
????????return?connectionThreadLocal.get();
????}

????public?static?void?main?(String[]?args)?throws?Exception?{
????????for?(int?i?=?0;?i?<?2;?i++)?{
????????????Thread?thread?=?new?Thread(()?->?{
????????????????Connection?connection?=?ConnectionUtils.getConnection();
????????????????System.out.println(Thread.currentThread().getName()?+?
????????????????"--------"?+?connection.toString());
????????????},?"thread"?+?i);
????????????thread.start();
????????}
????}
}

thread0--------org.postgresql.jdbc4.Jdbc4Connection@4fce58ae

thread1--------org.postgresql.jdbc4.Jdbc4Connection@257f7c5b

通過代碼可以看見兩個線程獲取了各自的連接對象,都是綁定在當(dāng)前線程上的,第一次獲取是調(diào)用initialValue這個方法的返回值來設(shè)定值的,如果調(diào)用set方法也會和當(dāng)前

線程綁定.ThreadLocal源碼實現(xiàn)分析參考:敬請期待Smile

不可變的對象

  • 滿足同步需求的另一種方法是使用不可變對象 (Immutable Object).如果某個對象在創(chuàng)建后其狀態(tài)就不能被修改,那么這個對象就是不可變對象.線程安全性是不可變對象的固有屬性之一.不可變對象一定是線程安全的.

  • 當(dāng)滿足一下條件時,對象才是不可變的:

  1. 對象創(chuàng)建以后其狀態(tài)就不能修改.

  2. 對象的所有域都是final類型.

  3. 對象是正確創(chuàng)建的(在對象的創(chuàng)建期間,this引用沒有逸出).

Final 域

  1. final 類型的域是不能修改的(但如果final引用的對象是可變的,那么這些被引用的對象是可以修改的).在Java內(nèi)存模型中,final域能夠確保初始化過程的安全性.即使對象是可變的,通過將對象的某些域聲明為final類型,仍然可以簡化對狀態(tài)的判斷.通過將域聲明為final類型,也相當(dāng)于告訴維護(hù)人員這些域是不會變化的.

  2. 某些時候不可變對象提供了一種弱類型的原子性,如下代碼示例:

public?class?OneValueCache?{

????private?final?BigInteger?lastNumber;

????private?final?BigInteger[]?lastFactors;

????public?OneValueCache?(BigInteger?i?,?BigInteger[]?fastFactors)?{
????????lastNumber?=?i;
????????lastFactors?=?Arrays.copyOf(fastFactors,fastFactors.length);
????}

????public?BigInteger[]?getFactors?(BigInteger?i)?{
????????if?(lastNumber?==?null?||?!lastNumber.equals(i))?{
????????????return?null;
????????}?else?{
????????????return?Arrays.copyOf(lastFactors,lastFactors.length);
????????}
????}

?
}

代碼分析:OneValueCache 有兩個final 域的變量,并在構(gòu)造函數(shù)時初始化它們(沒有提供其它初始化數(shù)據(jù)方案,因為要保證初始化后狀態(tài)的不可變),在getFactors 方法里面沒有返回原數(shù)組引用,如果這樣那就不安全了因為lastFactors數(shù)組的域是不可變的,但是引用對應(yīng)的內(nèi)容是可以修改的,所以要是有copyOf方法,返回一個新數(shù)組(也可以使用clone方法).如果我們要修改lastNumber和lastFactors只有調(diào)用構(gòu)造方法重新構(gòu)造一個不可變對象,而構(gòu)造對象需要這兩個變量一起傳入,要么成功要么失敗,所以說不可變對象是一種弱類型的原子性.


對于訪問和更新多個相關(guān)變量時出現(xiàn)的競爭問題,可以通過將這些變量全部保存在一個不可變對象中來消除.如果是一個可變對象,那么就必須使用鎖來確保原子性.如果是一個不可變對象,那么當(dāng)前獲得了帶對象的引用后,就不必?fù)?dān)心另一個線程會修改對象的狀態(tài).如果要更新這些變量,那么只有重新建一個新的容器對象,但其他使用原有對象的線程仍然看到對象處于一致狀態(tài)(其它線程看見的還是原來的對象,如果要保證可見性,可以使用volatile關(guān)鍵字.)


向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI