如何解决没有 volatile 的双重检查锁是错误的?
我使用 jdk1.8。我认为没有 volatile 的双重检查锁是正确的。 我多次使用 countdownlatch 测试,对象是单例。 如何证明它一定需要“volatile”?
更新 1
抱歉,我的代码没有格式化,因为我无法接收一些 JavaScript 公共类 DCLTest {
private static /*volatile*/ Singleton instance = null;
static class Singleton {
public String name;
public Singleton(String name) {
try {
//We can delete this sentence,just to simulate varIoUs situations
Thread.sleep(1);
} catch (InterruptedException e) {
e.printstacktrace();
}
this.name = name;
}
}
public static Singleton getInstance() {
if (null == instance) {
synchronized (Singleton.class) {
if (null == instance) {
instance = new Singleton(Thread.currentThread().getName());
}
}
}
return instance;
}
public static void test() throws InterruptedException {
int count = 1;
while (true){
int size = 5000;
final String[] strs = new String[size];
final CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < size; i++) {
final int index = i;
new Thread(()->{
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printstacktrace();
}
Singleton instance = getInstance();
strs[index] = instance.name;
}).start();
}
Thread.sleep(100);
countDownLatch.countDown();
Thread.sleep(1000);
for (int i = 0; i < size-1; i++) {
if(!(strs[i].equals(strs[i+1]))){
System.out.println("i = " + strs[i] + ",i+1 = "+strs[i+1]);
System.out.println("need volatile");
return;
}
}
System.out.println(count++ + " times");
}
}
public static void main(String[] args) throws InterruptedException {
test();
}
}
解决方法
您没有看到的关键问题是指令可以重新排序。因此,它们在源代码中的顺序与它们在内存中的应用顺序不同。 CPU 和编译器是原因或这种重新排序。
我不会详细介绍双重检查锁定的整个示例,因为有很多示例可用,但会为您提供足够的信息来进行更多研究。
如果你有以下代码:
if(singleton == null){
synchronized{
if(singleton == null){
singleton = new Singleton("foobar")
}
}
}
然后在幕后会发生这样的事情。
if(singleton == null){
synchronized{
if(singleton == null){
tmp = alloc(Singleton.class)
tmp.value = "foobar"
singleton = tmp
}
}
}
到目前为止,一切都很好。但以下重新排序是合法的:
if(singleton == null){
synchronized{
if(singleton == null){
tmp = alloc(Singleton.class)
singleton = tmp
tmp.value = "foobar"
}
}
}
所以这意味着一个尚未完全构造的单例(尚未设置值)已写入单例全局变量。如果一个不同的线程读取这个变量,它可以看到一个部分创建的对象。
还有其他潜在问题,如原子性(例如,如果值字段很长,它可能会碎片化,例如读/写撕裂)。还有可见性;例如编译器可以优化代码,以便优化内存中的加载/存储。请记住,从内存而不是缓存中读取的思考从根本上是有缺陷的,也是我在 SO 上看到的最常遇到的误解;甚至很多前辈都弄错了。原子性、可见性和重新排序是 Java 内存模型的一部分,并且使单例变量可变,解决了所有这些问题。它消除了数据竞争(您可以查找更多详细信息)。
如果你想成为真正的硬核,在创建对象和分配给单例之间放置一个 [storestore] 屏障就足够了,在读取端放置一个 [loadload] 屏障就足够了。但这远远超出了大多数工程师的理解,并且在大多数情况下不会对性能产生太大影响。
如果您想检查某些东西是否会损坏,请查看 JCStress:
https://github.com/openjdk/jcstress
这是一个很棒的工具,可以帮助您证明代码已损坏。
,如何证明一定需要“volatile”?
作为一般规则,您无法通过测试来证明多线程应用程序的正确性。您可能能够证明不正确,但即使如此也不能保证。正如你所观察的那样。
您没有成功地使您的应用程序失败的事实并不能证明它是正确的。
证明正确性的方法是进行正式的(即数学)分析。
很容易证明,当 singleton
不是 volatile
时,在某些执行中,发生在之前。这可能导致错误的结果,例如初始化发生不止一次。但不保证您会得到错误的结果。
另一方面是,如果使用 volatile
,发生在之前的关系与代码逻辑相结合足以构建一个正式的(数学)证明,您将总是得到正确的结果。
(我不打算在这里构造证明。太费力了。)
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。