1. 概念
顾名思义,单例模式也就是说只有一个实例,这种模式也是属于很常用的一种设计模式,也很容易理解。当然,面试也是会常问到的。
- 目的
单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
- 解决的问题
-
保证一个类只有一个实例。 为什么会有人想要控制一个类所拥有的实例数量? 最常见的原因是控制某些共享资源 (例如数据库或文件) 的访问权限。
它的运作方式是这样的: 如果你创建了一个对象, 同时过一会儿后你决定再创建一个新对象, 此时你会获得之前已创建的对象, 而不是一个新对象。
注意, 普通构造函数无法实现上述行为, 因为构造函数的设计决定了它必须总是返回一个新对象。
-
为该实例提供一个全局访问节点。 还记得你 (好吧, 其实是我自己) 用过的那些存储重要对象的全局变量吗? 它们在使用上十分方便, 但同时也非常不安全, 因为任何代码都有可能覆盖掉那些变量的内容, 从而引发程序崩溃。
和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象。 但是它可以保护该实例不被其他代码覆盖。
还有一点: 你不会希望解决同一个问题的代码分散在程序各处的。 因此更好的方式是将其放在同一个类中, 特别是当其他代码已经依赖这个类时更应该如此。
-
所以,要注意一点:单例模式并不是单单指一个类只能有一个实例,还有另一层意思:可以严格的控制全局变量。
2. 特点
- 优点
- 可以保证一个类只有一个实例。
- 获得了一个指向该实例的全局访问节点。
- 仅在首次请求单例对象时对其进行初始化。
- 缺点
- 违反了_单一职责原则_。 该模式同时解决了两个问题。
- 单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等。
- 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。
- 单例的客户端代码单元测试可能会比较困难, 因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的, 而且绝大部分语言无法重写静态方法, 所以你需要想出仔细考虑模拟单例的方法。 要么干脆不编写测试代码, 或者不使用单例模式。
- 使用场景
-
如果程序中的某个类对于所有客户端只有一个可用的实例, 可以使用单例模式。
单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。 该方法可以创建一个新对象, 但如果该对象已经被创建, 则返回已有的对象。
-
如果你需要更加严格地控制全局变量, 可以使用单例模式
单例模式与全局变量不同, 它保证类只存在一个实例。 除了单例类自己以外, 无法通过任何方式替换缓存的实例。请注意, 你可以随时调整限制并设定生成单例实例的数量, 只需修改 获取实例方法, 即 getInstance 中的代码即可实现。
3. 八种实现
在讲实现之前,先了解两个概念,单例模式的实现就分为这两种方式,分别是:饿汉式和懒汉式。
- 饿汉式:见名知义,饿汉就是很饿的意思,会狼吞虎咽,延申到Java里面就表示在一开始就在内存中创建了一个单例实体。
- 懒汉式:和饿汉式相反,懒汉式表示只有当使用到单例实体的时候,才会去创建他。这种方式比较节省内存,但保证这种机制的难度也比较大。
-
饿汉式(静态常量)
class Type1{ /** * 构造器私有,外部不能new */ private Type1(){} /** * 实例对象 */ private static final Type1 instance = new Type1(); /** * 获取实例的静态方法 */ public static Type1 getInstance(){ return instance; } }
- 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题**。
- 缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
- 说明:这种方式基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,在单例模式中大多数都是调用 getInstance 方法, 但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 就没有达到 lazy loading 的效果。
- 结论:如果很明确知道这个单例一定会被使用,可以使用这种方式。
-
饿汉式(静态代码块)
class Type2{ /** * 构造器私有,外部不能new */ private Type2(){} /** * 实例对象 */ private static Type2 instance; /** * 静态代码块 */ static { instance = new Type2(); } /** * 获取实例的静态方法 */ public static Type2 getInstance(){ return instance; } }
优缺点同第一种,只是换了一种写法。
-
懒汉式(线程不安全)
class Type3{ /** * 构造器私有,外部不能new */ private Type3(){} /** * 实例对象 */ private static Type3 instance; /** * 获取实例的静态方法,只有调用了才会创建实例 */ public static Type3 getInstance(){ if(instance == null) instance = new Type3(); return instance; } }
- 优点:起到了 Lazy Loading 的效果,但是只能在单线程下使用
- 缺点:如果在多线程下,一个线程进入了 if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式
- 结论:单线程情况下可以用,多线程情况下不可用
-
懒汉式(线程安全,同步方法)
class Type4{ /** * 构造器私有,外部不能new */ private Type4(){} /** * 实例对象 */ private volatile static Type4 instance; /** * 获取实例的静态方法,只有调用了才会创建实例,线程安全 */ public static synchronized Type4 getInstance(){ if(instance == null) instance = new Type4(); return instance; } }
- 优点:实现了懒加载和线程安全
- 缺点:性能存在问题,每个线程在想获得类的实例时候,执行 getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了。方法进行同步效率太低
- 结论:实际开发中,不建议使用
-
懒汉式(线程安全,同步代码块)
class Type5{ /** * 构造器私有,外部不能new */ private Type5(){} /** * 实例对象 */ private volatile static Type5 instance; /** * 获取实例的静态方法,只有调用了才会创建实例 */ public static Type5 getInstance(){ synchronized (Type5.class){ if(instance == null) instance = new Type5(); } return instance; } }
- 说明:这种方式本意上是为了解决第五种低性能问题,但是他其实这样改了以后是线程不安全的,这是一种错误的写法。
- 结论:不可以使用!
-
懒汉式(双重检查)
class Type6{ /** * 构造器私有,外部不能new */ private Type6(){} /** * 实例对象 */ private volatile static Type6 instance; /** * 获取实例的静态方法,只有调用了才会创建实例 */ public static Type6 getInstance(){ if (instance == null){ synchronized (Type6.class){ if(instance == null) instance = new Type6(); } } return instance; } }
- 优点:实现了懒加载,也实现了线程安全,性能也有保证
- 说明:实际开发情况中,建议使用
-
懒汉式(静态内部类)
class Type7{ /** * 构造器私有,外部不能new */ private Type7(){} /** * 写一个静态内部类,该类中有一个静态属性 INSTANCE */ private static class SingletonInstance { private static final Type7 INSTANCE = new Type7(); } /** * 获取实例的静态方法,只有调用了才会创建实例 */ public static Type7 getInstance(){ return SingletonInstance.INSTANCE; } }
-
优点:线程安全,懒加载
-
说明:静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance 方法,才会装载 SingletonInstance 类,从而完成 Singleton 的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
-
结论:建议使用!
-
-
懒汉式(枚举)
public class SingletonTest08 { public static void main(String[] args) { Singleton instance = Singleton.INSTANCE; Singleton instance2 = Singleton.INSTANCE; System.out.println(instance == instance2); System.out.println(instance.hashCode()); System.out.println(instance2.hashCode()); instance.sayOK(); } //使用枚举,可以实现单例, 推荐 enum Singleton { //属性 INSTANCE; public void sayOK() { System.out.println("ok~"); } } }
- 优点:懒加载、线程安全,还能防止反序列化重新创建新的对象。
- 结论:推荐使用!