成人无码视频,亚洲精品久久久久av无码,午夜精品久久久久久毛片,亚洲 中文字幕 日韩 无码

資訊專欄INFORMATION COLUMN

Java并發(fā)編程之設(shè)計(jì)線程安全的類

SoapEye / 2678人閱讀

摘要:有的情況下我們希望自己設(shè)計(jì)的類可以讓客戶端程序員們不需要使用額外的同步操作就可以放心的在多線程環(huán)境下使用,我們就把這種類成為線程安全類。

設(shè)計(jì)線程安全的類

前邊我們對(duì)線程安全性的分析都停留在一兩個(gè)可變共享變量的基礎(chǔ)上,真實(shí)并發(fā)程序中可變共享變量會(huì)非常多,在出現(xiàn)安全性問題的時(shí)候很難準(zhǔn)確定位是哪塊兒出了問題,而且修復(fù)問題的難度也會(huì)隨著程序規(guī)模的擴(kuò)大而提升(因?yàn)樵诔绦虻母鱾€(gè)位置都可以隨便使用可變共享變量,每個(gè)操作都可能導(dǎo)致安全性問題的發(fā)生)。比方說我們?cè)O(shè)計(jì)了一個(gè)這樣的類:

public class Increment {
    private int i;

    public void increase() {
        i++;
    }

    public int getI() {
        return i;
    }
}

然后有很多客戶端程序員在多線程環(huán)境下都使用到了這個(gè)類,有的程序員很聰明,他在調(diào)用increase方法時(shí)使用了適當(dāng)?shù)耐讲僮鳎?/p>

public class RightUsageOfIncrement {

    public static void main(String[] args) {
        Increment increment = new Increment();

        Thread[] threads = new Thread[20];  //創(chuàng)建20個(gè)線程
        for (int i = 0; i < threads.length; i++) {
            Thread t = new Thread(new Runnable() {

                @Override
                public void run() {
                    for (int i = 0; i < 100000; i++) {
                        synchronized (RightUsageOfIncrement.class) {    // 使用Class對(duì)象加鎖
                            increment.increase();
                        }
                    }
                }
            });
            threads[i] = t;
            t.start();
        }

        for (int i = 0; i < threads.length; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        System.out.println(increment.getI());
    }
}

在調(diào)用Incrementincrease方法的時(shí)候,使用RightUsageOfIncrement.class這個(gè)對(duì)象作為鎖,有效的對(duì)i++操作進(jìn)行了同步,的確不錯(cuò),執(zhí)行之后的結(jié)果是:

2000000

可是并不是每個(gè)客戶端程序員都會(huì)這么聰明,有的客戶端程序員壓根兒不知道啥叫個(gè)同步,所以寫成了這樣:

public class WrongUsageOfIncrement {

    public static void main(String[] args) {
        Increment increment = new Increment();

        Thread[] threads = new Thread[20];  //創(chuàng)建20個(gè)線程
        for (int i = 0; i < threads.length; i++) {
            Thread t = new Thread(new Runnable() {

                @Override
                public void run() {
                    for (int i = 0; i < 100000; i++) {
                        increment.increase();   //沒有進(jìn)行有效的同步
                    }
                }
            });
            threads[i] = t;
            t.start();
        }

        for (int i = 0; i < threads.length; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        System.out.println(increment.getI());
    }
}

沒有進(jìn)行有效同步的執(zhí)行結(jié)果是(每次執(zhí)行都可能不一樣):

1815025

其實(shí)對(duì)于Increment這個(gè)類的開發(fā)者來說,本質(zhì)上是把對(duì)可變共享變量的必要同步操作轉(zhuǎn)嫁給客戶端程序員處理。有的情況下我們希望自己設(shè)計(jì)的類可以讓客戶端程序員們不需要使用額外的同步操作就可以放心的在多線程環(huán)境下使用,我們就把這種類成為線程安全類。其實(shí)就是類庫設(shè)計(jì)者把一些在多線程環(huán)境下可能導(dǎo)致安全性問題的操作封裝到類里邊兒,比如Incrementincrease方法,我們可以寫成這樣:

public synchronized void increase() {
    i++;
}

也就是說把對(duì)可變共享變量i可能造成多線程安全性問題的i++操作在Increment類內(nèi)就封裝好,其他人直接調(diào)用也不會(huì)出現(xiàn)安全性問題。使用封裝也是無奈之舉:你無法控制其他人對(duì)你的代碼調(diào)用,風(fēng)險(xiǎn)始終存在,封裝使無意中破壞設(shè)計(jì)約束條件變得更難。

封裝變量訪問

找出共享、可變的字段

設(shè)計(jì)線程安全類的第一步就是要找出所有的字段,這里的字段包括靜態(tài)變量也包括成員變量,然后再分析這些字段是否是共享并且可變的。

首先辨別一下字段是否是共享的。由于我們無法控制客戶端程序員以怎樣的方式來使用這個(gè)類,所以我們可以通過訪問權(quán)限,也就是public權(quán)限、protected權(quán)限、 默認(rèn)權(quán)限以及private權(quán)限來控制哪些代碼是可以被客戶端程序員調(diào)用的,哪些是不可以調(diào)用的。一般情況下,我們需要把所有字段都聲明為 private 的,把對(duì)它們的訪問都封裝到方法中,對(duì)這些方法再進(jìn)行必要的同步控制,也就是說我們只暴露給客戶端程序員一些可以調(diào)用的方法來間接的訪問到字段,因?yàn)槿绻苯影炎侄伪┞督o客戶端程序員的話,我們無法控制客戶端程序員如何使用該字段,比如他可以隨意的在多線程環(huán)境下對(duì)字段進(jìn)行累加操作,從而不能保證把所有同步邏輯都封裝到類中。所以如果一個(gè)字段是可以通過對(duì)外暴露的方法訪問到,那這個(gè)字段就是共享的。

然后再看一下字段是否是可變的。如果該字段的類型是基本數(shù)據(jù)類型,可以看一下類所有對(duì)外暴露的方法中是否有修改該字段值的操作,如果有,那這個(gè)字段就是可變的。如果該字段的類型是非基本數(shù)據(jù)類型的,那這個(gè)字段可變就有兩層意思了,第一是在對(duì)外暴露的方法中有直接修改引用的操作,第二是在對(duì)外暴露的方法中有直接修改該對(duì)象中字段的操作。比如一個(gè)類長(zhǎng)這樣:

public class MyObj {
    private List list;

    public void m1() {
        list = new ArrayList<>(); //直接修改字段指向的對(duì)象
    }

    public void m2() {
        list[0] = "aa"; //修改該字段指向?qū)ο蟮淖侄?    }
}

代碼中的m1m2都可以算做是修改字段list,如果類暴露的方法中有這兩種修改方式中的任意一種,就可以算作這個(gè)字段是可變的。

小貼士:是不是把字段聲明成final類型,該字段就不可變了呢?

如果該字段是基本數(shù)據(jù)類型,那聲明為final的確可以保證在程序運(yùn)行過程中不可變,但是如果該字段是非基本數(shù)據(jù)類型,那么需要讓該字段代表的對(duì)象中的所有字段都是不可變字段才能保證該final字段不可變。

所以在使用字段的過程中,應(yīng)該盡可能的讓字段不共享或者不可變,不共享或者不可變的字段才不會(huì)引起安全性問題哈哈。

這讓我想起了一句老話:只有死人才不會(huì)說話~

用鎖來保護(hù)訪問

確定了哪些字段必須是共享、可變的之后,就要分析在哪些對(duì)外暴露的方法中訪問了這些字段,我們需要在所有的訪問位置都進(jìn)行必要的同步處理,這樣才可以保證這個(gè)類是一個(gè)線程安全類。通常,我們會(huì)使用來保證多線程在訪問共享可變字段時(shí)是串行訪問的。

但是一種常見的錯(cuò)誤就是:只有在寫入共享可變字段時(shí)才需要使用同步,就像這樣:

public class Test {
    private int i;

    public int getI() {
        return i;
    }

    public synchronized void setI(int i) {
        this.i = i;
    }
}

為了使Test類變?yōu)?b>線程安全類,也就是需要保證共享可變字段i在所有外界能訪問的位置都是線程安全的,而上邊getI方法可以訪問到字段i,卻沒有進(jìn)行有效的同步處理,由于內(nèi)存可見性問題的存在,在調(diào)用getI方法時(shí)仍有可能獲取的是舊的字段值。所以再次強(qiáng)調(diào)一遍:我們需要在所有的訪問位置都進(jìn)行必要的同步處理。

使用同一個(gè)鎖

還有一點(diǎn)需要強(qiáng)調(diào)的是:如果使用鎖來保護(hù)共享可變字段的訪問的話,對(duì)于同一個(gè)字段來說,在多個(gè)訪問位置需要使用同一個(gè)鎖。

我們知道如果多個(gè)線程競(jìng)爭(zhēng)同一個(gè)鎖的話,在一個(gè)線程獲取到鎖后其他線程將被阻塞,如果是使用多個(gè)鎖來保護(hù)同一個(gè)共享可變字段的話,多個(gè)線程并不會(huì)在一個(gè)線程訪問的時(shí)候阻塞等待,而是會(huì)同時(shí)訪問這個(gè)字段,我們的保護(hù)措施就變得無效了。

一般情況下,在一個(gè)線程安全類中,我們使用同步方法,也就是使用this對(duì)象作為鎖來保護(hù)字段的訪問就OK了~。

封不封裝取決于你的心情

雖然面向?qū)ο蠹夹g(shù)封裝了安全性,但是打破這種封裝也沒啥不可以,只不過安全性會(huì)更脆弱,增加開發(fā)成本和風(fēng)險(xiǎn)。也就是說你把字段聲明為public訪問權(quán)限也沒人攔得住你,當(dāng)然你也可能因?yàn)槟撤N性能問題而打破封裝,不過對(duì)于我們實(shí)現(xiàn)業(yè)務(wù)的人來說,還是建議先使代碼正確運(yùn)行,再考慮提高代碼執(zhí)行速度吧~。

不變性條件

現(xiàn)實(shí)中有些字段之間是有實(shí)際聯(lián)系的,比如說下邊這個(gè)類:

public class SquareGetter {
    private int numberCache;    //數(shù)字緩存
    private int squareCache;    //平方值緩存

    public int getSquare(int i) {
        if (i == numberCache) {
            return squareCache;
        }
        int result = i*i;
        numberCache = i;
        squareCache = result;
        return result;
    }

    public int[] getCache() {
        return new int[] {numberCache, squareCache};
    }
}

這個(gè)類提供了一個(gè)很簡(jiǎn)單的getSquare功能,可以獲取指定參數(shù)的平方值。但是它的實(shí)現(xiàn)過程使用了緩存,就是說如果指定參數(shù)和緩存的numberCache的值一樣的話,直接返回緩存的squareCache,如果不是的話,計(jì)算參數(shù)的平方,然后把該參數(shù)和計(jì)算結(jié)果分別緩存到numberCachesquareCache中。

從上邊的描述中我們可以知道,squareCache不論在任何情況下都是numberCache平方值,這就是SquareGetter類的一個(gè)不變性條件,如果違背了這個(gè)不變性條件的話,就可能會(huì)獲得錯(cuò)誤的結(jié)果。

在單線程環(huán)境中,getSquare方法并不會(huì)有什么問題,但是在多線程環(huán)境中,numberCachesquareCache都屬于共享的可變字段,而getSquare方法并沒有提供任何同步措施,所以可能造成錯(cuò)誤的結(jié)果。假設(shè)現(xiàn)在numberCache的值是2,squareCache的值是3,一個(gè)線程調(diào)用getSquare(3),另一個(gè)線程調(diào)用getSquare(4),這兩個(gè)線程的一個(gè)可能的執(zhí)行時(shí)序是:

兩個(gè)線程執(zhí)行過后,最后numberCache的值是4,而squareCache的值竟然是9,也就意味著多線程會(huì)破壞不變性條件。為了保持不變性條件我們需要把保持不變性條件的多個(gè)操作定義為一個(gè)原子操作,即用鎖給保護(hù)起來。

我們可以這樣修改getSquare方法的代碼:

public synchronized int getSquare(int i) {
    if (i == numberCache) {
        return squareCache;
    }
    int result = i*i;
    numberCache = i;
    squareCache = result;
    return result;
}

但是不要忘了將代碼都放在同步代碼塊是會(huì)造成阻塞的,能不進(jìn)行同步,就不進(jìn)行同步,所以我們修改一下上邊的代碼:

public int getSquare(int i) {

    synchronized(this) {
        if (i == numberCache) {  // numberCache字段的讀取需要進(jìn)行同步
            return squareCache;
        }
    }

    int result = i*i;   //計(jì)算過程不需要同步

    synchronized(this) {   // numberCache和squareCache字段的寫入需要進(jìn)行同步
        numberCache = i;
        squareCache = result;
    }
    return result;
}

雖然getSquare方法同步操作已經(jīng)做好了,但是別忘了SquareGetter類getCache方法也訪問了numberCachesquareCache字段,所以對(duì)于每個(gè)包含多個(gè)字段的不變性條件,其中涉及的所有字段都需要被同一個(gè)鎖來保護(hù),所以我們?cè)傩薷囊幌?b>getCache方法:

public synchronized int[] getCache() {
    return new int[] {numberCache, squareCache};
}

這樣修改后的SquareGetter類才屬于一個(gè)線程安全類。

使用volatile修飾狀態(tài)

使用鎖來保護(hù)共享可變字段雖然好,但是開銷大。使用volatile修飾字段來替換掉鎖是一種可能的考慮,但是一定要記住volatile是不能保證一系列操作的原子性的,所以只有我們的業(yè)務(wù)場(chǎng)景符合下邊這兩個(gè)情況的話,才可以考慮:

對(duì)變量的寫入操作不依賴當(dāng)前值,或者保證只有單個(gè)線程進(jìn)行更新。

該變量不需要和其他共享變量組成不變性條件。

比方說下邊的這個(gè)類:

public class VolatileDemo {

    private volatile int i;

    public int getI() {
        return i;
    }

    public void setI(int i) {
        this.i = i;
    }
}

VolatileDemo中的字段i并不和其他字段組成不變性條件,而且對(duì)于可以訪問這個(gè)字段的方法getIsetI來說,并不需要以來i的當(dāng)前值,所以可以使用volatile來修飾字段i,而不用在getIsetI的方法上使用鎖。

避免this引用逸出

我們先來看一段代碼:

public class ExplicitThisEscape {

    private final int i;

    public static ThisEscape INSTANCE;

    public ThisEscape() {
        INSTANCE = this;
        i = 1;
    }
}

在構(gòu)造方法中就把this引用給賦值到了靜態(tài)變量INSTANCE中,而別的線程是可以隨時(shí)訪問INSTANCE的,我們把這種在對(duì)象創(chuàng)建完成之前就把this引用賦值給別的線程可以訪問的變量的這種情況稱為 this引用逸出,這種方式是極其危險(xiǎn)的!,這意味著在ThisEscape對(duì)象創(chuàng)建完成之前,別的線程就可以通過訪問INSTANCE來獲取到i字段的信息,也就是說別的線程可能獲取到字段i的值為0,與我們期望的final類型字段值不會(huì)改變的結(jié)果是相違背的。所以千萬不要在對(duì)象構(gòu)造過程中使this引用逸出。

上邊的this引用逸出是通過顯式將this引用賦值的方式導(dǎo)致逸出的,也可能通過內(nèi)部類的方式神不知鬼不覺的造成this引用逸出:

public class ImplicitThisEscape {

    private final int i;

    private Thread t;

    public ThisEscape() {
        t = new Thread(new Runnable() {
            @Override
            public void run() {
                // ... 具體的任務(wù)
            }
        });
        i = 1;
    }
}

雖然在ImplicitThisEscape的構(gòu)造方法中并沒有顯式的將this引用賦值,但是由于Runnable內(nèi)部類的存在,作為外部類的ImplicitThisEscape,內(nèi)部類對(duì)象可以輕松的獲取到外部類的引用,這種情況下也算this引用逸出。

this引用逸出意味著創(chuàng)建對(duì)象的過程是不安全的,在對(duì)象尚未創(chuàng)建好的時(shí)候別的線程就可以來訪問這個(gè)對(duì)象。雖然我們不確定客戶端程序員會(huì)怎么使用這個(gè)逸出的this引用,但是風(fēng)險(xiǎn)始終存在,所以強(qiáng)烈建議千萬不要在對(duì)象構(gòu)造過程中使this引用逸出。

總結(jié)

客戶端程序員不靠譜,我們有必要把線程安全性封裝到類中,只給客戶端程序員提供線程安全的方法。

認(rèn)真找出代碼中既共享又可變的變量,并把它們使用鎖來保護(hù)起來,同一個(gè)字段的多個(gè)訪問位置需要使用同一個(gè)鎖來保護(hù)。

對(duì)于每個(gè)包含多個(gè)字段的不變性條件,其中涉及的所有字段都需要被同一個(gè)鎖來保護(hù)。

在對(duì)變量的寫入操作不依賴當(dāng)前值以及該變量不需要和其他共享變量組成不變性條件的情況下可以考慮使用volatile變量來保證并發(fā)安全。

千萬不要在對(duì)象構(gòu)造過程中使this引用逸出。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/74145.html

相關(guān)文章

  • 后端好書閱讀與推薦

    摘要:后端好書閱讀與推薦這一兩年來養(yǎng)成了買書看書的習(xí)慣,陸陸續(xù)續(xù)也買了幾十本書了,但是一直沒有養(yǎng)成一個(gè)天天看書的習(xí)慣。高級(jí)程序設(shè)計(jì)高級(jí)程序設(shè)計(jì)第版豆瓣有人可能會(huì)有疑問,后端為啥要學(xué)呢其實(shí)就是為了更好的使用做鋪墊。 后端好書閱讀與推薦 這一兩年來養(yǎng)成了買書看書的習(xí)慣,陸陸續(xù)續(xù)也買了幾十本書了,但是一直沒有養(yǎng)成一個(gè)天天看書的習(xí)慣。今天突然想要做個(gè)決定:每天至少花1-3小時(shí)用來看書。這里我準(zhǔn)備把這...

    clasnake 評(píng)論0 收藏0
  • 后端好書閱讀與推薦

    摘要:后端好書閱讀與推薦這一兩年來養(yǎng)成了買書看書的習(xí)慣,陸陸續(xù)續(xù)也買了幾十本書了,但是一直沒有養(yǎng)成一個(gè)天天看書的習(xí)慣。高級(jí)程序設(shè)計(jì)高級(jí)程序設(shè)計(jì)第版豆瓣有人可能會(huì)有疑問,后端為啥要學(xué)呢其實(shí)就是為了更好的使用做鋪墊。 后端好書閱讀與推薦 這一兩年來養(yǎng)成了買書看書的習(xí)慣,陸陸續(xù)續(xù)也買了幾十本書了,但是一直沒有養(yǎng)成一個(gè)天天看書的習(xí)慣。今天突然想要做個(gè)決定:每天至少花1-3小時(shí)用來看書。這里我準(zhǔn)備把這...

    Juven 評(píng)論0 收藏0
  • 淺談Java并發(fā)編程系列(一)—— 如何保證線程安全

    摘要:比如需要用多線程或分布式集群統(tǒng)計(jì)一堆用戶的相關(guān)統(tǒng)計(jì)值,由于用戶的統(tǒng)計(jì)值是共享數(shù)據(jù),因此需要保證線程安全。如果類是無狀態(tài)的,那它永遠(yuǎn)是線程安全的。參考探索并發(fā)編程二寫線程安全的代碼 線程安全類 保證類線程安全的措施: 不共享線程間的變量; 設(shè)置屬性變量為不可變變量; 每個(gè)共享的可變變量都使用一個(gè)確定的鎖保護(hù); 保證線程安全的思路: 1. 通過架構(gòu)設(shè)計(jì) 通過上層的架構(gòu)設(shè)計(jì)和業(yè)務(wù)分析來避...

    mylxsw 評(píng)論0 收藏0
  • 第10章:并發(fā)和分布式編程 10.1并發(fā)性和線程安全

    摘要:并發(fā)模塊本身有兩種不同的類型進(jìn)程和線程,兩個(gè)基本的執(zhí)行單元。調(diào)用以啟動(dòng)新線程。在大多數(shù)系統(tǒng)中,時(shí)間片發(fā)生不可預(yù)知的和非確定性的,這意味著線程可能隨時(shí)暫?;蚧謴?fù)。 大綱 什么是并發(fā)編程?進(jìn)程,線程和時(shí)間片交織和競(jìng)爭(zhēng)條件線程安全 策略1:監(jiān)禁 策略2:不可變性 策略3:使用線程安全數(shù)據(jù)類型 策略4:鎖定和同步 如何做安全論證總結(jié) 什么是并發(fā)編程? 并發(fā)并發(fā)性:多個(gè)計(jì)算同時(shí)發(fā)生。 在現(xiàn)代...

    instein 評(píng)論0 收藏0
  • <java并發(fā)編程實(shí)戰(zhàn)>學(xué)習(xí)四

    摘要:對(duì)象的組合介紹一些組合模式,這些模式能夠使一個(gè)類更容易成為線程安全的,并且維護(hù)這些類時(shí)不會(huì)無意破壞類的安全性保證。狀態(tài)變量的所有者將決定采用何種加鎖協(xié)議來維持變量狀態(tài)的完整性。所有權(quán)意味著控制權(quán)。 對(duì)象的組合 介紹一些組合模式,這些模式能夠使一個(gè)類更容易成為線程安全的,并且維護(hù)這些類時(shí)不會(huì)無意破壞類的安全性保證。 設(shè)計(jì)線程安全的類 在設(shè)計(jì)線程安全類的過程中,需要包含以下三個(gè)基本要素: ...

    tainzhi 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

閱讀需要支付1元查看
<