美文网首页
java中单例模式的实现

java中单例模式的实现

作者: malikcheng | 来源:发表于2019-07-19 11:13 被阅读0次

实现单例

单例模式是:确保一个类只有一个实例,并提供全局唯一访问点。
唯一访问点需要把构造方法私有,提供获取实例的公共方法
全局都是一个实例:实例的对象必须是static

基于以上几点,可以写出基本懒汉式和饿汉式

一、饿汉式
public class FirstSingle {
//饿汉式  类加载就创建了对象
private FirstSingle() {
}
private static FirstSingle single = new FirstSingle();

public static FirstSingle getSingle() {
    return single;
}
}

二、懒汉式 延时使用时创建对象
public class FirstSingle {
//懒汉式  延时使用时创建对象
private FirstSingle() {
    
}
private static FirstSingle single = null;

public static FirstSingle getSingle() {
    //保持外部获取时,只创建对象一次 ,使用同一个对象。若不判断,每次外部获取都会新建对象,与以前的不是同一个对象,违背了类只有一个实例原则!
    if(single == null)
        single = new FirstSingle();
    return single;
    //赖汉局限:多线程下if语句判断不安全
}
}

三、 懒汉式多线程下优化 -> 双重校验式

①既然上述的懒汉式是线程不安全的,那就加锁使之变成线程安全

public class FirstSingle {
//既然上述的懒汉式是线程不安全的,那就加锁使之变成线程安全
private FirstSingle() {
    
}
private static FirstSingle single = null;

public synchronized static FirstSingle getSingle() {
    if(single == null)
        single = new FirstSingle();
    return single;
    //加同步赖汉局限:多线程下某一时间点只能一个线程访问,其他线程等待被阻塞时间过长,即使其他线程中的single已经被实例了,效率堪忧
}
}

加同步赖汉局限:多线程下某一时间点只能一个线程访问,其他线程等待被阻塞时间过长,即使其他线程中的single已经被实例了,效率堪忧

②进一步优化,既然上面有线程以及获取了single实例后的对象,所以根本不需要再进入if判断中,因此可以将同步锁的范围由整个方法缩小至if处

public class FirstSingle {
//进一步优化,既然上面有线程以及获取了single实例后的对象,所以根本不需要再进入if判断中,因此可以将同步锁的范围由整个方法缩小至if处
private FirstSingle() {
    
}
private static FirstSingle single = null;

public  static FirstSingle getSingle() {
    
    if(single == null)
        synchronized(FirstSingle.class) {
            //不过这时又出现新的问题。假设两个线程T1、T2都是没有得到single的实例。
            //1.T1先进入同步块后,T2进入if在同步块前等待了。
            //2.T1完成实例化,退出同步块释放同步锁,return返回
            //3.此刻因T2已经老早进入if中,不知道single已经被实例化,所以T2也进入同步块又实例化一遍,返回。这样造成两次getSingle的并不是统一对象!
            //其根本原因是T2进入同步块中没有在一次判断是否single已经实例过!
        single = new FirstSingle();
        }
    return single;
}
}

不过这时又出现新的问题。假设两个线程T1、T2都是没有得到single的实例。
1.T1先进入同步块后,T2进入if在同步块前等待了。
2.T1完成实例化,退出同步块释放同步锁,return返回
3.此刻因T2已经老早进入if中,不知道single已经被实例化,所以T2也进入同步块又实例化一遍,返回。这样造成两次getSingle的并不是统一对象!
其根本原因是T2进入同步块中没有在一次判断是否single已经实例过!

③既然上面分析 T2进入同步块,没有判断上次退出同步其他线程创建了实例。那加上判断是否可行

public class FirstSingle {
//既然上面分析 T2进入同步块,没有判断上次退出同步其他线程创建了实例。那加上判断是否可行
private FirstSingle() {
    
}
private static FirstSingle single = null;

public  static FirstSingle getSingle() {
    
    if(single == null)
        synchronized(FirstSingle.class) {
            if(single == null)
                //这里加上if是解决了上面的问题,
                //但是还有考虑JVM指令重排的特性,低概率的同步错误出现,多线程下可能会有某线程得到没有初始化的实例。
                //single = new FirstSingle(); 会有三步过程:
                    //1.为new FirstSingle()实例对象申请空间,对象的字段设置为默认值 0值
                    //2.初始化实例对象
                    //3.single指向实例的对象
                //指令重排后,1—>3—>2, T1 完成1,3 ,释放同步锁,T1还未完成2,没有return;T2进入同步块发现single不是null,返回single这个引用,但是2实例会、化对象还没有完成,出现了错误。
                //若T1在T2返回前完成了第2步,那T2返回也就是正确的对象。
                
                single = new FirstSingle();
        }
    return single;
}
}

④既然有指令重排,那就不让他指令重排,jdk提供volatile关键字。终极版如下 双重校验锁式

public class FirstSingle {
//既然有指令重排,那就不让他指令重排,jdk提供volatile关键字。终极版如下
private FirstSingle() {
    
}
// volatile 保证每个线程得到singlon值是最新的,可见性、防止jvm优化指令重排
private static volatile FirstSingle single = null;

public  static FirstSingle getSingle() {
    //避免线程已经得到实例都进入同步块
    if(single == null)
        synchronized(FirstSingle.class) {
            //第二个if 对于多个没有实例化的线程,避免在进入同步块之前其他线程已经实例过,防止重复实例
            if(single == null)
                //new 这句 加上volatile防止jvm优化指令重排,通知其他线程single的可见,避免出现上面不加volatile时的同步错误
                single = new FirstSingle();
        }
    return single;
}
}

四、静态内部类

利用私有的静态内部类防止外界访问且,因静态全部外部类对象只有一份内部类,内部类用以创建对象,符合了单例的全局只有一个对象。再上提供公共方法获取实例单例,符合提供全局的访问点。

优势:外部类加载时,内部类不会加载,内部不加载就不new,只有在内部类被引用,即调用getInstance时导致JVM加载内部类
优势:线程安全性,和延迟加载单例的实例化

package designpatten.singleton;

//静态内部类方式 实现单例
public class SingletonInnerClass {

private SingletonInnerClass() {}
//私有静态内部类 防止外界访问且全部外部类对象只有一份内部类,

private static class SingletonHolder {
     static SingletonInnerClass single = new SingletonInnerClass(); 
}

public SingletonInnerClass getInstance() {
    return SingletonHolder.single;
}
}
//保证了单例的唯一性:在第一次创建单例后,该外部类的在方法去的常量池已经存在,再下一次getInstance会把single的符号引用直接变成直接引用去找常量池的单例
//所以在除第一初始单例外,其他调用getInstance会直接返回单例对象,这里像是饿汉式

//线程安全性:jvm会保证在进行类加载时<clinit>会加载类的static部分,当然这里如加载外部类不会主动加载静态内部类,
//因为jvm有且仅有的5中情况对类初始化中没有对静态内部类初始,所以它是被动初始的.
//jvm保证一个线程在初始化类期间是线程安全的,也就说其他线程若要初始同一个会被阻塞。这点保证了在线程调用getInstance初始内部类是线程安全

    类加载时机:JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。
1.遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
3.当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
5.当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

这5种情况被称为是类的主动引用,

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。

相关文章

网友评论

      本文标题:java中单例模式的实现

      本文链接:https://www.haomeiwen.com/subject/rndjlctx.html