我最近在我的代码库中找到了这个gem:
/** This class is used to "publish" changes to a non-volatile variable.
*
* Access to non-volatile and volatile variables cannot be reordered,
* so if you make changes to a non-volatile variable before calling publish,
* they are guaranteed to be visible to a thread which calls syncChanges
*
*/
private static class Publisher {
//This variable may not look like it's doing anything, but it really is.
//See the documentaion for this class.
private volatile AtomicInteger sync = new AtomicInteger(0);
void publish() {
sync.incrementAndGet();
}
/**
*
* @return the return value of this function has no meaning.
* You should not make *any* assumptions about it.
*/
int syncChanges() {
return sync.get();
}
}
这样使用:
线程1
float[][] matrix;
matrix[x][y] = n;
publisher.publish();
线程2
publisher.syncChanges();
myVar = matrix[x][y];
线程1是一个连续运行的后台更新线程。线程2是一个HTTP工作线程,它不关心它读取的内容是以任何方式一致的还是原子的,只是写入“最终”到达并且不会作为并发神的提供而丢失。
现在,这会触发我所有的警告铃声。自定义并发算法深入到无关代码中。
不幸的是,修复代码并非易事。 Java对并发原始矩阵的支持并不好。看起来最明确的解决方法是使用ReadWriteLock
,但这可能会对性能产生负面影响。显而易见,正确性更为重要,但似乎我应该在将其从性能敏感区域中删除之前证明这是 正确。
根据the java.util.concurrent documentation,以下创建happens-before
关系:
线程中的每个动作都发生在该线程中的每个动作之前,该动作在程序的顺序中稍后出现。
在对该相同字段的每次后续读取之前发生对易失性字段的写入。易失性字段的写入和读取与进入和退出监视器具有相似的内存一致性效果,但不需要互斥锁定。
所以听起来像是:
因此代码确实为矩阵建立了一个先发条件链。
但我不相信。并发很难,我不是域专家。 我错过了什么?这确实安全吗?
答案 0 :(得分:4)
就可见性而言,您需要的是在任何易失性字段上的易读写入。这可以工作
final float[][] matrix = ...;
volatile float[][] matrixV = matrix;
线程1
matrix[x][y] = n;
matrixV = matrix; // volatile write
线程2
float[][] m = matrixV; // volatile read
myVar = m[x][y];
or simply
myVar = matrixV[x][y];
但这只适用于更新一个变量。如果编写器线程正在写入多个变量并且读取线程正在读取它们,则读者可能会看到不一致的图片。通常它由读写锁处理。写时复制可能适合某些使用模式。
Doug Lea为Java8提供了一个新的“StampedLock”http://gee.cs.oswego.edu/dl/jsr166/dist/jsr166edocs/jsr166e/StampedLock.html,它是一个读写锁的版本,对于读锁来说要便宜得多。但它也很难使用。基本上读者获取当前版本,然后继续读取一堆变量,然后再次检查版本;如果版本没有更改,则在读取会话期间没有并发写入。
答案 1 :(得分:4)
对于发布矩阵的单个更新看起来确实安全,但当然它不提供任何原子性。这是否合适取决于您的应用程序,但它应该记录在这样的实用程序类中。
但是,它包含一些冗余,可以通过制作sync
字段final
来改进。此字段的volatile
访问权限是两个内存屏障中的第一个;通过契约,调用incrementAndGet()
对内存的影响与写入和读取volatile变量的影响相同,调用get()
与读取具有相同的效果。
因此,代码可以单独依赖这些方法提供的同步,并使字段本身final
。
答案 2 :(得分:2)
使用volatile
并不是同步所有内容的灵丹妙药。保证如果另一个线程读取volatile变量的更新值,它们也会看到在此之前对非volatile变量进行的每个更改。但没有什么可以保证其他线程会读取更新后的值。
在示例代码中,如果您对matrix
进行多次写入然后调用publish()
,而另一个线程调用synch()
然后读取矩阵,那么另一个线程可能会看到一些,全部或没有变化:
请参阅this article
答案 3 :(得分:1)
你正确地提到了之前关系发生的规则#2
在对该相同字段的每次后续读取之前发生对易失性字段的写入。
但是,它不保证在绝对时间轴上的syncChanges()之前调用publish()。让我们稍微改变你的例子。
主题1:
matrix[0][0] = 42.0f;
Thread.sleep(1000*1000); // assume the thread was preempted here
publisher.publish(); //assume initial state of sync is 0
主题2:
int a = publisher.syncChanges();
float b = matrix[0][0];
a和b变量的选项有哪些?
如何处理?这取决于业务逻辑。