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


单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。

单例模式一般体现在类声明中,单例的类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

适用场合:

  • 需要频繁的进行创建和销毁的对象;
  • 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
  • 工具类对象;
  • 频繁访问数据库或文件的对象。

比如:许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

优点:

  • 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如网站首页页面缓存)。
  • 避免对资源的多重占用(比如写文件操作)。

二、实现方式

1、普通饿汉式(线程安全,不能延时加载

所谓饿汉。这是个比较形象的比喻。对于一个饿汉来说,他希望他想要用到这个实例的时候就能够立即拿到,而不需要任何等待时间。

public class Singleton {

    private final static Singleton INSTANCE = new Singleton();

    private Singleton(){}

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

优点:写法简单 线程安全

通过static的静态初始化方式,在该类第一次被加载的时候,就有一个SimpleSingleton的实例被创建出来了。这样就保证在第一次想要使用该对象时,他已经被初始化好了。

同时,由于该实例在类被加载的时候就创建出来了,所以也避免了线程安全问题。

JVM类加载机制中:

“ 并发:

  虚拟机会保证一个类的类构造器<clinit>()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器<clinit>(),其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。

特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行<clinit>()方法,因为在同一个类加载器下,一个类型只会被初始化一次。 ”

缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。

在类被加载的时候对象就会实例化。这也许会造成不必要的消耗,因为有可能这个实例根本就不会被用到。

想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。

解决不能Lazy Loading懒加载问题的办法:第一种是使用静态内部类的形式。第二种是使用懒汉式。下文会介绍。

2、静态代码块饿汉式(线程安全,不能延时加载

public class Singleton {

    private static Singleton instance;

    static {
        instance = new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

和第一种一样,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。

3、静态内部类(线程安全,延迟加载,效率高

public class Singleton {

    private Singleton() {}

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

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

加载类 Singleton 时不会实例化对象,加载类 SingletonInstance 时才会实例化对象(也就是调用Singleton的getInstance方法时),实现了延迟加载。

关于类加载机制:JVM类加载机制

优点:线程安全,延迟加载,效率高。

4、枚举(线程安全,不能延时加载

public enum Singleton {
    INSTANCE;
    public void whateverMethod() {

    }
}

这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过,但是不代表他不好。

原理其实也是利用类加载机制实现线程安全。

反编译后:

public final class Singleton extends Enum<Singleton> {
    public static final Singleton INSTANCE = new Singleton("INSTANCE", 0);
    private static final Singleton[] $VALUES;

    public static Singleton[] values() {
        return (Singleton[])$VALUES.clone();
    }

    public static Singleton valueOf(String string) {
        return Enum.valueOf(Singleton.class, string);
    }

    private Singleton(String string, int n) {
        super(string, n);
    }

    public void whateverMethod() {
    }

    static {
        $VALUES = new Singleton[]{INSTANCE};
    }
}

关于枚举原理:JDK源码学习笔记——Enum枚举使用及原理

优点:简单 线程安全

缺点:不能延迟加载 使用较少

5、普通懒汉式(线程不安全,可延时加载

public class Singleton {

    private static Singleton singleton;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

优点:可以实现延迟加载

缺点:线程不安全

多个线程可能同时进入if 中,创建出多个实例

6、synchronized 懒汉式(线程安全,可延时加载,效率低

public class Singleton {

    private static Singleton singleton;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

优点:可以实现延迟加载,线程安全

缺点:效率低

只有第一次创建实例的时候需要同步,其他情况都不需要。

我们知道synchronized是一个效率比较低的加锁方式,而每次获取实例都会同步加锁(本身不需要同步,直接返回 instance 即可),效率会很低。

7、双重校验锁懒汉式(线程安全,可延时加载,效率高

详细可参考:Java并发(七):双重检验锁定DCL   Java并发(二):Java内存模型

对于第六中方法进行优化,减小锁的粒度:

public class Singleton {
        private static Singleton singleton;
        Integer a;

        private Singleton(){}

        public static Singleton getInstance(){
            if(singleton == null){                              // 1 只有singleton==null时才加锁,性能好
                synchronized (Singleton.class){                 // 2
                    if(singleton == null){                      // 3
                        singleton = new Singleton();            // 4
                    }
                }
            }
            return singleton;
        }
    }

会因为重排序出现问题:

线程A发现变量没有被初始化, 然后它获取锁并开始变量的初始化。

由于某些编程语言的语义,编译器生成的代码允许在线程A执行完变量的初始化之前,更新变量并将其指向部分初始化的对象。

线程B发现共享变量已经被初始化,并返回变量。由于线程B确信变量已被初始化,它没有获取锁。如果在A完成初始化之前共享变量对B可见(这是由于A没有完成初始化或者因为一些初始化的值还没有穿过B使用的内存(缓存一致性)),程序很可能会崩溃。

利用volatile限制重排序:

public class Singleton {

    private static volatile Singleton singleton;

    private Singleton() {}

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

三、单例与序列化

1、序列化对单例的破坏

双重检验锁实现单例:

public class Singleton implements Serializable{
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

测试序列化对单例的影响:

public class SerializableDemo1 {
    //为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
    //Exception直接抛出
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //Write Obj to file
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
        oos.writeObject(Singleton.getSingleton());
        //Read Obj from file
        File file = new File("tempFile");
        ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
        Singleton newInstance = (Singleton) ois.readObject();
        //判断是否是同一个对象
        System.out.println(newInstance == Singleton.getSingleton());
    }
}
//false

通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。

2、分析

ois.readObject();  调用的 readOrdinaryObject 方法

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        //此处省略部分代码

        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        //此处省略部分代码

        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }

isInstantiable:如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。针对serializable和externalizable我会在其他文章中介绍。

desc.newInstance:该方法通过反射的方式调用无参构造方法新建一个对象。

hasReadResolveMethod:如果实现了serializable 或者 externalizable接口的类中包含readResolve则返回true

invokeReadResolve:通过反射的方式调用要被反序列化的类的readResolve方法。

原因:序列化会通过反射调用无参数的构造方法创建一个新的对象

解决:在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。

3、解决

public class Singleton implements Serializable{
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    privatereturn singleton;
    }
}

总结:一旦实现了Serializable接口之后,就不再是单例的了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象。解决办法就是使用readResolve()方法来避免此事发生。

四、关于枚举实现单例的序列化问题

为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:

在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

所以,枚举实现的单例不会有序列化问题

 

 

参考资料 / 相关推荐:

Java并发(二):Java内存模型

Java并发(七):双重检验锁定DCL 

JDK源码学习笔记——Enum枚举使用及原理

JVM类加载机制

单例模式的八种写法比较

设计模式(二)——单例模式

深度分析Java的枚举类型—-枚举的线程安全性及序列化问题


作者:那股泥石流,发布于:2019/01/14
原文:https://www.cnblogs.com/hexinwei1/p/10254415.html