如何做到真正的单例
前言
单例设计模式可以说是最常使用的设计模式了,它的作用是确保某个类只有一个实例,自行实例化并向整个系统提供这个实例。为什么使用单例模式呢?简单来说选择单例模式就是为了避免不一致状态, 减少对象再初始化过程中消耗大量内存,导致性能下降。在实际应用中,线程池、缓存、日志对象、对话框对象常被设计成单例,大家都知道,想要实现单例,最起码需要满足以下3个条件:
- 构造方法私有化 :让外部的对象不能使用构造方法来创建实例对象;
- 定义类型为本身类型的成员 ;
- 在类中提供一个公有且静态的创建类实例的方法 。
实现方式
- 饿汉式 :是直接在成员变量处实例化对象,这是相当饿,要吃东西,哈哈。使用场景:饿汉式是类被加载到虚拟机就直接实例化一个对象,当类加载的过程中会加载静态资源,加载到方法区,所以其应该应用在确定装载这个类的过程中不怎么消耗内存的前提下使用,如果初始化了又不使用,很浪费内存,不推荐使用饿汉式单例,一旦使用需要确定这个单例一定在项目中使用;
- 懒汉式 :类的装载过程中没有实例化,只有使用的时候再实例化,就是我们所说的懒加载。
实现方式中涉及到线程安全问题?科普一下:
计算机系统资源分配的单位为进程,同一个进程中允许多个线程并发执行,并且多个线程会共享进程范围内的资源:例如内存地址。当多个线程并发访问同一个内存地址并且内存地址保存的值是可变的时候可能会发生线程安全问题,因此需要内存数据共享机制来保证线程安全问题。
对应到java服务来说,在虚拟中的共享内存地址是java的堆内存,所以饿汉式是在类加载的时候进行加载的,所以不存在线程安全问题,但是懒汉式是存在线程安全问题的
解决饿汉式的线程安全问题
下图是通过CountDownLatch 模拟实现并发调用SingletonDemo2.getInstance()方法,如果计数器不为0 那么就阻塞主线程,如果计数器为0,那么主线程继续执行, 理想情况下并发量是1000,但也不能保证。
如上实际运行图中代码输出 instanceSet的大小,可以知道instanceSet的有时会大于1,证明实例变量不唯一,产生了线程安全问题。那为什么会出现这种情况呢?大家看看下面的代码,是否是线程安全?
总结起来就是现在CPU都是多核运行,系统会通过指令重排优化性能,在指令重排的情况下,双重加锁并不能实行真正的线程安全,需要通过关键字volatile实现:
即:pravite static volatile SingletonDemo4 instance =null;
反射暴力破解单例
从上图可以看到三次打印的结果已经不是同一个实例了,就这样,暴力反射成功的破坏了单例模式,内部类也不能解决,反射也能拿到内部类。那最后怎么解决呢?最安全的方法是通过枚举实现 ?
如下图是建立枚举类,通过SingletonDemo6.INSTANCE进行调用,Enum类内部会有一个构造函数,该构造函数只能有编译器调用,我们是无法手动操作的,不妨看看Enum类的主要源码
当然Enum构造函数也是存在的,该函数只能由编译器调用,毕竟我们只能使用enum关键字定义枚举
.
//枚举的构造方法,只能由编译器调用
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
大家可以自己测一下,暴力反射已经不能破坏这个单例了,在内存中只会被实例化一次,其实单例也可用内部类实现,但是它体现不了单例的特点
还有一句话,不能指望一个单例模式解决所有的问题。//dao
枚举详解推荐下面这篇博文
https://www.cnblogs.com/alter888/p/9163612.html