1. 程式人生 > 程式設計 >Java併發程式設計入門(十八)再論執行緒安全

Java併發程式設計入門(十八)再論執行緒安全

Java極客  |  作者  /  鏗然一葉
這是Java極客的第 46 篇原創文章

一、無需加鎖的執行緒安全場景

如下幾種場景無需加鎖就能做到執行緒安全:

1.不變物件

2.執行緒封閉

3.棧封閉

4.ThreadLocal

I、不變物件

經典併發程式設計描述物件滿足不變性有以下條件:

1.物件建立後狀態就不再變化。

2.物件的所有域都是final型別。

3.建立物件期間,this引用沒有溢位。

實際對於第2點描述不完全準確:

1.只要成員變數是私有的,並且只提供只讀操作,就可能做到執行緒安全,並不一定需要final修飾,注意這裡說的是可能,原因見第2點。

2.如果成員變數是個物件,並且外部可寫,那麼也不能保證執行緒安全,例如:

public class Apple {
    public static void main(String[] args) {
        Dictionary dictionary = new Dictionary();
        Map<String,String> map = dictionary.getMap();
        //這個操作後,導致下一步的操作結果和預期不符,預期不符就不是執行緒安全
        map.clear();
        System.out.println(dictionary.translate("蘋果"));
    }
}

class
Dictionary
{ private final Map<String,String> map = new HashMap<String,String>(); public Dictionary() { map.put("蘋果","Apple"); map.put("橘子","Orange"); } public String translate(String cn) { if (map.containsKey(cn)) { return map.get(cn); } return
"UNKONWN"; } public Map<String,String> getMap() { return map; } } 複製程式碼

因此對不變物件的正確理解應該是:

1.物件建立後狀態不再變化(所有成員變數不再變化)

2.只有只讀操作。

3.任何時候物件的成員都不會溢位(成員不被其他外部物件進行寫操作),而不僅僅只是在構建時。

另,一些書籍和培訓提到不變類應該用final修飾,以防止類被繼承後子類不安全,個人覺得子類和父類本身就不是一個物件,我們說一個類是否執行緒安全說的是這個類本身,而不需要關心子類是否安全。

II、執行緒封閉

如果物件只在單執行緒中使用,不在多個執行緒中共享,這就是執行緒封閉。

例如web應用中獲取連線池中的資料庫連線訪問資料庫,每個web請求是一個獨立執行緒,當一個請求獲取到一個資料庫連線後,不會再被其他請求使用,直到資料庫連線關閉(回到連線池中)才會被其他請求使用。

III、棧封閉

物件只在區域性程式碼塊中使用,就是棧封閉的,例如:

     public void print(Vector v) {
         int size = v.size();
         for (int i = 0; i < size; i++) {
             System.out.println(v.get(i));
         }
     }
複製程式碼

變數size是區域性變數(棧封閉),Vector又是執行緒安全的容器,因此對於這個方法而言是執行緒安全的。

VI、ThreadLocal

通過ThreadLocal儲存的物件只對當前執行緒可見,因此也是執行緒安全的。

二、常見誤解的執行緒安全場景

I、執行緒安全容器總是安全

有些容器執行緒安全指的是原子操作執行緒安全,並非所有操作都安全,非執行緒安全的操作如:IF-AND-SET,容器迭代,例如:

public class VectorDemo {

    public static void main(String[] args) {
        Vector<String> tasks = new Vector<String>();
        for (int i = 0; i < 10; i++) {
            tasks.add("task" + i);
        }

        Thread worker1 = new Thread(new Worker(tasks));
        Thread worker2 = new Thread(new Worker(tasks));
        Thread worker3 = new Thread(new Worker(tasks));

        worker1.start();
        worker2.start();
        worker3.start();
    }
}

class Worker implements Runnable {

    private Vector<String> tasks;

    public Worker(Vector<String> tasks) {
        this.tasks = tasks;
    }

    public void run() {
        //如下操作非執行緒安全,多個執行緒同時執行,在判斷時可能都滿足條件,但實際處理時可能已經不再滿足條件
        while (tasks.size() > 0) {
            //模擬業務處理
            sleep(100);
            //實際執行時,這裡可能已經不滿足tasks.size() > 0
            System.out.println(Thread.currentThread().getName() + " " + tasks.remove(0));
        }
    }

    private void sleep(long millis) {
        try {
            TimeUnit.MILLISECONDS.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

輸出日誌:

Thread-0 task0
Thread-1 task2
Thread-2 task1
Thread-1 task3
Thread-2 task5
Thread-0 task4
Thread-0 task6
Thread-1 task8
Thread-2 task7
Thread-1 task9
Exception in thread "Thread-0" Exception in thread "Thread-2" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 0
	at java.util.Vector.remove(Vector.java:831)
	at com.javashizhan.concurrent.demo.safe.Worker.run(VectorDemo.java:46)
	at java.lang.Thread.run(Thread.java:745)
java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 0
	at java.util.Vector.remove(Vector.java:831)
	at com.javashizhan.concurrent.demo.safe.Worker.run(VectorDemo.java:46)
	at java.lang.Thread.run(Thread.java:745)
複製程式碼

可以看到其中一個工作執行緒在tasks.remove(0)時,由於集合中已經沒有資料而丟擲異常。要做到執行緒安全則要對非原子操作加鎖,修改後的程式碼如下:

    public void run() {
        //對非原子操作加鎖
        synchronized (tasks) {
            while (tasks.size() > 0) {
                sleep(100);
                System.out.println(Thread.currentThread().getName() + " " + tasks.remove(0));
            }
        }
    }
複製程式碼

II、final修飾的物件執行緒安全

在上述例子中,即使用final修飾Vector也非執行緒安全,final不代表被修飾物件是屬於執行緒安全的不變物件。

III、volatile修飾物件執行緒安全

volatile關鍵字修飾的物件只能保證可見性,這類變數不快取在CPU的快取中,這樣能保證如果A執行緒先修改了volatile變數的值,那麼B執行緒後讀取時就能看到最新值,而可見性不等於執行緒安全。

三、狹義執行緒安全和廣義執行緒安全

我們說Vector是執行緒安全的,但上面的例子已經說明:並非所有場景下Vector的操作都是執行緒安全的,但明明Vector又被公認為是執行緒安全的,這怎麼解釋?

由此,我們就可以定義狹義執行緒安全和廣義執行緒安全:

1.狹義:物件的每一個單個操作均執行緒安全

2.廣義:物件的每一個單個操作和組合操作都執行緒安全

對於上面例子中的Vector要修改為廣義執行緒安全,就需要在remove操作中做二次判斷,如果容器中已經沒有物件,就返回null,方法簽名可以修改為existsAndRemove,當然,為了做到廣義執行緒安全,修改的方法還不僅僅只有這一個。

四、總結

本文描述了不加鎖情況下執行緒安全的場景,以及容易誤解的執行緒安全場景,再到狹義執行緒安全和廣義執行緒安全,理解了這些,可以讓我們更清楚何時該加鎖,何時不需要加鎖,從而更有效的編寫執行緒安全程式碼。

end.


相關閱讀:
Java併發程式設計(一)知識地圖
Java併發程式設計(二)原子性
Java併發程式設計(三)可見性
Java併發程式設計(四)有序性
Java併發程式設計(五)建立執行緒方式概覽
Java併發程式設計入門(六)synchronized用法
Java併發程式設計入門(七)輕鬆理解wait和notify以及使用場景
Java併發程式設計入門(八)執行緒生命週期
Java併發程式設計入門(九)死鎖和死鎖定位
Java併發程式設計入門(十)鎖優化
Java併發程式設計入門(十一)限流場景和Spring限流器實現
Java併發程式設計入門(十二)生產者和消費者模式-程式碼模板
Java併發程式設計入門(十三)讀寫鎖和快取模板
Java併發程式設計入門(十四)CountDownLatch應用場景
Java併發程式設計入門(十五)CyclicBarrier應用場景
Java併發程式設計入門(十六)秒懂執行緒池差別
Java併發程式設計入門(十七)一圖掌握執行緒常用類和介面


Java極客站點: javageektour.com/