设计模式之单例模式

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

一、定义

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

二、实例

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

(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);

  

上述代码运行结果是:

 

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

=======================================================

我是Liusy,一个喜欢健身的程序员。

欢迎关注微信公众号【Liusy01】,一起交流Java技术及健身,获取更多干货,领取Java进阶干货,一起成为Java大神。

来都来了,关注一波再溜呗。

posted @ 2020-10-20 22:50  上古伪神  阅读(113)  评论(0编辑  收藏  举报