我的GOF23之单例模式

写在最前面

作为一个电气工程师,研究等离子体方向,最近在自学设计模式,此为整理博客。
设计模式可以分为三大类,分别是创建型设计模式、行为型设计模式以及结构型设计模式。

单例模式是创建型的设计模式的一种。

心法:

  1. 构造器私有

  2. 私有单例对象

  3. 公有静态方法获取单例对象

各种实现方式

饿汉式

public class Singleton{
    private static Singleton instance = new Singleton();
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        return instance;
    }
}

特点:线程安全,但是没有实现lazy load.所谓的lazy load,根据Bob Lee的说法,

In production, you typically want to eagerly load all your singletons so you catch errors early and take any performance hit up front, but in tests and during development, you only want to load what you absolutely need so as not to waste time.

那么如何实现lazy load呢?

懒汉式

public class Singleton{
    private static Singleton instance;
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

懒汉式的确实现了lazy load的问题,但是它也带来了新的问题,线程安全无法保证。我们可以在getInstance()方法上加synchronized,当然也可以给instance加volatile,这同样是Bob Lee的看法,他说

But volatile isn't that much faster than synchronized, synchronized is pretty fast nowadays.

但是与此同时,我还看到了更深入的讨论,如

http://stackoverflow.com/a/46... So the overall cost of a volatile read will roughly equivalent of a memory load and can be as cheap as a L1 cache access. However if another core is writing to the volatile variable, the cache-line will be invalidated requiring a main memory or perhaps an L3 cache access. The actual cost will depend heavily on the CPU architecture.

这就太深入了,暂时不考虑,但是下面的建议还是不错的:

Nevertheless you shouldn't make a variable volatile unless you know that it will be accessed from multiple threads outside of synchronized blocks. Even then you should consider whether volatile is the best choice versus synchronized, AtomicReference and its friends, the explicit Lock classes, etc.

因此我并未在如下的代码中添加volatile关键字

线程安全的懒汉式

public class Singleton{
    private static Singleton instance;
    
    private Singleton(){
    }
    
    public synchronized static Singleton getInstance(){
        if(instance == null){
                instance = new Singleton();
        }
        return instance;
    }
}

双锁机制Double-Checked Locking(DCL)可以对性能进行进一步的优化

双锁机制

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

双锁机制的问题:

So what's broken about DCL?
DCL relies on an unsynchronized use of the resource field. That appears to be harmless, but it is not. To see why, imagine that thread A is inside the synchronized block, executing the statement resource = new Resource(); while thread B is just entering getResource(). Consider the effect on memory of this initialization. Memory for the new Resource object will be allocated; the constructor for Resource will be called, initializing the member fields of the new object; and the field resource of SomeClass will be assigned a reference to the newly created object.
However, since thread B is not executing inside a synchronized block, it may see these memory operations in a different order than the one thread A executes. It could be the case that B sees these events in the following order (and the compiler is also free to reorder the instructions like this): allocate memory, assign reference to resource, call constructor. Suppose thread B comes along after the memory has been allocated and the resource field is set, but before the constructor is called. It sees that resource is not null, skips the synchronized block, and returns a reference to a partially constructed Resource! Needless to say, the result is neither expected nor desired.
When presented with this example, many people are skeptical at first. Many highly intelligent programmers have tried to fix DCL so that it does work, but none of these supposedly fixed versions work either. It should be noted that DCL might, in fact, work on some versions of some JVMs -- as few JVMs actually implement the JMM properly. However, you don't want the correctness of your programs to rely on implementation details -- especially errors -- specific to the particular version of the particular JVM you use.
Other concurrency hazards are embedded in DCL -- and in any unsynchronized reference to memory written by another thread, even harmless-looking reads. Suppose thread A has completed initializing the Resource and exits the synchronized block as thread B enters getResource(). Now the Resource is fully initialized, and thread A flushes its local memory out to main memory. The resource's fields may reference other objects stored in memory through its member fields, which will also be flushed out. While thread B may see a valid reference to the newly created Resource, because it didn't perform a read barrier, it could still see stale values of resource's member fields.

简单说来,DCL导致的问题是,初始化实例的写入操作和实例字段的写入操作能够被编译器或者缓冲区重排序,重排序可能会导致返回部分构造的一些东西。就是我们读取到了一个没有初始化的对象。
对此,有更好的解决方法,即保证了线程安全,又实现了lasy load——Initialization on Demand Holder即静态内部类实现

静态内部类

public class Singleton{
    private Singleton(){
    }

    private static class SingletonHolder{
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

以上方法都有可能通过反射或序列化与反序列化来破解,具体内容参见 反射与(反)序列化问题,而枚举类则没有如上问题

最理想的实现——枚举

public enum Singleton{
    INSTANCE;
}

反射与(反)序列化问题

反射是如何破解单例的

    public class Reflect{
        public static void main(String[] args) throws Exception{
            Class<Singleton> clazz = (Class<Singleton>)Class.forName("bin.pattern.Singleton");
            Constructor c = clazz.getDeclaredConstructor(null);
            c.setAccessible(true);
            Singleton s1 = Singleton.getInstance();
            Singleton s2 = (Singleton)c.newInstance();
            System.out.println(s1 == s2); 
        }
    }

返回false,说明产生了新的对象。修复方法:Singleton.java

public class Singleton{
    private static Singleton instance;
    private Singleton(){
        if(instance != null){
            throw new RuntimeException();
        }
    }
}

序列化与反序列化是如何破解单例的

public class Serialization{
    public static void main(String[] args) throws IOException,ClassNotFoundException{
        Singleton s1 = Singleton.getInstance();
        FileOutputStream fos = new FileOutputStream("/Users/bin/Documents/workspace/myjdk/a.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s1);
        
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/Users/bin/Documents/workspace/myjdk/a.txt"));
        Singleton s2 = (Singleton)ois.readObject();
        
        System.out.println(s1 == s2);
    }
}

输出false,说明又跪了。解决方法:Singleton.java

public class Singleton implements Serializable{
    //添加方法
    private Object readResolve() {
        return SingletonHolder.INSTANCE;
    }
}

菟潞寺沙弥
303 声望55 粉丝