JAVA单例模式
一:懒汉模式,线程不安全
懒加载(在需要的时候创建对象)
public class Singleton1 { private static Singleton1 instance; private Singleton1(){} public static Singleton1 getInstance(){ if(instance == null){ instance = new Singleton1(); } return instance; } }
二:懒汉模式,线程安全
/** * 懒汉模式,线程安全 * */ public class Singleton2 { private static Singleton2 singleton2; private Singleton2(){} public static synchronized Singleton2 getInstance(){ if(singleton2==null){ singleton2=new Singleton2(); } return singleton2; } }
效率极为低下,不建议使用
三:饿汉模式,线程安全
public class Singleton3 { private static Singleton3 singleton3=new Singleton3(); private Singleton3(){} public static synchronized Singleton3 getInstance(){ return singleton3; } }
没有达到懒加载的目的,不推荐
四:饿汉模式,线程安全
public class Singleton4 { private static Singleton4 instance ; static { instance = new Singleton4(); } private Singleton4(){} public static synchronized Singleton4 getInstance(){ return instance; } }
等同于第三种
五:静态内部类,线程安全
public class Singleton5 { private Singleton5(){} public static Singleton5 getInstance(){ return SingletonHolder.INSTANCE; } private static class SingletonHolder{ private static Singleton5 INSTANCE=new Singleton5(); } }
与第三第四种的区别是:前者在类加载时就会创建实例,假如实例耗费内存,则是一种不明智的选择,后者则会懒加载
第六种:枚举类,线程安全
public enum Singleton6 { INSTANCE; public void whateverMethod(){} }
安卓推荐,不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象
第七种:双重校验锁,线程安全
public class Singleton7 { private Singleton7() {}; // 私有化构造方法 private static volatile Singleton7 singleTon = null; public static Singleton7 getInstance() { // 第一次校验 if (singleTon == null) { synchronized (Singleton7.class) { // 第二次校验 if (singleTon == null) { singleTon = new Singleton7(); } } } return singleTon; } }
分析:
第一次校验:由于单例模式只需要创建一次实例,如果后面再次调用getInstance方法时,则直接返回之前创建的实例,因此大部分时间不需要执行同步方法里面的代码,大大提高了性能。如果不加第一次校验的话,那跟上面的懒汉模式没什么区别,每次都要去竞争锁。
第二次校验:如果没有第二次校验,假设线程t1执行了第一次校验后,判断为null,这时t2也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来t2获得锁,创建实例。这时t1又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。
为什么要加volatile关键字呢?
需要注意的是,private static volatile SingleTon3 singleTon=null;需要加volatile关键字,否则会出现错误。问题的原因在于JVM指令重排优化的存在。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。
详细解释一下为什么要使用volatile
public class Singleton {
private static Singleton s;
private Singleton(){};
public static Singleton getInstance() { //1
if(s == null) { //2
synchronized (Singleton.class) { //3
if(s == null) { //4
s = new Singleton(); //5
}
}
}
return s; //6
}
}
以前不了解为什么需要volatile关键字,后来发现在并发情况下,如果没有volatile关键字,在第5行会出现问题
对于第5行 s = new Singleton(); //5
可以分解为3个步骤:
1 memory=allocate();// 分配内存 相当于c的malloc
2 ctorInstanc(memory) //初始化对象
3 s=memory //设置s指向刚分配的地址
上面的代码在编译器运行时,可能会出现重排序 从1-2-3 排序为1-3-2
如此在多线程下就会出现问题
例如现在有2个线程A,B
线程A在执行第5行代码时,B线程进来,而此时A执行了 1和3,没有执行2,此时B线程判断s不为null (语句2)直接返回一个未初始化的对象,就会出现问题