所有的单例模式大致可以归为5类,除了注册式外,其他几类实现方式步骤大致为构造方法私有,提供私有静态变量,提供全局访问点。
下面会对这五类单例实现具体分析其优缺点
一、饿汉式
类加载的时候就实例化对象
优点:程序的效率比较高
缺点:当有大量类时会占用较大内存,是典型的空间换时间的做法。
代码较为基础省略
二、懒汉式
懒汉式单例正好相反是在使用到该类的时候进行初始化实例
优点:节约了宝贵的内存空间
缺点:效率比较低且存在线程安全问题
思考:1.具体是怎么引起的问题?
有两种结果
a.结果是正常的
1.多线程环境下,两个线程按照正常顺序执行,获取的仍是单例对象
2.多线程环境下,两个线程同时进入if判断语句,A线程在创建完对象准备return 的时候 B线程获取CPU开始执行,也创建了对象且return出去。 导致B线程把结果给覆盖了,打印出来的是同一个实例,其实内部发生了很多^_^
这个是假的单例。
b.破坏了单例
多线程环境下,两个线程同时进入if判断,且A线程打印完结果,B线程开始执行,也创建了对象。导致实际获取了多个对象,破坏了单例
2.那么我们如何解决线程安全问题?
version1
public class LazySingleInstance {
private LazySingleInstance() {
}
private static LazySingleInstance instance;
public static synchronized LazySingleInstance getInstance() {
if (instance == null) {
instance = new LazySingleInstance();
}
return instance;
}
}
这种写法可以解决上述问题,但是加上synchronized之后代码执行效率会降低。
version2
public class LazyDoubleCheckedInstance {
private LazyDoubleCheckedInstance() {
}
private static volatile LazyDoubleCheckedInstance instance;
public static LazyDoubleCheckedInstance getInstance() {
//第一次检查是否要加锁
if (instance == null) {
synchronized (LazyDoubleCheckedInstance.class) {
//第二次检查该对象是否存在
if (instance == null) {
instance = new LazyDoubleCheckedInstance();
}
}
}
return instance;
}
}
可以在version1的基础上进行优化,当对象不为空就直接返回结果,不需要加锁。
第二次判断是多线程环境下可能导致破坏单例的情况。这样使用了双重检查锁之后 就可以完美解决线程安全问题。眼见的同学可能已经看到静态变量instance加了一个volatile关键字,这是因为多线程环境下
new LazyDoubleCheckedInstance()在JVM底层会被拆分成3个指令
1.memory = allocate(); //1:分配对象的内存空间
2.ctorInstance(memory); //2:初始化对象
3.instance = memory; //3:设置instance指向刚分配的内存地址
这3种指令没有互相依赖关系 所以为了提高效率,这三个指令是会进行指令重排序的。
重排序后可能是下面的结果
1.memory = allocate(); //1:分配对象的内存空间
3.instance = memory; //3:设置instance指向刚分配的内存地址
2.ctorInstance(memory); //2:初始化对象
这样的话对象还没初始化就被分配到了内存地址,线程B虽然进不去synchronized关键字,但是第一个if判断结果不为空,就会返回一个未初始化完成的对象出去,可能会导致问题。所以加上了volatile关键字保证了内存的可见性。
但是这样做代码不够优雅,尤其是里面的两个if语句对于新手不够友好,可读性差。下面看下升级版的
version3
静态内部类单例
public class SingleInnerClassInstance {
private SingleInnerClassInstance() {
}
public static SingleInnerClassInstance getInstance() {
return InnerSingleInstance.instance;
}
private static class InnerSingleInstance {
private static SingleInnerClassInstance instance = new SingleInnerClassInstance();
}
}
我们可以利用java语法优势,这种写法即不会占用内存,因为类加载的时候我们只会加载
SingleInnerClassInstance.class 文件 只有当我们使用内部类的时候才会加载SingleInnerClassInstance$InnerSingleInstance.class文件
并且不会有线程安全问题,是一种比较优雅的写法。
但是这种写法也有个问题,就是能被反射破坏。我们该如何解决呢?
这也就引出了注册时单例,由于篇幅原因,后面几种单例会在下一篇文章更新,到时候我们一起来探讨~ 下期见