目录
为什么需要单例模式呢?
单例模式的实现
单例模式的实现方式选择:
单例模式是日常开发过程中最常用的一种设计模式了,在java中,顾名思义就是说一个类只允许一个实例存在,一般满足两点:
1)构造方法必须是私有化的,以确保不会在外部被实例化,只能通过该类提供的静态方法以获取唯一实例
2)提供静态方法,用于返回该类的唯一实例
我认为一方面可以解决多线程并发访问的问题,另一方面也能节约系统开销,便于管理。以配置为例:
假设某台服务器的相关配置信息都放在某个文件目录中,我们只需定义一个专门的配置文件读取类,定义成单例,由该类专门负责读取配置文件信息,其他进程有需要配置信息均由该类进行读取,这在一方面也解决了复杂环境下的配置管理。这种唯一的全局对象,另一方面也便于统筹管理
单例模式分为两种,饿汉式单例和懒汉式单例,两者的区别就是在何时初始化实例
饿汉式单例:在类加载时就初始化实例,以空间换时间,即如果该实例一直不被使用,依然会被创建,便会造成资源的浪费。由于在类加载时即初始化实例故不存在线程安全问题,代码如下:
public class Singleton { private Singleton() {} private static Singleton singleton = new Singleton(); public static Singleton getInstance() { return singleton; } }懒汉式单例:懒加载模式,即在使用该实例的时候才去初始化,以时间换空间,当同一时刻多个线程同时去初始化的时候很容易造成线程安全问题,下面先贴懒汉式单例原始线程不安全代码,再给出线程安全的推荐的两种使用方式:DCL懒汉以及静态内部类懒汉
原始代码:
public class Singleton { private Singleton() {} private static Singleton singleton; public static Singleton getInstance() { if (null == singleton) { singleton = new Singleton(); } return singleton; } }原始代码在多线程环境下很容易造成线程安全问题
DCL懒汉式单例:
DCL即Double Check Lock,双重检查锁定,先贴代码:
public class Singleton { private Singleton() {} private volatile static Singleton singleton; public static Singleton getInstance() { if (null == singleton) { synchronized (Singleton.class) { if (null == singleton) { singleton = new Singleton(); } } } return singleton; } }可以看到上面代码中做了两次非空判断,第一次非空判断是保证只在第一次初始化调用时进行下面的加锁操作,避免因加锁操作而造成效率问题。
这里还有一个注意点,假设线程1执行到singleton = new Singleton();这一行代码,该语句在jvm执行会被编译成8条汇编指令,大致执行以下三件事:
1、给singleton实例分配内存(在堆内存中开辟内存空间)
2、初始化singleton构造器(在堆内存中实例化singleton的各个参数)
3、将singleton对象指向分配的内存空间(把对象指向堆内存空间,到这一步singleton才是非null)
但是jvm为了性能考虑,允许指令重排序,即实际执行时可能会不按照上述顺序执行,那就容易出问题了,假设变成了如下顺序:
1、给singleton实例分配内存
2、将singleton对象指向分配的内存空间
3、初始化singleton构造器
由于执行到2时,singleton实例已经非null,此时如果线程2正好进来,第一个非空判断判定为false,直接返回singleton实例并使用,接着就顺理成章的报错,因为对象还未初始化。这就是著名的DCL失效问题
解决该问题的本质就是禁止指令重排序,jdk1.5之后推出的volatile关键词就有该功能,声明singleton对象即可~
静态内部类懒汉式单例:
public class Singleton { private Singleton() {} private static class SingletonLayzer { private static Singleton singleton = new Singleton(); } public static Singleton getInstance() { return SingletonLayzer.singleton; } }从性能上来考虑,该方式比DCL会好,毕竟避免了同步带来的性能开销。静态内部类的好处就是外部类在被类加载时并不会同时加载内部类,只有在内部类被调用时才会加载,因此并不会第一时刻就初始化实例,故而不占内存。当第一次调用getInstance方法时,内部类会从外部类的运行时常量池中将符号引用替换为直接引用,此时singleton实例即被真正创建,之后每个线程来获取时,由于getInstance方法并没有去new对象,因此每次取的都是同一个实例。
那么静态内部类是如何保证线程安全的呢?虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。
Ps:<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。
一般情况下直接使用静态内部类懒汉的方式实现是最好的,如果有额外的需求,比如需要进行传参操作,此时静态内部类无法满足该需求,那么可以使用DCL懒汉模式,可自己斟酌。