返回顶部

单例模式(Singleton)

概念

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

特点: 
    1)单例类确保自己只有一个实例(构造函数私有:不被外部实例化,也不被继承)。
    2)单例类必须自己创建自己的实例。
    3)单例类必须为其他对象提供唯一的实例。

类图

单例模式的应用

    资源管理器,回收站,打印机资源,线程池,缓存,配置信息类,管理类,控制类,门面类,代理类通常被设计为单例类

    如果程序有多个类加载器又同时使用单例模式就有可能多个单例并存就要找相应解决方法了

 

1、饿汉式

public class Singleton {
    //在自己内部定义自己一个实例,private 只供内部调用
    private static Singleton instance = new Singleton();
    //将构造函数设置为私有
    private Singleton(){
    }
    //静态工厂方法,提供了一个供外部访问得到对象的静态方法
    public static Singleton getInstance() {
        return instance;
    }
}

2、懒汉式(线程不安全)

//懒汉式单例类.在第一次调用的时候实例化自己   
public class Singleton {  
    private Singleton() {}  
    private static Singleton single = null;  
    //静态工厂方法   
    public static Singleton getInstance() {  
         if (single == null) {    
             single = new Singleton();  
         }    
        return single;  
    }  
} 

这种写法lazy loading很明显,但是在多线程不能正常工作。

3、懒汉式(线程安全)

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

这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是同步是需要开销的,我们只需要在初始化的时候同步,而正常的代码执行路径不需要同步。

4、双重检查锁定

public class DoubleCheckedLockingSingleton {  
    // java中使用双重检查锁定机制,由于Java编译器和JIT的优化的原因,产生指令重排序,系统无法保证我们期望的执行次序。  
    // 在java5.0修改了内存模型,使用volatile声明的变量可以强制屏蔽编译器和JIT的优化工作  
    private volatile static DoubleCheckedLockingSingleton uniqueInstance;  

    private DoubleCheckedLockingSingleton() {  
    }  

    public static DoubleCheckedLockingSingleton getInstance() {  
        DoubleCheckedLockingSingleton localRef = uniqueInstance;
        if (uniqueInstance == null) {  
            synchronized (DoubleCheckedLockingSingleton.class) {  
                localRef = uniqueInstance;
                if (localRef == null) {  
                    uniqueInstance = localRef = new DoubleCheckedLockingSingleton();  
                }  
            }  
        }  
        return localRef ;  
    }  
}

注意上面的变量localRef,“似乎”看上去显得有点多余。但是实际上绝大多数时候uniqueInstance已经被初始化,引入ocalRef可以使得volatile的只被访问一次(利用return localRef代替return uniqueInstance),这样可以使得这个单例的整体性能提升25%。更多可以参考wiki百科,  Effective Java 中文第二版,p. 250。

指令重排序,是为了优化指令,提高程序运行效率。指令重排序包括编译器重排序和运行时重排序。JVM规范规定,指令重排序可以在不影响单线程程序执行结果前提下进行。例如 instance = new Singleton() 可分解为如下伪代码:

memory = allocate();   //1:分配对象的内存空间  
ctorInstance(memory);  //2:初始化对象  
instance = memory;     //3:设置instance指向刚分配的内存地址  

经过重排序后如下:

memory = allocate();   //1:分配对象的内存空间  
instance = memory;     //3:设置instance指向刚分配的内存地址  
                       //注意,此时对象还没有被初始化!  
ctorInstance(memory);  //2:初始化对象 

将第2步和第3步调换顺序,在单线程情况下不会影响程序执行的结果,但是在多线程情况下就不一样了。线程A执行了instance = memory(这对另一个线程B来说是可见的),此时线程B执行外层 if (instance == null),发现instance不为空,随即返回,但是得到的却是未被完全初始化的实例,在使用的时候必定会有风险,所以通过Volatile关键字来屏蔽。

5、延迟初始化/占位类模式/静态内部类模式

public class Singleton {  
    private static class InstanceHolder {  
        private static Singleton instance = new Singleton();  
    }  
  
    private Singleton() {  
    }  
  
    public static Singleton getInstance() {  
        return InstanceHolder.instance;  
    }  
} 
根据jvm规范,当某对象第一次调用Singleton.getInstance()时,Singleton类被首次主动使用,jvm对其进行加载初始化(此时并不会调用Singleton()构造方法),然后Singleton调用getInstance()方法,该方法中,又首次主动使用了InstanceHolder类,所以要对InstanceHolder类进行加载初始化,初始化中,INSTANCE常量被赋值时才调用了 Singleton的构造方法Singleton(),完成了实例化构建并返回该实例。
当再有对象(也许是在别的线程中)再次调用Singleton.getInstance()时,因为已经初始化过了(类的加载只有一次),不会再进行初始化步骤,所以直接返回INSTANCE常量即同一个Singleton实例。
内部类的加载机制:加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。 详看:静态内部类何时初始化

6、枚举式

public enum SingletonClass
{
    INSTANCE;
}

 

改进

第五个延迟初始化的方式,在反射的作用下,单例结构是会被破坏的,测试代码如下所示:

public class LazySingleton2Test {
    public static void main(String[] args) {
        //创建第一个实例
        LazySingleton2 instance1 = LazySingleton2.getInstance();
    
        //通过反射创建第二个实例
        LazySingleton2 instance2 = null;
        try {
            Class<LazySingleton2> clazz = LazySingleton2.class;
            Constructor<LazySingleton2> cons = clazz.getDeclaredConstructor();
            cons.setAccessible(true);
            instance2 = cons.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }

        //检查两个实例的hash值
        System.out.println("Instance 1 hash:" + instance1.hashCode());
        System.out.println("Instance 2 hash:" + instance2.hashCode());
    }
}

输出:
Instance 1 hash:1694819250
Instance 2 hash:1365202186

由哈希值可以看出,反射破坏了单例的特性,因此有延迟初始化改进一版:

public class LazySingleton3 {

    private static boolean initialized = false;

    private LazySingleton3() {
        synchronized (LazySingleton3.class) {
            if (initialized == false) {
                initialized = !initialized;
            } else {
                throw new RuntimeException("单例已被破坏");
            }
        }
    }

    static class SingletonHolder {
        private static final LazySingleton3 instance = new LazySingleton3();
    }

    public static LazySingleton3 getInstance() {
        return SingletonHolder.instance;
    }
}

此时再运行一次测试类,出现如下提示

java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at test.LazySingleton3Test.main(LazySingleton3Test.java:21)
Caused by: java.lang.RuntimeException: 单例已被破坏
    at singleton.LazySingleton3.<init>(LazySingleton3.java:12)
    ... 5 more
Instance 1 hash:359023572

在分布式系统中,有些情况下你需要在单例类中实现 Serializable 接口。使改进一版的类实现Serializable接口,再测试。

public class LazySingleton3Test {
    public static void main(String[] args) {
        try {
            LazySingleton3 instance1 = LazySingleton3.getInstance();
            ObjectOutput out = null;

            out = new ObjectOutputStream(new FileOutputStream("filename.ser"));
            out.writeObject(instance1);
            out.close();

            //deserialize from file to object
            ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser"));
            LazySingleton3 instance2 = (LazySingleton3) in.readObject();
            in.close();

            System.out.println("instance1 hashCode=" + instance1.hashCode());
            System.out.println("instance2 hashCode=" + instance2.hashCode());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出:
instance1 hashCode=2051450519
instance2 hashCode=1510067370

看到了两个实例类。为了避免此问题,我们需要提供 readResolve() 方法的实现。readResolve()代替了从流中读取对象。这就确保了在序列化和反序列化的过程中没人可以创建新的实例。  修改之,再有延迟初始化改进二版:

public class LazySingleton4 implements Serializable {

    private static boolean initialized = false;

    private LazySingleton4() {
        synchronized (LazySingleton4.class) {
            if (initialized == false) {
                initialized = !initialized;
            } else {
                throw new RuntimeException("单例已被破坏");
            }
        }
    }

    static class SingletonHolder {
        private static final LazySingleton4 instance = new LazySingleton4();
    }

    public static LazySingleton4 getInstance() {
        return SingletonHolder.instance;
    }
    
    private Object readResolve() {
        return getInstance();
    }
}

此时,在运行测试类,输出如下:
instance1 hashCode=2051450519
instance2 hashCode=2051450519

这表示此时已能保证序列化和反序列化的对象是一致的。

 

 

 

参考链接:

singleton模式四种线程安全的实现

双重检查锁定(double-checked locking)与单例模式

设计模式:单例模式(Singleton)

单例模式的七种写法以及注意事项

自己动手实现牛逼的单例模式

 

posted @ 2018-06-11 21:53  jaden好青年  阅读(195)  评论(0编辑  收藏  举报