Java 如何在没有同步方法的情况下安全地从不同的线程刷新缓冲区?

Java 如何在没有同步方法的情况下安全地从不同的线程刷新缓冲区?,java,multithreading,synchronized,Java,Multithreading,Synchronized,有多个线程,比如B、C和D,每个线程都以高频率将小数据包写入缓冲区。他们拥有自己的缓冲区,没有其他人会写入缓冲区。写作必须尽可能快,我已经确定使用synchronized会让速度慢得令人无法接受 缓冲区只是字节数组,以及第一个空闲元素的索引: byte[] buffer; int index; public void write(byte[] data) { // some checking that the buffer won't overflow... not important

有多个线程,比如B、C和D,每个线程都以高频率将小数据包写入缓冲区。他们拥有自己的缓冲区,没有其他人会写入缓冲区。写作必须尽可能快,我已经确定使用
synchronized
会让速度慢得令人无法接受

缓冲区只是字节数组,以及第一个空闲元素的索引:

byte[] buffer;
int index;

public void write(byte[] data) {
    // some checking that the buffer won't overflow... not important now
    System.arraycopy(data, 0, buffer, index, data.length);
    index += data.length;
}
每隔一段时间,线程a就会出现,将每个人的缓冲区刷新到一个文件中。如果这部分有一些开销也没关系,所以在这里使用
synchronized
没有问题

现在的问题是,当线程a刷新缓冲区时,其他线程可能正在写入缓冲区。这意味着两个线程几乎同时尝试写入
索引。这将导致数据损坏,我想防止数据损坏,但在
write()
方法中不使用
synchronized

我觉得,使用正确的操作顺序和一些
易失性
字段,这一定是可能的。有什么好主意吗?

您是否尝试过使用同步的解决方案,但发现它的性能不够好?你说你已经确定它的速度慢得让人无法接受——有多慢,你是否已经有了绩效预算?通常,获得一个无争用的锁非常便宜,所以我不认为这是个问题

很可能会有一些聪明的无锁解决方案,但它可能比需要访问共享数据时只进行同步要复杂得多。我知道无锁编码是非常流行的,当你能做到的时候,它可以很好地扩展——但是如果你有一个线程干扰另一个线程的数据,那么很难安全地完成它。需要明确的是,当我可以使用专家创建的高级抽象时,我喜欢使用无锁代码,比如.NET4中的并行扩展。如果我能帮忙的话,我就是不喜欢使用易失性变量之类的低级抽象

尝试锁定,并对其进行基准测试。找出可接受的性能,并将简单解决方案的性能与该目标进行比较

当然,一个选择是重新设计。。。冲洗是否必须在不同的线程中主动进行?单个写入线程是否可以周期性地将缓冲区交给刷新线程(并启动不同的缓冲区)?那会使事情简单得多

编辑:关于你的“同花顺信号”的想法——我一直在考虑类似的问题。但是你需要小心你是如何做到的,这样即使一个线程需要很长时间来处理它正在做的事情,信号也不会丢失。我建议你让线程A发布一个“刷新计数器”。。。每个线程都有自己的上次刷新时间计数器

编辑:刚刚意识到这是Java,不是C(更新:)

使用
AtomicLong.incrementAndGet()
从线程A递增,使用
AtomicLong.get()
从其他线程读取。然后在每个线程中,比较您是否“最新”,必要时刷新:

private long lastFlush; // Last counter for our flush
private Flusher flusher; // The single flusher used by all threads 

public void write(...)
{
    long latestFlush = flusher.getCount(); // Will use AtomicLong.get() internally
    if (latestFlush > lastFlush)
    {
        flusher.Flush(data);
        // Do whatever else you need
        lastFlush = latestFlush; // Don't use flusher.getCount() here!
    }
    // Now do the normal write
}

注意,这假设您只需要检查Write方法中的刷新。显然,情况可能并非如此,但希望您能够适应这种想法。

您可以单独使用volatile安全地读/写缓冲区(如果您只有一个writer),但是,只有一个线程可以安全地刷新数据。为此,您可以使用一个环形缓冲区

对于@Jon的评论,我想补充一点,那就是测试起来要复杂得多。e、 我有一个“解决方案”,它一天持续处理10亿条消息,但第二天却不断被破坏,因为盒子里的内容更多

同步后,您的延迟应低于2微秒。有了锁,你可以把时间缩短到1微秒。在忙着等待一个volatile时,您可以将其降低到每字节3-6 ns(线程之间传输数据所需的时间变得很重要)

注意:随着数据量的增加,锁的相对成本变得不那么重要。e、 如果你通常写200字节或更多,我不会担心差异


我采取的一种方法是使用带有两个直接ByteBuffer的交换机,避免在关键路径中写入任何数据(即,仅在我处理完所有内容后才写入数据,这并不重要)

您可以尝试实现。

可变变量和循环缓冲区

使用循环缓冲区,并使刷新线程“追踪”缓冲区周围的写操作,而不是在每次刷新后将索引重置为零。这允许在刷新期间进行写操作,而无需任何锁定

使用两个可变变量-
writeIndex
表示写入线程达到的位置,使用
flushIndex
表示刷新线程达到的位置。这些变量只由一个线程更新,并且可以由另一个线程原子地读取。使用这些变量将线程约束到缓冲区的各个部分。不允许刷新线程经过写入线程到达的位置(即刷新缓冲区的未写入部分)。不允许写入线程通过刷新线程的位置(即覆盖缓冲区中未刷新的部分)

写入线程循环:

  • 读写索引(原子)
  • 读取
    flushIndex
    (原子)
  • 检查此写入操作是否不会覆盖未刷新的数据
  • 写入缓冲区
  • 计算
    writeIndex的新值
  • 设置写入索引(原子)
冲洗螺纹环:

  • 读写索引(原子)
  • 读取
    flushIndex
    (原子)
  • 将缓冲区从
    flushIndex
    刷新到
    writeIndex-1
  • flushIndex
    (原子)设置为为为
    writeInd读取的值
    
    volatile int writeIndex = 0;
    volatile int flushIndex = 0;
    byte[] buffer = new byte[268435456];
    
    public void write(byte[] data) throws Exception {
        int localWriteIndex = writeIndex; // volatile read
        int localFlushIndex = flushIndex; // volatile read
    
        int freeBuffer = buffer.length - (localWriteIndex - localFlushIndex +
            buffer.length) % buffer.length;
    
        if (data.length > freeBuffer)
            throw new Exception("Buffer overflow");
    
        if (localWriteIndex + data.length <= buffer.length) {
            System.arraycopy(data, 0, buffer, localWriteIndex, data.length);
            writeIndex = localWriteIndex + data.length;
        }
        else
        {
            int firstPartLength = buffer.length - localWriteIndex;
            int secondPartLength = data.length - firstPartLength;
    
            System.arraycopy(data, 0, buffer, localWriteIndex, firstPartLength);
            System.arraycopy(data, firstPartLength, buffer, 0, secondPartLength);
    
            writeIndex = secondPartLength;
        }
    }
    
    public byte[] flush() {
        int localWriteIndex = writeIndex; // volatile read
        int localFlushIndex = flushIndex; // volatile read
    
        int usedBuffer = (localWriteIndex - localFlushIndex + buffer.length) %
            buffer.length;
        byte[] output = new byte[usedBuffer];
    
        if (localFlushIndex + usedBuffer <= buffer.length) {
            System.arraycopy(buffer, localFlushIndex, output, 0, usedBuffer);
            flushIndex = localFlushIndex + usedBuffer;
        }
        else {
            int firstPartLength = buffer.length - localFlushIndex;
            int secondPartLength = usedBuffer - firstPartLength;
    
            System.arraycopy(buffer, localFlushIndex, output, 0, firstPartLength);
            System.arraycopy(buffer, 0, output, firstPartLength, secondPartLength);
    
            flushIndex = secondPartLength;
        }
    
        return output;
    }
    
    final AtomicReference<byte[]> buffer=new AtomicReference<byte[]>(new byte[0]);
    void write(byte[] b){
        for(;;){
            final byte[] cur = buffer.get();
            final byte[] copy = Arrays.copyOf(cur, cur.length+b.length);
            System.arraycopy(b, 0, cur, cur.length, b.length);
            if (buffer.compareAndSet(cur, copy)){
                break;
            }
                //there was a concurrent write
                //need to handle it, either loop to add at the end but then you can get out of order
                //just as sync
        }
    }
    
    package bestsss.util;
    
    import java.util.Arrays;
    import java.util.concurrent.ConcurrentLinkedQueue;
    import java.util.concurrent.atomic.AtomicInteger;
    
    //the code uses ConcurrentLinkedQueue to simplify the implementation
    //the class is well - know and the main point is to demonstrate the  lock-free stuff
    public class TheBuffer{
        //buffer generation, if the room is exhaused need to update w/ a new refence
        private static class BufGen{
            final byte[] data;
            volatile int size;
    
            BufGen(int capacity, int size, byte[] src){
                this.data = Arrays.copyOf(src, capacity);
                this.size  = size;
            }
    
            BufGen append(byte[] b){
                int s = this.size;
                int newSize = b.length+s;
                BufGen target;
                if (newSize>data.length){
                    int cap = Integer.highestOneBit(newSize)<<1;
                    if (cap<0){
                        cap = Integer.MAX_VALUE;                    
                    }               
                    target = new BufGen(cap, this.size, this.data);             
                } 
                else if(newSize<0){//overflow 
                    throw new IllegalStateException("Buffer overflow - over int size");
                } else{ 
                    target = this;//if there is enough room(-service), reuse the buffer
                }
                System.arraycopy(b, 0, target.data, s, b.length);
                target.size = newSize;//'commit' the changes and update the size the copy part, so both are visible at the same time
                //that's the volatile write I was talking about
                return target;
            }       
        }
    
        private volatile BufGen buffer = new BufGen(16,0,new byte[0]);
    
        //read consist of 3 volatile reads most of the time, can be 2 if BufGen is recreated each time
        public byte[] read(int[] targetSize){//ala AtomicStampedReference
            if (!pendingWrites.isEmpty()){//optimistic check, do not grab the look and just do a volatile-read
                //that will serve 99%++ of the cases
                doWrite(null, READ);//yet something in the queue, help the writers
            }
            BufGen buffer = this.buffer;
            targetSize[0]=buffer.size;
            return  buffer.data;
        }
        public void write(byte[] b){
            doWrite(b, WRITE);
        }
    
        private static final int FREE = 0;
        private static final int WRITE = 1;
        private static final int READ= 2;
    
        private final AtomicInteger state = new AtomicInteger(FREE);
        private final ConcurrentLinkedQueue<byte[]> pendingWrites=new ConcurrentLinkedQueue<byte[]>();
        private void doWrite(byte[] b, int operation) {
            if (state.compareAndSet(FREE, operation)){//won the CAS hurray!
                //now the state is held "exclusive"
                try{
                    //1st be nice and poll the queue, that gives fast track on the loser
                    //we too nice 
                    BufGen buffer = this.buffer;
                    for(byte[] pending; null!=(pending=pendingWrites.poll());){
                        buffer = buffer.append(pending);//do not update the global buffer yet
                    }
                    if (b!=null){
                        buffer = buffer.append(b);
                    }
                    this.buffer = buffer;//volatile write and make sure any data is updated
                }finally{
                    state.set(FREE);
                }
            } 
            else{//we lost the CAS, well someone must take care of the pending operation 
                if (b==null)
                    return;
    
                pendingWrites.add(b);           
            }
        }
    
    
        public static void main(String[] args) {
            //usage only, not a test for conucrrency correctness
            TheBuffer buf = new TheBuffer();        
            buf.write("X0X\n".getBytes());
            buf.write("XXXXXXXXXXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXXXXXXXXXXXXXXXXXXX\n".getBytes());
            buf.write("Hello world\n".getBytes());
            int[] size={0};
            byte[] bytes = buf.read(size);
            System.out.println(new String(bytes, 0, size[0]));
        }
    }
    
    package bestsss.util;
    
    import java.util.ArrayList;
    import java.util.concurrent.ConcurrentLinkedQueue;
    
    public class TheSimpleBuffer {
        private final ConcurrentLinkedQueue<byte[]> writes =new ConcurrentLinkedQueue<byte[]>();
        public void write(byte[] b){
            writes.add(b);
        }
    
        private byte[] buffer;
        public byte[] read(int[] targetSize){
            ArrayList<byte[]> copy = new ArrayList<byte[]>(12);
            int len = 0;
            for (byte[] b; null!=(b=writes.poll());){
                copy.add(b);
                len+=b.length;
                if (len<0){//cant return this big, overflow 
                    len-=b.length;//fix back;
                    break;
                }
            }
            //copy, to the buffer, create new etc....
            //...
    
            ///
            targetSize[0]=len;
            return buffer; 
        }
    
    }
    
    import java.util.concurrent.atomic;    
    
    byte[] buffer;
    AtomicInteger index;
    
    public void write(byte[] data) {
        // some checking that the buffer won't overflow... not important now
        System.arraycopy(data, 0, buffer, index, data.length);
        index.addAndGet(data.length);
    }
    
    public int getIndex() {
        return index.get().intValue();
    }
    
    byte[] buffer;
    int index;
    ReentrantReadWriteLock lock;
    
    public void write(byte[] data) {
        lock.writeLock().lock();
        // some checking that the buffer won't overflow... not important now
        System.arraycopy(data, 0, buffer, index, data.length);
        index += data.length;
        lock.writeLock.unlock();
    }
    
    object.lock.readLock().lock(); 
    // flush the buffer      
    object.index = 0;                     
    object.lock.readLock().unlock();
    
    final int SIZE = 99;
    byte[] buffer = new byte[SIZE];
    int index;
    // Use default non-fair lock to maximise throughput (although some writer threads may wait longer)
    ReentrantLock lock = new ReentrantLock();
    
    // called by many threads
    public void write(byte[] data) {
        lock.lock();
        // some checking that the buffer won't overflow... not important now        
        System.arraycopy(data, 0, buffer, index, data.length);
        index += data.length;
        lock.unlock();
    }
    
    // Only called by 1 thread - or implemented in only 1 thread:
    public byte[] flush() {
        byte[] rval = new byte[index];
        lock.lock();
        System.arraycopy(buffer, 0, rval, 0, index);
        index = 0;
        lock.unlock();
        return rval;
    }
    
    LinkedBlockingQueue<byte[]> jobs;//here the buffers intended to be flushed are pushed into 
    LinkedBlockingQueue<byte[]> pool;//here the flushed buffers are pushed into for reuse
    
    while (someCondition) {
         job = jobs.take();
         actualOutput(job);
         pool.offer(job);
    }
    
    void flush() {
         jobs.offer(this.buffer);
         this.index = 0;
         this.buffer = pool.poll();
         if (this.buffer == null) 
              this.buffer = createNewBuffer();
    }
    void write(byte[] data) {
        // some checking that the buffer won't overflow... not important now
        System.arraycopy(data, 0, buffer, index, data.length);
        if ((index += data.length) > threshold) 
             this.flush();
    }