我有一个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;
}
}
答案 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;
}
}
}
因此,当两个线程在列表上运行时,一个线程可以添加单个元素,也可以一次遍历整个列表。您没有在代码的这些部分上执行并行执行。
关于列表的同步版本(即Vector
,Collections.synchronizedList()
)。这些可能性能较差,因为同步实际上会失去并行执行,因为一次只有一个线程可以运行受保护的块。此外,它们可能仍然倾向于ConcurrentModificationException
,这甚至可能出现在单个线程中。如果在迭代器创建和迭代器之间修改数据结构,则抛出它。因此,这些数据结构不会解决您的问题。
我也不建议手动同步,因为简单地做错的风险太高(同步错误或不同的监控,太大的同步块,......)
<强> TL; DR 强>
答案 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;
}
}
}