2D易失性数组:自我赋值帮助还是需要AtomicIntegerArray?

时间:2013-08-22 09:04:44

标签: java multithreading thread-safety volatile

我正在编写一个音频DSP应用程序,我选择使用生产者 - 消费者模型。我一直在阅读很多关于volatile和其他线程问题的内容,但我对我的案例的某些细节有一些疑问 - 特别是,我需要在线程之间共享的一个问题是数组数组。

我有一个代表生产者的类。为了允许处理时间的变化,生产者存储n个缓冲区,每当有更多的音频数据可用时它将填充轮换,并将缓冲区传递给用户线程。

我将从我的问题开始,然后我将尝试详细解释我的系统 - 对于长篇文章感到抱歉,谢谢你的支持!我也非常感谢关于我的实现和线程安全的一般性评论。

我的缓冲区由volatile byte[][]数组表示。我很清楚volatile只会使引用变得不稳定,但是阅读了SO和各种博客帖子后,似乎我有两个选择:

我可以使用AtomicIntegerArray。但是:

  • 我是否会为这样的应用程序降低性能?

  • 原子性甚至是我需要的吗?我打算一次写入整个数组,然后我需要它对另一个线程可见,我不需要每个个人写入是原子的或可见的立即

如果我理解正确(例如this blog post),则自我分配(在我的情况下为buffers[currentBuffer] = buffers[currentBuffer])将确保发布,您将在下面的代码中看到。

  • 这是否正确,是否会导致所有最近的写入变得可见?

  • 这是否适用于像这样的2D数组?


我将简要介绍一下生产者类;这些是实例变量:

// The consumer - just an interface with a process(byte[]) method
AudioInputConsumer consumer;

// The audio data source
AudioSource source;

// The number of buffers
int bufferCount;

// Controls the main producer loop
volatile boolean isRunning = false;

// The actual buffers
volatile byte[][] buffers;

// The number of buffers left to process.
// Shared counter - the producer inrements and checks it has not run
// out of buffers, while the consumer decremenets when it processes a buffer
AtomicInteger buffersToProcess = new AtomicInteger(0);

// The producer thread.
Thread producerThread;

// The consumer thread.
Thread consumerThread;

启动producerThreadconsumerThread后,他们只会分别执行方法producerLoopconsumerLoop

producerLoop在等待音频数据时阻塞,读入缓冲区,在缓冲区上执行自我分配,然后使用AtomicInteger实例向消费者发出信号循环。

private void producerLoop() {
  int bufferSize = source.getBufferSize();
  int currentBuffer = 0;

  while (isRunning) {
    if (buffersToProcess.get() == bufferCount) {
      //This thread must be faster than the processing thread, we have run out
      // of buffers: decide what to do
      System.err.println("WARNING: run out of buffers");
    }

    source.read(buffers[currentBuffer], 0, bufferSize); // Read data into the buffer
    buffers[currentBuffer] = buffers[currentBuffer];    // Self-assignment to force publication (?)
    buffersToProcess.incrementAndGet();                 // Signal to the other thread that there is data to read
    currentBuffer = (currentBuffer + 1) % bufferCount;  // Next buffer
  }
}

consumerLoop等待AtomicInteger buffersToProcess大于零,然后调用使用者对象对数据执行任何操作。之后buffersToProcess递减,我们等待它再次变为非零。

private void consumerLoop() {
  int currentBuffer = 0;

  while (isRunning) {
    if (buffersToProcess.get() > 0) {
      consumer.process(buffers[currentBuffer]);          // Process the data
      buffersToProcess.decrementAndGet();                // Signal that we are done with this buffer
      currentBuffer = (currentBuffer + 1) % bufferCount; // Next buffer
    }
    Thread.yield();
  }
}

非常感谢!

3 个答案:

答案 0 :(得分:2)

你确实需要原子性,因为写入数组是一个非原子过程。具体来说,Java肯定永远不会保证对数组成员的写入将保持不可见到其他线程,直到您选择发布它们为止。

一种选择是每次创建一个新数组,完全初始化它,然后通过volatile发布,但由于Java坚持必须先将新分配的数组清零,这可能会产生很大的成本,由于GC开销。您可以使用“双缓冲”方案来克服这个问题,您只需保留两个阵列并在它们之间切换。这种方法有其危险性:一个线程可能仍在从数组中读取,您的写入线程已经将其标记为非活动状态。这在很大程度上取决于代码的精确细节。

唯一的另一个选择是在一个经典的,无聊的synchronized块中进行整个阅读和写作。这具有在延迟方面非常可预测的优点。就个人而言,如果完全受到实际性能问题的影响,我会从这开始,继续进行更复杂的事情。

您也可以使用读写锁进行锁定,但这只会在多个线程同时读取数组时获得回报。这似乎不是你的情况。

答案 1 :(得分:1)

看看java.util.concurrent.ArrayBlockingQueue。

当您在阻塞队列上调用take()时,它会自动等待,直到某些内容可用。

只需将byte []放在队列中,消费者就会处理它。在这种情况下,需要知道要处理多少缓冲区是无关紧要的。通常,"终止"队列中的项目将代表最后一个缓冲区。将byte []包装在一个带有布尔终止标志的类中会有所帮助。

此致 优素福

答案 2 :(得分:0)

除了@Marko Topolnik上面解释了volatile和atomic如何工作之外,如果你仍然希望在写入数组时实现跨线程可见性的效果,你可以使用Unsafe.putLongVolatile(),Unsafe.putObjectVolatile()以及同一家族中的所有其他方法。

是的,不像Java那样,但它可以解决您的问题。