基本数组写入的Java并发可见性
我最近在我的代码库中发现了这个宝石:基本数组写入的Java并发可见性,java,concurrency,memory-barriers,java-memory-model,Java,Concurrency,Memory Barriers,Java Memory Model,我最近在我的代码库中发现了这个宝石: /** 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,
/** 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();
matrix[x][y] = n;
matrixV = matrix; // volatile write
线程2
publisher.syncChanges();
myVar = matrix[x][y];
float[][] m = matrixV; // volatile read
myVar = m[x][y];
or simply
myVar = matrixV[x][y];
线程1是一个持续运行的后台更新线程。线程2是一个HTTP工作线程,它不关心它读取的内容是否以任何方式一致或原子,只关心写入“最终”到达那里,并且不会作为并发神的产品丢失
现在,这触发了我所有的警钟。自定义并发算法写在不相关代码的深处
不幸的是,修复代码并不是一件小事。Java对并发基元矩阵的支持不是很好。解决这个问题的最清晰的方法似乎是使用ReadWriteLock
,但这可能会对性能产生负面影响。显然,正确性更为重要,但在将其从性能敏感区域剥离出来之前,我似乎应该证明这是不正确的
根据,以下创建发生在关系之前:
线程中的每个动作都发生在该线程中的每个动作之前,该线程中的每个动作都是按照程序的顺序稍后出现的
对易失性字段的写入发生在对该字段的每次后续读取之前。易失性字段的写入和读取与进入和退出监视器具有类似的内存一致性效果,但不需要互斥锁定
听起来像是:
- 矩阵写入发生在发布()之前(规则1)
- 发布()发生在syncChanges()之前(规则2)
- syncChanges()发生在矩阵读取之前(规则1)
因此,代码确实为矩阵建立了一个先发生后发生的链
但我不相信。并发很难,我不是领域专家。
我错过了什么?这确实安全吗?就可见性而言,您只需要在任何易失性字段上进行易失性写读。这会管用的
final float[][] matrix = ...;
volatile float[][] matrixV = matrix;
线程1
float[][] matrix;
matrix[x][y] = n;
publisher.publish();
matrix[x][y] = n;
matrixV = matrix; // volatile write
线程2
publisher.syncChanges();
myVar = matrix[x][y];
float[][] m = matrixV; // volatile read
myVar = m[x][y];
or simply
myVar = matrixV[x][y];
但这只适用于更新一个变量。如果写入线程正在写入多个变量,而读取线程正在读取这些变量,则读取器可能会看到不一致的图片。通常由读写锁处理。写时复制可能适用于某些使用模式
DougLea为Java8提供了一个新的“StampedLock”,它是读写锁的一个版本,读锁的价格要便宜得多。但它也很难使用。基本上读者得到当前版本,然后继续阅读一系列变量,然后再次检查版本;如果版本没有更改,则在读取会话期间没有并发写入。使用volatile
并不是同步所有内容的灵丹妙药。可以保证,如果另一个线程读取一个易失性变量的更新值,那么他们也会看到在此之前对一个非易失性变量所做的每一次更改。但是没有任何东西保证另一个线程将读取更新的值
在示例代码中,如果您多次写入matrix
,然后调用publish()
,而另一个线程调用synch()
,然后读取该矩阵,则另一个线程可能会看到部分、全部或无任何更改:
- 如果从publish()读取更新的值,则所有更改
- 如果它读取旧的发布值,并且所有更改都没有泄漏,则不会进行任何更改
- 如果它读取以前发布的值,则会进行一些更改,但有些更改已泄漏
请参见这对于向矩阵发布单个更新看起来是安全的,但当然它不提供任何原子性。这是否正确取决于您的应用程序,但它可能应该记录在这样的实用程序类中
但是,它包含一些冗余,可以通过使sync
字段final
进行改进。该字段的volatile
访问是两个存储屏障中的第一个;根据约定,调用incrementAndGet()
对内存的影响与对易失性变量的写入和读取相同,而调用get()
对内存的影响与读取相同
因此,代码可以仅依赖于这些方法提供的同步,并使字段本身成为最终的
您正确地提到了关系发生之前的规则#2
对易失性字段的写入发生在对该字段的每次后续读取之前
但是,它不能保证在绝对时间线上的syncChanges()之前调用publish()。让我们稍微改变一下你的例子
线程1:
线程2:
a和b变量的选项有哪些
- a是0,b可以是0或42
- a是1,b是42,因为关系发生在之前
- a大于1(线程2由于某种原因速度较慢,线程1幸运地发布了几次更新),b的值取决于业务逻辑和处理矩阵的方式—它是否取决于以前的状态李>
- 如果线程2时不时地轮询矩阵的状态,并且在这两者之间有一些过时的值是完全可以的,那么如果最终会处理正确的值,那么就让它保持原样李>
- 如果线程2不关心错过的更新,但它总是希望观察最新的矩阵,那么就使用copy on write collections或使用ReaderWriteLock,如上所述 <> LI>如果线程2关心单个更新,则应该以更智能的方式处理,您可能需要考虑WaIT()/NoTIFY()模式,并在更新矩阵时通知线程2。李>
publisher.sync()代码>在线程2中
bepublisher.syncChanges()
?即使对矩阵进行了一次更新,这个publisher
类也毫无用处。另一方面也没有保证