使用foreach迭代ArrayList时的线程安全性

Nik*_*lai 9 java multithreading android arraylist thread-safety

我有一个ArrayList实例化并在后台线程上填充(我用它来存储Cursor数据).同时,它可以在主线程上访问,并通过使用foreach进行迭代.所以这显然可能导致抛出异常.

我的问题是什么是使这个类字段线程安全的最佳实践,而不是每次都复制它或使用标志?

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

Ger*_*cke 5

通常,对不是线程安全的数据结构进行并发操作是一个非常糟糕的主意。您不能保证将来的实现不会改变,这可能会严重影响应用程序的运行时行为,即java.util.HashMap在被同时修改时会导致无限循环。

为了同时访问List,Java提供了java.util.concurrent.CopyOnWriteArrayList。使用此实现将以多种方式解决您的问题:

  • 它是线程安全的,允许并发修改
  • 遍历列表快照不受并发添加操作的影响,从而允许并发添加和迭代
  • 比同步快

另外,如果使用内部数组的副本是一个严格的要求(在您的情况下,我无法想象,该数组会很小,因为它仅包含对象引用,可以非常有效地将其复制到内存中),您可以同步地图上的访问。但这将需要正确初始化Map,否则您的代码可能会抛出a,NullPointerException因为不能保证线程执行的顺序(您假设populateList()之前已启动,因此列表已初始化。使用同步块时,请选择保护的明智地进行阻止。run() 在同步块中使用方法时,读取器线程必须等待,直到处理完游标的结果为止(这可能需要一段时间),因此您实际上失去了所有并发。

如果您决定使用同步块,则将进行以下更改(并且我不主张它们是完全正确的):

初始化列表字段,以便我们可以同步对其的访问:

private List<String> mList = new ArrayList<>(); //initialize the field
Run Code Online (Sandbox Code Playgroud)

同步修改操作(添加)。不要从同步块内部的游标中读取数据,因为如果它是低延迟操作,则在该操作期间无法读取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);
}
Run Code Online (Sandbox Code Playgroud)

读取迭代必须在同步块内部,因此在迭代时不会添加任何元素:

synchronized(mList){ 
  for (String name : mList) {
    if (name.equals(query) {
      return true;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

因此,当两个线程在列表上操作时,一个线程可以添加单个元素,也可以一次遍历整个列表。您没有在代码的这些部分上并行执行。

关于名单(即同步的版本VectorCollections.synchronizedList())。那些性能可能较差,因为在同步时您实际上失去了prallel执行,因为一次只能有一个线程运行受保护的块。此外,它们可能仍然容易发生ConcurrentModificationException,甚至可能在单个线程中发生。如果在迭代器创建之间修改了数据结构,则应该抛出该异常。因此,这些数据结构不会解决您的问题。

我也不建议手动同步,因为简单地做错了就太大了(在错误或不同的监视下进行同步,太大的同步块,...)

TL; DR

用一个 java.util.concurrent.CopyOnWriteArrayList

  • “不复制”的要求是毫无意义的。当然,CopyOnWriteArrayList确实复制了内容,但这是实现细节的一部分。唯一的选择是拥有一个同步块,在映射本身上进行同步,因为使用`null`初始化时容易出错。使用Vector不会阻止ConcurrentModificationException,因为它与并发无关(在迭代器尝试进行时抛出,但是在创建迭代器后修改了列表) (2认同)