2016-08-09 25 views
6

我有一個ArrayList正在實例化並在後臺線程上填充(我用它來存儲Cursor數據)。同時它可以在主線程中訪問,並通過使用foreach進行迭代。所以這顯然可能會導致拋出異常。使用foreach迭代ArrayList時的線程安全

我的問題是什麼是最好的做法,使這個類字段線程安全,而無需每次複製或使用標誌?

class SomeClass { 

    private final Context mContext; 
    private List<String> mList = null; 

    SomeClass(Context context) { 
     mContext = context; 
    } 

    public void populateList() { 
     new Thread(new Runnable() { 
      @Override 
      public void run() { 
       mList = new ArrayList<>(); 

       Cursor cursor = mContext.getContentResolver().query(
         DataProvider.CONTENT_URI, null, null, null, null); 
       try { 
        while (cursor.moveToNext()) { 
         mList.add(cursor.getString(cursor.getColumnIndex(DataProvider.NAME))); 
        } 
       } catch (Exception e) { 
        Log.e("Error", e.getMessage(), e); 
       } finally { 
        if (cursor != null) { 
         cursor.close(); 
        } 
       } 
      } 
     }).start(); 
    } 

    public boolean searchList(String query) { // Invoked on the main thread 
     if (mList != null) { 
      for (String name : mList) { 
       if (name.equals(query) { 
        return true; 
       } 
      } 
     } 

     return false; 
    } 
} 

回答

3

總的來說這是一個非常糟糕的主意就不是線程安全的一個數據結構同時運行。您不能保證實現將來不會發生變化,這可能嚴重影響應用程序的運行時行爲,即java.util.HashMap在併發修改時會導致無限循環。

爲了同時訪問列表,Java提供了java.util.concurrent.CopyOnWriteArrayList。使用該實施將解決您的問題以不同的方式:

  • 它是線程安全的,允許併發修改
  • 遍歷列表的快照不受併發添加操作,允許同時添加和迭代
  • 它比同步更快

或者,如果使用不內部陣列的副本被一個嚴格要求(我不能在你的情況想象,數組是相當小的,因爲它僅包含對象引用,它可以在內存中非常有效地進行復制),您可以同步在地圖上的訪問。 但這需要地圖時使用synchronized塊進行正確的初始化,否則你的代碼可能會拋出一個NullPointerException因爲線程執行的順序是不確定的(你假設populateList()之前啓動,因此該列表被初始化。 ,明智地選擇了保護塊如果你有run()方法的一個synchronized塊的全部內容,讀者線程必須等待,直到從遊標的結果進行處理 - 這可能需要一段時間 - 所以你實際上是鬆散的所有併發。

如果你決定去爲synchronized塊,我會做如下修改(我不主張,他們是完全正確的):

初始化列表字段,所以我們可以在其上同步訪問:

private List<String> mList = new ArrayList<>(); //initialize the field 

同步修改操作(添加)。不要從同步塊內的光標讀取數據,因爲如果它的低延時操作中,mList不能在運行過程中讀取,阻止所有其他線程相當長一段時間。

//mList = new ArrayList<>(); remove that line in your code 
String data = cursor.getString(cursor.getColumnIndex(DataProvider.NAME)); //do this before synchronized block! 
synchronized(mList){ 
    mList.add(data); 
} 

讀迭代必須是同步塊內,所以沒有元素被添加,而在其上迭代:

synchronized(mList){ 
    for (String name : mList) { 
    if (name.equals(query) { 
     return true; 
    } 
    } 
} 

所以當兩個線程在列表上進行操作,一個線程可以添加單個元素或一次迭代整個列表。代碼的這些部分沒有並行執行。

關於列表的同步版本(即VectorCollections.synchronizedList())。這些可能性能較差,因爲在同步時,實際上會失去並行執行,因爲一次只有一個線程可能運行受保護的塊。此外,他們可能仍然傾向於ConcurrentModificationException,甚至可能發生在單個線程中。如果數據結構在迭代器創建和迭代器之間進行修改,則會拋出它。所以這些數據結構不會解決你的問題。

我不建議manualy同步爲好,因爲僅僅這樣做是錯誤的風險太高(在錯誤的或不同的監察同步,過大的同步塊,...)

TL; DR

使用java.util.concurrent.CopyOnWriteArrayList

+0

我更喜歡'CopyOnWriteArrayList',但OP說「沒有複製」。也許他想保證「一次只有一個線程可以運行受保護的塊」。 – beatngu13

+2

「不要複製」 - 要求是毫無意義的。當然,CopyOnWriteArrayList *會複製內容,但這是實現細節的一部分。唯一的選擇是擁有一個同步塊,在地圖本身上進行同步,這在使用'null'初始化時很容易出錯。使用'Vector'不會阻止'ConcurrentModificationException',因爲它與併發無關(在迭代器嘗試繼續時會拋出,但在創建迭代器後列表已被修改) –

+0

謝謝說明。我猜'CopyOnWriteArrayList'對我來說更好(因爲我在UI線程上訪問這個字段)。 – Nikolai

1

你可以使用一個Vector這是線程安全的等效ArrayList

編輯:感謝Fildor's comment,現在我知道這並不迴避ConcurrentModificationException使用多個線程被拋出:

只有單一的通話將被同步。例如,一個add不能被調用,而另一個線程正在調用add。但是改變列表將導致CME在另一個線程上迭代時被拋出。你可以閱讀該主題的迭代器文檔。

也很有趣:

長話短說:切勿使用Vector

+0

只需用一個Vector不會避免受到ConcurrentModificationException的。 – Fildor

+0

@Fildor但是,只有當同一個線程試圖迭代和修改,或者我錯了嗎?我認爲同步可以防止多個線程同時訪問數據結構。 – beatngu13

+2

是的,但這與CME無關。只有單個呼叫將被同步。例如,一個add不能被調用,而另一個線程正在調用add。但是改變列表將導致CME在另一個線程上迭代時被拋出。你可以閱讀該主題的迭代器文檔。我自己陷入了陷阱 - 我痛苦地學會了;) – Fildor

1

使用Collections.synchronizedList(new ArrayList<T>());

例:

Collections.synchronizedList(mList); 
+0

無助於防止併發修改。 – Fildor

1

Java的synchronized塊http://www.tutorialspoint.com/java/java_thread_synchronization.htm

class SomeClass { 

    private final Context mContext; 
    private List<String> mList = null; 

    SomeClass(Context context) { 
     mContext = context; 
    } 

    public void populateList() { 
     new Thread(new Runnable() { 
      @Override 
      public void run() { 
       synchronized(SomeClass.this){ 
        mList = new ArrayList<>(); 

        Cursor cursor = mContext.getContentResolver().query(
          DataProvider.CONTENT_URI, null, null, null, null); 
        try { 
         while (cursor.moveToNext()) { 
          mList.add(cursor.getString(cursor.getColumnIndex(DataProvider.NAME))); 
         } 
        } catch (Exception e) { 
         Log.e("Error", e.getMessage(), e); 
        } finally { 
         if (cursor != null) { 
          cursor.close(); 
         } 
        } 
       } 
      } 
     }).start(); 
    } 

    public boolean searchList(String query) { // Invoked on the main thread 
    synchronized(SomeClass.this){ 
      if (mList != null) { 
       for (String name : mList) { 
        if (name.equals(query) { 
         return true; 
        } 
       } 
      } 

      return false; 
     } 
    } 
} 
+0

你確定,同步的塊在同一個監視器上同步嗎? –

+0

你是正確的runnable應該有確切的指向class.this對象現在(更新後)我很確定它應該工作 –

+1

並將整個run()方法放入同步塊將阻止訪問相當長一段時間...假設你迭代1000個元素,每個元素需要1秒檢索,應用程序在〜16分鐘內沒有響應 –