单例模式之饿汉式和懒汉式

常见的单例模式有两种:饿汉式;懒汉式。

饿汉式代码如下:

/**
 * 单例模式之饿汉式
 * @author leon
 * @time 2018年4月27日 下午2:28:14
 */
public class HungrySingleton {
    // 三要素
    // 1.私有对象
    private static  HungrySingleton hungrySingleton = new HungrySingleton();
    // 2.私有构造器
    private HungrySingleton() {}
    // 3.返回实例的静态方法
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

饿汉式的实例对象是在类初始化时就生成的,而类的初始化时线程安全的,因此饿汉式也是线程安全的。不过,其对象如果特别庞大,类似于MyBatis的Configuration,应用的初始化速度会相对较慢,即没有延迟加载的特性。

懒汉式代码如下:

/**
 * 2. 懒汉式单例模式
 * @author leon
 * @time 2018年4月12日 上午6:54:45
 */
public class LazySingleton{
    // 三要素
    // 1.私有对象
    private static final LazySingleton LAZY_SINGLETON;
    // 2.私有构造器
    private LazySingleton() throws Exception {}
    // 3.返回实例的静态方法
    public static synchronized LazySingleton getInstance() throws Exception {
        if(LAZY_SINGLETON == null) {
            LAZY_SINGLETON = new LazySingleton();
        }
        return LAZY_SINGLETON;
    }
}

懒汉式的实例对象在类初始化的时候并没有生成,而是在返回实例的同步静态方法中生成的,实现了延迟加载的特性。由于java字节码重排和锁的持有和释放的问题,可能会造成对象重复生成的问题,因此非线程安全。

而反射和序列化会生成不一样的对象,下面就如何规避这两种情况做一下解决方案的分析。

首先是反射,反射生成class对象方式为三种:Class.forName(“”);obj.class;Class.class。通过反射我们可以获取class对象,进而获取构造器,通过将构造器的属性转换为公有,进而通过构造器生成实例对象。代码如下:

LazySingleton lazySingleton1 = LazySingleton.getInstance();
LazySingleton lazySingleton2 = LazySingleton.getInstance();
// 并没有重写hashCode和equals方法,因此下面比较的是对象引用的内存地址
System.out.println(lazySingleton1 == lazySingleton2);// true    
Constructor<LazySingleton> constructor = LazySingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingleton lazySingleton3 = constructor.newInstance();
System.out.println(lazySingleton2 == lazySingleton3);// false

面对此问题,还没有好的解决方法,解决方法就是在私有构造器里抛出重复生成对象的异常,代码如下:

private LazySingleton() throws Exception {
    // 重复生成对象的时候,抛出异常,防止通过反射生成
    if(lazySingleton != null) {
        throw new Exception("LazySingleton 是单例对象,不能重复生成");
    }
}

其次是序列化,首先肯定LazySingleton要实现序列化接口Serializable,通过下面的代码测试可知,反序列化生成的是不一样的实例对象。测试代码如下:

testObjectOutputStream(lazySingleton2, new File("e:\\obj.txt"));
LazySingleton lazySingleton4  = testObjectInputStream(new File("e:\\obj.txt"));
System.out.println(lazySingleton2 == lazySingleton4);//false

// 反序列化
@SuppressWarnings("unchecked")
public static <T> T testObjectInputStream(File file) {// 读
    try(InputStream in = new FileInputStream(file);ObjectInputStream objectInputStream = new ObjectInputStream(in);){
        return (T) objectInputStream.readObject();
    }catch(Exception e){
        return null;
    }
}
// 序列化
public static boolean testObjectOutputStream(Object obj, File file) {// 写
    try(OutputStream out = new FileOutputStream(file);ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);){
        objectOutputStream.writeObject(obj);
    }catch(Exception e){
        return false;
    }
    return true;
}

怎么解决反序列的问题,就是在类的内部添加如下方法:

// 防止通过反序列化的方式生成重复对象
private Object readResolve() throws ObjectStreamException {
    return lazySingleton;
}

测试结果确实生成了同样的对象,测试代码和之前一样结果如下:

扫描二维码关注公众号,回复: 157957 查看本文章
System.out.println(lazySingleton2 == lazySingleton4);//true

为什么能够通过添加此代码解决此问题呢?看源码可知在readResolve方法被实现的时候,序列化的时候是通过反射调用的此方法返回的实例对象(具体请参看ObjectInputStream和ObjectStreamClass类)。

还有另外三种:双检查锁式;枚举式;静态内部类式,在后面的博文中再论述。

猜你喜欢

转载自blog.csdn.net/jian_j_z/article/details/80109336