3 回答
TA贡献1863条经验 获得超2个赞
我想知道仅在方法上同步是否线程安全
add
?contains
如果不同步,是否有任何可能的问题?
简短回答:否和是。
有两种解释方式:
直观的解释
Java 同步(以其各种形式)防止许多事情,包括:
两个线程同时更新共享状态。
一个线程试图读取状态,而另一个正在更新它。
线程看到过时的值,因为内存缓存尚未写入主内存。
在您的示例中,同步add
就足以确保两个线程不能同时更新HashSet
,并且两个调用都将在最新HashSet
状态下运行。
但是,如果contains
也不同步,则contains
调用可能会与调用同时发生add
。这可能导致contains
调用看到 的中间状态HashSet
,从而导致不正确的结果,或者更糟。如果调用不是同时发生的,这也会发生,因为更改没有立即刷新到主内存和/或读取线程没有从主内存读取。
内存模型解释
JLS 指定了 Java 内存模型,它规定了多线程应用程序必须满足的条件,以保证一个线程可以看到另一个线程所做的内存更新。该模型是用数学语言表达的,并不容易理解,但要点是当且仅当从写入到后续读取之间存在一系列 happen before关系时,才能保证可见性。如果写入和读取在不同的线程中,那么线程之间的同步是这些关系的主要来源。例如在
// thread one
synchronized (sharedLock) {
sharedVariable = 42;
}
// thread two
synchronized (sharedLock) {
other = sharedVariable;
}
假设线程一的代码在线程二的代码之前运行,则线程一释放锁和线程二获取锁之间存在happens before关系。有了这个和“程序顺序”的关系,我们就可以建立一个从写入42到赋值到的链条other。这足以保证other将被分配42(或可能是变量的以后值)并且sharedVariable之前没有任何值42被写入它。
如果synchronized块不在同一个锁上同步,第二个线程可能会看到一个过时的值sharedVariable;即之前写入的一些值42被分配给它。
TA贡献1810条经验 获得超5个赞
该代码对于该 synchronized (pSet) { }部分是线程安全的:
if (!pSet.contains(x)) {
synchronized (pSet) {
// Here you are sure to have the updated value of pSet
if (!pSet.contains(x)) {
// do some exclusive work with x.
pSet.add(x);
}
}
因为在synchronized对象的声明中pSet:
一个且只有一个线程可能在这个块中。
在其中,pSet它的更新状态也由与 synchronized 关键字的 happens-before 关系保证。
因此,无论等待线程的第一个语句返回的值是什么if (!pSet.contains(x)),当这个被等待的线程醒来并进入语句时 synchronized,它都会设置最后更新的值pSet。因此,即使前一个线程添加了相同的元素,第二个线程 if (!pSet.contains(x))也会返回false。
但是这段代码对于if (!pSet.contains(x))在写入Set.
根据经验,不应该使用未设计为线程安全的集合来执行并发的写入和读取操作,因为集合的内部状态可能处于正在进行/不一致的状态,以进行同时发生的读取操作一个写操作。
虽然一些非线程安全的集合实现在事实中接受了这样的用法,但这根本不能保证它总是正确的。
所以你应该使用线程安全的Set实现来保证整个线程安全。
例如:
Set<String> pSet = ConcurrentHashMap.newKeySet();
这在引擎盖下使用 a ConcurrentHashMap,因此没有读取锁和最小的写入锁(仅在要修改的条目上而不是整个结构上)。
TA贡献1982条经验 获得超2个赞
不,
您不知道在另一个线程添加期间哈希集可能处于什么状态。可能正在进行根本性的更改,例如存储桶的拆分,因此在另一个线程添加期间包含可能会返回false ,即使该元素将存在于单线程 HashSet 中。在那种情况下,您将尝试第二次添加元素。
更糟糕的情况:由于两个线程同时使用的内存中的 HashSet 处于临时无效状态,contains可能会陷入死循环或抛出异常。
添加回答
举报