Java静态初始化器是否线程安全?

144
我正在使用静态代码块来初始化注册表中的一些控制器。我的问题是,我能保证这个静态代码块只在第一次加载类时被绝对调用吗?我知道我不能保证代码块何时被调用,我猜测它是在类装入程序首次加载它时发生的。我意识到我可以在静态代码块中对类进行同步,但我猜这实际上已经发生了?一个简单的代码示例如下:
class FooRegistry {

    static {
        //this code must only ever be called once 
        addController(new FooControllerImpl());
    }

    private static void addController(IFooController controller) { 
        // ...
    }
}

还是我应该这样做;

class FooRegistry {

    static {
        synchronized(FooRegistry.class) {
            addController(new FooControllerImpl());
        }
    }

    private static void addController(IFooController controller) {
        // ...
    }
}

10
我不喜欢这个设计,因为它无法进行测试。看一下依赖注入。 - dfa
6个回答

209

是的,Java 静态初始化器是线程安全的(使用第一种选项)。

不过,如果你想确保代码仅被执行一次,你需要确保该类只被单个类加载器加载。静态初始化仅在每个类加载器中执行一次。


2
然而,一个类可以被多个类加载器加载,所以addController可能会被调用多次(无论是否同步调用)。 - Matthew Murdoch
4
等等,那么我们是说静态代码块实际上会被每个加载该类的类加载器调用?嗯...我猜这应该还好,但我在想在OSGI环境中运行这种代码会怎样,因为有多个bundle类加载器。 - simon622
1
是的,静态代码块将为加载类的每个类加载器调用。 - Matthew Murdoch
3
是的,但是它将在每个ClassLoader中的不同类对象中运行。 这些不同的Class对象仍具有相同的完全限定名称,但代表着无法彼此转换的不同类型。 - Erwin Bolwidt
1
这是否意味着在 https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom 中,实例持有者中的 'final' 关键字是多余的? - spc16670

13

这是一种您可以用于惰性初始化的技巧。

enum Singleton {
    INSTANCE;
}

或适用于 Java 5.0 之前的版本

class Singleton {
   static class SingletonHolder {
      static final Singleton INSTANCE = new Singleton();
   }
   public static Singleton instance() {
      return SingletonHolder.INSTANCE;
   }
}

由于SingletonHolder中的静态块只会在线程安全的情况下运行,因此您不需要任何其他锁定。只有在调用instance()方法时,SingletonHolder类才会被加载。


20
您的答案基于静态块只会在全局执行一次这个事实——这也是被问到的问题。 - Michael Myers
2
我认为在多类加载器环境中,这也不安全,你怎么看? - Ahmad
2
@Ahmad 多类加载器环境旨在允许每个应用程序拥有自己的单例。 - Peter Lawrey
1
不需要嵌套类。当INSTANCE字段直接在Singleton中声明时(如enum变体的情况),此构造方式的工作方式相同。 - Holger

4

通常情况下,静态初始化程序中的所有内容都发生在使用该类的任何内容之前,因此通常不需要同步。但是,该类对静态初始化程序调用的任何内容都是可访问的(包括导致其他静态初始化程序被调用)。

类可以由类加载器加载,但不一定立即初始化。当然,一个类可以被多个类加载器加载,从而成为具有相同名称的多个类。


3
是的,有点像。一个静态初始化器只会被调用一次,因此按照这个定义,它是线程安全的——你需要两个或更多的静态初始化器调用才能获得线程争用。
话虽如此,静态初始化器在许多其他方面令人困惑。实际上,没有指定它们被调用的顺序。如果你有两个类的静态初始化器彼此依赖,这会变得非常混乱。如果你使用了一个类但不使用静态初始化器将设置的内容,那么不能保证类加载器会调用静态初始化器。
最后,请记住你正在同步的对象。我知道这并不是你真正在问的,但请确保你的问题不是在问是否需要使addController()线程安全。

5
它们的调用顺序非常明确:按照源代码中的顺序。 - mafu
此外,无论您是否使用它们的结果,它们总是被调用。除非这在Java 6中已更改。 - mafu
9
在一个类中,初始化程序跟随代码。在给定两个或更多类的情况下,很难确定哪个类首先被初始化,一个类是否在另一个类开始之前完全初始化,或者事物是如何“交错”的。例如,如果两个类都有静态初始化程序相互引用,事情会很快变得混乱。我认为有一些方法可以引用到另一个类的静态 final int 而不调用初始化程序,但我不想就这个问题进行辩论。 - Matt
1
它确实会变得丑陋,我会避免它。但是有一种明确定义的方法来解决循环。引用《Java编程语言第四版》: 页码:75,章节:2.5.3. 静态初始化:“如果发生循环,X的静态初始化程序仅在调用Y的方法时执行到某个点。当Y依次调用X方法时,该方法将与尚未执行的其余静态初始化程序一起运行。” - KANJICODER

0

2
不,它们可以被运行多次。 - lmat - Reinstate Monica
5
每个类加载器可以运行一次,不是每次都能运行。 - ruurd
1
基本答案:静态初始化只运行一次。 高级答案:静态初始化每个类加载器只运行一次。 第一个评论令人困惑,因为措辞混淆了这两个答案。 - KANJICODER

-4

基本上,由于您想要一个单例实例,因此您应该以更或多或少的传统方式进行操作,并确保您的单例对象仅被初始化一次。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接