7

我有一个被多个线程访问的 List 对象。更新列表的主要是一个线程,在某些情况下是两个线程。根据正在处理的用户请求的数量,可以从该列表中读取一到五个线程。该列表不是要执行的任务队列,它是同时检索和更新的域对象的列表。

现在有几种方法可以使对该列表的访问成为线程安全的:
- 使用同步块
- 使用普通(即读写操作共享相同的锁)
- 使用 ReadWriteLock -
使用新的ConcurrentBLLABLBA集合类之一

我的问题:
考虑到关键部分通常不包含很多操作(主要是添加/删除/插入或从列表中获取元素),最佳的使用方法是什么?
你能推荐另一种方法,上面没有列出吗?

一些限制
- 最佳性能至关重要,内存使用量不大
- 它必须是有序列表(当前在ArrayList上同步),尽管不是排序列表(即不使用 Comparable 或 Comparator 排序,而是根据插入顺序)
-列表很大,最多包含 100000 个域对象,因此使用 CopyOnWriteArrayList 之类的东西是不可行
的 - 写入/更新关键部分通常非常快,执行简单的添加/删除/插入或替换(设置​​)
- 读取操作将主要执行一个 elementAt(index) 调用大部分时间,虽然一些读取操作可能会进行二进制搜索,或者 indexOf(element)
- 没有对列表进行直接迭代,尽管像 indexOf(..) 这样的操作将遍历列表

4

5 回答 5

3

您必须使用顺序列表吗?如果映射类型结构更合适,您可以使用ConcurrentHashMap. 使用列表,aReadWriteLock可能是最有效的方法。

编辑以反映 OP 的编辑:对插入顺序进行二进制搜索?您是否在二进制搜索中存储时间戳并将其用于比较?如果是这样,您可以使用时间戳作为键,并ConcurrentSkipListMap作为容器(维护键顺序)。

于 2008-10-16T09:32:59.760 回答
1

阅读线程在做什么?如果他们正在迭代列表,那么您确实需要确保在整个迭代过程中没有人接触列表,否则您可能会得到非常奇怪的结果。

如果您可以准确定义所需的语义,则应该可以解决该问题 - 但您可能会发现您需要编写自己的集合类型才能正确有效地完成它。或者,CopyOnWriteArrayList可能已经足够好 - 如果可能很昂贵。基本上,你越能约束你的要求,它就越有效率。

于 2008-10-16T08:58:14.463 回答
1

我不知道这是否是解决问题的可行方案,但是......对我来说,使用数据库管理器来保存大量数据并让它管理事务是有意义的

于 2008-10-16T10:05:14.233 回答
1

赞同 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 ();
}

显然,您必须自己填写很多细节。

于 2008-10-16T15:09:04.590 回答
1

您可以使用实现同步的包装器:

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... 

这是一个简单的解决方案。在诉诸更复杂的解决方案之前,我会先尝试一下。

于 2009-03-23T15:32:34.703 回答