使用foreach遍历ArrayList时的线程安全

我有一个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;

}

}

回答:

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

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

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

或者,如果 使用 内部 数组的副本是 严格的

要求(我无法想象您的情况,则该数组相当小,因为它仅包含对象引用,可以非常有效地将其复制到内存中),您可以同步地图上的访问。但这将需要正确初始化Map,否则您的代码可能会抛出a,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,甚至可能在单个线程中发生。如果在迭代器创建之间修改了数据结构,则应该抛出该异常。因此,这些数据结构不会解决您的问题。

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

用一个

java.util.concurrent.CopyOnWriteArrayList

以上是 使用foreach遍历ArrayList时的线程安全 的全部内容, 来源链接: utcz.com/qa/427131.html

回到顶部