多种单列模式详解

单线程下的实现方式

懒汉式

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if(null == instance) {
            instance = new Singleton();
        }
        return instance;    
    }
}
  • 当单例对象较为庞大时,不易提前初始化
  • 避免了只要程序初始化时就会强制初始化单例对象的无脑行为
  • 无法保证线程安全 

多线程下的实现方式

饿汉式

public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Single getInstance() {
        return instance;
    }
}
  • 简单粗暴
  • 适合在初始化时就要用到单例的情况
  • 适合单例对象初始化占用内存小,快速
  • 在加载这个类的时候会马上创建此唯一的单件实例,JVM保证在任何线程访问instance之前就会创建完毕

懒汉式同步锁

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        synchronized(Singleton.class) {
            if(null == instance) {
                instance = new Singleton();
            }
        }
        return instance;    
    }
}
  • 使用阻塞保证了同步,但是效率将大大降低

双重校验锁

public class Singleton {
    private static Singleton instance = null;

    private Singleton(){

    }

    public static Singleton getInstance() {
        if(null == instance) {
            synchronized (Singleton.class) {
                if(null == instance)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

加入了提前判空逻辑,这样会节省许多不必要的等待(如果不为null,直接返回就OK,不需要在被阻塞住必须等前一个线程释放锁)
这种写法其实也有一个极端危险的情况,问题就出现在这句代码的内部执行顺序上instance = new Singleton() 
在JVM编译的过程中会出现指令重排的优化过程,这就会导致当 instance实际上还没初始化,就可能被分配了内存空间,也就是说会出现 instance !=null 但是又没初始化的情况,这样就会导致返回的 instance 不完整。我们来看看这个场景:假设线程一执行到instance = new Singleton()这句,这里看起来是一句话,但实际上它并不是一个原子操作(原子操作的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形)。事实上高级语言里面非原子操作有很多,我们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大致做了3件事情: 1.给Singleton的实例分配内存。 2.初始化Singleton的构造器 3.将instance对象指向分配的内存空间(注意到这步instance就非null了)。 但是,由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程二上,这时候instance因为已经在线程一内执行过了第三点,instance已经是非空了,所以线程二直接拿走instance,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来。 DCL的写法来实现单例是很多技术书、教科书(包括基于JDK1.4以前版本的书籍)上推荐的写法,实际上是不完全正确的。的确在一些语言(譬如C语言)上DCL是可行的,取决于是否能保证2、3步的顺序。在JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字,因此如果JDK是1.5或之后的版本,只需要将instance的定义改成“private volatile static Singleton instance = null;”就可以保证每次都去instance都从主内存读取,就可以使用DCL的写法来完成单例模式 
 

public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>(); 

    private Singleton() {}

    public static Singleton getInstance() {
        for (;;) {
            Singleton singleton = INSTANCE.get();
            if (null != singleton) {
                return singleton;
            }

            singleton = new Singleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}

CAS原理可以看这篇文章,这里简单总结一下
用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度
CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销
保证单例模式线程安全的另一种思路
思想
“利用”JVM的原生机制去保证线程安全,JVM已经为我们提供了同步机制(JVM保证在任何线程访问公共变量之前就会创建完毕): 
在static{}块中进行初始化
static变量的初始化
访问final字段
枚举变量的初始化
等等
内部类实现单例
基于饿汉实现方式,饿汉是线程安全的,它的缺点就是无法啊延迟加载,所以我们用内部类来解决这个问题 
外层类的初始化不会波及内部类

public class Singleton {
    private Singleton(){

    }

    public static Singleton getInstance() {
        return SingleHolder.instance;
    }

    private static class SingleHolder {
        public static Singleton instance = new Singleton();
    }
}

枚举类型实现单例

public enum Singleton {
    instance;

    //...todo
}

你在需要用的地方`Singleton instance = Singleton.instance;`既可
使用单例模式的风险和解决方案
风险
除了上面分别介绍的各种实现方案的风险之外,还有两种风险会发生在除了枚举实现单例模式的其它所有方案中: 
反射攻击:通过反射调用私有的构造方法会创建出不同的实例
反序列化攻击:通过反序列化同样会创造完全不同的实例
解决方案
通过枚举类型实现的单例可以完美解决以上的问题,java从1.5对于枚举类型开始支持:无偿提供序列化机制和抵御反射攻击的能力,即在面对复杂的序列化或者反射攻击的时候不会创建出多个实例,而且是线程安全的.
为什么说枚举类型实现单例模式是接近完美的?
看一个经过反编译后的字节码文件(枚举)我们就会明白枚举的而本质了:
 

public abstract class Singleton extends Enum
{

    private Singleton(String s, int i)
    {
        super(s, i);
    }

    protected abstract void read();

    protected abstract void write();

    public static Singleton[] values()
    {
        Singleton asingleton[];
        int i;
        Singleton asingleton1[];
        System.arraycopy(asingleton = ENUM$VALUES, 0, asingleton1 = new Singleton[i = asingleton.length], 0, i);
        return asingleton1;
    }

    public static Singleton valueOf(String s)
    {
        return (Singleton)Enum.valueOf(singleton/Singleton, s);
    }

    Singleton(String s, int i, Singleton singleton)
    {
        this(s, i);
    }

    public static final Singleton INSTANCE;
    private static final Singleton ENUM$VALUES[];

    static 
    {
        INSTANCE = new Singleton("INSTANCE", 0) {

            protected void read()
            {
                System.out.println("read");
            }

            protected void write()
            {
                System.out.println("write");
            }

        };
        ENUM$VALUES = (new Singleton[] {
            INSTANCE
        });
    }
}

类是abstract,自然的阻止了反射的攻击
“枚举成员”在static块中进行初始化,自然是线程安全 
枚举类型实现单例会丢失一些类的特性,但这些其实远远没有它的优点吸引人,而且在我们的目的是使用单例这种设计模式的情况下无伤大雅(意思是并不需要那些特性)
总结
一句话,强烈推荐使用枚举方式实现单例

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值