首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【设计模式-单例模式】

【设计模式-单例模式】

作者头像
Liusy
发布2020-09-01 16:22:25
5020
发布2020-09-01 16:22:25
举报
文章被收录于专栏:Liusy01Liusy01

上一篇写了建造者模式,不知道你们看懂了没,反正我是理解了。

今天来说一下同样属于创建型模式的单例模式,相信这个模式普遍都清楚,因为平时在编码的时候都会进行相应的使用,我这边就当做日志记录一下。免得以后忘了还得去搜,我发现我记忆里非常差,很多东西很快就忘记了,年纪大了没办法。

一、定义

保证一个类仅有一个实例,并提供全局访问点。就是打死也不会生成第二个实例。一般用在工具类上,可以降低内存消耗

二、实例

单例模式有几种,分别是饿汉式、懒汉式、静态内部类单例以及枚举类单例。

(1)饿汉式

顾名思义,饿汉式就是整个项目在启动的时候进行创建。此时可以使用静态代码块。

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

此时外部还是可以通过new的方式创建一个实例化对象,只需要添加一个私有的构造方法,外部就无法通过new实例化对象。

private HungrySingleton() {
}

但是,注意,此时依然是可以通过反射进行构造对象的,例如:

Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class;
Constructor<HungrySingleton> constructor = hungrySingletonClass.getDeclaredConstructor(null);
1、打开私有方法的权限
constructor.setAccessible(true);
HungrySingleton hungrySingleton = constructor.newInstance();
System.out.println(hungrySingleton == HungrySingleton.getInstance());

运行结果如下:

这个方法好像只能通过枚举类单例才能解决。

此时还是会有一个问题,当单例对象进行序列化之后,通过反序列化出来的结果是不一样的。

比如:

1、这个类需要实现Serializable才能进行序列化
HungrySingleton hungrySingleton = HungrySingleton.getInstance();
2、序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serial_single"));
oos.writeObject(hungrySingleton);
3、反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("serial_single"));
HungrySingleton unserialInstance = (HungrySingleton) ois.readObject();
4、查看原有类与反序列化后生成的类是否相同
System.out.println(hungrySingleton == unserialInstance);

结果是false

此时需要在HungrySingleton 类中实现readResolve方法。

private Object readResolve(){
    return instance;
}

此时再看上述代码的运行结果

此时反序列化之后就是相同的了,这是为啥来,看一下源码:

点进ObjectInputStream 的readObject方法,然后进入Object obj = readObject0(false);这个方法。

此时会有一个判断读取对象类型的switch代码块,我们的是object类,所以进入readOrdinaryObject方法

而isInstantiable这个方法是只要实现了serializable就返回true

可以看到,这里会根据读取的class去实例化一个新的对象。

但是,下面还有一段代码,看这个desc.hasReadResolveMethod()方法,这个就是判断有没有实现readResolve方法的

boolean hasReadResolveMethod() {
      requireInitialized();
      return (readResolveMethod != null);
  }

而这个readResolveMethod字段是一个Method类型的,其在ObjectStreamClass构造方法中。

所以只要实现了readResolve方法就可以反序列化出一样的对象。

(2)懒汉式

懒汉式的意思是只有需要用到这个类的时候才会进行创建。

public class LazySingleton {
    private static LazySingleton instance = null;
    private LazySingleton(){}
    public static LazySingleton getInstance() {
        1、if (instance == null) {
            2、instance = new LazySingleton();
        }
        return instance;
    }
}

这种的话就会有线程不安全的情况,比如线程A跑到1处,线程B跑到2处还没进行实例化的时候,线程A依然会进入if代码块进行实例化,此时会产生两个实例化对象。

此时可以进行加锁:用synchronized 关键字

public synchronized static LazySingleton getInstance() {
    if (instance == null) {
        instance = new LazySingleton();
    }
    return instance;
}

这样的话会影响性能,因为把整个方法都锁定了,当线程A获取锁之后,其他线程就只能等待线程A把锁释放。

这个时候可以只对实例化的代码块加锁,这就是双层锁定了,例如:

public static LazySingleton getDoubleCheckInstance() {
    1、if (instance == null) {
        synchronized (LazySingleton.class) {
            if (instance == null) {
                2、instance = new LazySingleton();
            }
        }
    }
    return instance;
}

此时又会有一个问题,在instance = new LazySingleton();这行代码会发生指令重排的现象,创建实例化对象的过程是有三步,

1、分配内存

2、初始化对象

3、将堆内对象的地址赋值给instance

第2和第3是没有前后关系的,可以先执行第3步。

这个就会发生这种情况,线程A获取锁进来了,在代码2处实例化对象时先将地址赋值给instance,此时instance就不是空对象了,其他线程刚好进入代码1处,instance不为null,此时就会直接返回。

这个情况的解决方法是volatile关键字,其可以禁止指令重排。只需在单例对象加上此关键字。

private static volatile LazySingleton instance = null;

(3)静态内部类单例模式

public class StaticInnerClassSingleton {
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton 
                                    = new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }
}

(4)枚举类单例

public enum EnumSingleton {
    INSTANCE;
    private String data;
    EnumSingleton() {
        this.data = new String("abc");
    }
    public String getInstance () {
        return data;
    }
}

枚举类是无法通过反射去实例化对象的

Class<EnumSingleton> enumSingletonClass = EnumSingleton.class;
Constructor<EnumSingleton> constructor = enumSingletonClass.getDeclaredConstructor(null);
constructor.setAccessible(true);
EnumSingleton hungrySingleton = constructor.newInstance();

上述代码运行之后会抛异常:

而且枚举类天然是序列化和反序列化是一致的

EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serial_single"));
oos.writeObject(enumSingleton);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("serial_single"));
EnumSingleton unserialInstance = (EnumSingleton) ois.readObject();

System.out.println(enumSingleton == unserialInstance);

上述代码运行结果是:

所以综合上述所讲,枚举类单例是最优的解决方案。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-03-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Liusy01 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档