单例设计模式是GoF23种最常用的设计模式之一,无论是第三方类库还是日常开发几乎都可以看到单例的影子,单例设计模式提供了一种在多线程的情况下保证实例的唯一性解决方案,为了比较7中实现方式的优劣,我们从三个角度来对其进行评估:线程按安全、高性能、懒加载。
饿汉式
package SingletonPkg;
/**
* @author Heian
* @time 19/01/21 12:49
* @copyright(C) 2019 深圳市北辰德科技股份有限公司
* 用途:饿汉式单例设计模式
*/
public final class Singleton {
private byte[] data = new byte[1024];//实例变量
private static Singleton instance = new Singleton ();
private Singleton(){
//私有化构造防止外部类new
}
public static Singleton getInstance(){
return instance;
}
}
饿汉式的关键在于instance作为类变量并且直接得到了初始化(只要你调用了这个Singleton这个类,那么肯定会初始化它,会初始化Singleton就肯定会导致instance变量的初始化(初始化并不一定会走构造方法注意),假设这里instance不是类变量则该类也就不会得到初始化,并且该类是final修饰的,因为此类不能被重写,因为子类的初始化会导致父类的初始化),那么instance实例将会创建,包括其中的实例变量都会得到初始化(类加载中的第三阶段),比如1k的空间同时被创建,instance作为类变量在类初始化的过程中会被收集进<clinit>()方法,该方法能够保证同步,在多线程的情况下也只会被实例化一次,但是instance被classloader加载后可能很长一段时间才能使用,那就意味着instance实例所开辟的堆内存会驻留更久的时间。
总结起来就是一个类属性较少,占用内存资源不多,饿汉式的方式未尝不可,相反,如果一个类中的成员都是比较重的资源,那这种方式会有些不妥,比如单例类属于耗时操作。而且这种模式无法进行懒加载。
懒汉式
package SingletonPkg;
/**
* @author Heian
* @time 19/01/21 12:49
* @copyright(C) 2019 深圳市北辰德科技股份有限公司
* 用途:懒汉式单例设计模式
*/
public final class Singleton2 {
private byte[] data = new byte[1024];//实例变量
private static Singleton2 instance = null;//定义实际变量,但不直接初始化
private Singleton2(){
//私有化构造防止外部类new
}
public static Singleton2 getInstance(){
if(null == instance){
instance = new Singleton2 ();
}
return instance;
}
}
当Singleton2.class被初始化的时候instance并不会被实例化,在getInstance方法中会判断instance是否已经被实例化,看起来没什么问题但是在多线程的环境下,则会导致instance会被实例化一次以上,无法保证单例的唯一性。由于instance类变量是静态修饰,即加载过程会将其存入方法区,假设线程1检测到为instance为null会创建实例new,但是由于cpu时间片切换(大多数数据不一致都是因此)导致先进行了线程2,而此时线程1并未将创建好的instance实例刷新到主内存中(不能保证可见性),使得线程2再次创建。
懒汉式+同步方法
懒汉式方式可以保证实例的懒加载,但是无法保证实例的唯一性,所以对程序稍加修改(增加同步约束)
package SingletonPkg;
/**
* @author Heian
* @time 19/01/21 12:49
* @copyright(C) 2019 深圳市北辰德科技股份有限公司
* 用途:懒汉式+同步 单例设计模式
*/
public final class Singleton3 {
private byte[] data = new byte[1024];//实例变量
private static Singleton3 instance = null;//定义实际变量,但不直接初始化
private Singleton3(){
//私有化构造防止外部类new
}
//加入同步控制,同一时刻只有一个线程进入
public static synchronized Singleton3 getInstance(){
if(null == instance){
instance = new Singleton3 ();
}
return instance;
}
}
采用懒汉式 + 同步的机制既满足了instance实例的唯一性,又满足了懒加载,但由于syncronized关键字天生的排他性导致了getInstance方法只能同一时刻被一个线程访问,性能低下。
Double-Check
这种设计模式比较聪明,提供了一种高效的同步策略,那就是首次初始化时加锁,之后允许多个线程同时进入getInstance方法获取类的实例
package SingletonPkg;
import java.net.Socket;
/**
* @author Heian
* @time 19/01/21 12:49
* @copyright(C) 2019 深圳市北辰德科技股份有限公司
* 用途:Volatile + Double-check 单例设计模式
*/
public final class Singleton5 {
private byte[] data = new byte[1024];//实例变量
private static volatile Singleton5 instance = null;//定义实际变量,但不直接初始化
private Singleton5(){
//私有化构造防止外部类new
}
//加入同步控制,同一时刻只有一个线程进入
public static Singleton5 getInstance(){
if(null == instance){
synchronized (Singleton5.class){
if (null == instance){
instance = new Singleton5 ();
}
}
}
return instance;
}
}
上述程序在多线程的情况下,多个线程进入getInstance()方法,但此时instance类变量未被赋值,只有一个线程才能进入同步代码块,完成对instance的实例化,之后就不需要对instance类变量的同步保护了。这种方式看起来既满足了懒加载由保证了instance实例的唯一性,而且规避了syncronized排他性的缺陷,但还是存在问题(指令重排序)。指令重排序是为了优化指令,提高程序运行效率。指令重排序包括编译器重排序和运行时重排序。JVM规范规定,指令重排序可以在不影响单线程程序执行结果前提下进行。例如 instance = new Singleton() 可分解为如下伪代码:
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址
但是经过重排序后如下:
memory = allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象
将第2步和第3步调换顺序,在单线程情况下不会影响程序执行的结果,但是在多线程情况下就不一样了。线程A执行了instance = memory(这对另一个线程B来说是可见的),此时线程B执行外层 if (instance == null),发现instance不为空,随即返回,但是得到的却是未被完全初始化的实例,在使用的时候必定会有风险,这正是双重检查锁定的问题所在!
---------------------
作者:zhangzeyuaaa
来源:CSDN
原文:https://blog.csdn.net/zhangzeyuaaa/article/details/42673245
版权声明:本文为博主原创文章,转载请附上博文链接!
Volatile + Double-Check
Double-check虽然是一种巧妙的程序设计,但是有可能会引起类成员变量的实例化发生于instance之后,这一切是由于JVM运行时指令重排序导致的,而很容易想到volatile关键字便可以防止指令重排序的发生,如下
private static volatile Singleton4 instance = null;//实例化instance永远发生于socket之前
Holder方式
Holder方式完全借助了类加载机制,代码如下
package SingletonPkg;
/**
* @author Heian
* @time 19/01/21 12:49
* @copyright(C) 2019 深圳市北辰德科技股份有限公司
* 用途:Holder 单例设计模式
*/
public final class Singleton6 {
private byte[] data = new byte[1024];//实例变量
private static class Holder{
private static Singleton6 instance = new Singleton6 ();//静态内部类持有singleton的实例,并且可被直接初始化
}
private Singleton6(){
//私有化构造防止外部类new
}
public static Singleton6 getInstance(){
return Holder.instance;
}
}
在Singleton类中并没有instance静态成员,而是将其放到了静态内部类Holder之中,因此在Singleton类中初始化过程中并不会导致Holder类的初始化,Holder类中定义了Singleton静态变量,并且直接初始化,当Holder被主动使用则会创建Singleton实例,Singleton实例的创建在java程序编译时期收集至<clinit>()方法,该方法又是同步方法,同步方法可以保证内存的可见性、JVM指令的顺序性和原子性。Holder方式的单例设计模式也是最好的设计之一。
枚举方式
此方式是《effective java》作者力荐,枚举方式不允许被继承,同时是线程安全,并且只能被实例化一次,但枚举类型不能进行懒加载,对于Singleton主动使用,比如调用其中的静态方法则instance会立即得到实例化。
package SingletonPkg;
import sun.dc.pr.PRError;
/**
* @author Heian
* @time 19/01/21 12:49
* @copyright(C) 2019 深圳市北辰德科技股份有限公司
* 用途:枚举方式 单例设计模式
*/
public enum Singleton7 {//枚举类不允许多继承,已经继承了Enum类
INSTANCE;
private byte[] data = new byte[1024];
Singleton7(){
System.out.println ("实例化");//默认私有private
}
public static void method(){
//调用该方法也会主动使用Singleton
}
public static Singleton7 getInstance(){
return INSTANCE;
}
}