“原子”更新整个阵列

时间:2013-03-15 23:51:46

标签: java performance data-structures concurrency locking

我有一个编写器线程和单个读者线程来更新和处理数组池(存储在map中的引用)。写入与读取的比率几乎为5:1(写入延迟是一个问题)。

编写器线程需要根据某些事件更新池中数组的少数元素。整个写操作(所有元素)都需要是原子的。

我想确保读者线程读取先前更新的数组,如果编写器线程正在更新它(类似于易失性但在整个数组而不是单个字段)。基本上,我可以负担得起读取陈旧的值,但不能阻止。

此外,由于写入非常频繁,因此在读/写时创建新对象或锁定整个数组会非常昂贵。

是否有更高效的数据结构可供使用或使用更便宜的锁?

8 个答案:

答案 0 :(得分:2)

这个想法怎么样:编写器线程不会改变数组。它只是将更新排队。

读者线程,只要它进入需要数组稳定快照的读取会话,就会将排队更新应用到数组中,然后读取数组。

class Update
{
    int position;
    Object value;
}

ArrayBlockingQueue<Update> updates = new ArrayBlockingQueue<>(Integer.MAX_VALUE);

void write()
{
    updates.put(new Update(...));
}

Object[] read()
{
    Update update;
    while((update=updates.poll())!=null)
        array[update.position] = update.value;

    return array;
}

答案 1 :(得分:2)

另一个想法,假设数组只包含20个双打。

有两个数组,一个用于写入,一个用于读取。

Reader在读取期间锁定读取数组。

read()
    lock();
    read stuff
    unlock();

Writer首先修改写入数组,然后 tryLock 读取数组,如果锁定失败,则罚款,write()返回;如果锁定成功,则将写入数组复制到读取数组,然后释放锁定。

write()
    update write array
    if tryLock()
        copy write array to read array
        unlock()

读者可以被阻止,但只能复制20个双打所需的时间,这是短暂的。

读者应使用自旋锁,例如do{}while(tryLock()==false);,以避免被暂停。

答案 2 :(得分:2)

  

是否有更高效的数据结构?

是的,绝对!它们被称为持久数据结构。他们只能通过存储差异相对于先前版本来表示vector / map / etc的新版本。所有版本都是不可变的,这使得它们适合于并发(编写者不会干扰/阻止读者,反之亦然)。

为了表达更改,可以在引用类型(例如AtomicReference)中存储对持久数据结构的引用,并更改这些引用指向的内容 - 而不是结构本身。< / p>

Clojure提供了持久数据结构的顶级实现。它们是用纯粹,高效的Java编写的。

以下程序公开了如何使用持久数据结构来解决您描述的问题。

import clojure.lang.IPersistentVector;
import clojure.lang.PersistentVector;

public class AtomicArrayUpdates {

    public static Map<Integer, AtomicReference<IPersistentVector>> pool
        = new HashMap<>();
    public static Random rnd = new Random();
    public static final int SIZE = 60000;
    // For simulating the reads/writes ratio
    public static final int SLEEP_TIMÉ = 5;

    static {        
        for (int i = 0; i < SIZE; i++) {
            pool.put(i, new AtomicReference(PersistentVector.EMPTY));
        }
    }

    public static class Writer implements Runnable {   
        @Override public void run() {
            while (true) {
                try {
                    Thread.sleep(SLEEP_TIMÉ);
                } catch (InterruptedException e) {}

                int index = rnd.nextInt(SIZE);
                IPersistentVector vec = pool.get(index).get();

                // note how we repeatedly assign vec to a new value
                // cons() means "append a value".
                vec = vec.cons(rnd.nextInt(SIZE + 1)); 
                // assocN(): "update" at index 0
                vec = vec.assocN(0, 42); 
                // appended values are nonsense, just an example!
                vec = vec.cons(rnd.nextInt(SIZE + 1)); 

                pool.get(index).set(vec);

            }
        }
    }

    public static class Reader implements Runnable {
        @Override public void run() {
            while (true) {
                try {
                    Thread.sleep(SLEEP_TIMÉ * 5);
                } catch (InterruptedException e) {}

                IPersistentVector vec = pool.get(rnd.nextInt(SIZE)).get();
                // Now you can do whatever you want with vec.
                // nothing can mutate it, and reading it doesn't block writers!
            }
        } 
    }

    public static void main(String[] args) {
        new Thread(new Writer()).start();
        new Thread(new Reader()).start();
    }
}

答案 3 :(得分:1)

我会这样做:

  • 同步整个事情,看看性能是否足够好。考虑到你只有一个编写器线程和一个读者线程,争用率很低,这可以很好地运行

    private final Map<Key, double[]> map = new HashMap<> ();
    
    public synchronized void write(Key key, double value, int index) {
        double[] array = map.get(key);
        array[index] = value;
    }
    
    public synchronized double[] read(Key key) {
        return map.get(key);
    }
    
  • 如果它太慢,我会让编写器复制数组,更改一些值并将新数组放回地图。请注意array copies are very fast - 通常,20项数组最有可能需要不到100纳秒

    //If all the keys and arrays are constructed before the writer/reader threads 
    //start, no need for a ConcurrentMap - otherwise use a ConcurrentMap
    private final Map<Key, AtomicReference<double[]>> map = new HashMap<> ();
    
    public void write(Key key, double value, int index) {
        AtomicReference<double[]> ref = map.get(key);
        double[] oldArray = ref.get();
        double[] newArray = oldArray.clone();
        newArray[index] = value;
        //you might want to check the return value to see if it worked
        //or you might just skip the update if another writes was performed
        //in the meantime
        ref.compareAndSet(oldArray, newArray);
    }
    
    public double[] read(Key key) {
        return map.get(key).get(); //check for null
    }
    
  

由于写入非常频繁,因此在读/写时创建新对象或锁定整个数组会非常昂贵。

多频繁?除非每毫秒有数百个你应该没事。

另请注意:

  • 对象创建在Java中相当便宜(想想大约10个CPU周期=几纳秒)
  • 短寿命对象的垃圾收集通常是免费的(只要该对象停留在年轻一代,如果无法访问则不会被GC访问)
  • 而长期存在的对象具有GC性能影响,因为它们需要复制到旧一代

答案 4 :(得分:0)

以下变体的灵感来自my previous answerone of zhong.j.yu's

作者不会干扰/阻止读者,反之亦然,并且没有线程安全/可见性问题或精细推理。

public class V2 {

    static Map<Integer, AtomicReference<Double[]>> commited = new HashMap<>();
    static Random rnd = new Random();

    static class Writer {
        private Map<Integer, Double[]> writeable = new HashMap<>();
        void write() {        
            int i = rnd.nextInt(writeable.size());   
            // manipulate writeable.get(i)...
            commited.get(i).set(writeable.get(i).clone());
        }
    }

    static class Reader{
        void read() {
            double[] arr = commited.get(rnd.nextInt(commited.size())).get();
            // do something useful with arr...
        } 
    }

}

答案 5 :(得分:0)

  • 您需要两个静态引用:readArraywriteArray以及一个简单的互斥锁,用于跟踪写入何时更改。

  • 有一个名为changeWriteArray的锁定函数对writeArray的deepCopy进行更改:

    synchronized String [] changeWriteArray(String [] writeArrayCopy,其他参数转到这里){           //这里对writeArray的deepCopy进行更改

          //then return deepCopy
          return writeArrayCopy;
    }
    
  • 请注意changeWriteArray是函数式编程,实际上没有副作用,因为它返回的副本既不是readArray也不是writeArray

  • 无论是谁changeWriteArray,都必须将其称为writeArray = changeWriteArray(writeArray.deepCopy())

  • changeWriteArrayupdateReadArray都会更改互斥锁,但只会updateReadArray进行检查。如果设置了互斥锁,updateReadArray只会将readArray的引用指向writeArray的实际块

编辑:

@vemv关于你提到的答案。虽然这些想法是相同的,但差别很大:两个静态引用是static,因此没有花时间将更改实际复制到readArray;相反,readArray的指针移动到指向writeArray。实际上,我们通过changeWriteArray生成的tmp数组进行交换。此处的锁定也是最小的,因为读取不需要锁定,因为在任何给定时间您都可以拥有多个读取器。

事实上,通过这种方法,您可以保留并发读者的数量,并检查计数器为零,以便何时使用readArray更新writeArray;再次,进一步说阅读根本不需要锁定。

答案 6 :(得分:0)

改进@ zhong.j.yu的答案,排队写入而不是尝试在它们发生时执行它们是一个好主意。但是,当更新速度如此之快以至于读者不断加入更新时,我们必须解决这个问题。我的想法是,如果reades只执行在读取之前排队的写入,并忽略后续写入(那些将是下次阅读解决。)

您需要编写自己的同步队列。它将基于链表,并且只包含两种方法:

public synchronised enqeue(Write write);

此方法将原子排队写入。当写入速度快于将它们排队时,可能会出现死锁,但我认为每秒必须有数十万次写入才能实现。

public synchronised Element cut();

这将原子地清空队列并将其头部(或尾部)作为Element对象返回。它将包含一系列其他元素(Element.next等等,只是通常的链接列表),所有那些代表自上次读取以来的写入链。然后队列将为空,准备接受新写入。然后,读者可以跟踪元素链(此时将是独立的,未被后续写入触及),执行写入,最后执行读取。当读取器处理读取时,新写入将在队列中排队,但这些将是下次读取的问题。

我写了一次,虽然在C ++中,代表一个声音数据缓冲区。有更多的写入(驱动程序发送更多数据),而不是读取(数据上的一些数学内容),而写入必须尽快完成。 (数据是实时的,所以我需要在驱动程序中下一批准备就绪之前保存它们。)

答案 7 :(得分:0)

我有一个有趣的解决方案,使用三个数组和一个易失的布尔切换。基本上,两个线程都有其自己的数组。此外,还有一个通过切换控制的共享阵列。

编写器完成并且切换允许时,它将新写入的数组复制到共享数组中并翻转切换。

类似地,在阅读器启动之前,如果切换允许,它将共享数组复制到其自己的数组中并翻转切换。

public class MolecularArray {
    private final double[] writeArray;
    private final double[] sharedArray;
    private final double[] readArray;

    private volatile boolean writerOwnsShared;

    MolecularArray(int length) {
        writeArray = new double[length];
        sharedArray = new double[length];
        readArray = new double[length];
    }

    void read(Consumer<double[]> reader) {
        if (!writerOwnsShared) {
            copyFromTo(sharedArray, readArray);
            writerOwnsShared = true;
        }
        reader.accept(readArray);
    }

    void write(Consumer<double[]> writer) {
        writer.accept(writeArray);
        if (writerOwnsShared) {
            copyFromTo(writeArray, sharedArray);
            writerOwnsShared = false;
        }
    }

    private void copyFromTo(double[] from, double[] to) {
        System.arraycopy(from, 0, to, 0, from.length);
    }
}
  • 这取决于“单一作者线程和单一读者”的假设。
  • 它永远不会阻塞。
  • 它使用恒定(尽管很大)的内存。
  • 在没有任何介入read的情况下重复调用write不会复制,反之亦然。
  • 读者不一定会看到最新数据,但会看到从前一个write开始的第一个read开始的数据(如果有)。

我想,可以使用两个共享数组来改善这一点。