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

时间:2016-08-09 08:34:11

标签: java android multithreading 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;
    }
}

4 个答案:

答案 0 :(得分:5)

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

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

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

或者,如果使用内部数组的副本是严格要求(我无法想象你的例如,数组相当小,因为它只包含对象引用,可以非常有效地在内存中复制),您可以在地图上同步访问。 但是这需要正确地初始化Map,否则你的代码可能抛出NullPointerException,因为线程执行的顺序不能得到保证(你假设populateList()之前已经启动了,所以列表被初始化了。 使用同步块时,请明智地选择受保护的块。如果你在同步块中拥有run()方法的全部内容,那么读者线程必须等到光标的结果被处理 - 这可能需要一段时间 - 所以你实际上已经失去了所有并发。

如果你决定进行同步阻止,我会做出以下更改(而且我没有声明,他们完全正确):

初始化列表字段,以便我们可以同步访问权限:

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,这甚至可能出现在单个线程中。如果在迭代器创建和迭代器之间修改数据结构,则抛出它。因此,这些数据结构不会解决您的问题。

我也不建议手动同步,因为简单地做错的风险太高(同步错误或不同的监控,太大的同步块,......)

<强> TL; DR

使用java.util.concurrent.CopyOnWriteArrayList

答案 1 :(得分:1)

您可以使用Vector,这是ArrayList的线程安全等效。

编辑:感谢Fildor's comment,我现在知道这不会避免使用多个线程抛出ConcurrentModificationException

  

只会同步一个电话。例如,当另一个线程调用add时,无法调用一个add。但是改变列表将导致在迭代另一个线程时抛出CME。您可以在该主题上阅读迭代器的文档。

同样有趣:

长话短说:不要使用Vector

答案 2 :(得分:1)

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

例如:

Collections.synchronizedList(mList);

答案 3 :(得分:1)

java synchronized block 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;
        }
    }
}