单例模式可以说只要是一个合格的开发都会写,但是如果要深究,小小的单例模式可以牵扯到很多东西,比如:多线程是否安全?是否懒加载?性能等等。还有你知道几种单例模式的写法呢?如何防止反射破坏单例模式?@H_301_1@
一、 单例模式
1.1 定义
单例模式就是在程序运行中只实例化一次,创建一个全局唯一对象。有点像?Java ?的静态变量,但是单例模式要优于静态变量:@H_301_1@
- 静态变量在程序启动的时候
JVM 就会进行加载,如果不使用,会造成大量的资源浪费;
- 单例模式能够实现懒加载,能够在使用实例的时候才去创建实例。
开发工具类库中的很多工具类都应用了单例模式,比例线程池、缓存、日志对象等,它们都只需要创建一个对象,如果创建多份实例,可能会带来不可预知的问题,比如资源的浪费、结果处理不一致等问题。@H_301_1@
1.2 单例的实现思路
- 静态化实例对象;
- 私有化构造方法,禁止通过构造方法创建实例;
- 提供一个公共的静态方法,用来返回唯一实例。
1.3 单例的好处
- 只有一个对象,内存开支少、性能好;
- 避免对资源的多重占用;
- 在系统设置全局访问点,优化和共享资源访问。
二、 单例模式的实现
- 饿汉模式
- 懒汉模式
- 双重检查锁模式
- 静态内部类单例模式
- 枚举类实现单例模式
@H_404_54@2.1 饿汉模式
在定义静态属性时,直接实例化了对象@H_301_1@
public class HungryMode {
2.1.1 优点
由于使用了static 关键字,保证了在引用这个变量时,关于这个变量的所以写入操作都完成,所以保证了JVM 层面的线程安全@H_301_1@
2.1.2 缺点
不能实现懒加载,造成空间浪费:如果一个类比较大,我们在初始化的时就加载了这个类,但是我们长时间没有使用这个类,这就导致了内存空间的浪费。@H_301_1@
所以,能不能只有用到?getInstance() 方法,才会去初始化单例类,才会加载单例类中的数据。所以就有了:懒汉式。@H_301_1@
2.2 懒汉模式
懒汉模式是一种偷懒的模式,在程序初始化时不会创建实例,只有在使用实例的时候才会创建实例,所以懒汉模式解决了饿汉模式带来的空间浪费问题。@H_301_1@
2.2.1 懒汉模式的一般实现
public class LazyMode {
但是这种实现在多线程的情况下是不安全的,有可能会出现多份实例的情况:@H_301_1@
if (instance == null) {
instance = new LazyMode();
}
假设有两个线程同时进入到上面这段代码,因为没有任何资源保护措施,所以两个线程可以同时判断的?instance ?都为空,都将去初始化实例,所以就会出现多份实例的情况。@H_301_1@
2.2.2 懒汉模式的优化
我们给getInstance() 方法加上synchronized 关键字,使得getInstance() 方法成为受保护的资源就能够解决多份实例的问题。@H_301_1@
public class LazyModeSynchronized {
2.2.3 懒汉模式的优点
实现了懒加载,节约了内存空间。@H_301_1@
2.2.4 懒汉模式的缺点
- 在不加锁的情况下,线程不安全,可能出现多份实例;
- 在加锁的情况下,会使程序串行化,使系统有严重的性能问题。
懒汉模式中加锁的问题,对于getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的问题。由此也产生了一种新的实现模式:双重检查锁模式。@H_301_1@
2.3 双重检查锁模式
2.3.1 双重检查锁模式的一般实现
public class DoubleCheckLockMode {
private static DoubleCheckLockMode instance;
双重检查锁模式解决了单例、性能、线程安全问题,但是这种写法同样存在问题:在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM 在实例化对象的时候会进行优化和指令重排序操作。@H_301_1@
2.3.2 什么是指令重排?
private SingletonObject(){
上面的构造函数SingletonObject() ,JVM ?会对它进行指令重排序,所以执行顺序可能会乱掉,但是不管是那种执行顺序,JVM ?最后都会保证所以实例都完成实例化。?如果构造函数中操作比较多时,为了提升效率,JVM ?会在构造函数里面的属性未全部完成实例化时,就返回对象。双重检测锁出现空指针问题的原因就是出现在这里,当某个线程获取锁进行实例化时,其他线程就直接获取实例使用,由于JVM 指令重排序的原因,其他线程获取的对象也许不是一个完整的对象,所以在使用实例的时候就会出现空指针异常问题。@H_301_1@
2.3.3 双重检查锁模式优化
要解决双重检查锁模式带来空指针异常的问题,只需要使用volatile 关键字,volatile 关键字严格遵循happens-before 原则,即:在读操作前,写操作必须全部完成。@H_301_1@
public class DoubleCheckLockModelVolatile {
2.4 静态内部类模式
静态内部类模式也称单例持有者模式,实例由内部类创建,由于?JVM ?在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性由static 修饰,保证只被实例化一次,并且严格保证实例化顺序。@H_301_1@
public class StaticInnerClassMode {
private StaticInnerClassMode(){
}
这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方:@H_301_1@
- 饿汉式方式是只要
Singleton 类被装载就会实例化,没有Lazy-Loading 的作用;
- 静态内部类方式在
Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance() 方法,才会装载SingletonInstance 类,从而完成Singleton 的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。@H_301_1@
所以这种方式在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。@H_301_1@
2.5 枚举类实现单例模式
因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。@H_301_1@
public class EnumerationMode {
private EnumerationMode(){
}
适用场合:
- 需要频繁的进行创建和销毁的对象;
- 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
- 工具类对象;
- 频繁访问数据库或文件的对象。
三、单例模式的问题及解决办法
除枚举方式外,其他方法都会通过反射的方式破坏单例@H_301_1@
3.1 单例模式的破坏
控制台打印:@H_301_1@
输出结果为:1454171136,1454171136,1195396074
从输出的结果我们就可以看出obj1 和obj2 为同一对象,obj3 为新对象。obj3 是我们通过反射机制,进而调用了私有的构造函数,然后产生了一个新的对象。@H_301_1@
3.2 如何阻止单例破坏
可以在构造方法中进行判断,若已有实例,则阻止生成新的实例,解决办法如下:@H_301_1@
public class StaticInnerClassModeProtection {
private static boolean flag = false;
private StaticInnerClassModeProtection(){
synchronized(StaticInnerClassModeProtection.class){
if(flag == false){
flag = true;
}else {
throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取!");
}
}
}
测试:@H_301_1@
控制台打印:@H_301_1@
Caused by: java.lang.RuntimeException: 实例已经存在,请通过 getInstance()方法获取!
at cn.van.singleton.demo.mode.StaticInnerClassModeProtection.<init>(StaticInnerClassModeProtection.java:22)
... 35 more
四、总结
4.1 各种实现的对比
名称 |
饿汉模式 |
懒汉模式 |
双重检查锁模式 |
静态内部类实现 |
枚举类实现 |
|