实现单例模式

Implement the singleton pattern with a twist

这是求职面试问题。

Implement the singleton pattern with a twist. First, instead of storing one instance, store two instances. And in every even call of getInstance(), return the first instance and in every odd call of getInstance(), return the second instance.

我的实现如下:

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 调用。

想法?

你没有说单例必须延迟初始化,所以我假设不是...

你可以over-thinking它。试试这个:

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");
    }
}

如果您担心反射攻击,只需使用枚举,即 bullet-proof,例如:

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();
    }
}

最重要的是,需要声明 evenInstanceoddInstance 变量 volatile。请参阅著名的 "Double-Checked Locking is Broken" 声明: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();
    } 
}

Do you see a problem? The first call may enter getInstance and the thread get preempted. The second call may then enter getInstance but will get the oddInstance instead of the evenInstance.

Obviously, this can be prevented by making getInstance synchronized, but it's unnecessary. The synchronization is only required twice in the lifecycle of the singleton, not for every single getInstance call.

如果你真的想“解决”这个“问题”,你唯一的选择就是同步getInstance。但是,如何真正看待这个问题呢?如果第一个线程在 getInstance 之后被抢占怎么办?

在多线程中,事件的绝对顺序并不是完全确定的。所以你总是有这样的风险,即行动似乎是 out-of-order.


btw:针对“反射攻击”的攻击存在严重缺陷!它阻止了 evenInstance 的构建!我猜你应该把 || 改成 &&。但这仍然不能给你任何保证,因为“反射攻击”可能发生在第一次和第二次通话之间。您必须在 class 加载时间预构建两个实例才能确保 99%。

如果您担心,您绝对不应该实施 CloneableSerializable