这里最近的一个问题有以下代码(好吧,类似于这个)来实现没有同步的单例。
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
现在,我想我明白这是怎么回事了。因为实例是static final
,它在任何线程调用getInstance()
之前就已经构建好了,所以实际上不需要同步。
只有当两个线程试图同时调用getInstance()
时才需要同步(并且该方法在第一次调用时而不是在"static final"
时进行构造)。
public class Singleton {
private Singleton() {}
private static Singleton instance = null;
public static synchronized Singleton getInstance() {
if (instance == null)
instance = new Singleton();
return instance;
}
}
我唯一的想法是使用static final
方法可能会引入排序问题,就像c++静态初始化顺序惨败一样。
现在,我想我明白这是怎么回事了。由于实例是静态的final,它在任何线程调用getInstance()之前就已经构建好了,所以实际上不需要同步。
不完全是。它是在SingletonHolder
类初始化时构建的,这发生在第一次调用getInstance
时。类加载器有一个单独的锁定机制,但是在类加载之后,不需要进一步的锁定,所以这个方案只需要足够的锁定来防止多次实例化。
首先,Java真的有这个问题吗?我知道类中的顺序是完全指定的,但它是否以某种方式保证类之间的顺序一致(例如与类装入器)?
Java确实存在一个问题,即类初始化周期可能导致一些类在初始化之前(技术上讲,在所有静态初始化块运行之前)观察另一个类的静态final。
考虑class A {
static final int X = B.Y;
// Call to Math.min defeats constant inlining
static final int Y = Math.min(42, 43);
}
class B {
static final int X = A.Y;
static final int Y = Math.min(42, 43);
}
public class C {
public static void main(String[] argv) {
System.err.println("A.X=" + A.X + ", A.Y=" + A.Y);
System.err.println("B.X=" + B.X + ", B.Y=" + B.Y);
}
}
运行C打印
A.X=42, A.Y=42
B.X=0, B.Y=42
但是在你发布的习惯用法中,helper和singleton之间没有循环,所以没有理由更喜欢延迟初始化。
现在,我想我明白这是什么了做的事情。因为实例是静态的最后,它比任何一个都早建成线程将调用getInstance()其实没有必要同步。
。SingletonHolder
类只有在第一次调用SingletonHolder.INSTANCE
时才会加载。final
对象只有在完全构造之后才会对其他线程可见。这种惰性初始化称为Initialization on demand holder idiom
.
在Effective Java中,Joshua Bloch注意到"这种习惯用法……利用了类在使用之前不会初始化的保证[JLS, 12.4.1]。"
您描述的模式起作用有两个原因
- 类在第一次访问时被加载并初始化(通过SingletonHolder)。实例) 类加载和初始化在Java中是原子的
所以你确实以线程安全和有效的方式执行延迟初始化。这种模式是同步惰性init的双锁(不工作)解决方案的更好替代方案。
您急切地初始化,因为您不必编写同步块或方法。这主要是因为同步通常被认为是昂贵的
关于第一个实现的一点注意事项:这里有趣的是类初始化是用来取代经典同步的。
类初始化定义得非常好,因为没有代码可以访问类的任何内容,除非它被完全初始化(即所有静态初始化代码都已运行)。由于一个已经加载的类可以在零开销的情况下访问,这就限制了"同步"开销,只有在需要进行实际检查的情况下(例如:"类加载/初始化了吗?").
使用类加载机制的一个缺点是当它崩溃时很难调试。如果由于某种原因,Singleton
构造函数抛出异常,那么getInstance()
的第一个调用者将获得该异常(包装在另一个异常中)。
NoClassDefFoundError
)。因此,如果第一个调用者以某种方式忽略了问题,那么将永远无法找出到底是哪里出了问题。
如果您只使用同步,那么第二个调用将尝试再次实例化Singleton
,并且可能会遇到相同的问题(甚至成功!)。
类在运行时被访问时被初始化。init的顺序就是执行的顺序。
这里的"Access"指的是规范中指定的受限操作,下一节将讨论初始化。
第一个例子中的内容相当于
public static Singleton getSingleton()
{
synchronized( SingletonHolder.class )
{
if( ! inited (SingletonHolder.class) )
init( SingletonHolder.class );
}
return SingletonHolder.INSTANCE;
}
(一旦初始化,同步块变得无用;JVM会将其优化关闭。)
在语义上,这与第二个暗示没有什么不同。这并没有真正超越"双重检查锁定",因为它是双重检查锁定。
因为它依赖于类初始化语义,所以它只适用于静态实例。一般来说,惰性求值并不局限于静态实例;假设每个会话有一个实例。
首先,Java真的有这个问题吗?我知道类中的顺序是完全指定的,但它是否以某种方式保证类之间的顺序一致(例如与类装入器)?
可以,但程度不如c++:
-
如果没有依赖循环,静态初始化将按照正确的顺序进行
-
如果一组类的静态初始化存在依赖循环,则该类的初始化顺序是不确定的。
-
然而,Java保证静态字段的默认初始化(null/zero/false)发生在任何代码看到字段的值之前。因此,无论初始化顺序如何,类(理论上)都可以被编写为做正确的事情。
其次,如果顺序是一致的,为什么惰性构造选项会是有利的?
延迟初始化在很多情况下都是有用的:
-
当初始化有你不希望发生的副作用除非对象实际要被使用
-
当初始化是昂贵的,你不希望它浪费时间做不必要的…或者你想让更重要的事情更快发生(例如显示UI)。
-
当初始化依赖于静态初始化时不可用的状态时。(不过您需要小心,因为当惰性初始化被触发时,状态也可能不可用。)
您也可以使用同步getInstance()
方法实现延迟初始化。它更容易理解,尽管它使getInstance()
稍微慢了一些。
第一个版本中的代码是正确的和最好的安全惰性构造单例的方法。Java内存模型保证INSTANCE将:
- 仅在第一次实际使用时初始化(即惰性),因为只有在第一次使用时才加载类
- 只构造一次,所以它是完全线程安全的,因为所有的静态初始化都保证在类可用之前完成
版本1是一个很好的模式。
版本2是线程安全的,但有点昂贵,更重要的是,严重限制并发/吞吐量
我不进入你的代码片段,但我有你的问题的答案。是的,Java有一个初始化顺序的惨败。我碰到了相互依赖的枚举。示例如下:
enum A {
A1(B.B1);
private final B b;
A(B b) { this.b = b; }
B getB() { return b; }
}
enum B {
B1(A.A1);
private final A a;
B(A a) { this.a = a; }
A getA() { return a; }
}
关键是在创建实例A.A1时B.B1必须存在。要创造A.A1 . B.B1必须存在。
我的实际用例有点复杂——枚举之间的关系实际上是父子关系,因此一个枚举返回对其父数组的引用,但返回其子数组的第二个数组。子字段是枚举的私有静态字段。有趣的是,在Windows上进行开发时,一切都运行良好,但在生产环境(即solaris)中,子数组的成员为空。数组有适当的大小,但它的元素为空,因为它们在数组实例化时不可用。
因此,我在第一次调用时结束了同步初始化。:-)
Java中唯一正确的单例不能通过类声明,而是通过enum声明:
public enum Singleton{
INST;
... all other stuff from the class, including the private constructor
}
用法如下:
Singleton reference1ToSingleton=Singleton.INST;
所有其他方法都不排除通过反射或类的源直接存在于应用程序源中的重复实例化。Enum不包括所有。(Enum中的最后一个clone方法确保枚举常量永远不能被克隆)