Java 单例模式的延迟初始化。Volatile 与同步方法。

huangapple go评论69阅读模式
英文:

Java singleton lazy initialization. Volatile vs synchronized method

问题

为什么我们需要在字段上添加volatile来防止无效数据检索?难道我们不能通过在方法声明上添加synchronized而不是在代码块上添加synchronized来实现相同的效果?

public class LazySingleton {

    private static volatile LazySingleton instance;

    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton() {
    }

    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}
英文:

Why do we need to add volatile to field to prevent invalid data retrieval? Can't we do the same thing by adding synchronized to method declaration instead of to a block of code?

public class LazySingleton {

private static volatile LazySingleton instance;

private LazySingleton() {
}

public static LazySingleton getInstance() {
    if (instance == null) {
        synchronized (LazySingleton.class) {
            if (instance == null) {
                instance = new LazySingleton();
            }
        }
    }
    return instance;
}
}

and

public class LazySingleton {

private static LazySingleton instance;

private LazySingleton() {
}

public static synchronized LazySingleton getInstance() {
    if (instance == null) {
        instance = new LazySingleton();
    }
    return instance;
}
}

答案1

得分: 1

以下是您要翻译的内容:

"你不需要双重锁定。根本不需要。

这个想法通常是:'嘿,除非代码实际上需要那个单例,否则创建那个单例就是浪费时间,所以让它只在有人第一次需要它的时候才创建。'

这是一个合理的考虑,尽管在实际情况下,绝大多数单例根本不需要进行任何相关的初始化过程(它们的构造函数是空的或微不足道的)。即使不是这样,Java 已经 对类本身应用了这个原则。你可能认为 Java 在启动时会加载__每个__类路径上的__每个__类,然后才通过调用你的 main 方法来执行你的应用程序。不是这样的。相反,Java 什么都不加载,直接执行你的主方法。在执行这个过程中,你的主类将被加载。而在执行__那个__过程中,你的主类所“触及”的一切都是软加载的。

主类“触及”的意思是,例如,你的主类的某个字段的类型是 String,或者当然是你的主方法的签名中出现了 String - 这会导致 Java 类加载器系统暂停尝试实际执行你的 main 方法,而去加载 String

但过程到此结束 - 基本上,因为你在 Main 的某个地方提到了一个类型,并不意味着 Java 就会完全加载那个类型(即加载所有与之相关的东西,就像追踪蜘蛛网上的路径,很快就会把整个网都填满)。相反,只有在执行实际使用该类型的代码时,这些类型才会被“完全加载”。

当然,一旦某个类型已经被加载,Java 会确保它不会被再次加载,并且 Java 会确保任何给定的类不会被初始化两次,即使有2个线程同时尝试第一次使用一个类。例如,如果你这样做:

class Test {
  private static final String foo = hello();

  private static String hello() {
    System.out.println("Hello!");
    return "";
  }
}

上面的代码在任何包含它的 Java 程序的执行中只有三种可能性:

  • Hello! 永远不会被打印 - 如果这个类从未被“需要”加载。

  • 它只被打印一次,在该 JVM 的生命周期中__永远__只打印一次。

  • 打印多次,但只有在添加更多的类加载器,实际上意味着克隆该类的实例被加载,这是一个不太可能发生的学术边角情况,不计算在内(如果你使用了双重锁定,在这种情况下它也会初始化多次)。

  • 这比你在这里可能做的任何事情都要__简单__和__快__。

因此,请__不要懒加载单例__。是的,有一亿个教程提到了它。他们都错了。这种情况确实发生过。关于广泛流传的错误观念,甚至有整个维基百科文章。有明确证据(如上所示),表明某种观念是错误的,即使这种观念很普遍,也应该予以考虑。

因此,对于99.5%的所有单例情况,我们得出了正确的答案:


class MySingleton {
  private static final MySingleton INSTANCE = new MySingleton();

  public static MySingleton of() {
    return INSTANCE;
  }
}

简单。有效。而且已经是懒加载,因为 Java 的类加载器系统本身就是懒加载的。

唯一的例外是,几乎不太可能出现的情况,如果可以合理地期望代码会“触及”一个类(在某个地方提到该类,例如,将 ThatSingleton.class 用作表达式,或者有一个参数类型为 ThatSingleton 的方法,很可能永远不会被调用),但实际上不需要单例的实例。这是一种极端奇怪的情况 - 通常,如果有人提到单例类型,那么代码将立即调用“获取单例”的方法。毕竟,提到类型但不调用“获取单例”的方法的主要原因是你想调用静态方法。谁会用静态方法创建单例?那... 真的很傻。

例外情况适用于我!

我怀疑。

但如果是这样,我们仍然可以依赖 Java 提供的最佳、最安全、最快的“仅初始化一次”机制,即类加载器:

public class MySingleton {
  public static MySingleton of() {
    class MySingletonHolder {
      static final MySingleton INSTANCE = new MySingleton();
    }
    return MySingletonHolder.INSTANCE;
  }
}

编辑:上面的代码是/u/Holger提出的一个很好的建议,但它需要一个相对较新的 Java 版本,因为将static的东西放在内部类中曾经是无效的 Java 代码。如果编译上面的代码告诉你“你不能在非静态类中放置静态元素”或类似的错误,只需将 Holder 类移到方法外部并标记为private static final

这在绝对意义上(即没有例外,一切皆如此)是最

英文:

You don't need double locking. AT ALL.

The idea is generally 'hey, it is a waste of time to create that singleton unless code actually needs that singleton, so lets make it only the first time somebody needs it'.

A fair consideration, though, of course, in practice, the vast majority of singletons do no relevant initialization process whatsoever (their constructors are empty or trivial). Even if not, java already applies this principle for classes itself. You may think that java, upon starting, loads every class on the classpath first, and only then starts executing your app by invoking your main method. Not so. Instead, java loads nothing, and starts by executing your main method. As part of doing this, your main class will have to be loaded. And as part of doing that, everything your main class 'touches' is soft-loaded.

Touched by your main class means e.g. one of the fields in your main class has String as a type, or of course String which shows up in the signature of the main method - that causes the java classloader system to pause its attempting to actually execute your main method and go load String instead.

The process ends there though - in basis just because you mention a type someplace in Main does not mean java will then fully load that type (i.e. load everything that touches, like tracing a path across a spiderweb that soon colours in the entire web). Instead those types are only 'fully loaded' once you execute code that actually uses that type.

And, of course, once some type has been loaded, java ensures it is never loaded a second time, and, java ensures any given class is never initialized twice, even if 2 threads try to use a class for the first time simultaneously. e.g. if you do:

class Test {
  private static final String foo = hello();

  private static String hello() {
    System.out.println("Hello!");
    return "";
  }
}

Any execution of any java program with the above code in it has only three options:

  • Hello! is never printed - if this class is never 'needed' to be loaded.

  • It is printed exactly once, ever, for the lifetime of that JVM.

  • More than once, but only if you add more classloaders which effectively means clones of this class end up being loaded which is an academic corner case that doesn't count (had you used double locking it would also initialize more than once in this case).

  • This is simpler and faster than anything you could possibly do here.

Hence, do not lazy load singletons. Yes, a billion tutorials mention it. They're all wrong. It happens. There are entire wikipedia articles about commonly believed falsehoods. Objective proof (such as the above) showing a belief is false should be heeded even if the belief is widespread.

Thus, we get to the right answer for 99.5% of all singletons:


class MySingleton {
  private static final MySingleton INSTANCE = new MySingleton();

  public static MySingleton of() {
    return INSTANCE;
  }
}

Simple. Effective. and already lazy-loading because java's class loader system itself is lazy loading.

The one exception to this rule, which should come up almost never, is if it is reasonable to expect that code will 'touch' a class (mention the class someplace, e.g. use ThatSingleton.class as an expression or have a method that has a parameter of type ThatSingleton which nobody is likely to ever call), but never actually want an instance of the singleton. That is an extremely weird situation to be in - normally if anybody so much as mentions the singleton type, that code will immediately invoke the 'get me that singleton' method. After all, the primary reason to mention a type but not call the 'get me the singleton' method is if you want to invoke static methods on it. Who makes singletons with static methods? That's... silly.

The exception applies to me!

I doubt it.

But if it does, we can still rely on the best, safest, fastest 'init only once' mechanism java offers, which is the classloader:

public class MySingleton {
  public static MySingleton of() {
    class MySingletonHolder {
      static final MySingleton INSTANCE = new MySingleton();
    }
    return MySingletonHolder.INSTANCE;
  }
}

EDIT: The above snippet is a nice suggestion by /u/Holger, but it requires a relatively new java version as sticking static things in inner classes used to be invalid java code. If compiling the above code tells you that 'you cannot put static elements in a non-static class' or similar, simply move the Holder class outside the method and mark it private static final.

This is objectively and in an absolute sense (As in, no exceptions, period) the best option - even in that exceedingly exotic case as described before. new MySingleton() will never be executed unless somebody invokes of(), in which case it will be executed precisely once. No matter how many threads are doing it simultaneously.

Explanatory: What's wrong with your snippets

Your question inherently implies that you think synchronized is either simpler than volatile, faster than volatile, or both. This simply is not true: volatile is usually faster, which is why the volatile example shows up more often as 'the best way to do it' (it is not - using the classloader is better, see above).

Explanatory: What is the hubbub about?

The general problem with your synchronized example is that any call to of() will have to acquire a lock and then release it immediately after checking that, indeed, INSTANCE is already set. Just the synchronized part (the acquiring of the lock) is, itself, quite slow (thousands, nay, millions of times slower than new MySingleton() unless your singleton constructor does something very very serious, such as parse through a file). You pay this cost even days after your app has started - after all, every call to of(), even the millionth, still has to go through synchronized.

The various tricks such as double-locking etc try to mitigate that cost - to stop paying it once we've gotten well past initializing things. The simple double-locking doesn't actually 'work' (is not guaranteed to only ever make 1 instance), with volatile it does and now you're paying the lesser, but still significant cost of reading a volatile field every time anybody calls the 'get me the singleton instance' method. This also is potentially quite slow.

All java code has to go through the initialisation gate anytime it touches any class. Which is why the classloader system has very efficient processes for this. Given that you already have to pass that gate, might as well piggyback your lazy initialization needs off of it - which gets us back to: Use my first snippet. Unless you have written something completely ridiculous such as a singleton that also has static methods, in which case, use my second snippet. And know that all blogs, tutorials, examples, etc that show any sort of double locking anything are all far worse.

答案2

得分: 0

为了防止无效数据检索,为什么我们需要在字段上添加volatile关键字?

如果您的LazySingleton.instance不是volatile的话,那么第一个示例中的以下测试就没有正确同步,因此会表现出未定义的行为。

无论是否使用volatile,该示例都是双重检查锁定的变体。关于这一点有很多评论,因为曾经在某些领域推广过它,但是Java和C++中的许多实现都是有问题的。特别是从第一个示例中删除volatile关键字会破坏它。

我们是否可以通过将同步添加到方法声明而不是代码块来做同样的事情?

是的,在您的第二个代码中,确实进行了正确的同步,并且具有单例只在首次请求时实例化的属性。虽然实现这一点的细节不同,因此它们可能不会表现完全相同。

英文:

> Why do we need to add volatile to field to prevent invalid data retrieval?

If your LazySingleton.instance is not volatile, then the first
>
> if (instance == null) {
>

test in your first example is not properly synchronized, and therefore exhibits undefined behavior.

Volatile or not, that example is a variation on double-checked locking. You will find much commentary about that, because at one time it was promoted in some quarters, but many implementations in Java and C++ are broken. In particular, removing the volatile from your first example breaks it.

> Can't we do the same thing by adding synchronized to method declaration instead of to a block of code?

Yes, in the sense that your second code is correctly synchronized and does have the property that the singleton is instantiated only once, on the first request for it. The details of how that is achieved are not the same, so they might not perform identically.

huangapple
  • 本文由 发表于 2023年7月20日 21:56:16
  • 转载请务必保留本文链接:https://go.coder-hub.com/76730639.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定