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

資訊專欄INFORMATION COLUMN

JCIP閱讀筆記之線程安全性

nanchen2251 / 1914人閱讀

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

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

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

一、什么是線程安全性?

當多個線程訪問某個類時,這個類始終都能表現出正確的行為,那么這個類就是線程安全的。

示例:一個無狀態的Servlet

從request中獲取數值,然后因數分解,最后將結果封裝到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);
        }
    }

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

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

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

二、原子性

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

示例:在沒有同步的情況下統計已處理請求數量的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同時進入到 *1 這行,那么他們都讀取到count = 0,然后進行自增,那么count就會變成1,很明顯這不是我們想要的結果,因為我們丟失了一次自增。

1. 競態條件

這里有一個概念:競態條件(Race Condition),指的是,在并發編程中,由于不恰當的執行時序而出現不正確的結果。

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

大多數競態條件的本質是,基于一種可能失效的觀察結果來做出判斷 or 執行某個計算,即“先檢查后執行”。

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

count++大致包含三步:

取當前count值 *1

count加一 *2

寫回count *3

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

這就是一個典型的由于不恰當的執行時序而產生不正確的結果的例子,即發生競態條件。

2. 延遲初始化中的競態條件

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

    @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 是否已經被初始化,如果已經初始化過則返回現有的instance,否則再創建新的instance,然后再返回,這樣就可以避免在后來的調用中執行這段高開銷的代碼路徑。

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

當然,單例模式有很多其他經典的線程安全的實現方式,像DCL、靜態內部類、枚舉都可以保證線程安全,在這里就不贅述了。

三、加鎖機制

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

具體實現如下:

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

@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(替代對象引用的線程安全類)來保證每個操作的原子性,但是整個過程仍然存在競態條件,我們無法同時更新lastNumber和lastFactors,比如線程A執行到 1之后set了新的lastNumber,但此時還沒有更新lastFactors,然后線程B進入到了 2,發現已經該數字已經有緩存,便進入 *3,但此時線程A并沒有同時更新lastFactors,所以線程B現在get的i的因數分解結果是錯誤的。

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

1. 內置鎖
synchronized (lock) {
    // 訪問或修改由鎖保護的共享狀態
}

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

比如,我們對一個計數操作進行同步

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();
    }

}

最后輸出的結果是:

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

synchronized關鍵字編譯后會在同步塊前后形成 monitorenter 和 monitorexit 這兩個字節碼指令

  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

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

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

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

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

不過,虛擬機本身也在對其不斷地進行一些優化。

2. 重入

什么是重入?

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

重入的一種實現方法是:每個鎖有一個計數值,若計數值為0,則該鎖沒被任何線程擁有。當一個線程想拿這個鎖時,計數值加1;當一個線程退出同步塊時,計數值減1。計數值為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不是一個可重入鎖,那么上面代碼必將產生死鎖。Child和Parent類中doSomething方法都被synchronized修飾,我們在調用子類的重載的方法時,已經獲取到了synchronized鎖,而該方法內又調用了父類的doSomething,會再次嘗試獲取該synchronized鎖,如果synchronized不是可重入的鎖,那么在調用super.doSomething()時將無法獲取父類的鎖,線程會永遠停頓,等待一個永遠也無法獲得的鎖,即發生了死鎖。

四、活躍性與性能

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

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

當然不是的,只是我們需要恰當且小心地使用。

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

下面來看在JCIP中,作者是怎么實現在簡單性和并發性之間的平衡的:

緩存最近執行因數分解的數值及其計算結果的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();
                }
            }

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

            encodeIntoResponse(resp, factors);
        }

    }

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

安全性是實現了,那么性能上呢?

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

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

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

參考自:
《Java Concurrency in Practice》
以及其他網絡資源

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/69370.html

相關文章

  • 【Java并發編程的藝術】第二章讀書筆記synchronized關鍵字

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

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

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

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

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

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

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

    mcterry 評論0 收藏0

發表評論

0條評論

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