gpt4 book ai didi

Java并发场景——需要同步还是不需要?

转载 作者:太空狗 更新时间:2023-10-29 22:43:09 25 4
gpt4 key购买 nike

这是交易。我有一个 HashMap ,其中包含我称为“程序代码”的数据,它存在于一个对象中,如下所示:

Class Metadata
{
private HashMap validProgramCodes;
public HashMap getValidProgramCodes() { return validProgramCodes; }
public void setValidProgramCodes(HashMap h) { validProgramCodes = h; }
}

我有很多很多读取器线程,每个线程都会调用一次 getValidProgramCodes(),然后将该 HashMap 用作只读资源。

到目前为止一切顺利。这就是我们变得有趣的地方。

我想放入一个计时器,它每隔一段时间生成一个新的有效程序代码列表(不管如何),并调用 setValidProgramCodes。

我的理论——我需要帮助来验证——是我可以继续按原样使用代码,而无需进行显式同步。它是这样的:在更新 validProgramCodes 时,validProgramCodes 的值总是好的——它是指向新 HashMap 或旧 HashMap 的指针。 这是一切都取决于的假设。拥有旧散列图的读者是可以的;他可以继续使用旧值,因为在他释放它之前它不会被垃圾回收。每个读者都是短暂的;它很快就会死去,并被一个新的取而代之并获得新的值(value)。

这站得住脚吗?我的主要目标是在绝大多数没有更新发生的情况下避免昂贵的同步和阻塞。我们大约每小时只更新一次,读者不断地进进出出。

最佳答案

使用 volatile

这是一个线程关心另一个线程在做什么的情况吗?然后 JMM FAQ有答案:

Most of the time, one thread doesn't care what the other is doing. But when it does, that's what synchronization is for.

针对那些说 OP 的代码按原样是安全的人的回应,考虑一下:Java 的内存模型中没有任何东西可以保证在启动新线程时该字段将刷新到主内存。此外,只要更改在线程中检测不到,JVM 就可以自由地重新排序操作。

从理论上讲,读取器线程不能保证看到对 validProgramCodes 的“写入”。在实践中,他们最终会,但你不能确定什么时候。

我建议将 validProgramCodes 成员声明为“volatile”。速度差异可以忽略不计,并且无论引入何种 JVM 优化,它都将保证您的代码现在和将来的安全。

这里有一个具体的建议:

import java.util.Collections;

class Metadata {

private volatile Map validProgramCodes = Collections.emptyMap();

public Map getValidProgramCodes() {
return validProgramCodes;
}

public void setValidProgramCodes(Map h) {
if (h == null)
throw new NullPointerException("validProgramCodes == null");
validProgramCodes = Collections.unmodifiableMap(new HashMap(h));
}

}

不变性

除了用 unmodifiableMap 包装它之外,我还在复制映射 (new HashMap(h))。这使得即使 setter 的调用者继续更新映射“h”也不会改变的快照。例如,他们可能会清除 map 并添加新条目。

依赖接口(interface)

在风格上,通常最好用 ListMap 等抽象类型声明 API,而不是像 ArrayList 这样的具体类型和 HashMap。 如果具体类型需要更改(就像我在这里所做的那样),这会在将来提供灵 active 。

缓存

将“h”分配给“validProgramCodes”的结果可能只是对处理器缓存的写入。即使在新线程启动时,“h”对新线程也是不可见的,除非它已被刷新到共享内存。除非有必要,否则良好的运行时将避免刷新,使用 volatile 是表明这是必要的一种方式。

重新排序

假设以下代码:

HashMap codes = new HashMap();
codes.putAll(source);
meta.setValidProgramCodes(codes);

如果 setValidCodes 只是 OP 的 validProgramCodes = h;,编译器可以像这样自由地重新排序代码:

 1: meta.validProgramCodes = codes = new HashMap();
2: codes.putAll(source);

假设在编写器第 1 行执行后,读取器线程开始运行此代码:

 1: Map codes = meta.getValidProgramCodes();
2: Iterator i = codes.entrySet().iterator();
3: while (i.hasNext()) {
4: Map.Entry e = (Map.Entry) i.next();
5: // Do something with e.
6: }

现在假设writer线程在reader的第2行和第3行之间的map上调用了“putAll”,Iterator底层的map经历了一次并发修改,抛出了一个runtime exception——一个非常断断续续的,看似莫名其妙的runtime exception在测试期间从未产生过。

并发编程

任何时候你有一个线程关心另一个线程在做什么,你必须有某种内存屏障来确保一个线程的操作对另一个线程可见。如果一个线程中的事件必须在另一个线程中的事件之前发生,您必须明确指出。否则没有任何保证。实际上,这意味着 volatilesynchronized

不要吝啬。错误程序无法完成工作的速度有多快并不重要。此处显示的示例简单而人为,但请放心,它们说明了现实世界中的并发错误,由于其不可预测性和平台敏感性,这些错误非常难以识别和解决。

其他资源

关于Java并发场景——需要同步还是不需要?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/300316/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com