封面来源:单例设计模式 (refactoringguru.cn) ,如有侵权,请联系删除。
1. 什么是单例模式
百度百科是这么说的:
单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)。
菜鸟教程关于单例模式的介绍如下:
意图: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决: 一个全局使用的类频繁地创建与销毁。
何时使用: 当您想控制实例数目,节省系统资源的时候。
如何解决: 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码: 构造函数是私有的。
简单来说:单例模式保证一个类仅有一个实例,并提供一个访问它的全局访问点,其关键是构造函数私有的。
2. 饿汉式与懒汉式
2.1 饿汉式
饿汉式的特点:类加载的时候就实例化,并且创建单例对象。
饿汉式是最简单的单例模式,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /**
* @author 默烦 2020/10/29
* <p>
* 饿汉式单例
*/
public class Hungry {
private Hungry () {
}
private final static Hungry HUNGRY = new Hungry () ;
public static Hungry getInstance (){
return HUNGRY;
}
}
但是饿汉式也有问题,最主要一点就是可能会浪费资源 。比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Hungry {
private final byte [] data1 = new byte [ 1024 * 1024 ] ;
private final byte [] data2 = new byte [ 1024 * 1024 ] ;
private final byte [] data3 = new byte [ 1024 * 1024 ] ;
private final byte [] data4 = new byte [ 1024 * 1024 ] ;
private Hungry () {
}
private final static Hungry HUNGRY = new Hungry () ;
public static Hungry getInstance () {
return HUNGRY;
}
}
上述代码一运行,byte 数组就会被初始化,并放入内存中。但如果我一直没有使用 getInstance() 方法,一直不用 Hungry 类,这不就浪费内存了吗?
有没有一种方法让类加载时不进行实例化,只有用的时候才实例化呢?
2.2 懒汉式
懒汉式的特点:类加载时没有生成单例,只有当第一次调用 getlnstance() 方法时才去创建这个单例。
初始代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /**
* @author 默烦 2020/10/29
* <p>
* 懒汉式单例
*/
public class LazyMan {
private LazyMan () {
}
private static LazyMan lazyMan ;
public static LazyMan getInstance () {
if (lazyMan == null ){
lazyMan = new LazyMan ();
}
return lazyMan;
}
}
但是这样的懒汉式有问题,在并发多线程环境下并不是单例的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class LazyMan {
private LazyMan () {
System . out . println ( Thread . currentThread (). getName () + " ok" );
}
private static LazyMan lazyMan ;
public static LazyMan getInstance () {
if (lazyMan == null ){
lazyMan = new LazyMan ();
}
return lazyMan;
}
public static void main ( String [] args ) {
for ( int i = 0 ; i < 20 ; i++) {
new Thread (() -> {
LazyMan . getInstance ();
}). start ();
}
}
}
运行上述代码后,控制台可能会出现:
可以看到出现了两个实例,这显然就不是单例的了。
为了避免这种情况,我们可以对代码加锁,修改 getInstance() 方法:
1 2 3 4 5 6 7 8 9 10 11 // 双重锁定检查
public static LazyMan getInstance () {
if (lazyMan == null ) {
synchronized ( LazyMan . class ) {
if (lazyMan == null ){
lazyMan = new LazyMan () ; // 不是一个原子性操作
}
}
}
return lazyMan ;
}
这就是双重锁定模式(DCL,Double Check Lock)。
早期 JVM 中因为同步的开销巨大,为了降低实现单例模式中同步带来的开销,人们想出了很多技巧,DCL 便是其中一种。
但是 DCL 懒汉式还是有问题,因为:
1 lazyMan = new LazyMan () ;
不是一个原子性操作。它会进行三个步骤:
1、分配对象内存空间
2、执行构造方法初始化对象
3、把对象指向这个空间,此时 lazyMan != null
由于指令重排,本来是按 123 顺序的执行可能会变成 132。假设这时有一个线程 A 运行了 lazyMan = new LazyMan();,按照 132 的步骤进行执行,又恰好在第三步执行完,第二步还没执行时线程 B 来了,由于这时候 lazyMan != null 就会直接返回 lazyMan。由于线程 A 没有执行第二步,lazyMan 对象还未初始化,内部的属性极有可能都是 null,将增加出现空指针的概率。
同时需要注意:synchronized虽然保证了线程的原子性(即 synchronized 块中的语句要么全部执行,要么一条也不执行),但单条语句编译后形成的指令并不是一个原子操作(即可能该条语句的部分指令未得到执行,就被切换到另一个线程了)。
因此,需要禁止指令重排,即使用 volatile 修饰的变量。对此,优化后的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class LazyMan {
private LazyMan () {
System . out . println ( Thread . currentThread (). getName () + " ok" );
}
// 添加 volatile 关键字
private volatile static LazyMan lazyMan ;
// 双重锁定检查
public static LazyMan getInstance () {
if (lazyMan == null ) {
synchronized ( LazyMan . class ) {
if (lazyMan == null ){
lazyMan = new LazyMan ();
}
}
}
return lazyMan;
}
public static void main ( String [] args ) {
for ( int i = 0 ; i < 20 ; i++) {
new Thread (() -> {
LazyMan . getInstance ();
}). start ();
}
}
}
两次判断 null 的作用
第一次判断 null 的作用:减少进入同步代码块的次数,提高效率。去掉这次判断并不会影响单例对象的创建,但每次获取对象都需要进入同步代码块中,这会降低代码执行效率。
第二次判断 null 的作用:控制对象实例是单例的。假设线程 A、B 同时执行到同步代码块前,线程 A 先抢到锁,完成对象的创建,然后线程 B 进入同步代码块。如果没有这次 null 值判断,线程 B 又会将一个新的实例赋值给成员变量,这就违背了单例的原则。
3. 使用静态嵌套类实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /**
* @author 默烦 2020/10/29
* <p>
* 静态嵌套类
*/
public class Holder {
private Holder () {
}
public static Holder getInstance () {
return InnerClass . HOLDER ;
}
public static class InnerClass {
private static final Holder HOLDER = new Holder () ;
}
}
使用这种方式也可以保证创建的对象是懒汉单例的,而且还是线程安全的。
静态嵌套类的优点是:外部类加载时并不需要立即加载静态嵌套类,静态嵌套类不被加载则不去初始化 HOLDER,故而不占内存。即当 Holder 第一次被加载时,并不需要去加载 InnerClass,只有当 getInstance() 方法第一次被调用时,才会去初始化 HOLDER,第一次调用 getInstance() 方法会导致虚拟机加载 InnerClass 类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
至于是怎么保证线程安全的,可以参考这篇文章:深入理解单例模式:静态内部类单例原理
4. 强大的反射
反射那是一个相当流弊的东西,它可以无视 private 关键字,也就是说它会破坏封装性。说远了,使用反射就将破坏我们编写的单例模式。
修改懒汉式最后给出的示例代码,对 main() 方法进行修改,利用反射创建两个 LazyMan 对象:
1 2 3 4 5 6 7 8 9 10 11 public static void main ( String [] args) throws Exception {
LazyMan instance = LazyMan . getInstance ();
Constructor < LazyMan > declaredConstructor = LazyMan . class . getDeclaredConstructor ( null );
declaredConstructor . setAccessible ( true );
LazyMan lazyMan = declaredConstructor . newInstance ();
System . out . println (instance);
System . out . println (lazyMan);
System . out . println (instance == lazyMan); // false
}
运行截图如下:
哦豁,完犊子。创建的两个对象不是同一个,单例模式失效!
道高一尺,魔高一丈,我们可以在构造方法中加锁并判断,判断成功就抛出异常。修改私有的构造方法:
1 2 3 4 5 6 7 8 private LazyMan () {
synchronized ( LazyMan . class ) {
if (lazyMan != null ) {
throw new RuntimeException ( "不要试图使用反射破坏单例" ) ;
}
}
System . out . println ( Thread . currentThread (). getName () + " ok" );
}
然后再运行:
芜湖,✈️
别得意,看我修改一下 main() 方法中的测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 public static void main ( String [] args) throws Exception {
// LazyMan instance = LazyMan.getInstance();
Constructor < LazyMan > declaredConstructor = LazyMan . class . getDeclaredConstructor ( null );
declaredConstructor . setAccessible ( true );
LazyMan instance = declaredConstructor . newInstance ();
LazyMan lazyMan = declaredConstructor . newInstance ();
System . out . println (instance);
System . out . println (lazyMan);
System . out . println (instance == lazyMan); // false
}
再次运行:
玩完儿咯,还是创建出两个对象。
小事情,添加一个标志位进行判断即可,再次修改私有的构造方法:
1 2 3 4 5 6 7 8 9 10 private LazyMan () {
synchronized ( LazyMan . class ) {
if ( ! flag) {
flag = true ;
} else {
throw new RuntimeException ( "不要试图使用反射破坏单例" ) ;
}
}
System . out . println ( Thread . currentThread (). getName () + " ok" );
}
再次运行:
诶嘿,成功!💪
不就是引入标志位?照样用反射解决它!修改一下 main() 方法中的测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static void main ( String [] args) throws Exception {
Field flag = LazyMan . class . getDeclaredField ( "flag" );
flag . setAccessible ( true );
Constructor < LazyMan > declaredConstructor = LazyMan . class . getDeclaredConstructor ( null );
declaredConstructor . setAccessible ( true );
LazyMan instance = declaredConstructor . newInstance ();
flag . set (instance, false );
LazyMan lazyMan = declaredConstructor . newInstance ();
System . out . println (instance);
System . out . println (lazyMan);
System . out . println (instance == lazyMan); // false
}
运行:
完了,这咋整啊?
点开 newInstance() 的源码,源码中有这样一段判断:
1 2 if (( clazz . getModifiers () & Modifier . ENUM ) != 0 )
throw new IllegalArgumentException ( "Cannot reflectively create enum objects" ) ;
这段判断就是说不能通过反射创建枚举对象,枚举对象自带单例模式!
5. 枚举
enum 的全称为 enumeration, 是 JDK 1.5 中引入的新特性。
在 Java 中,被 enum 关键字修饰的类型就是枚举类型。形式如下:
1 enum Color { RED , GREEN , BLUE }
明白了基本使用,创建一个枚举测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /**
* @author 默烦 2020/10/29
*/
public enum EnumSingle { // 枚举也是一个类
INSTANCE ;
public EnumSingle getInstance () {
return INSTANCE;
}
}
class Test {
public static void main ( String [] args ) {
EnumSingle instance = EnumSingle . INSTANCE ;
EnumSingle instance2 = EnumSingle . INSTANCE ;
System . out . println ( instance . hashCode ());
System . out . println ( instance2 . hashCode ());
System . out . println (instance == instance2);
}
}
运行结果:
哟嚯?好像还行。
用反射创建枚举试试?修改一下 Test 测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Test {
public static void main ( String [] args ) throws Exception {
EnumSingle instance = EnumSingle . INSTANCE ;
Constructor < EnumSingle > declaredConstructor = EnumSingle . class . getDeclaredConstructor ( null );
declaredConstructor . setAccessible ( true );
EnumSingle enumSingle = declaredConstructor . newInstance ();
System . out . println ( instance . hashCode ());
System . out . println ( enumSingle . hashCode ());
System . out . println (instance == enumSingle);
}
}
运行后有:
出现了一个异常,表示 EnumSingle 类中没有空参构造函数。
但并不是我们在 newInstance() 源码中看到的异常:
1 2 if (( clazz . getModifiers () & Modifier . ENUM ) != 0 )
throw new IllegalArgumentException ( "Cannot reflectively create enum objects" ) ;
我们反编译看看:
可以看到,枚举确实是一个类,而且也有空参构造函数。这…
我们需要使用更加专业的反编译工具,比如 JAD。
JAD 下载链接:JAD下载镜像
进入链接后,找到对应的地方进行下载:
下载之后进行解压,找到 jad.exe 并将其放在需要进行反编译的目录:
在这个目录下打开 cmd,我们需要反编译 EnumSingle.class,因此输入命令:
1 jad -sjava EnumSingle.class
然后会在当前目录下生成反编译后的 Java 文件 EnumSingle.java,打开它:
终于看到不再是无参构造函数了,而是有参构造函数,参数类型分别是 String 和 int。
既然如此,我们修改 Test 测试类中的测试方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Test {
public static void main ( String [] args ) throws Exception {
EnumSingle instance = EnumSingle . INSTANCE ;
// 修改此处
Constructor < EnumSingle > declaredConstructor =
EnumSingle . class . getDeclaredConstructor ( String . class , int . class );
declaredConstructor . setAccessible ( true );
EnumSingle enumSingle = declaredConstructor . newInstance ();
System . out . println ( instance . hashCode ());
System . out . println ( enumSingle . hashCode ());
System . out . println (instance == enumSingle);
}
}
再次运行代码:
所以应该使用枚举来实现真正的单例。