但是具体实现起来,就会有各种需要考虑的问题在里面。接下来我将把我最近学习的感悟分享出来,有不对的地方还希望大家指正!谢谢了。
1、经典饿汉式:
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
特点:类加载(初始化)的时候就创建了对象,构造函数私有化保证了单例,
缺点:如果这个类特别庞大,初始化时将会特别缓慢,还有就是如果我们用不到这个类,它仍然会创建出来,浪费了资源。
2、经典懒汉式:
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
特点:通过构造函数私有化以及getInstance()方法加上synchronized关键字修饰来保证单例,并且只会在我们第一次需要这个对象的时候创建对象。
缺点:synchronized关键字是一个重锁(对象锁),它会每次调用getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了。
3、懒汉式变种—双重检查结构(不加volatile关键字修饰):
package cn.hzy.creationPattern.singleton;
public class Singleton3 {
private static Singleton3 instance = null;
private Singleton3(){
}
public static Singleton3 getInstance(){
if (instance == null) {
synchronized (instance) {
if (instance == null) {
instance = new Singleton3();
}
}
}
return instance;
}
}
特点:属于懒汉式的变种,上面懒汉式的特点都有,但是这里优化了性能问题,没有给getInstance()方法加锁,而是只给instance = new Singleton3();加锁,也就是说只在初始化的时候会加锁,后面的访问因为instance!=null,就不会加锁。
缺点:乍一看这种模式既没有线程安全问题,又保证了单例,貌似完美了,但是JVM在创建对象的时候有可能为了优化性能而进行指令重排,
看似简单的一句 instance = new Singleton3(); JVM在创建对象的时候会有三个步骤:
1、给Singleton3分配一个内存空间
2、初始化Singleton3(也就是创建Singleton3对象)
3、将instance指向刚分配的内存空间地址
但是有可能JVM为了编译的优化提高效率就有可能变成下面一种顺序:
1、给Singleton3分配一个内存空间
2、将instance指向刚分配的内存空间地址
3、初始化Singleton3(也就是创建Singleton3对象)
其实这种情况在单线程情况下是毫无影响的,结果都一样,但是如果在多线程情况下,就有可能导致错误。
比如:A、B两个线程访问getInstance()方法,A先进入第一个if判断,然后进入synchronized块,开始初始化Singleton3,由于发生了指令重排,将instance指向刚分配的内存空间地址(此时未创建Singleton3对象),在这个时候,B访问getInstance()方法,B进入第一个if判断,因为instance已经指向了一个存在的内存空间地址,即instance!=null,此时直接返回instance(未初始化),然后再调用的时候如果A还没有初始化完毕那么就会报空指针错误。(概率很低)
解决方案:加上volatile关键字修饰,
volatile:
特性一:内存可见性,即线程A对volatile变量的修改,其他线程获取的volatile变量都是最新的。
特性二:可以禁止指令重排序。
修改如下:将 private static Singleton3 instance = null; 改为 private static volatile Singleton3 instance = null;
4、静态内部类:
package cn.hzy.creationPattern.singleton;
public class Singleton4 {
private Singleton4() {}
public static Singleton4 getInstance() {
return SingletonFactory.instance;
}
private static class SingletonFactory {
private static Singleton4 instance = new Singleton4();
}
}
特点:按特征也是属于懒汉模式,因为只会在我们需要用的时候才会创建实例对象,这里通过构造函数私有化,使用内部类来维护单例的实现,因为JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次, 并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心Singleton3出现的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。
缺点:貌似这个就完美了,但是静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去的。
5、枚举:
public enum Singleton {
INSTANCE;
public void method() {
}
}
直接调用SingleTon.INSTANCE就是单例。
特点:创建枚举默认就是线程安全的
优点:简直不要太多,1、写法简单,对比上面的实例就能发现。2、可以防止反射攻击。
针对上面的反射攻击我这里简单说一下:在上面的1、2、3、4种单例模式里面,如果不对构造函数做一些安全处理,我们可以很轻松通过反射拿到构造器并且创建不只一个实例对象,就不再是单例了。但是对于枚举,即时你通过反射拿到构造器,在创建对象实例的时候也会报错,因为枚举是可以防止反射攻击的。
怎么对构造函数做一些安全处理?
可以立一个flag,在创建一个对象实例后,改变flag的值,通过判断抛出异常。
比如:
private static boolean flag = false;
private Singleton (){
synchronized (Singleton .class) {
if(false == flag){
flag = !flag;
} else {
throw new RuntimeException("单例模式正在被反射攻击!!!");
}
}
}
通过在构造函数里面增加一个判断来保证不被反射攻击。
总结:通过上面的分析我们能够看到各种单例模式优劣,其实实际开发中第一种反而用得比较多,因为既然我们都设置了这么一个单例类,那么肯定是系统中会需要这个类,(所以至于创建了这个单例类而又不去用它这种情况,肯定是当时得设计人员酒后设计。。。),至于初始化比较慢,慢就慢呗,那一点影响不在乎,而且在用的时候快啊!!!
还有就是我更加推荐使用枚举,优点上面已经说了。