国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

JCIP閱讀筆記之線程安全性

nanchen2251 / 1907人閱讀

摘要:大多數(shù)都是線程安全的,所以極大降低了在實現(xiàn)線程安全性的復(fù)雜性。只有在處理請求需要保存一些信息的情況下,線程安全性才會成為一個問題。雖然這種方式可以保證線程安全,但是性能方面會有些問題。

本文是作者在閱讀JCIP過程中的部分筆記和思考,純手敲,如有誤處,請指正,非常感謝~

可能會有人對書中代碼示例中的注解有疑問,這里說一下,JCIP中示例代碼的注解都是自定義的,并非官方JDK的注解,因此如果想要在自己的代碼中使用,需要添加依賴。移步:jcip.net

一、什么是線程安全性?

當(dāng)多個線程訪問某個類時,這個類始終都能表現(xiàn)出正確的行為,那么這個類就是線程安全的。

示例:一個無狀態(tài)的Servlet

從request中獲取數(shù)值,然后因數(shù)分解,最后將結(jié)果封裝到response中

    @ThreadSafe
    public class StatelessFactorizer implements Servlet {
        public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            encodeIntoResponse(resp, factors);
        }
    }

這是一個無狀態(tài)的Servlet,什么是無狀態(tài)的?不包含任何域或者對其他類的域的引用。service里僅僅是用到了存在線程棧上的局部變量的臨時狀態(tài),并且只能由正在執(zhí)行的線程訪問。

所以,如果有一個線程A正在訪問StatelessFactorizer類,線程B也在訪問StatelessFactorizer類,但是二者不會相互影響,最后的計算結(jié)果仍然是正確的,為什么呢?因為這兩個線程并沒有共享狀態(tài),他們各自訪問的都是自己的局部變量,所以像這樣 無狀態(tài)的對象都是線程安全的

大多數(shù)Servlet都是線程安全的,所以極大降低了在實現(xiàn)Servlet線程安全性的復(fù)雜性。只有在Servlet處理請求需要保存一些信息的情況下,線程安全性才會成為一個問題。

二、原子性

我理解的原子性就是指一個操作是最小范圍的操作,這個操作要么完整的做要么不做,是一個不可分割的操作。比如一個簡單的賦值語句 x = 1,就是一個原子操作,但是像復(fù)雜的運算符比如++, --這樣的不是原子操作,因為這涉及到“讀取-修改-寫入”的一個操作序列,并且結(jié)果依賴于之前的狀態(tài)。

示例:在沒有同步的情況下統(tǒng)計已處理請求數(shù)量的Servlet(非線程安全)

    @NotThreadSafe
    public class UnsafeCountingFactorizer implements Servlet {
        private long count = 0;

        public long getCount() {
            return count;
        }

        public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            count++; // *1
            encodeIntoResponse(resp, factors);
        }
    }

在上面這段代碼中,count是一個公共的資源,如果有多個線程,比如線程A, B同時進(jìn)入到 *1 這行,那么他們都讀取到count = 0,然后進(jìn)行自增,那么count就會變成1,很明顯這不是我們想要的結(jié)果,因為我們丟失了一次自增。

1. 競態(tài)條件

這里有一個概念:競態(tài)條件(Race Condition),指的是,在并發(fā)編程中,由于不恰當(dāng)?shù)膱?zhí)行時序而出現(xiàn)不正確的結(jié)果。

在count自增的這個計算過程中,他的正確性取決于線程交替執(zhí)行的時序,那么就會發(fā)生競態(tài)條件。

大多數(shù)競態(tài)條件的本質(zhì)是,基于一種可能失效的觀察結(jié)果來做出判斷 or 執(zhí)行某個計算,即“先檢查后執(zhí)行”。

還是拿這個count自增的計算過程舉例:

count++大致包含三步:

取當(dāng)前count值 *1

count加一 *2

寫回count *3

那么在這個過程中,線程A首先去獲取當(dāng)前count,然后很不幸,線程A被掛起了,線程B此時進(jìn)入到 1,他取得的count仍然為0,然后繼續(xù) 2,count = 1,現(xiàn)在線程B又被掛起了,線程A被喚醒繼續(xù) 2,此時線程A觀察到的仍然是自己被掛起之前count = 0的結(jié)果,實際上是已經(jīng)失效的結(jié)果,線程A再繼續(xù) 2,count = 1,然后 *3,最后得到結(jié)果是count = 1,然后線程B被喚醒后繼續(xù)執(zhí)行,得到的結(jié)果也是count = 1。

這就是一個典型的由于不恰當(dāng)?shù)膱?zhí)行時序而產(chǎn)生不正確的結(jié)果的例子,即發(fā)生競態(tài)條件。

2. 延遲初始化中的競態(tài)條件

這是一個典型的懶漢式的單例模式的實現(xiàn)(非線程安全)

    @NotThreadSafe
    public class Singleton {
        private static Singleton instance;

        private Singleton() {}

        public static Singleton getInstance() {
            if (instance == null) { // *1
                instance = new Singleton();
            }

            return instance;
        }
    }

在 *1 判空后,即實際需要使用時才初始化對象,也就是延遲初始化。這種方式首先判斷 instance 是否已經(jīng)被初始化,如果已經(jīng)初始化過則返回現(xiàn)有的instance,否則再創(chuàng)建新的instance,然后再返回,這樣就可以避免在后來的調(diào)用中執(zhí)行這段高開銷的代碼路徑。

在這段代碼中包含一個競態(tài)條件,可能會破壞該類的正確性。假設(shè)有兩個線程A, B,同時進(jìn)入到了getInstance()方法,線程A在 *1 判斷為true,然后開始創(chuàng)建Singleton實例,但是A會花費多久能創(chuàng)建完,以及線程的調(diào)度方式都是不確定的,所以有可能A還沒創(chuàng)建完實例,B已經(jīng)判空返回true,最終結(jié)果就是創(chuàng)建了兩個實例對象,沒有達(dá)到單例模式想要達(dá)到的效果。

當(dāng)然,單例模式有很多其他經(jīng)典的線程安全的實現(xiàn)方式,像DCL、靜態(tài)內(nèi)部類、枚舉都可以保證線程安全,在這里就不贅述了。

三、加鎖機制

還是回到因數(shù)分解那個例子,如果希望提升Servlet的性能,將剛計算的結(jié)果緩存起來,當(dāng)兩個連續(xù)的請求對相同的值進(jìn)行因數(shù)分解時,可以直接用上一次的結(jié)果,無需重新計算。

具體實現(xiàn)如下:

該Servlet在沒有足夠原子性保證的情況下對其最近計算結(jié)果進(jìn)行緩存(非線程安全)

@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet {
    private final AtomicReference lastNumber
            = new AtomicReference<>();
    private final AtomicReference lastFactors
            = new AtomicReference<>();

    public void service (ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        if (i.equals(lastNumber.get())) // *2
            encodeIntoResponse(resp, lastFactors.get()); // *3
        else {
            BigInteger[] factors = factor(i);
            lastNumber.set(i); // *1
            lastFactors.set(factors);
            encodeIntoResponse(resp, factors);
        }
    }
}

很明顯這個Servlet不是線程安全的,盡管使用了AtomicReference(替代對象引用的線程安全類)來保證每個操作的原子性,但是整個過程仍然存在競態(tài)條件,我們無法同時更新lastNumber和lastFactors,比如線程A執(zhí)行到 1之后set了新的lastNumber,但此時還沒有更新lastFactors,然后線程B進(jìn)入到了 2,發(fā)現(xiàn)已經(jīng)該數(shù)字已經(jīng)有緩存,便進(jìn)入 *3,但此時線程A并沒有同時更新lastFactors,所以線程B現(xiàn)在get的i的因數(shù)分解結(jié)果是錯誤的。

Java提供了一些鎖的機制來解決這樣的問題。

1. 內(nèi)置鎖
synchronized (lock) {
    // 訪問或修改由鎖保護(hù)的共享狀態(tài)
}

在Java中,最基本的互斥同步手段就是synchronized關(guān)鍵字了

比如,我們對一個計數(shù)操作進(jìn)行同步

public class Test implements Runnable {
    private static int count;

    public Test() {
        count = 0;
    }

    @Override
    public void run() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) {
        Test test = new Test();
        Thread thread1 = new Thread(test, "thread1");
        Thread thread2 = new Thread(test, "thread2");
        thread1.start();
        thread2.start();
    }

}

最后輸出的結(jié)果是:

thread1:0
thread1:1
thread1:2
thread1:3
thread1:4
thread2:5
thread2:6
thread2:7
thread2:8
thread2:9

synchronized關(guān)鍵字編譯后會在同步塊前后形成 monitorenter 和 monitorexit 這兩個字節(jié)碼指令

  public void run();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: iconst_0
       5: istore_2
       6: iload_2
        // ......
      67: iinc          2, 1
      70: goto          6
      73: aload_1
      74: monitorexit
      75: goto          85
      78: astore        4
      80: aload_1
      81: monitorexit
      82: aload         4
      84: athrow
      85: return

在執(zhí)行monitorenter時會嘗試去獲取對象的鎖,如果這個對象沒被鎖定 or 當(dāng)前線程已擁有了這個對象的鎖,則計數(shù)器 +1 ,相應(yīng)地,執(zhí)行monitorexit時計數(shù)器 -1 ,計數(shù)器為0,則釋放鎖。如果獲取對象失敗,需要阻塞等待。

雖然這種方式可以保證線程安全,但是性能方面會有些問題。

因為Java的線程是映射到操作系統(tǒng)的原聲線程上的,所以如果要阻塞 or 喚醒一個線程,需要操作系統(tǒng)在系統(tǒng)態(tài)和用戶態(tài)之間轉(zhuǎn)換,而這種轉(zhuǎn)換會耗費很多處理器時間。

除此之外,這種同步機制在某些情況下有些極端,如果我們用synchronized關(guān)鍵字修飾前面提到的因式分解的service方法,那么在同一時刻就只有一個線程能執(zhí)行該方法,也就意味著多個客戶端無法同時使用因式分解Servlet,服務(wù)的響應(yīng)性非常低。

不過,虛擬機本身也在對其不斷地進(jìn)行一些優(yōu)化。

2. 重入

什么是重入?

舉個例子,一個加了X鎖的方法A,這個方法內(nèi)調(diào)用了方法B,方法B也加了X鎖,那么,如果一個線程拿到了方法A的X鎖,再調(diào)用方法B時,就會嘗試獲取一個自己已經(jīng)擁有的X鎖,這就是重入。

重入的一種實現(xiàn)方法是:每個鎖有一個計數(shù)值,若計數(shù)值為0,則該鎖沒被任何線程擁有。當(dāng)一個線程想拿這個鎖時,計數(shù)值加1;當(dāng)一個線程退出同步塊時,計數(shù)值減1。計數(shù)值為0時鎖被釋放。

synchronized就是一個可重入的鎖,我們可以用以下代碼證明一下看看:

Parent.java

public class Parent {
    public synchronized void doSomething() {
        System.out.println("Parent: calling doSomething");
    }
}

Child.java

public class Child extends Parent {
    public synchronized void doSomething() {
        System.out.println("Child: calling doSomething");
        super.doSomething(); // 獲取父類的鎖
    }

    public static void main(String[] args) {
        Child child = new Child();
        child.doSomething();
    }
}

輸出:

Child: calling doSomething
Parent: calling doSomething

如果synchronized不是一個可重入鎖,那么上面代碼必將產(chǎn)生死鎖。Child和Parent類中doSomething方法都被synchronized修飾,我們在調(diào)用子類的重載的方法時,已經(jīng)獲取到了synchronized鎖,而該方法內(nèi)又調(diào)用了父類的doSomething,會再次嘗試獲取該synchronized鎖,如果synchronized不是可重入的鎖,那么在調(diào)用super.doSomething()時將無法獲取父類的鎖,線程會永遠(yuǎn)停頓,等待一個永遠(yuǎn)也無法獲得的鎖,即發(fā)生了死鎖。

四、活躍性與性能

前面在內(nèi)置鎖部分提到過,如果用synchronized關(guān)鍵字修飾因式分解的service方法,那么每次只有一個線程可以執(zhí)行,程序的性能將會非常低下,當(dāng)多個請求同時到達(dá)因式分解Servlet時,這個應(yīng)用便會成為 Poor Concurrency。

那么,難道我們就不能使用synchronized了嗎?

當(dāng)然不是的,只是我們需要恰當(dāng)且小心地使用。

我們可以通過縮小同步塊,來做到既能確保Servlet的并發(fā)性,又能保證線程安全性。我們應(yīng)該盡量將不影響共享狀態(tài)且執(zhí)行時間較長的操作從同步塊中分離,從而縮小同步塊的范圍。

下面來看在JCIP中,作者是怎么實現(xiàn)在簡單性和并發(fā)性之間的平衡的:

緩存最近執(zhí)行因數(shù)分解的數(shù)值及其計算結(jié)果的Servlet(線程安全且高效的)

    @ThreadSafe
    public class CachedFactorizer implements Servlet {
        @GuardedBy("this") private BigInteger lastNumber;
        @GuardedBy("this") private BigInteger[] lastFactors;
        @GuardedBy("this") private long hits;
        @GuardedBy("this") private long cacheHits;

        // 因為hits和cacheHits也是共享變量,所以需要使用同步 *3
        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) { // *2
                ++hits;
                // 命中緩存
                if (i.equals(lastNumber)) {
                    ++cacheHits;
                    factors = lastFactors.clone();
                }
            }

            // 沒命中,則進(jìn)行計算
            if (factors == null) {
                factors = factor(i); // *3
                // 同步更新兩個共享變量
                synchronized (this) { // *1
                    lastNumber = i;
                    lastFactors = factors.clone();
                }
            }

            encodeIntoResponse(resp, factors);
        }

    }

首先,lastNumber和lastFactors作為兩個共享變量是肯定需要同步更新的,因此在 1 處進(jìn)行了同步。然后,在 2 處,判斷是否命中緩存的操作序列也必須同步。此外,在 *3 處,緩存命中計數(shù)器的實現(xiàn)也需要實現(xiàn)同步,因為計數(shù)器是共享的。

安全性是實現(xiàn)了,那么性能上呢?

前面我們說過,應(yīng)該盡量將 不影響共享狀態(tài)執(zhí)行時間較長 的操作從同步塊中分離,從而縮小同步塊的范圍。那么這個Servlet里不影響共享狀態(tài)的就是i和factos這兩個局部變量,可以看到作者已經(jīng)將其分離出;執(zhí)行時間較長的操作就是因式分解了,在 *3 處,CachedFactorizer已經(jīng)釋放了前面獲得的鎖,在執(zhí)行因式分解時不需要持有鎖。

因此,這樣既確保了線程安全,又不會過多影響并發(fā)性,并且在每個同步塊內(nèi)的代碼都“足夠短”。

總之,在并發(fā)代碼的設(shè)計中,我們要盡量設(shè)計好每個同步塊的大小,在并發(fā)性和安全性上做好平衡。

參考自:
《Java Concurrency in Practice》
以及其他網(wǎng)絡(luò)資源

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

轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/69370.html

相關(guān)文章

  • 【Java并發(fā)編程的藝術(shù)】第二章讀書筆記synchronized關(guān)鍵字

    摘要:在之前的文章中學(xué)習(xí)了關(guān)鍵字,可以保證變量在線程間的可見性,但他不能真正的保證線程安全。線程執(zhí)行到指令時,將會嘗試獲取對象所對應(yīng)的的所有權(quán),即嘗試獲得對象的鎖。從可見性上來說,線程通過持有鎖的方式獲取變量的最新值。 在之前的文章中學(xué)習(xí)了volatile關(guān)鍵字,volatile可以保證變量在線程間的可見性,但他不能真正的保證線程安全。 /** * @author cenkailun *...

    GT 評論0 收藏0
  • Java深入-框架技巧

    摘要:從使用到原理學(xué)習(xí)線程池關(guān)于線程池的使用,及原理分析分析角度新穎面向切面編程的基本用法基于注解的實現(xiàn)在軟件開發(fā)中,分散于應(yīng)用中多出的功能被稱為橫切關(guān)注點如事務(wù)安全緩存等。 Java 程序媛手把手教你設(shè)計模式中的撩妹神技 -- 上篇 遇一人白首,擇一城終老,是多么美好的人生境界,她和他歷經(jīng)風(fēng)雨慢慢變老,回首走過的點點滴滴,依然清楚的記得當(dāng)初愛情萌芽的模樣…… Java 進(jìn)階面試問題列表 -...

    chengtao1633 評論0 收藏0
  • Python - 收藏集 - 掘金

    摘要:首發(fā)于我的博客線程池進(jìn)程池網(wǎng)絡(luò)編程之同步異步阻塞非阻塞后端掘金本文為作者原創(chuàng),轉(zhuǎn)載請先與作者聯(lián)系。在了解的數(shù)據(jù)結(jié)構(gòu)時,容器可迭代對象迭代器使用進(jìn)行并發(fā)編程篇二掘金我們今天繼續(xù)深入學(xué)習(xí)。 Python 算法實戰(zhàn)系列之棧 - 后端 - 掘金原文出處: 安生??? 棧(stack)又稱之為堆棧是一個特殊的有序表,其插入和刪除操作都在棧頂進(jìn)行操作,并且按照先進(jìn)后出,后進(jìn)先出的規(guī)則進(jìn)行運作。 如...

    546669204 評論0 收藏0
  • 【讀書筆記】JVM垃圾收集與內(nèi)存分配策略

    摘要:堆和方法區(qū)只有在程序運行時才能確定內(nèi)存的使用情況,垃圾回收器所關(guān)注的主要就是這部分內(nèi)存。虛擬機會根據(jù)當(dāng)前系統(tǒng)的運行情況收集性能監(jiān)控信息,動態(tài)調(diào)整比率參數(shù)以提供最合適的停頓時間或最大的吞吐量。 Tip:內(nèi)容為對《深入理解Java虛擬機》(周志明 著)第三章內(nèi)容的總結(jié)和筆記。這是第一次拜讀時讀到的一些重點,做個分享,也為后面再次閱讀和實踐做保障。 3.1 概述 程序計數(shù)器、虛擬機棧、本地...

    mcterry 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<