我正在编写一个音频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;
启动producerThread
和consumerThread
后,他们只会分别执行方法producerLoop
和consumerLoop
。
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();
}
}
非常感谢!
答案 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那样,但它可以解决您的问题。