多线程环境下的单例模式

Singleton Pattern in Multi threaded environment

在我的面试中,面试官以单例模式开始他的问题。我写在下面。然后,他问 我们不应该在 getInstance 方法中检查 Nullity 吗?

我回答说,不需要,因为成员是静态类型并且同时被初始化。但是,好像他对我的不满意answer.Am我改不改?

class Single {

        private final static Single sing = new Single();       
        private Single() {
        }        
        public static Single getInstance() {
            return sing;
        }
    }

现在,下一个问题他要求为多线程环境编写单例 class。然后,我写了 double check singleton class.

  class MultithreadedSingle {        
        private static MultithreadedSingle single;       
        private MultithreadedSingle() {
        }        
        public static MultithreadedSingle getInstance() {
            if(single==null){
                    synchronized(MultithreadedSingle.class){
                      if(single==null){
                            single= new MultithreadedSingle(); 
                              }      
                      }
                   }
             return single;
        }
    }

然后,他对使用synchronized有异议,仔细检查说没用。为什么要检查两次,为什么要使用同步? 我试图用多种方案来说服他。但是,他没有。

后来,在家里我尝试了下面的代码,其中我使用简单的单例 class 和多线程。

public class Test {

    public static void main(String ar[]) {
        Test1 t = new Test1();
        Test1 t2 = new Test1();
        Test1 t3 = new Test1();
        Thread tt = new Thread(t);
        Thread tt2 = new Thread(t2);
        Thread tt3 = new Thread(t3);
        Thread tt4 = new Thread(t);
        Thread tt5 = new Thread(t);
        tt.start();
        tt2.start();
        tt3.start();
        tt4.start();
        tt5.start();

    }
}

final class Test1 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " : " + Single.getInstance().hashCode());
        }
    }

}
     class Single {

        private final static Single sing = new Single();       
        private Single() {
        }        
        public static Single getInstance() {
            return sing;
        }
    }

下面是输出:

Thread-0 : 1153093538
Thread-0 : 1153093538
Thread-0 : 1153093538
Thread-0 : 1153093538
Thread-0 : 1153093538
Thread-4 : 1153093538
Thread-1 : 1153093538
Thread-2 : 1153093538
Thread-3 : 1153093538
Thread-3 : 1153093538
Thread-3 : 1153093538
Thread-3 : 1153093538
Thread-3 : 1153093538
Thread-2 : 1153093538
Thread-2 : 1153093538
Thread-2 : 1153093538
Thread-2 : 1153093538
Thread-1 : 1153093538
Thread-1 : 1153093538
Thread-1 : 1153093538
Thread-1 : 1153093538
Thread-4 : 1153093538
Thread-4 : 1153093538
Thread-4 : 1153093538
Thread-4 : 1153093538

那么,问题来了,在多线程环境下是否有必要使用synchronizeor/and双重检查方法?似乎我的第一个代码本身(没有添加任何额外的代码行)就是这两个问题的答案。任何更正和知识共享将不胜感激。

您的第一个示例绝对正确,并且通常是单身人士的首选 "idiom"。另一种是做单元素枚举:

public enum Single {
    INSTANCE;

    ...
}

这两种方法非常相似,除非 class 是可序列化的,在这种情况下枚举方法更容易正确——但如果 class 不是可序列化的,我实际上作为一种风格问题,更喜欢你的枚举方法。注意 "accidentally" 由于实现接口或扩展本身可序列化的 class 而变为可序列化。

双重检查锁示例中的第二次无效检查也是正确的。但是,sing 字段 必须 volatile 才能在 Java 中工作;否则,一个线程写入 sing 和另一个线程读取它之间没有正式的 "happens-before" 边缘。这可能导致第二个线程看到 null 即使第一个线程分配给变量,或者,如果 sing 实例有状态,它甚至可能导致第二个线程只看到该状态的一部分(看到一个部分构造的对象)。

你的第一个答案对我来说似乎很好,因为无论如何都不可能出现竞争条件。

至于知识共享,在 Java 中实现单例的最佳方法是使用枚举。创建一个只有一个实例的枚举,仅此而已。至于代码示例 -

public enum MyEnum {
    INSTANCE;

    // your other methods
}

来自好书Effective Java-

[....] This approach is functionally equivalent to the public field approach, except that it is much more concise, provides the serialization machinery for free, and provides an ironclad guarantee against multiple instantiation, even in the face of sophisticated serialization or reflection attacks.[...] a single-element enum type is the best way to implement a singleton.

1) Class #1 适用于多线程环境

2) Class #2 是一个具有惰性初始化和双重检查锁定的单例,这是一个已知的模式,它需要使用同步。但是你的实现坏了,它需要 volatile 在场上。您可以在这篇文章中找到原因 http://www.javaworld.com/article/2074979/java-concurrency/double-checked-locking--clever--but-broken.html

3) 只有一个方法的单例不需要使用惰性模式,因为它class只会在第一次使用时加载和初始化。

根据 Double-checked_locking,这可能是最好的方法

class Foo {
    private volatile Helper helper;
    public Helper getHelper() {
        Helper result = helper;
        if (result == null) {
            synchronized(this) {
                result = helper;
                if (result == null) {
                    helper = result = new Helper();
                }
            }
        }
        return result;
    }
}

或使用 Initialization-on-demand holder idiom

public class Something {
    private Something() {}

    private static class LazyHolder {
        private static final Something INSTANCE = new Something();
    }

    public static Something getInstance() {
        return LazyHolder.INSTANCE;
    }
}

案例 #2 添加 'volatile' 关键字到静态字段 'single'.

使用 Double-Checked Locking 时考虑这种情况

  1. 线程A先进来,拿到锁,继续初始化对象。
  2. 根据 Java 内存模型 (JMM),在初始化 Java 对象之前为变量分配和发布内存。
  3. 线程B进来,因为双重检查锁,变量被初始化,没有获取到锁。
  4. 这并不能保证对象被初始化,即使这样,per-cpu 缓存也可能不会被更新。参考 Cache Coherence

现在介绍 volatile 关键字。

易失性变量总是写入主存。因此没有缓存不一致。