Java 双重检查锁定

发布于 2024-08-09 03:16:29 字数 637 浏览 2 评论 0原文

我最近偶然发现一篇文章讨论了 Java 中的双重检查锁定模式及其陷阱,现在我想知道我多年来一直使用的该模式的变体是否存在任何问题。

我已经查看了有关该主题的许多帖子和文章,并了解获取对部分构造的对象的引用的潜在问题,据我所知,我不认为我的实现是受这些问题的影响。下面的模式有什么问题吗?

如果没有,为什么人们不使用它?我从未在我见过的有关此问题的任何讨论中看到推荐它。

public class Test {
    private static Test instance;
    private static boolean initialized = false;

    public static Test getInstance() {
        if (!initialized) {
            synchronized (Test.class) {
                if (!initialized) {
                    instance = new Test();
                    initialized = true;
                }
            }
        }
        return instance;
    }
}

I happened upon an article recently discussing the double checked locking pattern in Java and its pitfalls and now I'm wondering if a variant of that pattern that I've been using for years now is subject to any issues.

I've looked at many posts and articles on the subject and understand the potential issues with getting a reference to a partially constructed object, and as far as I can tell, I don't think my implementation is subject to these issues. Are there any issues with the following pattern?

And, if not, why don't people use it? I've never seen it recommended in any of the discussion I've seen around this issue.

public class Test {
    private static Test instance;
    private static boolean initialized = false;

    public static Test getInstance() {
        if (!initialized) {
            synchronized (Test.class) {
                if (!initialized) {
                    instance = new Test();
                    initialized = true;
                }
            }
        }
        return instance;
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

中二柚 2024-08-16 03:16:29

双重检查锁定已损坏。由于initialized是一个原语,它可能不需要它是易失性的才能工作,但是没有什么可以阻止在初始化实例之前将initialized视为非同步代码的真实值。

编辑:为了澄清上述答案,最初的问题询问使用布尔值来控制双重检查锁定。如果没有上面链接中的解决方案,它将无法工作。您可以仔细检查实际设置布尔值的锁定,但在创建类实例时仍然存在指令重新排序的问题。建议的解决方案不起作用,因为在非同步块中看到初始化的布尔值为 true 后,实例可能无法初始化。

双重检查锁定的正确解决方案是使用 易失性(在实例字段上)并忘记初始化的布尔值,并确保使用 JDK 1.5 或更高版本,或者在最终字段中初始化它,如链接中所述文章和汤姆的回答,或者只是不使用它。

当然,整个概念看起来像是一个巨大的过早优化,除非您知道在获取此单例时会遇到大量线程争用,或者您已经分析了应用程序并发现这是一个热点。

Double check locking is broken. Since initialized is a primitive, it may not require it to be volatile to work, however nothing prevents initialized being seen as true to the non-syncronized code before instance is initialized.

EDIT: To clarify the above answer, the original question asked about using a boolean to control the double check locking. Without the solutions in the link above, it will not work. You could double check lock actually setting a boolean, but you still have issues about instruction reordering when it comes to creating the class instance. The suggested solution does not work because instance may not be initialized after you see the initialized boolean as true in the non-syncronized block.

The proper solution to double-check locking is to either use volatile (on the instance field) and forget about the initialized boolean, and be sure to be using JDK 1.5 or greater, or initialize it in a final field, as elaborated in the linked article and Tom's answer, or just don't use it.

Certainly the whole concept seems like a huge premature optimization unless you know you are going to get a ton of thread contention on getting this Singleton, or you have profiled the application and have seen this to be a hot spot.

笨笨の傻瓜 2024-08-16 03:16:29

如果initialized易失性,那么就可以工作。就像synchronized 一样,volatile 的有趣效果实际上与引用无关,而与我们对其他数据的影响无关。 instance 字段和 Test 对象的设置被强制发生在写入初始化之前。通过短路使用缓存值时,initialize 读取发生在读取实例以及通过引用到达的对象之前。拥有单独的 initialized 标志没有显着差异(除了它会导致代码更加复杂之外)。

(构造函数中用于不安全发布的 final 字段的规则略有不同。)

但是,在这种情况下您应该很少会看到该错误。第一次使用时遇到麻烦的机会微乎其微,而且是一场不重复的比赛。

代码过于复杂。你可以把它写成:

private static final Test instance = new Test();

public static Test getInstance() {
    return instance;
}

That would work if initialized was volatile. Just as with synchronized the interesting effects of volatile are not really so much to do with the reference as what we can say about other data. Setting up of the instance field and the Test object is forced to happen-before the write to initialized. When using the cached value through the short circuit, the initialize read happens-before reading of instance and objects reached through the reference. There is no significant difference in having a separate initialized flag (other than it causes even more complexity in the code).

(The rules for final fields in constructors for unsafe publication are a little different.)

However, you should rarely see the bug in this case. The chances of getting into trouble when using for the first time is minimal, and it is a non-repeated race.

The code is over-complicated. You could just write it as:

private static final Test instance = new Test();

public static Test getInstance() {
    return instance;
}
余生再见 2024-08-16 03:16:29

双重检查锁定确实被破坏了,并且该问题的解决方案实际上比这种习惯更容易以代码方式实现 - 只需使用静态初始值设定项。

public class Test {
    private static final Test instance = createInstance();

    private static Test createInstance() {
        // construction logic goes here...
        return new Test();
    }

    public static Test getInstance() {
        return instance;
    }
}

静态初始化程序保证在 JVM 第一次加载类时以及类引用返回到任何线程之前执行 - 使其本质上是线程安全的。

Double checked locking is indeed broken, and the solution to the problem is actually simpler to implement code-wise than this idiom - just use a static initializer.

public class Test {
    private static final Test instance = createInstance();

    private static Test createInstance() {
        // construction logic goes here...
        return new Test();
    }

    public static Test getInstance() {
        return instance;
    }
}

A static initializer is guaranteed to be executed the first time that the JVM loads the class, and before the class reference can be returned to any thread - making it inherently threadsafe.

烈酒灼喉 2024-08-16 03:16:29

这就是双重检查锁定被破坏的原因。

同步保证只有一个线程可以进入代码块。但它不能保证在同步部分中完成的变量修改对其他线程可见。只有进入同步块的线程才能保证看到更改。这就是双重检查锁定被破坏的原因 - 它在读者端不同步。读取线程可能会看到单例不为空,但单例数据可能未完全初始化(可见)。

排序由易失性提供。 易失性保证顺序,例如写入易失性单例静态字段保证对单例对象的写入将在写入易失性静态字段之前完成。它不会阻止创建两个对象的单例,这是由同步提供的。

类最终静态字段不需要是易失性的。在 Java 中,JVM 解决了这个问题。

请参阅我的帖子 对现实世界 Java 应用程序中的单例模式和损坏的双重检查锁定的回答,说明了关于双重检查锁定的单例示例,该示例看起来很聪明,但已损坏。

This is the reason why double checked locking is broken.

Synchronize guarantees, that only one thread can enter a block of code. But it doesn't guarantee, that variables modifications done within synchronized section will be visible to other threads. Only the threads that enters the synchronized block is guaranteed to see the changes. This is the reason why double checked locking is broken - it is not synchronized on the reader's side. The reading thread may see, that the singleton is not null, but singleton data may not be fully initialized (visible).

Ordering is provided by volatile. volatile guarantees ordering, for instance write to volatile singleton static field guarantees that writes to the singleton object will be finished before the write to volatile static field. It doesn't prevent creating singleton of two objects, this is provided by synchronize.

Class final static fields doesn't need to be volatile. In Java, the JVM takes care of this problem.

See my post, an answer to Singleton pattern and broken double checked locking in a real-world Java application, illustrating an example of a singleton with respect to double-checked locking that looks clever but is broken.

耶耶耶 2024-08-16 03:16:29

双重检查锁定是反模式。

延迟初始化持有者类是您应该查看的模式。

尽管有这么多其他答案,我认为我应该回答,因为仍然没有一个简单的答案可以说明为什么 DCL 在许多情况下被破坏,为什么它是不必要的以及你应该做什么。因此,我将引用 Goetz:Java 并发实践中的一句话,对我来说,它在有关 Java 内存模型的最后一章中提供了最简洁的解释。

这是关于变量的安全发布:

DCL 的真正问题是假设在没有同步的情况下读取共享对象引用时可能发生的最糟糕的事情是错误地看到过时的值(在本例中为 null );在这种情况下,DCL 惯用法通过在持有锁的情况下重试来补偿这种风险。但最坏的情况实际上更糟糕——可以看到引用的当前值,但对象状态的值是陈旧的,这意味着该对象可能处于无效或不正确的状态。

JMM(Java 5.0 及更高版本)中的后续更改使 DCL 能够在资源设为易失性的情况下工作,并且这种情况对性能的影响很小,因为易失性读取通常只比非易失性读取稍微昂贵一点。

然而,这个惯用语的效用在很大程度上已经过去了——激发它的力量(缓慢的无竞争同步、缓慢的 JVM 启动)不再发挥作用,使得它作为优化的效果降低。惰性初始化持有者习惯用法具有相同的优点并且更易于理解。

清单 16.6。延迟初始化持有者类习惯用法。

公共类ResourceFactory
    私有静态类ResourceHolder {
        公共静态资源资源 = new Resource();
    }

    公共静态资源 getResource() {
        返回ResourceHolder.resource;
    }
}

这就是实现它的方法。

Double-checked Locking is the anti-pattern.

Lazy Initialization Holder Class is the pattern you should be looking at.

Despite so many other answers, I figured I should answer because there still isn't one simple answer that says why DCL is broken in many contexts, why it is unnecessary and what you should do instead. So I'll use a quote from Goetz: Java Concurrency In Practice which for me provides the most succint explanation in its final chapter on the Java Memory Model.

It's about Safe Publication of variables:

The real problem with DCL is the assumption that the worst thing that can happen when reading a shared object reference without synchronization is to erroneously see a stale value (in this case, null ); in that case the DCL idiom compensates for this risk by trying again with the lock held. But the worst case is actually considerably worse—it is possible to see a current value of the reference but stale values for the object's state, meaning that the object could be seen to be in an invalid or incorrect state.

Subsequent changes in the JMM (Java 5.0 and later) have enabled DCL to work if resource is made volatile , and the performance impact of this is small since volatile reads are usually only slightly more expensive than nonvolatile reads.

However, this is an idiom whose utility has largely passed—the forces that motivated it (slow uncontended synchronization, slow JVM startup) are no longer in play, making it less effective as an optimization. The lazy initialization holder idiom offers the same benefits and is easier to understand.

Listing 16.6. Lazy Initialization Holder Class Idiom.

public class ResourceFactory
    private static class ResourceHolder {
        public static Resource resource = new Resource();
    }

    public static Resource getResource() {
        return ResourceHolder.resource;
    }
}

That's the way to do it.

旧时浪漫 2024-08-16 03:16:29

您可能应该使用 java.util.concurrent.atomic

You should probably use the atomic data types in java.util.concurrent.atomic.

顾忌 2024-08-16 03:16:29

如果“initialized”为 true,则“instance”必须完全初始化,与 1 加 1 等于 2 相同:)。因此,该代码是正确的。该实例仅实例化一次,但该函数可能被调用一百万次,因此它确实提高了性能,而无需检查一百万次同步。

If "initialized" is true, then "instance" MUST be fully initialized, same as 1 plus 1 equals 2 :). Therefore, the code is correct. The instance is only instantiated once but the function may be called a million times so it does improve the performance without checking synchronization for a million minus one times.

情场扛把子 2024-08-16 03:16:29

我一直在研究双重检查锁定习惯用法,根据我的理解,您的代码可能会导致读取部分构造的实例的问题,除非您的测试类是不可变的:

Java 内存模型为共享不可变对象提供了特殊的初始化安全保证。

即使不使用同步来发布对象引用,也可以安全地访问它们。

(引自非常值得推荐的《Java Concurrency in Practice》一书)

因此,在这种情况下,双重检查锁定惯用法将起作用。

但是,如果情况并非如此,请注意您在没有同步的情况下返回变量实例,因此实例变量可能未完全构造(您将看到属性的默认值而不是构造函数中提供的值)。

boolean变量没有添加任何东西来避免这个问题,因为它可能在Test类初始化之前被设置为true(synchronized关键字不能完全避免重新排序,某些语句可能会改变顺序)。 Java 内存模型中没有“happens-before”规则来保证这一点。

并且使布尔值成为 volatility 也不会添加任何内容,因为 32 位变量是在 Java 中自动创建的。双重检查锁定习惯也适用于它们。

从 Java 5 开始,您可以通过将实例变量声明为易失性来解决该问题。

您可以在这篇非常有趣的文章<中阅读有关双重检查惯用语的更多信息< /a>.

最后,我读过一些建议:

  • 考虑是否应该使用单例模式。许多人认为这是一种反模式。如果可能的话,首选依赖注入。检查这个

  • 在实现双重检查锁定优化之前,请仔细考虑是否确实有必要,因为在大多数情况下,这是不值得的。另外,请考虑在静态字段中构造 Test 类,因为延迟加载仅在构造类需要大量资源时才有用,而在大多数情况下,情况并非如此。

如果您仍然需要执行此优化,请检查此 链接 它提供了一些替代方案来实现与您正在尝试的效果类似的效果。

I've been investigating about the double checked locking idiom and from what I understood, your code could lead to the problem of reading a partially constructed instance UNLESS your Test class is immutable:

The Java Memory Model offers a special guarantee of initialization safety for sharing immutable objects.

They can be safely accessed even when synchronization is not used to publish the object reference.

(Quotations from the very advisable book Java Concurrency in Practice)

So in that case, the double checked locking idiom would work.

But, if that is not the case, observe that you are returning the variable instance without synchronization, so the instance variable may not be completely constructed (you would see the default values of the attributes instead of the values provided in the constructor).

The boolean variable doesn't add anything to avoid the problem, because it may be set to true before the Test class is initialized (the synchronized keyword doesn't avoid reordering completely, some sencences may change the order). There is no happens-before rule in the Java Memory Model to guarantee that.

And making the boolean volatile wouldn't add anything either, because 32 bits variables are created atomically in Java. The double checked locking idiom would work with them as well.

Since Java 5, you can fix that problem declaring the instance variable as volatile.

You can read more about the double checked idiom in this very interesting article.

Finally, some recommendations I've read:

  • Consider if you should use the singleton pattern. It is considered an anti-pattern by many people. Dependency Injection is preferred where possible. Check this.

  • Consider carefully if the double checked locking optimization is really necessary before implementing it, because in most cases, that wouldn't be worth the effort. Also, consider constructing the Test class in the static field, because lazy loading is only useful when constructing a class takes a lot of resources and in most of the times, it is not the case.

If you still need to perform this optimization, check this link which provides some alternatives for achieving a similar effect to what you are trying.

许你一世情深 2024-08-16 03:16:29

DCL 问题已被解决,尽管它似乎适用于许多虚拟机。这里有一篇关于这个问题的很好的文章 http://www.javaworld.com/article/2075306/java-concurrency/can-double-checked-locking-be-fixed-.html

多线程和内存一致性是比看起来更复杂的主题。 [...] 如果您只使用 Java 为此目的提供的工具——同步,您就可以忽略所有这些复杂性。 如果同步对可能已写入或可由另一个线程读取的变量的每次访问,则不会出现内存一致性问题。

正确解决此问题的唯一方法是避免惰性初始化(急切地执行)或在同步块内进行单一检查。使用布尔initialized相当于对引用本身进行空检查。第二个线程可能会看到 initialized 为 true,但 instance 可能仍为 null 或部分初始化。

The DCL problem is broken, even though it seems to works on many VMs. There is a nice writeup about the problem here http://www.javaworld.com/article/2075306/java-concurrency/can-double-checked-locking-be-fixed-.html.

multithreading and memory coherency are more complicated subjects than they might appear. [...] You can ignore all of this complexity if you just use the tool that Java provides for exactly this purpose -- synchronization. If you synchronize every access to a variable that might have been written, or could be read by, another thread, you will have no memory coherency problems.

The only way to solve this problem properly is to avoid lazy initialization (do it eagerly) or to single check inside a synchronized block. The use of the boolean initialized is equivalent to a null check on the reference itself. A second thread may see initialized being true but instance might still be null or partially initialized.

圈圈圆圆圈圈 2024-08-16 03:16:29

首先,对于单例,您可以使用枚举,如本问题 Implementing Singleton with枚举(Java 中)

其次,从 Java 1.5 开始,您可以使用具有双重检查锁定的 volatile 变量,如本文末尾所述:https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

First, for singletons you can use an Enum, as explained in this question Implementing Singleton with an Enum (in Java)

Second, since Java 1.5, you can use a volatile variable with double checked locking, as explained at the end of this article: https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

风流物 2024-08-16 03:16:29

在某些情况下仍然可以使用双重检查。

  1. 首先,如果您确实不需要单例,则双重检查仅用于不创建和初始化许多对象。
  2. 在构造函数/初始化块的末尾设置了一个 final 字段(这导致其他线程可以看到所有先前初始化的字段)。

There are still some cases when a double check may be used.

  1. First, if you really don't need a singleton, and double check is used just for NOT creating and initializing to many objects.
  2. There is a final field set at the end of the constructor/initialized block (that causes all previously initialized fields to be seen by other threads).
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文