Java併發編程入門(十八)再論線程安全

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

1、無需加鎖的線程安全場景

以下幾種場景無需加鎖就能作到線程安全:java

1.不變對象web

2.線程封閉數據庫

3.棧封閉編程

4.ThreadLocal緩存

I、不變對象

經典併發編程描述對象知足不變性有如下條件:安全

1.對象建立後狀態就再也不變化。bash

2.對象的全部域都是final類型。併發

3.建立對象期間,this引用沒有溢出。post

實際對於第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存儲的對象只對當前線程可見,所以也是線程安全的。

2、常見誤解的線程安全場景

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線程後讀取時就能看到最新值,而可見性不等於線程安全。

3、狹義線程安全和廣義線程安全

咱們說Vector是線程安全的,但上面的例子已經說明:並不是全部場景下Vector的操做都是線程安全的,但明明Vector又被公認爲是線程安全的,這怎麼解釋?

由此,咱們就能夠定義狹義線程安全和廣義線程安全:

1.狹義:對象的每個單個操做均線程安全

2.廣義:對象的每個單個操做和組合操做都線程安全

對於上面例子中的Vector要修改成廣義線程安全,就須要在remove操做中作二次判斷,若是容器中已經沒有對象,就返回null,方法簽名能夠修改成existsAndRemove,固然,爲了作到廣義線程安全,修改的方法還不只僅只有這一個。

4、總結

本文描述了不加鎖狀況下線程安全的場景,以及容易誤解的線程安全場景,再到狹義線程安全和廣義線程安全,理解了這些,可讓咱們更清楚什麼時候該加鎖,什麼時候不須要加鎖,從而更有效的編寫線程安全代碼。

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/

相關文章
相關標籤/搜索