使用foreach遍历ArrayList时的线程安全性 [英] Thread safety when iterating through an ArrayList using foreach

查看:1137
本文介绍了使用foreach遍历ArrayList时的线程安全性的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I've got an ArrayList which is being instantiated and populated on the background thread (I use it to store the Cursor data). At the same time it can be accessed on the main thread and iterated through using foreach. So this obviously may result in throwing an exception.

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

My question is what's the best practice to make this class field thread-safe without copying it every time or using flags?

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;
    }
}

推荐答案

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

In general it is a very bad idea to operate concurrently on a datastructure that is not thread-safe. You have no guarantee that the implementation will not change in the future, which may severly impact the runtime behavior of the application, i.e. java.util.HashMap causes infinite loops when being concurrently modified.

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

For accessing a List concurrently, Java provides the java.util.concurrent.CopyOnWriteArrayList. Using this implementation will solve your problem in various ways:

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

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

Alternatively, if not using a copy of the internal array is a strict requirement (which I can't imagine in your case, the array is rather small as it only contains object references, which can be copied in memory quite efficiently), you may synchronize the access on the map. But that would require the Map to be initialized properly, otherwise your code may throw a NullPointerException because the order of thread-execution is not guaranteed (you assume the populateList() is started before, so the list gets initialized. When using a synchronized block, choose the protected block wisely. In case you have the entire content of the run() method in a synchronized block, the reader thread has to wait until the results from the cursor are processed - which may take a while - so you actually loose all concurrency.

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

If you decide to go for the synchronized block, I'd make the following changes (and I don't claim, they are perfectly correct):

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

Initialize the list field so we can synchronize access on it:

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

同步修改操作(添加).不要从同步块内部的游标中读取数据,因为如果它是低等待时间的操作,则在该操作期间无法读取mList,从而阻塞了所有其他线程一段时间.

Synchronize the modification operation (add). Do not read the data from the cursor inside the synchronization block because if its a low latency operation, the mList could not be read during that operation, blocking all other threads for quite a while.

//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);
}

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

The read iteration must be inside the synchronization block, so no element gets added, while being iterated over:

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

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

So when two threads operate on the list, one thread can either add a single element or iterate over the entire list at a time. You have no parallel execution on these parts of the code.

关于列表的同步版本(即VectorCollections.synchronizedList()).这些功能的性能可能较差,因为在同步时,您实际上会丢失全部执行,因为一次只能有一个线程运行受保护的块.此外,它们可能仍倾向于ConcurrentModificationException,甚至可能出现在单个线程中.如果在迭代器创建之间修改了数据结构,并且应该继续进行迭代,则抛出该异常.因此,这些数据结构不会解决您的问题.

Regarding the synchronized versions of a List (i.e. Vector, Collections.synchronizedList()). Those might be less performant because with synchronization you actually lose prallel execution as only one thread may run the protected blocks at a time. Further, they might still be prone to ConcurrentModificationException, which may even occur in a single thread. It is thrown, if the datastructure is modified between iterator creation and iterator should proceed. So those datastructures won't solve your problem.

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

I do not recommend manualy synchronization as well, because the risk of simply doing it wrong is too high (synchronizing on the wrong or different monitory, too large synchronization blocks, ...)

TL; DR

使用 java.util.concurrent.CopyOnWriteArrayList

这篇关于使用foreach遍历ArrayList时的线程安全性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆