实现单例
单例模式是:确保一个类只有一个实例,并提供全局唯一访问点。
唯一访问点需要把构造方法私有,提供获取实例的公共方法
全局都是一个实例:实例的对象必须是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>()方法完毕。
网友评论