扭转实现单例模式

时间:2019-07-01 04:00:00

标签: java design-patterns 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而不是evenInstance

很显然,可以通过使getInstance同步来防止这种情况,但这是不必要的。在单例的生命周期中仅需要两次同步,而不是每个getInstance调用都需要同步。

想法?

3 个答案:

答案 0 :(得分:5)

最重要的是,evenInstanceoddInstance变量需要声明为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%的把握。

如果您对此感到担心,则绝对不应该实施CloneableSerializable