Java多線程理解:線程安全的集合對象


1、概念介紹

  • 線程安全就是多線程訪問時,采用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其他線程不能進行訪問直到該線程讀取完,其他線程才可使用。不會出現數據不一致或者數據污染。
  • 線程不安全就是不提供數據訪問保護,多線程先后更改數據會產生數據不一致或者數據污染的情況。
  • 一般使用synchronized關鍵字加鎖同步控制,來解決線程不安全問題。

2、線程安全的集合對象

  • ArrayList線程不安全,Vector線程安全;
  • HashMap線程不安全,HashTable線程安全;
  • StringBuilder線程不安全,StringBuffer線程安全。

3、代碼測試

  • ArrayList線程不安全:
    在主線程中新建100個子線程,分別向ArrayList中添加100個元素,最后打印ArrayList的size。

    public class Test { public static void main(String [] args){ // 用來測試的List List<String> data = new ArrayList<>(); // 用來讓主線程等待100個子線程執行完畢 CountDownLatch countDownLatch = new CountDownLatch(100); // 啟動100個子線程 for(int i=0;i<100;i++){ SampleTask task = new SampleTask(data,countDownLatch); Thread thread = new Thread(task); thread.start(); } try{ // 主線程等待所有子線程執行完成,再向下執行 countDownLatch.await(); }catch (InterruptedException e){ e.printStackTrace(); } // List的size System.out.println(data.size()); } } class SampleTask implements Runnable { CountDownLatch countDownLatch; List<String> data; public SampleTask(List<String> data,CountDownLatch countDownLatch){ this.data = data; this.countDownLatch = countDownLatch; } @Override public void run() { // 每個線程向List中添加100個元素 for(int i = 0; i < 100; i++) { data.add("1"); } // 完成一個子線程 countDownLatch.countDown(); } }

    7次測試輸出():

    9998 10000 10000 ArrayIndexOutOfBoundsException 10000 9967 9936
  • Vector線程安全:
    在主線程中新建100個子線程,分別向Vector中添加100個元素,最后打印Vector的size。

    public class Test { public static void main(String [] args){ // 用來測試的List List<String> data = new Vector<>(); // 用來讓主線程等待100個子線程執行完畢 CountDownLatch countDownLatch = new CountDownLatch(100); // 啟動100個子線程 for(int i=0;i<100;i++){ SampleTask task = new SampleTask(data,countDownLatch); Thread thread = new Thread(task); thread.start(); } try{ // 主線程等待所有子線程執行完成,再向下執行 countDownLatch.await(); }catch (InterruptedException e){ e.printStackTrace(); } // List的size System.out.println(data.size()); } } class SampleTask implements Runnable { CountDownLatch countDownLatch; List<String> data; public SampleTask(List<String> data,CountDownLatch countDownLatch){ this.data = data; this.countDownLatch = countDownLatch; } @Override public void run() { // 每個線程向List中添加100個元素 for(int i = 0; i < 100; i++) { data.add("1"); } // 完成一個子線程 countDownLatch.countDown(); } }

    7次測試輸出():

    10000 10000 10000 10000 10000 10000 10000
  • 使用synchronized關鍵字來同步ArrayList:

    public class Test { public static void main(String [] args){ // 用來測試的List List<String> data = new ArrayList<>(); // 用來讓主線程等待100個子線程執行完畢 CountDownLatch countDownLatch = new CountDownLatch(100); // 啟動100個子線程 for(int i=0;i<100;i++){ SampleTask task = new SampleTask(data,countDownLatch); Thread thread = new Thread(task); thread.start(); } try{ // 主線程等待所有子線程執行完成,再向下執行 countDownLatch.await(); }catch (InterruptedException e){ e.printStackTrace(); } // List的size System.out.println(data.size()); } } class SampleTask implements Runnable { CountDownLatch countDownLatch; List<String> data; public SampleTask(List<String> data,CountDownLatch countDownLatch){ this.data = data; this.countDownLatch = countDownLatch; } @Override public void run() { // 每個線程向List中添加100個元素 for(int i = 0; i < 100; i++) { synchronized(data){ data.add("1"); } } // 完成一個子線程 countDownLatch.countDown(); } }

    7次測試輸出():

    10000 10000 10000 10000 10000 10000 10000

3、原因分析

  • ArrayList在添加一個元素的時候,它會有兩步來完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
    在單線程運行的情況下,如果 Size = 0,添加一個元素后,此元素在位置 0,而且 Size=1;
    而如果是在多線程情況下,比如有兩個線程,線程 A 先將元素1存放在位置 0。但是此時 CPU 調度線程A暫停,線程 B 得到運行的機會。線程B向此 ArrayList 添加元素2,因為此時 Size 仍然等於 0 (注意,我們假設的是添加一個元素是要兩個步驟,而線程A僅僅完成了步驟1),所以線程B也將元素存放在位置0。然后線程A和線程B都繼續運行,都增加 Size 的值,結果Size都等於1。
    最后,ArrayList中期望的元素應該有2個,而實際元素是在0位置,造成丟失元素,故Size 等於1。導致“線程不安全”。
    ArrayList源碼:
    @Override public boolean add(E object) { Object[] a = array; int s = size; if (s == a.length) { Object[] newArray = new Object[s + (s < (MIN_CAPACITY_INCREMENT / 2) ? MIN_CAPACITY_INCREMENT : s >> 1)]; System.arraycopy(a, 0, newArray, 0, s); array = a = newArray; } a[s] = object; size = s + 1; modCount++; return true; }
  • Vector的所有操作方法都被同步了,既然被同步了,多個線程就不可能同時訪問vector中的數據,只能一個一個地訪問,所以不會出現數據混亂的情況,所以是線程安全的。
    Vector源碼:
    @Override public synchronized boolean add(E object) { if (elementCount == elementData.length) { growByOne(); } elementData[elementCount++] = object; modCount++; return true; }

4、線程安全的集合並不安全

分析以下場景:

synchronized(map){ Object value = map.get(key); if(value == null) { value = new Object(); map.put(key,value); } return value;}

由於線程安全的集合對象是基於單個方法的同步,所以即使map是線程安全的,也會產生不同步現象。
在非單個方法的場景下,我們仍然需要使用synchronized加鎖才能保證對象的同步。

代碼測試:

public class Test { public static void main(String [] args){ // 用來測試的List List<String> data = new Vector<>(); // 用來讓主線程等待100個子線程執行完畢 CountDownLatch countDownLatch = new CountDownLatch(100); // 啟動100個子線程 for(int i=0;i<1000;i++){ SampleTask task = new SampleTask(data,countDownLatch); Thread thread = new Thread(task); thread.start(); } try{ // 主線程等待所有子線程執行完成,再向下執行 countDownLatch.await(); }catch (InterruptedException e){ e.printStackTrace(); } // List的size System.out.println(data.size()); } } class SampleTask implements Runnable { CountDownLatch countDownLatch; List<String> data; public SampleTask(List<String> data,CountDownLatch countDownLatch){ this.data = data; this.countDownLatch = countDownLatch; } @Override public void run() { // 每個線程向List中添加100個元素 int size = data.size(); data.add(size,"1"); // 完成一個子線程 countDownLatch.countDown(); } }
997 993 995 996 997 998 997

5、總結

    • 如何取舍
      線程安全必須要使用synchronized關鍵字來同步控制,所以會導致性能的降低
      當不需要線程安全時,可以選擇ArrayList,避免方法同步產生的開銷;
      多個線程操作同一個對象時,可以選擇線程安全的Vector;
    • 線程不安全!=不安全
      有人在使用過程中有一個不正確的觀點:我的程序是多線程的,不能使用ArrayList要使用Vector,這樣才安全。
      線程不安全並不是多線程環境下就不能使用
      注意線程不安全條件:多線程操作同一個對象。比如上述代碼就是在多個線程操作同一個ArrayList對象。
      如果每個線程中new一個ArrayList,而這個ArrayList只在這一個線程中使用,那么是沒問題的。
    • 線程‘安全’的集合對象
      較復雜的操作下,線程安全的集合對象也無法保證數據的同步,仍然需要我們來處理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM