Java实现单例

1. 什么是单例模式

单例模式指的是在应用整个生命周期内只能存在一个实例。单例模式是一种被广泛使用的设计模式。他有很多好处,能够避免实例对象的重复创建,减少创建实例的系统开销,节省内存。
单例模式的要求有三点:

某个类只能有一个实例
它必须自行创建这个实例
他必须自行向整个系统提供整个实例

2. 单例模式和静态类的区别

首先理解一下什么是静态类,静态类就是一个类里面都是静态方法和静态field,构造器被private修饰,因此不能被实例化。Math类就是一个静态类。

知道了什么是静态类后,来说一下他们两者之间的区别:

1)首先单例模式会提供给你一个全局唯一的对象,静态类只是提供给你很多静态方法,这些方法不用创建对象,通过类就可以直接调用;

2)如果是一个非常重的对象,单例模式可以懒加载,静态类就无法做到;

那么时候时候应该用静态类,什么时候应该用单例模式呢?首先如果你只是想使用一些工具方法,那么最好用静态类,静态类比单例类更快,因为静态的绑定是在编译期进行的。如果你要维护状态信息,或者访问资源时,应该选用单例模式。还可以这样说,当你需要面向对象的能力时(比如继承、多态)时,选用单例类,当你仅仅是提供一些方法时选用静态类。

3.如何实现单例模式

1. 饿汉模式

所谓饿汉模式就是立即加载,一般情况下再调用getInstancef方法之前就已经产生了实例,也就是在类加载的时候已经产生了。这种模式的缺点很明显,就是占用资源,当单例类很大的时候,其实我们是想使用的时候再产生实例。因此这种方式适合占用资源少,在初始化的时候就会被用到的类。

class SingletonHungary {
    private static SingletonHungary singletonHungary = new SingletonHungary();
    //将构造器设置为private禁止通过new进行实例化
    private SingletonHungary() {

    }
    public static SingletonHungary getInstance() {
        return singletonHungary;
    }
}

2. 懒汉模式

懒汉模式就是延迟加载,也叫懒加载。在程序需要用到的时候再创建实例,这样保证了内存不会被浪费。针对懒汉模式,这里给出了5种实现方式,有些实现方式是线程不安全的,也就是说在多线程并发的环境下可能出现资源同步问题。

//单例模式的懒汉实现5--线程安全
//通过设置同步代码块,使用DCL双检查锁机制
//使用双检查锁机制成功的解决了单例模式的懒汉实现的线程不安全问题和效率问题
//DCL 也是大多数多线程结合单例模式使用的解决方案
//第一个if判断的作用:是为了提高程序的 效率,当SingletonLazy5对象被创建以后,再获取SingletonLazy5对象时就不用去验证同步代码块的锁及后面的代码,直接返回SingletonLazy5对象
//第二个if判断的作用:是为了解决多线程下的安全性问题,也就是保证对象的唯一。
class SingletonLazy5 {

    private static volatile SingletonLazy5 singletonLazy;

    private SingletonLazy5() {

    }

    public static SingletonLazy5 getInstance() {
        try {
            if (null == singletonLazy) {
                // 模拟在创建对象之前做一些准备工作
                synchronized (SingletonLazy5.class) {
                    if(null == singletonLazy) {
                        singletonLazy = new SingletonLazy5();
                    }
                }
            }
        } catch (InterruptedException e) {
        }
        return singletonLazy;
    }
}

volatile关键字
这里注意到在定义singletonLazy的时候用到了volatile关键字,这是为了防止指令重排序的,为什么要这么做呢,我们来看一个场景:

代码走到了 singletonLazy = new SingletonLazy5();看起来是一句话,但这并不是一个原子操作(要么全部执行完,要么全部不执行,不能执行一半),这句话被编译成8条汇编指令,大致做了3件事情:

1.给SingletonLazy5的实例分配内存。

2.初始化SingletonLazy5的构造器

3.将singletonLazy对象指向分配的内存空间(注意到这步instance就非null了)。

由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程二上,这时候singletonLazy因为已经在线程一内执行过了第三点,singletonLazy已经是非空了,所以线程二直接拿走singletonLazy,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来。

DCL的写法来实现单例是很多技术书、教科书(包括基于JDK1.4以前版本的书籍)上推荐的写法,实际上是不完全正确的。的确在一些语言(譬如C语言)上DCL是可行的,取决于是否能保证2、3步的顺序。在JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字,因此如果JDK是1.5或之后的版本,只需要将singletonLazy的定义加上volatile关键字,就可以保证每次都去singletonLazy都从主内存读取,并且可以禁止重排序,就可以使用DCL的写法来完成单例模式。当然volatile或多或少也会影响到性能,最重要的是我们还要考虑JDK1.42以及之前的版本,所以单例模式写法的改进还在继续。
3. 静态内部类
基于上面的考虑,我们可以使用静态内部类实现单例模式,代码如下:

//使用静态内部类实现单例模式--线程安全
class SingletonStaticInner {
    private SingletonStaticInner() {

    }
    private static class SingletonInner {
        private static SingletonStaticInner singletonStaticInner = new SingletonStaticInner();
    }
    public static SingletonStaticInner getInstance() {
        return SingletonInner.singletonStaticInner;
    }
}

可以看到使用这种方式我们没有显式的进行任何同步操作,那他是如何保证线程安全呢?和饿汉模式一样,是靠JVM保证类的静态成员只能被加载一次的特点,这样就从JVM层面保证了只会有一个实例对象。那么问题来了,这种方式和饿汉模式又有什么区别呢?不也是立即加载么?实则不然,加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。

可以说这种方式是实现单例模式的最优解。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值