Java 6 中并发访问列表的最佳方法
-
03-07-2019 - |
题
我有一个被多个线程访问的 List 对象。大多数情况下只有一个线程(在某些情况下有两个线程)更新列表。有 1 到 5 个线程可以从此列表中读取数据,具体取决于正在处理的用户请求的数量。该列表不是要执行的任务队列,而是同时检索和更新的域对象的列表。
现在有几种方法可以使对该列表的访问成为线程安全的:
-使用同步块
- 使用正常 锁 (IE。读和写操作共享相同的锁)
-使用 读写锁
- 使用新的之一 并发BLLABBBA 集合类
我的问题:
鉴于关键部分通常不包含大量操作(主要是添加/删除/插入或从列表中获取元素),最佳使用方法是什么?
您能推荐上面未列出的另一种方法吗?
一些限制
- 最佳性能很重要,内存使用量并不重要
-它必须是一个有序列表(当前正在同步 数组列表),尽管不是排序列表(即不是使用 Comparable 或 Comparator 排序,而是根据插入顺序排序)
-列表会很大,包含多达 100000 个域对象,因此使用像 CopyOnWriteArrayList 这样的东西是不可行的
-写入/更新关键部分通常非常快,执行简单的添加/删除/插入或替换(设置)
- 读取操作大多数时候主要执行 elementAt(index) 调用,尽管某些读取操作可能会执行二分搜索或 indexOf(element)
- 没有对列表进行直接迭代,尽管像indexOf(..)这样的操作将遍历列表
解决方案
您必须使用顺序列表吗?如果地图类型的结构更合适,您可以使用 ConcurrentHashMap
. 。有了一个清单,一个 ReadWriteLock
可能是最有效的方法。
编辑以反映OP的编辑:对插入顺序进行二分搜索?您是否在二分搜索中存储时间戳并使用它进行比较?如果是这样,您可以使用时间戳作为键,并且 ConcurrentSkipListMap
作为容器(维护密钥顺序)。
其他提示
阅读线程在做什么?如果他们正在迭代列表,那么您确实需要确保在整个迭代过程中没有人接触列表,否则您可能会得到非常奇怪的结果。
如果您可以精确地定义您需要的语义,那么应该可以解决问题 - 但您很可能会发现您需要编写自己的集合类型才能正确有效地完成它。或者, 写入数组列表时复制 可能已经足够好了——如果可能很贵的话。基本上,您对需求的限制越多,效率就越高。
我不知道这是否是问题的可能解决方案,但是......对我来说,使用数据库管理器来保存大量数据并让它管理事务是有意义的
我第二 Telcontar的建议 数据库的集合,因为它们实际上是为管理这种规模的数据并在线程之间进行协商而设计的,而内存中的集合则不是。
您说数据位于服务器上的数据库中,而客户端上的本地列表是为了用户界面。您不需要同时在客户端上保留所有 100000 个项目,或对其执行如此复杂的编辑。在我看来,您在客户端上想要的是数据库上的轻量级缓存。
编写一个缓存,一次仅存储客户端上当前的数据子集。该客户端缓存不会对自己的数据执行复杂的多线程编辑;相反,它将所有编辑提供给服务器,并侦听更新。当服务器上的数据发生变化时,客户端只是忘记旧数据并重新加载它。仅允许一个指定线程读取或写入集合本身。这样,客户端只需镜像服务器上发生的编辑,而不需要本身进行复杂的编辑。
是的,这是一个相当复杂的解决方案。它的组成部分是:
- 用于加载一系列数据(例如项目 478712 到 478901)而不是整个数据的协议
- 用于接收有关已更改数据的更新的协议
- 一个缓存类,通过服务器上的已知索引存储项目
- 属于与服务器通信的缓存的线程。这是唯一写入集合本身的线程
- 属于该缓存的线程,在检索数据时处理回调
- UI 组件实现的接口,允许它们在加载数据时接收数据
第一次尝试时,该缓存的骨架可能如下所示:
class ServerCacheViewThingy {
private static final int ACCEPTABLE_SIZE = 500;
private int viewStart, viewLength;
final Map<Integer, Record> items
= new HashMap<Integer, Record>(1000);
final ConcurrentLinkedQueue<Callback> callbackQueue
= new ConcurrentLinkedQueue<Callback>();
public void getRecords (int start, int length, ViewReciever reciever) {
// remember the current view, to prevent records within
// this view from being accidentally pruned.
viewStart = start;
viewLenght = length;
// if the selected area is not already loaded, send a request
// to load that area
if (!rangeLoaded(start, length))
addLoadRequest(start, length);
// add the reciever to the queue, so it will be processed
// when the data has arrived
if (reciever != null)
callbackQueue.add(new Callback(start, length, reciever));
}
class Callback {
int start;
int length;
ViewReciever reciever;
...
}
class EditorThread extends Thread {
private void prune () {
if (items.size() <= ACCEPTABLE_SIZE)
return;
for (Map.Entry<Integer, Record> entry : items.entrySet()) {
int position = entry.key();
// if the position is outside the current view,
// remove that item from the cache
...
}
}
private void markDirty (int from) { ... }
....
}
class CallbackThread extends Thread {
public void notifyCallback (Callback callback);
private void processCallback (Callback) {
readRecords
}
}
}
interface ViewReciever {
void recieveData (int viewStart, Record[] records);
void recieveTimeout ();
}
显然,有很多细节需要您自己填写。
您可以使用实现同步的包装器:
import java.util.Collections;
import java.util.ArrayList;
ArrayList list = new ArrayList();
List syncList = Collections.synchronizedList(list);
// make sure you only use syncList for your future calls...
这是一个简单的解决方案。在采取更复杂的解决方案之前我会尝试一下。