这是求职面试的问题。
通过扭曲实现单例模式。首先,代替 存储一个实例,存储两个实例。而且在
getInstance()
,返回第一个实例,并在getInstance()
,返回第二个实例。
我的实现如下:
public final class Singleton implements Cloneable, Serializable {
private static final long serialVersionUID = 42L;
private static Singleton evenInstance;
private static Singleton oddInstance;
private static AtomicInteger counter = new AtomicInteger(1);
private Singleton() {
// Safeguard against reflection
if (evenInstance != null || oddInstance != null) {
throw new RuntimeException("Use getInstance() instead");
}
}
public static Singleton getInstance() {
boolean even = counter.getAndIncrement() % 2 == 0;
// Make thread safe
if (even && evenInstance == null) {
synchronized (Singleton.class) {
if (evenInstance == null) {
evenInstance = new Singleton();
}
}
} else if (!even && oddInstance == null) {
synchronized (Singleton.class) {
if (oddInstance == null) {
oddInstance = new Singleton();
}
}
}
return even ? evenInstance : oddInstance;
}
// Make singleton from deserializaion
protected Singleton readResolve() {
return getInstance();
}
@Override
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException("Use getInstance() instead");
}
}
您看到问题了吗?第一次调用可能会输入getInstance
,线程将被抢占。然后,第二个呼叫可以输入getInstance
,但会得到oddInstance
而不是evenInstance
。
很显然,可以通过使getInstance
同步来防止这种情况,但这是不必要的。在单例的生命周期中仅需要两次同步,而不是每个getInstance
调用都需要同步。
想法?
答案 0 :(得分:5)
最重要的是,evenInstance
和oddInstance
变量需要声明为volatile
。请参阅著名的“双重检查锁定已损坏”声明:https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
此外,对于偶数和奇数实例,您实际上应该在同步块中使用不同的对象,以便可以同时构造它们。
最后,Singleton
构造函数中的检查被破坏,并将在第二次调用getInstance()
时抛出异常
除了可以,但最好不要自己进行并发工作。
public final class Singleton implements Cloneable, Serializable {
private static AtomicInteger counter = new AtomicInteger(1);
public static Singleton getInstance() {
if (counter.getAndIncrement() % 2 == 0) {
return EvenHelper.instance;
} else {
return OddHelper.instance;
}
}
private static class EvenHelper {
//not initialized until the class is used in getInstance()
static Singleton instance = new Singleton();
}
private static class OddHelper {
//not initialized until the class is used in getInstance()
static Singleton instance = new Singleton();
}
}
答案 1 :(得分:3)
您并不是说必须延迟初始化单例,所以我假设不是...
您可能会考虑过度。试试这个:
public final class Singleton implements Cloneable, Serializable {
private static Singleton[] instances = new Singleton[]{new Singleton(), new Singleton()};
private static AtomicInteger counter = new AtomicInteger();
private Singleton() {} // further protection not necessary
public static Singleton getInstance() {
return instances[counter.getAndIncrement() % 2];
}
// Make singleton from deserializaion
protected Singleton readResolve() {
return getInstance();
}
@Override
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException("Use getInstance() instead");
}
}
如果您担心反射攻击,请使用防弹枚举,例如:
public final class Singleton implements Cloneable, Serializable {
private static AtomicInteger counter = new AtomicInteger();
private enum SingletonInstance implements Cloneable, Serializable {
ODD, EVEN;
private Singleton instance = new Singleton();
}
private Singleton() {} // further protection not necessary
public static Singleton getInstance() {
return SingletonInstance.values()[counter.getAndIncrement() % 2].instance;
}
// Make singleton from deserializaion
protected Singleton readResolve() {
return getInstance();
}
}
答案 2 :(得分:0)
您看到问题了吗?第一次调用可能会输入
getInstance
,线程将被抢占。然后,第二个调用可能会输入getInstance,但将获取oddInstance而不是evenInstance。很显然,可以通过使
getInstance
同步来防止这种情况,但这是不必要的。在单例的生命周期中仅需要两次同步,而不是每个getInstance
调用都需要同步。
如果您确实要“修复”此“问题”,则唯一的选择是同步getInstance
。但是如何真正看到这个问题呢?如果第一个线程在getInstance
之后被抢占怎么办?
在多线程中,事件的绝对顺序不是完全确定的。因此,您总是有动作似乎乱序的风险。
btw:反对“反射攻击”有一个严重的缺陷!这样可以防止构造evenInstance
!我想您应该将||
更改为&&
。但这仍然不能为您提供任何保证,因为“反射攻击”可能在第一次呼叫和第二次呼叫之间。您必须在上课时预先构造两个实例,以确保99%的把握。
如果您对此感到担心,则绝对不应该实施Cloneable
或Serializable
!