Java并发性—改进读取时拷贝集合

Java并发性—改进读取时拷贝集合,java,concurrency,Java,Concurrency,我有一个多线程应用程序,其中共享列表具有经常写入、偶尔读取的行为 具体地说,许多线程将把数据转储到列表中,然后——稍后——另一个工作线程将抓取快照以持久化到数据存储中 这与讨论类似 这里提供了以下解决方案: class CopyOnReadList<T> { private final List<T> items = new ArrayList<T>(); public void add(T item) { synchron

我有一个多线程应用程序,其中共享列表具有经常写入、偶尔读取的行为

具体地说,许多线程将把数据转储到列表中,然后——稍后——另一个工作线程将抓取快照以持久化到数据存储中

这与讨论类似

这里提供了以下解决方案:

class CopyOnReadList<T> {

    private final List<T> items = new ArrayList<T>();

    public void add(T item) {
        synchronized (items) {
            // Add item while holding the lock.
            items.add(item);
        }
    }

    public List<T> makeSnapshot() {
        List<T> copy = new ArrayList<T>();
        synchronized (items) {
            // Make a copy while holding the lock.
            for (T t : items) copy.add(t);
        }
        return copy;
    }

}
类CopyOnReadList{
私有最终列表项=新的ArrayList();
公共作废新增(T项){
已同步(项目){
//在按住锁的同时添加项目。
项目。添加(项目);
}
}
公共列表makeSnapshot(){
列表副本=新建ArrayList();
已同步(项目){
//拿着锁复制一份。
对于(T:项目)副本,添加(T);
}
返回副本;
}
}
然而,在这个场景中(正如我从问题中学到的),在任何给定的时间,只有一个线程可以写入支持列表


是否有一种方法允许对备份列表进行高并发写入,而这些写入仅在
makeSnapshot()
调用期间锁定?

是的,有一种方法。如果您知道的话,它与ConcurrentHashMap的制作方式类似

您不应该从一个列表中为所有写入线程创建自己的数据结构,而应该使用几个独立的列表。每个这样的列表都应该有自己的锁来保护。add()方法应根据Thread.currentThread.id(例如,仅id%ListScont)为追加当前项选择列表。这将为.add()提供良好的并发属性——ListScont线程最多能够在没有争用的情况下进行写入

在makeSnapshot()上,您应该迭代所有列表,并为每个列表获取其锁定和复制内容


这只是一个想法,有很多地方可以改进

您可以使用
ConcurrentDoublyLinkedList
。这里有一个很好的实现

只要在创建快照时向前遍历列表,一切都会很好。此实现始终保留前向链。反向链有时是不准确的。

同步(~20 ns)非常快,即使其他操作可以允许并发,它们也可能较慢

private final Lock lock = new ReentrantLock();
private List<T> items = new ArrayList<T>();

public void add(T item) {
    lock.lock();
    // trivial lock time.
    try {
        // Add item while holding the lock.
        items.add(item);
    } finally {
        lock.unlock();
    }
}

public List<T> makeSnapshot() {
    List<T> copy = new ArrayList<T>(), ret;
    lock.lock();
    // trivial lock time.
    try {
        ret = items;
        items = copy;
    } finally {
        lock.unlock();
    }
    return ret;
}

public static void main(String... args) {
    long start = System.nanoTime();
    Main<Integer> ints = new Main<>();
    for (int j = 0; j < 100 * 1000; j++) {
        for (int i = 0; i < 1000; i++)
            ints.add(i);
        ints.makeSnapshot();
    }
    long time = System.nanoTime() - start;
    System.out.printf("The average time to add was %,d ns%n", time / 100 / 1000 / 1000);
}
这意味着,如果每秒创建3000万个条目,平均会有一个线程访问列表。如果每秒创建6000万个,则会出现并发问题,但此时可能会出现更多的资源分配问题


当争用率较高时,使用Lock.Lock()和Lock.unlock()会更快。但是,我怀疑您的线程将花费大部分时间构建要创建的对象,而不是等待添加对象。

首先,您应该调查这是否真的太慢。添加到
ArrayList
s是
O(1)
,因此如果列表具有适当的初始大小,
CopyOnReadList.add
基本上只是一个边界检查和对数组插槽的分配,速度非常快。(请记住,
CopyOnReadList
的编写是为了让人理解,而不是为了表现。)

如果您需要非锁定操作,您可以有如下功能:

class ConcurrentStack<T> {
    private final AtomicReference<Node<T>> stack = new AtomicReference<>();

    public void add(T value){
        Node<T> tail, head;
        do {
            tail = stack.get();
            head = new Node<>(value, tail);
        } while (!stack.compareAndSet(tail, head));
    }
    public Node<T> drain(){
        // Get all elements from the stack and reset it
        return stack.getAndSet(null);
    }
}
class Node<T> {
    // getters, setters, constructors omitted
    private final T value;
    private final Node<T> tail;
}
类ConcurrentStack{
私有最终AtomicReference堆栈=新的AtomicReference();
公共无效添加(T值){
节尾、节头;
做{
tail=stack.get();
头部=新节点(值,尾部);
}而(!stack.compareAndSet(tail,head));
}
公共节点排水管(){
//从堆栈中获取所有元素并重置它
返回stack.getAndSet(null);
}
}
类节点{
//省略了getter、setter和构造函数
私人最终T值;
私有最终节点尾部;
}
注意,虽然添加到这个结构中应该可以很好地处理高争用,但它有几个缺点。
drain
的输出迭代速度非常慢,它使用了相当多的内存(就像所有的链表一样),而且插入顺序也相反。(此外,它还没有经过真正的测试或验证,可能会影响到您的应用程序。但在Intertube上使用来自某个随机用户的代码总是有风险的。)

您可以使用允许多个线程在备份列表上并行执行添加操作,但只有一个线程可以创建快照。在准备快照的过程中,所有其他添加和快照请求都处于挂起状态

ReadWriteLock维护一对关联锁,其中一个用于 只读操作和一个用于写入的操作。可以保持读取锁 由多个读卡器线程同时执行,只要没有 作家。写锁是独占的

类CopyOnReadList{
//可以自由使用任何并发数据结构,以ConcurrentLinkedQueue为例
私有最终ConcurrentLinkedQueue项=新ConcurrentLinkedQueue();
private final ReadWriteLock rwLock=new ReentrantReadWriteLock();
private final Lock shared=rwLock.readLock();
private final Lock exclusive=rwLock.writeLock();
公共作废新增(T项){
shared.lock();//多个线程可以获得读锁
//如果items.add()从不抛出异常,那么try finally就太过了
试一试{
//在按住锁的同时添加项目。
项目。添加(项目);
}最后{
shared.unlock();
}
}
公共列表makeSnapshot(){
List copy=new ArrayList();//最好使用具有初始大小的LinkedList或ArrayList构造函数
exclusive.lock();//只有一个线程可以获得写锁,所有读锁也被阻塞
//如果for循环从不抛出异常,那么try finally就太过了
试一试{
//拿着锁复制一份。
对于(T:项目){
副本。添加(t);
class ConcurrentStack<T> {
    private final AtomicReference<Node<T>> stack = new AtomicReference<>();

    public void add(T value){
        Node<T> tail, head;
        do {
            tail = stack.get();
            head = new Node<>(value, tail);
        } while (!stack.compareAndSet(tail, head));
    }
    public Node<T> drain(){
        // Get all elements from the stack and reset it
        return stack.getAndSet(null);
    }
}
class Node<T> {
    // getters, setters, constructors omitted
    private final T value;
    private final Node<T> tail;
}
class CopyOnReadList<T> {

    // free to use any concurrent data structure, ConcurrentLinkedQueue used as an example
    private final ConcurrentLinkedQueue<T> items = new ConcurrentLinkedQueue<T>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock shared = rwLock.readLock();
    private final Lock exclusive = rwLock.writeLock(); 

    public void add(T item) {
        shared.lock(); // multiple threads can attain the read lock
        // try-finally is overkill if items.add() never throws exceptions
        try {
          // Add item while holding the lock.
          items.add(item);
        } finally {
          shared.unlock();
        }
    }

    public List<T> makeSnapshot() {
        List<T> copy = new ArrayList<T>(); // probably better idea to use a LinkedList or the ArrayList constructor with initial size
        exclusive.lock(); // only one thread can attain write lock, all read locks are also blocked
        // try-finally is overkill if for loop never throws exceptions
        try {
          // Make a copy while holding the lock.
          for (T t : items) {
            copy.add(t);
          }
        } finally {
          exclusive.unlock();
        }
        return copy;
    }

}