1. 程式人生 > >執行緒安全(單例與多例)

執行緒安全(單例與多例)

      又週五了,時間過得好快,住在集體宿舍,幾個宅男共處一室好是無聊,習慣性來到CSDN。今天一個應屆生同事突然問我為什麼老大要求我們不要在Service裡寫成員變數,說不安全,說為什麼不安全讓他自己去了解,看上去他沒有找到頭緒很是痛苦,想想當初這個問題也困擾過自己,向他解說的過程,順便寫下來和大家一起探討。老大說的好像對好像又不對,為什麼這樣說呢,因為我們專案的Service都是通過Spring去掃描,自然預設的都是單例,所以就有了執行緒安全問題。不在Service寫成員變數很簡單的就避免了執行緒不安全的情況,但一定要寫成員變數怎麼進行執行緒安全控制呢?這裡說的成員變數為非static, static成員變數後面另說。一是把Sevice在spring中配置成多例,從某種情況下避免了執行緒不安全的情況,還有一種方式是自寫程式碼進行執行緒安全控制。下面例出可能產生執行緒不安全的程式碼:

實體

public class Entity {

    private int count;

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }
}
實體單例
public class Single {
    public static final Entity entity = new Entity();

    public static Entity getEntity() {
        return entity;
    }
}
建立兩個執行緒
public class ThreadA extends Thread {
    Entity entity = Single.getEntity();

    public void run() {
        for (int i = 0; i < 5; i++) {
            entity.setCount(i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A" + entity.getCount());
        }
    }
}

public class ThreadB extends Thread {
    Entity entity = Single.getEntity();

    public void run() {
        for (int i = 5; i < 10; i++) {
            entity.setCount(i);
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B" + entity.getCount());
        }
    }
}

入口

public class Test {
    public static void main(String[] args) throws Exception {
        new ThreadA().start();
        new ThreadB().start();
    }

}
可能產生的一種輸出結果是:
B0
B6
A7
B1
B8
A9
B2
A2
A3
A4
回頭看我們的兩個執行緒中的for偱環,可能錯誤的認為A執行緒只會產生A0 A1 A2 A3 A4 ,    B執行緒只會產生B5 B6 B7 B8 B9, 但很顯然我們的輸出中出現了  B0 ,A7等你意料之外的資料,這裡就產生了執行緒不安全情況。我們把單例修改成多例試試,其它程式碼不變,只修改執行緒 A B的第一行程式碼:
public class ThreadA extends Thread {
    Entity entity = new Entity();

    public void run() {
        for (int i = 0; i < 5; i++) {
            entity.setCount(i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A" + entity.getCount());
        }
    }
}

public class ThreadB extends Thread {
    Entity entity = new Entity();

    public void run() {
        for (int i = 5; i < 10; i++) {
            entity.setCount(i);
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B" + entity.getCount());
        }
    }
}

執行結果如下:

B5
A0
B6
B7
A1
B8
B9
A2
A3
A4

資料正確,已通過多例控制執行緒安全,這個僅是在非static成員變數的情況,在多例情況下如果實體中的成員變數為static,修改實體程式碼

public class Entity {

    private static int count;

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }
}

執行結果如下:

B5
A6
B1
B7
A8
B2
B9
A9
A3
A4

又出現了A6等意外資料,要解決這種執行緒不安全問題可使用ThreadLocal, 相比synchronized個人比較喜歡ThreadLocal, synchronized排隊訪問,ThreadLocal建立變更副本,ThreadLocal通過空間換時間。修改實體

public class Entity {

    private static ThreadLocal<Integer> count = new ThreadLocal<Integer>();

    public int getCount() {
        return count.get();
    }

    public void setCount(int count) {
        this.count.set(count);
    }
}

執行結果如下:

B5
A0
B6
B7
A1
B8
B9
A2
A3
A4

已執行緒安全化。(多例    static成員變數)

前面例子說到單例 非static成員變數的情況也會出現執行緒不安全,我們把執行緒A B第一行程式碼再次修改成

Entity entity = Single.getEntity();

實體的第一行程式碼 修改成非static成員變數
  private  ThreadLocal<Integer> count = new ThreadLocal<Integer>();

執行如下:

B5
A0
B6
B7
A1
B8
B9
A2
A3
A4

已完成執行緒安全化。 單例 static成員變數也是執行緒不安全,一樣可以通過ThreadLocal執行緒安全化。

總之 單例  非static成員變數及static成員變數都是執行緒不安全, 多例 非static成員變數執行緒安全,但多例static成員變數會有執行緒不安全情況,

關於synchronized的實現還要考慮死鎖等情況,下回有時間再寫一下,本文有寫的不好不對的地方請指出,大家一起探討,謝謝!