線程安全性

什么是線程的安全性?

當(dāng)多個(gè)線程訪問(wèn)某個(gè)類時(shí),不管運(yùn)行時(shí)環(huán)境次啊用何種調(diào)度方式或者這些線程將如何交替執(zhí)行,這個(gè)類始終都能表現(xiàn)正確的行為,那么稱這個(gè)類是線程安全的。

什么是無(wú)狀態(tài)的類?

若一個(gè)類中既不包含任何域,也不包含任何對(duì)其他類中域的引用,那么我們稱這個(gè)類為無(wú)狀態(tài)的類。

如下,我們模擬一個(gè)簡(jiǎn)單的因數(shù)分解Servlet:首先從請(qǐng)求中提取出數(shù)值,執(zhí)行因數(shù)分解,然后將結(jié)果封裝到Servlet的響應(yīng)中。

@ThreadSafe
public class StatelessFactorizer implements Servlet{
    public void service(ServletRequest req, ServletResponse resp){
        //從請(qǐng)求中獲取到數(shù)值
        BigInteger i = extractFromRequest(req);
        //執(zhí)行因數(shù)分解
        BigInteger[] factors = factor(i);
        //將結(jié)果封裝到響應(yīng)中
        encodeIntoResponse(resp, factors);
    }
}

上面的StatelessFactorizer類為無(wú)狀態(tài)的類,因?yàn)樗话魏斡蚝推渌愔杏虻囊谩?/p>

無(wú)狀態(tài)對(duì)象一定是線程安全的

假設(shè)有兩個(gè)線程A和B對(duì)StatelessFactorizer類進(jìn)行訪問(wèn),在訪問(wèn)的過(guò)程中,A不會(huì)影響到B的結(jié)果,B也不會(huì)影響到A的結(jié)果。因?yàn)檫@兩個(gè)線程并沒(méi)有共享狀態(tài),就如同它們?cè)谠L問(wèn)不同的實(shí)例。由于線程訪問(wèn)無(wú)狀態(tài)對(duì)象的行為不會(huì)影響其他線程中操作的正確性,因此無(wú)狀態(tài)對(duì)象是線程安全的。

在原有的基礎(chǔ)上,我們添加一個(gè)變量count來(lái)記錄Servlet請(qǐng)求次數(shù)。如下:

@ThreadSafe
public class StatelessFactorizer implements Servlet{
    //用來(lái)表示Servlet請(qǐng)求的次數(shù)
    private long count = 0;

    public long getCount(){
        return count;
    }

    public void service(ServletRequest req, ServletResponse resp){
        //從請(qǐng)求中獲取到數(shù)值
        BigInteger i = extractFromRequest(req);
        //執(zhí)行因數(shù)分解
        BigInteger[] factors = factor(i);
        //請(qǐng)求次數(shù)累加
        count ++;
        //將結(jié)果封裝到響應(yīng)中
        encodeIntoResponse(resp, factors);
    }
}

這個(gè)時(shí)候StatelessFactorizer類將不再是“無(wú)狀態(tài)”的,因?yàn)樗俗兞縞ount。也就意味著,該類可能是線程不安全的。

我們先來(lái)看到service中對(duì)count的操作count++,該操作看起來(lái)是一個(gè)操作,但這個(gè)操作并非原子的,因而它不會(huì)作為一個(gè)不可分割的操作來(lái)執(zhí)行。實(shí)際上,它包含了三個(gè)獨(dú)立的操作:

a. 讀取count的值
b. count值加一
c. 將計(jì)算的結(jié)果寫入count中

假設(shè)有三個(gè)線程A,B,C同時(shí)訪問(wèn)該類,A調(diào)用service方法,執(zhí)行了count++操作,假設(shè)count初始為8,這時(shí)B和C都在調(diào)用getCount()方法來(lái)獲取到count值,那么存在以下情況:

1. B,C 在A執(zhí)行c操作之前調(diào)用了getCount()方法,那么B和C獲取到的count值都為8;
2. B,C 在A執(zhí)行c操作之后調(diào)用了getCount()方法,那么B和C獲取到的count值都為9;
3. B在A執(zhí)行c操作之前調(diào)用了getCount()方法,而C在之后調(diào)用,那么B獲取的count值為8,而C獲取到的是9。
4. B在A執(zhí)行c操作之后調(diào)用了getCount()方法,而C在之前調(diào)用,那么B獲取的count值為9,而C獲取到的是8。

上面的情況說(shuō)明,線程中不同的交替運(yùn)行情況將導(dǎo)致count值不同,意味著該類并不是線程安全的。

競(jìng)態(tài)條件

當(dāng)某個(gè)計(jì)算的正確性取決于多個(gè)線程的交替執(zhí)行時(shí)序時(shí),那么就會(huì)發(fā)生競(jìng)態(tài)條件。

實(shí)際上,我們上面所說(shuō)的4種情況便屬于競(jìng)態(tài)條件,只有前兩種情況是我們所希望的,而后兩種情況并不是我們所想看見的。

對(duì)于競(jìng)態(tài)條件來(lái)說(shuō),我們可以采取的辦法是”先檢查后執(zhí)行“首先觀察某個(gè)條件是否為真,然后根據(jù)這個(gè)觀察結(jié)果來(lái)采用相應(yīng)的動(dòng)作。常見的一種情況是延遲初始化

延遲初始化的目的是將對(duì)象的初始化操作推遲到實(shí)際被使用時(shí)才進(jìn)行,并確保只被初始化一次。

@NotThreadSafe
public class LazyInitRace{
    private ExpensiveObject instance = null;

    public ExpensiveObject getInstance(){
        if (instance == null){
            instance = new ExpensiveObject();
        }
        return instance;
    }
}

但實(shí)際上,這樣的方案還是存在競(jìng)態(tài)條件:假設(shè)線程A和B同時(shí)執(zhí)行g(shù)etInstance,A若看到instance為空,那么會(huì)創(chuàng)建一個(gè)新的實(shí)例對(duì)象,同樣B也會(huì)進(jìn)行這樣的操作。而此時(shí)instance是否為空,取決于不可預(yù)測(cè)的時(shí)序,以及線程的調(diào)度方式和A需要實(shí)例化對(duì)象的時(shí)間。若B在檢查instance是否為空時(shí),A沒(méi)有完成初始化操作,那么將導(dǎo)致A和B創(chuàng)建兩個(gè)不同的實(shí)例對(duì)象。

正確避免競(jìng)態(tài)條件:

要避免競(jìng)態(tài)條件,就必須在某個(gè)線程修改該變量時(shí),通過(guò)某種方式防止其他線程使用這個(gè)變量,從而確保其他線程只能在修改操作完成之前或之后讀取和修改狀態(tài),而不是在修改的過(guò)程中。

我們對(duì)之前的Servlet進(jìn)行修改:

@ThreadSafe
public class StatelessFactorizer implements Servlet{
    //用來(lái)表示Servlet請(qǐng)求的次數(shù)
    private final AtomicLong count = new AtomicLong{0};

    public long getCount(){
        return count.get();
    }

    public void service(ServletRequest req, ServletResponse resp){
        //從請(qǐng)求中獲取到數(shù)值
        BigInteger i = extractFromRequest(req);
        //執(zhí)行因數(shù)分解
        BigInteger[] factors = factor(i);
        //請(qǐng)求次數(shù)累加
        count.incrementAndGet();
        //將結(jié)果封裝到響應(yīng)中
        encodeIntoResponse(resp, factors);
    }
}

在java.util.concurrent.atomic包中包含了一些原子變量類,用于實(shí)現(xiàn)數(shù)值和對(duì)象引用上的原子狀態(tài)轉(zhuǎn)換。通過(guò)AtomicLong來(lái)替代long類型,能夠確保所有對(duì)計(jì)數(shù)器狀態(tài)的訪問(wèn)都是原子的。

當(dāng)在無(wú)狀態(tài)的類中添加一個(gè)狀態(tài)時(shí),如果該狀態(tài)完全由線程安全的對(duì)象進(jìn)行管理,那么這個(gè)類仍然是線程安全的。

假設(shè)我們希望提升Servlet的性能:將最近的結(jié)果緩存起來(lái),當(dāng)連續(xù)兩個(gè)的請(qǐng)求相同的數(shù)值進(jìn)行因式分解時(shí),可以直接使用上一次的計(jì)算結(jié)果。

要實(shí)現(xiàn)該種緩存策略,需要保存兩個(gè)狀態(tài):最近執(zhí)行因數(shù)分解的數(shù)值,以及分解結(jié)果

@ThreadSafe
public class UnsafeCachingFactorizer implements Servlet{
    //保存最近的數(shù)值
    private final AtomicReference<BigInteger> lastNumber 
        = new AtomicReference<BigInteger>();
    //保存最近的因式分解的結(jié)果
    private final AtomicReference<BigInteger[]> lastFactors
        = new AtomicReference<BigInteger[]>();

    public void service(ServletRequest req, ServletResponse resp){
        BigInteger i = extractFromRequest(req);
        //若該次請(qǐng)求與上次請(qǐng)求一致,從緩存中取值
        if (i.euqals(lastNumber.get())) {
            encodeIntoResponse(resp, lastFactors.get());
        } 
        //否則,調(diào)用方法計(jì)算并加入到緩存中
        else {
            BigInteger[] factors = factor(i);
            lastNumber.set(i);
            lastFactors.set(factors);
            encodeIntoResponse(resp, factors);
        }
    }
}

為了線程的安全性,我們采用了兩個(gè)原子引用來(lái)保證類不存在競(jìng)態(tài)條件。但實(shí)際上,這樣的做法能否保證不存在競(jìng)態(tài)條件呢?

我們假設(shè)線程A,B同時(shí)訪問(wèn)該類,并且緩存中的最近記錄lastNumber為6,lastFactors為[2,3],現(xiàn)在A請(qǐng)求的數(shù)值為8,因?yàn)閕 != 6,因此,會(huì)調(diào)用factor方法,并更新緩存,lastNumber.set(i),當(dāng)A執(zhí)行到這步時(shí),B進(jìn)來(lái)了,判斷l(xiāng)astNumber是否為8,結(jié)果是為8,然后B就直接獲取lastFactors中的值[2,3],獲取后,A才執(zhí)行lastFactors.set(factors)

對(duì)于,上面假設(shè)的情況,雖然偶然性很大,但是依舊存在這種可能,因?yàn)榫€程的運(yùn)行是難以預(yù)測(cè)的!

實(shí)際上,出現(xiàn)上面的問(wèn)題,主要是lastNumber和lastFactors在更新上存在不一致的情況。因此:

要保證狀態(tài)的一致性,就需要在單個(gè)原子操作中更新所有相關(guān)的狀態(tài)變量。

對(duì)于上面采用原子引用無(wú)法解決安全性問(wèn)題,那么應(yīng)該采取何種措施去解決?

實(shí)際上,Java提供了一種內(nèi)置的鎖機(jī)制來(lái)支持原子性:同步代碼塊。

使用方法如下:

synchronized (lock){
  //訪問(wèn)或修改由鎖保護(hù)的共享狀態(tài)
}
  • 每個(gè)Java對(duì)象都可以用作一個(gè)實(shí)現(xiàn)同步的鎖,這些鎖稱為“內(nèi)置鎖”。線程在進(jìn)入同步代碼塊之前會(huì)自動(dòng)獲得鎖,并且在退出同步代碼塊時(shí)自動(dòng)釋放鎖。
  • Java的內(nèi)置鎖相當(dāng)于一種互斥體,這意味著最多一個(gè)線程能持有這種鎖。當(dāng)線程A嘗試獲取一個(gè)由線程B持有的鎖時(shí),線程A必須等待或阻塞,直到線程B釋放這個(gè)鎖。
@ThreadSafe
public class SynchronizedFactorizer implements Servlet{
    @GuardedBy("this") private BigInteger lastNumber;
    @GuardedBy("this") private BigInteger[] lastFactors;

    //使用Synchronized關(guān)鍵字
    public Synchronized void service(ServletRequest req, ServletResponse resp){
        BigInteger i = extractFromRequest(req);
        //若該次請(qǐng)求與上次請(qǐng)求一致,從緩存中取值
        if (i.euqals(lastNumber)) {
            encodeIntoResponse(resp, lastFactors);
        } 
        //否則,調(diào)用方法計(jì)算并加入到緩存中
        else {
            BigInteger[] factors = factor(i);
            lastNumber = i;
            lastFactors = factors;
            encodeIntoResponse(resp, factors);
        }
        
    }
}

如上,我們使用Synchronized關(guān)鍵字來(lái)修飾service方法,因此同一時(shí)刻只有一個(gè)線程來(lái)執(zhí)行service。如此,類必然是線程安全的,但實(shí)際上這種方法是過(guò)于極端。因?yàn)槎鄠€(gè)客戶端無(wú)法同時(shí)使用因式分解Servlet,這樣的服務(wù)響應(yīng)性非常低。

Paste_Image.png

我們對(duì)此,做如下修改,并增加了“計(jì)數(shù)器”和“緩存命中計(jì)數(shù)器”:

@ThreadSafe
public class SynchronizedFactorizer implements Servlet{
    @GuardedBy("this") private BigInteger lastNumber;
    @GuardedBy("this") private BigInteger[] lastFactors;
    //表示請(qǐng)求次數(shù)
    @GuardedBy("this") private long hits;
    //表示緩存命中次數(shù)
    @GuardedBy("this") private long cacheHits;

    //使用Synchronized關(guān)鍵字獲取hits,保證在獲取的過(guò)程中,其他線程不會(huì)改變hits
    public Synchronized long getHits(){
        return hits;
    }

    //使用Synchronized關(guān)鍵字獲取命中率,保證在獲取的過(guò)程中,其他線程不會(huì)改變hits和xacheHits
    public Synchronized double getCacheHitRadio(){
        return (double) cacheHits / (double) hits;
    }

    public void service(ServletRequest req, ServletResponse resp){
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = null;
        //使用Synchronized關(guān)鍵字,保證在取緩存過(guò)程中,不會(huì)改變lastNumber和lastFactors
        Synchronized (this){
            ++ hits;
            if (i.equals(lastNumber)) {
                ++ cacheHits;
                factors = lastFactors.clone();
            }
        }
        
        if (factors == null) {
            factors = factor(i);
            //使用Synchronized關(guān)鍵字,保證在寫緩存中,其他線程無(wú)法讀取lastNumber和lastFactors
            Synchronized(this){
                lastNumber = i;
                lastFactors = factors.clone();
            }
        }
        encodeIntoResponse(resp, factors);
    }
}

實(shí)際上,我們知道該類為線程不安全主要是因?yàn)閘astNumber和lastFactors的讀寫并發(fā)問(wèn)題。因此,我們只要保證在對(duì)lastNumber和lastFactors讀寫過(guò)程中保證只有一個(gè)線程即可,而其他時(shí)候,則允許多線程并發(fā)執(zhí)行,比如這里的factors方法,如此一來(lái),我們便可以兼并安全性和性能。

內(nèi)置鎖是可重入的

如果某個(gè)線程試圖獲得一個(gè)已經(jīng)由它自己持有的鎖,那么這個(gè)請(qǐng)求就會(huì)成功。

如何實(shí)現(xiàn)可重入的內(nèi)置鎖?

為每個(gè)鎖關(guān)聯(lián)一個(gè)獲取計(jì)數(shù)值和一個(gè)所有者線程

當(dāng)計(jì)數(shù)值為0時(shí),這個(gè)鎖就被認(rèn)為是沒(méi)有被任何線程持有。當(dāng)線程請(qǐng)求一個(gè)未被持有的鎖時(shí),JVM將記下鎖的持有者,并且將獲取計(jì)數(shù)值置為1。如果同一個(gè)線程再次獲取這個(gè)鎖,計(jì)數(shù)值將遞增,而當(dāng)線程退出同步代碼塊時(shí),計(jì)數(shù)器會(huì)相應(yīng)地遞減。當(dāng)計(jì)數(shù)值為0時(shí),這個(gè)鎖將被釋放。

public class Widget(){
    public synchronized void dosomething(){
        ...
    }
}

public class LoggingWidget extends Widget{
    public synchronized void dosomething(){
        System.out.println(this.toString() + ": calling dosomething");
        //調(diào)用父類中的dosomething方法
        super.dosomething();
    }
}

如上,我們?cè)O(shè)置類Widget和其子類LoggingWidget,并在子類中dosomething方法中調(diào)用父類的dosomething方法。

假設(shè),內(nèi)置鎖是不可重入的,我們看會(huì)出現(xiàn)什么情況:首先子類中dosomething方法用synchronized修飾,因此會(huì)獲取到Widget上的鎖,然后,執(zhí)行完輸出后,會(huì)調(diào)用父類的dosomething方法,因?yàn)楦割惖脑摲椒ㄒ脖籹ynchronized所修飾,因此需要獲取Widget的鎖,而Widget的鎖已經(jīng)被子類所獲取,那么將導(dǎo)致無(wú)法執(zhí)行父類的dosomething方法,同樣,子類也是無(wú)法執(zhí)行,導(dǎo)致陷入了死鎖。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容