单例模式是应用最广泛的设计模式之一,这里我们使用3W原则来认识单例模式。
一、Who(什么是单例模式)
一个类只有一个实例,并且只有一个全局获取入口。
二、Why(为什么要使用单例模式)
1) 控制资源的使用,通过线程同步来控制资源的并发访问;
2)控制实例产生的数量,达到节约资源的目的。
3)作为通信媒介使用,也就是数据共享,它可以在不建立直接关联的条件下,让多个不相关的两个线程或者进程之间实现通信。
比如,数据库连接池的设计一般采用单例模式,数据库连接是一种数据库资源
三、How(如何实现单例模式)
1、饿汉模式(线程安全)
/** * 单例模式(饿汉模式):在声明静态变量的时候,就初始化类实例(线程安全) * 1、私有化构造函数 * 2、创建一个静态的类实例变量 * 3、对外提供一个getInstance方法,返回2中的静态实例变量 */ public class SingleTonOfHunger implements Serializable{ private static SingleTonOfHunger singleTonOfHungerInstance = new SingleTonOfHunger(); private SingleTonOfHunger () {} public static SingleTonOfHunger getInstance() { return singleTonOfHungerInstance; } }
2、懒汉模式(线程不安全)
/** * 单例模式(懒汉模式):在声明静态变量的时候,不初始化类实例,在获取单例的时候才初始化实例(延迟加载),当单例对象占用内存 * 过大时,可以提高性能,但是存在线程不安全的问题 * 1、私有化构造函数 * 2、创建一个静态的类实例变量 * 3、对外提供一个getInstance方法,返回2中的静态实例变量 */ public class SingleTonOfLazy implements Serializable{ private static SingleTonOfLazy instance; private SingleTonOfLazy() { if (instance != null) { //如果不是第一次构建,则直接抛出异常。不让创建(该方法可以防止反射破坏单例) throw new RuntimeException(); } } public static SingleTonOfLazy getInstance() { if(instance == null) { instance = new SingleTonOfLazy(); } return instance; } private Object readResolve() throws Exception { return getInstance(); } }
饿汉模式是线程不安全的,如果要实现线程安全的的单例模式,可以给getInstance方法加上Synchronized同步关键字,但是由于直接在方法上使用synchronized关键字,即类锁同步,使得只能使用每次调用都要进行同步操作,性能比较低。所以引出一下DCL模式的单例实现
3、DCL(Double CheckLock)
/** * 线程安全的单例模式:使用了DCL双检查锁机制 * 实现线程安全的单例模式还有以下方式: * 1、使用静态内部类实现单例模式 * * 2、序列化与反序列化模式的单例实现 * 序列化对象的hashCode和反序列化后得到的对象的hashCode值不一样,反序列化后返回的对象是重新实例化的,单例被破坏了 * 解决方案如下: * //该方法在反序列化时会被调用,该方法不是接口定义的方法,有点儿约定俗成的感觉 protected Object readResolve() throws ObjectStreamException { System.out.println("调用了readResolve方法!"); return MySingletonHandler.instance; } * 3、使用静态代码块的方式 * 4、使用枚举数据类型实现单例模式:枚举类的构造方法在类加载是被实例化 */ public class DoubleCheckSingleTon { private static DoubleCheckSingleTon instance; private DoubleCheckSingleTon(){} /** * 为什么不直接在getInstance方法上使用Synchronized同步,同步方法,粒度过大,影响性能。 * @return 返回双检查锁线程安全的单例模式 */ public static DoubleCheckSingleTon getInstance() { if(instance == null) { synchronized (DoubleCheckSingleTon.class) { //降低同步的粒度,但此时存在线程不安全的问题,需使用双检查锁机制 if(instance == null) { //DCL双重检查锁 instance = new DoubleCheckSingleTon(); } } } return instance; } }
通过两个判断,第一层是避免不必要的同步,第二层判断是否为null。
可能会出现DCL模式失效的情况。
DCL模式失效:
singleton=new Singleton();这句话执行的时候,会进行下列三个过程:
1.分配内存。
2.初始化构造函数和成员变量。
3.将对象指向分配的空间。
由于JMM(Java Memory Model)的规定,可能会出现1-2-3和1-3-2两种情况。
所以,就会出现线程A进行到1-3时,就被线程B取走,然后就出现了错误。这个时候DCL模式就失效了。
Sun官方注意到了这种问题,于是就在JDK1.5之后,具体化了volatile关键字(volatile修饰的变量、常量不会参与序列化),这时候只用调整一行代码即可。
private volatile static DoubleCheckSingleton instance;
4、静态内部类实现单例
public class StaticInnerClassSingleTon { private StaticInnerClassSingleTon(){} public static StaticInnerClassSingleTon getSingleton(){ return SingletonHolder.singleton; } private static class SingletonHolder{ private final static StaticInnerClassSingleTon singleton=new StaticInnerClassSingleTon(); } }
第一次加载Singleton类不会加载SingletonHolder类,但是调用getSingleton时,才会加载SingletonHolder,才会初始化singleton。即确保了线程安全,又保证了单例对象的唯一性,延迟了单例的实例化。这是最推荐的方式。
5、枚举模式实现单例
通过枚举来写单例模式非常简单,什么都不用想,这是枚举的最大优点,
public enum Singleton{ singleton; public void hello(){ System.out.print("hello"); } }
这样很简单,满足线程安全,并且避免了序列化和反射攻击。
除了枚举模式,其他模式在实现了Serializable接口后,反序列化时单例会被破坏。所以要重写readResolve()方法。
可能由于平时我们使用枚举都是为了表示类别,大家都很少使用这种方式去写单例模式。但是这确实是实现单例最简单,也比较推荐的方式。