Java 使用twist实现singleton模式
这是一个面试问题 通过扭曲实现单例模式。首先,而不是 存储一个实例,存储两个实例。在每一个偶数的电话里Java 使用twist实现singleton模式,java,design-patterns,singleton,Java,Design Patterns,Singleton,这是一个面试问题 通过扭曲实现单例模式。首先,而不是 存储一个实例,存储两个实例。在每一个偶数的电话里 getInstance(),在对 getInstance(),返回第二个实例 我的执行情况如下: public final class Singleton implements Cloneable, Serializable { private static final long serialVersionUID = 42L; private static Singleton
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
,而不是evencInstance
显然,这可以通过使getInstance
同步来防止,但这是不必要的。在单例的生命周期中只需要两次同步,而不是每次调用都需要同步
想法?你不是说单例必须惰性地初始化,所以我假设不是 你可能想得太多了。试试这个:
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");
}
}
如果您担心反射攻击,只需使用enum,它是防弹的,类似于:
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();
}
}
最重要的是,需要声明
evenInstance
和oddInstance
变量volatile
。请参阅著名的“双重检查锁定已断开”声明:
此外,对于偶数和奇数实例,您应该在同步块中使用不同的对象,以便可以同时构造它们
最后,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();
}
}
你看到问题了吗?第一次调用可能进入getInstance
,线程被抢占。然后,第二个调用可能会进入getInstance,但将获得oddInstance而不是evenInstance
显然,这可以通过使getInstance
同步来防止,但这是不必要的。在单例的生命周期中只需要两次同步,而不是每次调用都需要同步
如果您真的想“修复”这个“问题”,您唯一的选择就是同步getInstance
。但人们如何看待这个问题呢?如果第一个线程正好在getInstance
之后被抢占呢
在多线程中,事件的绝对顺序不是完全确定的。所以你总是有这样的风险,行动似乎是不正常的
顺便说一句:对“反射攻击”的攻击有一个严重的缺陷!它阻止构建甚至实例
!我想你应该把|
改成&&
。但这仍然不能给您任何保证,因为“反射攻击”可能在第一次调用和第二次调用之间。您必须在类加载时预构造这两个实例,以确保99%的安全性
如果您担心它,那么您肯定不应该实现Cloneable
或Serializable
这并不能解决我描述的比赛条件。您仍然可以抢占线程1,而线程2出现并获得错误的实例。@AbhijitSarkar不,这段代码没有竞争条件,因为这两个实例都是在允许调用getInstance()
方法之前创建的(由类装入器在装入类时创建)。这不是我要说的,我的代码在没有更改的情况下可以很好地处理创建。要求是第一个调用获取奇数调用的实例,第二个调用获取偶数实例。我的代码中的错误,也存在于你的代码中,是第二次调用可能会得到奇数实例。在我看来,enum
方法非常聪明,但不可行。它总是看起来像一个黑客,不是很方便。如果你给我你的库,并且这个库声明了一个enum singletonistance
,我希望它是一个enum
。可能这个enum
用于控制返回的方法实例。我绝对不希望它背后有像singleton(例如database connector)这样的复杂类。这意味着这种方法附加了一个约定,可能是一个命名约定。这要求您的库的使用者了解此约定。@BionicCode我同意您的看法,enum单例模式感觉像是一种黑客行为,因为它不是一个实际的enum,但a)OP似乎担心如何防御反射黑客行为(火中带火),B)ppl不断将此模式推到我们的脖子上:(这是如何解决我在问题和对@Bohemian答案的评论中描述的问题的?这个问题实际上并不存在。如果两个调用不在同一线程中,那么“第一个”调用是先执行getAndIncrement
的调用,该顺序决定了哪一个获得偶数实例。没有其他“first”的定义可以应用。“first”的定义也不适用当然还有另一个定义。首先是首先进入getInstance
方法的线程。但是,由于解释取决于您询问的对象,我认为您所说的可以被视为可行的替代方法。我想补充一点,当使用此模式时,JVM的内存管理可以保证真正的线程安全,正如内部类“evenheloper.instance
的多次(并发)调用被阻止(或同步),直到类被创建。此外,这种方法隐式延迟加载实际的单例实例。另一个副作用是,这种方法消除了不安全的if语句(双重检查模式)。@Abhijit,“进入方法”实际上是一个多步骤的过程,而不是一个原子操作,它的任何内存效果都不一定是在其他核上按顺序进行的w.r.t.操作,除了内存限制