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

查看:75
本文介绍了使用 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:

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

或者,如果使用内部数组的副本是严格要求(在您的情况下我无法想象,数组很小,因为它只包含对象引用,可以非常有效地复制到内存中),您可以同步映射上的访问.但这需要正确初始化 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天全站免登陆