java併發編程(四)線程共享模型 一、什麼是線程共享模型? 二、線程共享模型存在什麼問題? 三、解決方案 四、變量的安全分析 五、常見的線程安全類

一、什麼是線程共享模型?

在前面的章節中,我們介紹了計算機的共享模型,和java的線程共享模型:

1)計算機共享模型

2)java線程共享模型

如上所示,無論是哪種模型,都有線程或cpu自己的運行時緩存或內存,同時都有主內存。

二、線程共享模型存在什麼問題?

首先看下面的代碼,兩個線程,每個線程分別對i進行++操作,加100000次,結果會得到200000嗎:

/**
 * @description: 線程共享模型問題
 * @author:weirx
 * @date:2021/11/25 9:48
 * @version:3.0
 */
public class ThreadSharedModelProblems {

    static int i = 0;

    /**
     * 兩個長度的門閂
     */
    static CountDownLatch countDownLatch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 100000; j++) {
                i++;
            }
            // 減少門閂數
            countDownLatch.countDown();
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 100000; j++) {
                i++;
            }
            // 減少門閂數
            countDownLatch.countDown();
        });
        t2.start();
        //阻塞等待門閂數降爲0
        countDownLatch.await();

        System.out.println("i = " + i);
    }

結果:

i = 143188

產生的原因呢?主要是因爲i++並不是一個原子性操作。i++操作的JVM字節碼如下:

getstatic     #2                  // 獲取靜態變量i
iconst_1                          // 定義局部變量1
iadd                              // 執行自加1操作
putstatic     #2                  // 將自加1後的值賦給靜態變量i
return

那麼結合上面的例子和線程共享模型就會是如下模式:

線程t1和t2同時去主內存獲取獲取i的值,並進行自加1的操作,然後再將值賦回給主線程,因爲這兩個線程之間是沒有順序的,且沒有任何的關聯,勢必會造成線程t1,剛寫入主內存的值,被t2覆蓋,而t1再次取值,就不是上次的值了。

以上呢就是共享資源所導致的問題。

一段代碼塊內如果存在對共享資源的多線程讀寫操作,稱這段代碼塊爲臨界區

多個線程在臨界區內執行,由於代碼的執行序列不同而導致結果無法預測,稱之爲發生了競態條件

三、解決方案

爲了避免臨界區的競態條件發生,有多種手段可以達到目的。

  • 阻塞式的解決方案:synchronized,Lock
  • 非阻塞式的解決方案:原子變量

下文重點講解使用synchronized解決上面的問題。

3.1 synchronized對象鎖

對象鎖:它採用互斥的方式讓同一時刻至多隻有一個線程能持有對象鎖,其它線程再想獲取這個對象鎖時就會阻塞住。

可以理解這個對象爲一個房間,這個房間一次只能有一個人進入,代碼如下:

public class ThreadSharedModelProblems {

    static int i = 0;

    /**
     * 兩個長度的門閂
     */
    static CountDownLatch countDownLatch = new CountDownLatch(2);

    /**
     * 定義一個不可變的對象,此處可以理解成一個房間
     */
    static final Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 100000; j++) {
                // 爭奪進入房間的機會
                synchronized (obj){
                    i++;
                }
            }
            // 減少門閂數
            countDownLatch.countDown();
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 100000; j++) {
                // 爭奪進入房間的機會
                synchronized (obj){
                    i++;
                }
            }
            // 減少門閂數
            countDownLatch.countDown();
        });
        t2.start();
        //阻塞等待門閂數降爲0
        countDownLatch.await();

        System.out.println("i = " + i);
    }

}

synchronized 實際是用對象鎖保證了臨界區內代碼的原子性。臨界區內的代碼對外是不可分割的,不會被線程切換所打斷

如何理解上面這句話的後半句?cpu在運行時,會發生線程上下文的切換,假設t1正持有對象,及在房間內進行++操作,如果此時cpu時間片用完了,這個t1就會釋放佔用的cpu資源,但是對象鎖仍然被其持有,t2仍然不能獲得對象鎖。只有當cpu在給t1分配時間片,並完成此次循環操作後,t2纔有機會去獲得對象鎖。

3.2 對象鎖的優化

java是一門面向對象的語言,所以像上一章節的對象鎖不是好的實現方式,我們應該將其放在對象當中。

寫一個Room對象,將++操作和對象鎖放在其中,代碼如下所示:

Room:

public class Room {

    int i = 0;

    public int getI() {
        synchronized (this) {
            return i;
        }
    }

    public void add() {
        synchronized (this) {
            i++;
        }
    }
}

main方法:

/**
 * @description: 線程共享模型問題
 * @author:weirx
 * @date:2021/11/25 9:48
 * @version:3.0
 */
public class ThreadSharedModelProblems {

    /**
     * 兩個長度的門閂
     */
    static CountDownLatch countDownLatch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();

        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 100000; j++) {
                room.add();
            }
            // 減少門閂數
            countDownLatch.countDown();
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 100000; j++) {
                room.add();
            }
            // 減少門閂數
            countDownLatch.countDown();
        });
        t2.start();
        //阻塞等待門閂數降爲0
        countDownLatch.await();

        System.out.println("i = " + room.getI());
    }
}

synchronized (this)當中的this是什麼呢?其實就是Room這個對象本身,如下所示:

3.3 方法上的synchronized

1)普通方法上的synchronized,等同於加在當前對象上,如下面代碼,test1等同於test2

2)靜態方法上的synchronized,等同於加在類上,如下面代碼,test3等同於test4

public class MethodSynchronized {

    public synchronized void test1() {
        System.out.println("this is test1");
    }

    public void test2() {
        synchronized (this) {
            System.out.println("this is test2");
        }
    }

    public static synchronized void test3() {
        System.out.println("this is test3");
    }

    public void test4() {
        synchronized (MethodSynchronized.class) {
            System.out.println("this is test4");
        }
    }
}

3.4 何謂“線程八鎖”?

其實就是考察 synchronized 鎖住的是哪個對象,我們主要要記住以下兩點:

  • 普通方法鎖住的是this(當前對象),而靜態方法鎖住的是類(class)
  • 同一時刻,只有一個線程能夠持有鎖

所謂線程八鎖,就是八種不同鎖的情況,下面我就不舉例了,但是要能夠分析,基本在以下幾種類型中:

  • 同一個對象,內部無論幾個非靜態方法有鎖,都是互斥的
  • 同一個類的不同對象,鎖不互斥
  • 對象鎖,即this,與類鎖(class),是不互斥的
  • 同一個類的內部兩個靜態方法的鎖,是互斥的

四、變量的安全分析

  • 成員變量與靜態變量是線程安全的嗎?

    如果它們沒有共享,則線程安全。

    如果它們被共享了,根據它們的狀態是否能夠改變,又分兩種情況:

    • 如果只有讀操作,則線程安全
    • 如果有讀寫操作,則這段代碼是臨界區,需要考慮線程安全
  • 局部變量是線程安全的嗎?
    局部變量是線程安全的。

    但局部變量引用的對象則未必

    • 如果該對象沒有逃離方法的作用訪問,它是線程安全的
    • 如果該對象逃離方法的作用範圍,需要考慮線程安全。(比如由於內部類重寫方法,該方法使用了修改了局部變量,且該方法被共享了,則會導致該變量的不安全,可以對這種方法時使用final,或設置爲pravite)。

五、常見的線程安全類

常見的線程安全類其實也分爲兩個方面:

  • 使用鎖(synchronized,Lock,CAS)

    StringBuffer
    Random
    Vector
    Hashtable
    java.util.concurrent 包下的類

    需要注意的是,上面舉例的類,他們的方法都是原子性的,但是組合使用後並不能保證原子性,需要我們自己進行控制。

  • 不可變類(final)

    String
    Integer

    String、Integer 等都是不可變類,因爲其內部的狀態不可以改變,因此它們的方法都是線程安全的


關於線程共享模型以及synchronized的簡單使用就介紹到這裏了,有幫助的話點個贊吧。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章