详解单例模式

概念:
  java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例、饿汉式单例、登记式单例。
  单例模式有以下特点:
  1、单例类只能有一个实例。
  2、单例类必须自己创建自己的唯一实例。
  3、单例类必须给所有其他对象提供这一实例。
  
  单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

1.恶汉式单例模式

//饿汉式单例类.在类初始化时,已经自行实例化 
public class Singleton {
     
    private Singleton() {
     }
    private static final Singleton instance = new Singleton();
    //静态工厂方法 
    public static Singleton getInstance() {
     
        return instance;
    }
}

饿汉式在类创建的同时就创建好了对象,是线程安全的。

2.懒汉单例模式

//懒汉式单例类.在第一次调用的时候实例化自己 
public class Singleton {
     
    private Singleton() {
     }
    private static Singleton instance = null;
    //静态工厂方法 
    public static Singleton getInstance() {
     
         if (instance == null) {
       
             instance = new Singleton();
         }  
        return single;
    }
}

懒汉式的单例模式在并发环境下,就不是单例的了,下面对懒汉式进行改造

将getInstance()方法加锁

public class Singleton {
     
    private Singleton() {
     }
    private static Singleton instance = null;
    //静态工厂方法 
    public synchronized static Singleton getInstance() {
     
         if (instance == null) {
       
             instance = new Singleton();
         }  
        return instance;
    }
}

虽然保证了线程安全,但是当多个线程调用时效率太低了。

双重校验锁

public class Singleton {
     
    private Singleton() {
     }
    private static volatile Singleton instance = null;
    //静态工厂方法 
    public static Singleton getInstance() {
     
        if(instance == null) {
     
			synchronized (Singleton.class) {
     
				if(instance == null) {
     
					instance = new Singleton();
				}
			}
		}
        return instance;
    }
}

接下来我们对双重校验锁进行分析

第一个if判断主要是为了效率,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作,因此可以大幅度降低synchronized带来的性能开销。

第二个if判断主要是为了安全,当有两个线程都经过了第一个if之后,其中的一个线程获取到了锁,然后创建对象释放锁,第二个线程获取到了锁之后,instance不为null,直接使用已经创建好的对象了。

细心的同学会发现,在声明变量时,有instance被volatile这个关键字修饰,那么这个volatile有什么用?为什么要加这个关键字呢?

首先我们直到volatile的作用是保证可见性和禁止指令重排序。在懒汉式中,好像没有用到可见性,如果有请各位批评纠正。。。

getInstance()方法对应的字节码指令:

0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn

其中

  • 17 表示创建对象,将对象引用入栈 // new Singleton
  • 20 表示复制一份对象引用 // 引用地址
  • 21 表示利用一个对象引用,调用构造方法
  • 24 表示利用一个对象引用,赋值给 static INSTANCE

从字节码可以看到创建一个对象实例,可以分为三步:

  • 分配对象内存
  • 调用构造器方法,执行初始化
  • 将对象引用赋值给变量。

也许 jvm 会优化为:先执行 24,再执行 21,即先将引用赋值给变量,在初始化。如果两个线程 t1,t2 按如下时间序列执行:
详解单例模式_第1张图片
解释一下,t1线程执行到将引用赋值给变量时(此时的引用时内存中的一些无效地址),当t2线程执行时instance不为null,直接return,然后使用这个对象时还没有调用构造方法,此时出错。

而volatile的禁止指令重排序就能解决这个问题
详解单例模式_第2张图片

volatile可以保证在赋值给共享变量之前,已经创建好了对象,当t2线程读取时,instance不为空且时一个真正的对象了。

你可能感兴趣的:(设计模式)