java - 仅在添加到 HashSet 时同步是否是线程安全的?
问题描述
想象一下,有一个主线程创建一个 HashSet 并启动许多工作线程,将 HashSet 传递给它们。
就像下面的代码一样:
void main() {
final Set<String> set = new HashSet<>();
final ExecutorService threadExecutor =
Executors.newFixedThreadPool(10);
threadExecutor.submit(() -> doJob(set));
}
void doJob(final Set<String> pSet) {
// do some stuff
final String x = ... // doesn't matter how we received the value.
if (!pSet.contains(x)) {
synchronized (pSet) {
// double check to prevent multiple adds within different threads
if (!pSet.contains(x)) {
// do some exclusive work with x.
pSet.add(x);
}
}
}
// do some stuff
}
我想知道仅在 add 方法上同步是否是线程安全的?contains
如果不同步,是否有任何可能的问题?
我的直觉告诉我这很好,在离开同步块更改后对所有线程都应该可见,但 JMM 有时可能违反直觉。
PS我不认为它是How to lock multiple resources in java multithreading的重复 即使两者的答案可能相似,这个问题解决了更特殊的情况。
解决方案
我想知道仅在
add
方法上同步是否是线程安全的?contains
如果不同步,是否有任何可能的问题?
简短的回答:不,是。
有两种解释方式:
直观的解释
Java 同步(以各种形式)可以防止许多事情,包括:
- 两个线程同时更新共享状态。
- 一个线程试图读取状态,而另一个线程正在更新它。
- 由于内存缓存尚未写入主内存,线程看到过时的值。
在您的示例中,同步add
足以确保两个线程不能同时更新HashSet
,并且两个调用都将在最新HashSet
状态下运行。
但是,如果contains
不同步,则contains
呼叫可能与add
呼叫同时发生。这可能会导致contains
调用看到 的中间状态HashSet
,从而导致不正确的结果,或者更糟。如果调用不是同时发生的,也可能发生这种情况,因为更改没有立即刷新到主内存和/或读取线程没有从主内存读取。
记忆模型解释
JLS 指定了 Java 内存模型,它规定了多线程应用程序必须满足的条件,以保证一个线程看到另一个线程进行的内存更新。该模型用数学语言表示,不易理解,但要点是当且仅当在从写入到后续读取的关系之前存在一连串发生时,才能保证可见性。如果写入和读取在不同的线程中,那么线程之间的同步是这些关系的主要来源。例如在
// thread one
synchronized (sharedLock) {
sharedVariable = 42;
}
// thread two
synchronized (sharedLock) {
other = sharedVariable;
}
假设线程一的代码在线程二的代码之前运行,那么线程一释放锁和线程二获取锁之间存在着先于关系。有了这个和“程序顺序”的关系,我们可以建立一个从 write of42
到 assignment to的链other
。这足以保证other
将被分配42
(或可能是变量的后续值),而不是sharedVariable
之前42
写入的任何值。
如果没有在synchronized
同一个锁上同步块,第二个线程可能会看到一个过时的值sharedVariable
; 即之前写入它的一些值42
被分配给它。
推荐阅读
- google-app-maker - Google 应用制作工具中的文本编辑器小部件样式
- string - 字符串格式未在标签文本中正确显示
- r - xaringan metropolis 代码输出中没有连字
- json - Pentaho 数据集成在 json 中使用插入/更新匹配键
- templates - 如何在 Openshift 模板中将用户添加到 ClusterRoleBinding
- php - 如何在页面重新加载时保留 $_REQUEST["id"] 值
- javascript - Codeigniter - 加载另一个视图单击按钮
- laravel - 使用 Laravel 的未知自定义元素 Vue
- installation - 在 Linux/UNIX 上从源代码构建 Ghostscript
- shell - ESXi shell 脚本删除消息