单例设计模式

单例模式

定义:一个类只能有一个实例,且这个类能自行创建这个实例的一种模式。

1. 单例模式的优缺点

1.1 优点

  • 由于单例模式在内存中只存在一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显了;
  • 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在JavaEE中采用单例模式时需要注意JVM垃圾回收机制);
  • 单例模式可以避免对资源的多重占用;
  • 单例模式可以在系统设置全局的访问点,优化和共享资源访问;

1.2 缺点

  • 单例模式一般没有接口,扩展很困难,如果要扩展,除了修改代码基本上没有第二种途径可以实现。那么为什么单例模式不能增加接口呢?因为接口对单例模式时没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽象类是不能被实例化的。当然在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境来进行判断。
  • 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象;
  • 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例得到,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中了;

2. 单例模式的使用场景

在一个系统中,要求一个类有且仅有一个对象,如果出现多个对下个就会出现“不良反应”,可以采用单例模式,具体的场景如下:

  • 要求生成唯一序列化的环境;
  • 在整个项目中需要一个共享访问点或共享数据。例如一个web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保线程是安全的;
  • 创建一个对象需要消耗的资源过多。如要访问IO和数据库等资源;
  • 需要定义大量的静态常量和惊天方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。

3. 单例模式的实现

3.1 分析

我们都知道,一个类想要实例化那肯定是要调用这个类的构造方法的,那么我们可以通过将构造方法给私有化,这样子别人就无法使用它的构造方法来实例化了。问题又来了,别人用不了这个类的构造方法,又该怎么对他进行实例化呢?我们都知道面向对象有三大特性:封装、继承与多态。那思路就很明确了,我们可以使用封装的方法将构造方法与实例的属性私有化,将实例化这个工作交给这个类自己,对外提供一个公开的获取这个实例的方法。但是还有一个问题,我都不能把这个类实例化,那该怎么调用这个类的方法呢?聪明的朋友们应该已经想到了,我们可以将这个方法写成静态方法,就不需要进行实例化就能拿到了,将实例属性也写成静态的,并且使用构造方法对其赋值。至此,我们已经分析完了单例模式的实现了。以上全是文字分析,太枯燥了,大家估计都看得很懵了,下面我们结合代码来看一下。

3.2 代码实现

3.2.1 饿汉模式
class Singleton {
    // 静态属性在程序一运行时就初始化,从而实例化Singleton的对象
    private static Singleton singleton = new Singleton();
    // 构造方法私有化,禁止别人使用,从而保证只有一个实例
    private Singleton(){}
    // 提供外界可以获得该实例的方法
    public static Singleton getInstance() {
        return singleton;
    }
}

饿汉模式在类加载的时候就创建了单例类的对象。

如果创建单例对象会消耗大量资源,那么采用懒汉模式就不是一个明智的选择了。

3.2.2 懒汉模式

懒汉模式与饿汉模式的区别在于,懒汉模式并不是在类加载时创建对象,而是在第一次获取这个对象时创建,也就是延迟加载的意思。

3.2.2.1 不加锁实现
class Singleton {
    private static Singleton singleton = null;
    private Singleton(){}
    public static Singleton getInstance() {
        // 判断对象是否创建
        if(null == singleton) {
            // 创建对象
            singleton = new Singleton();
        }
        return singleton;
    }
}

这种实现有一个明显的问题,就是没有考虑线程安全问题,在多线程并发的情况下,会并发调用 getInstance() 方法,从而导致系统同时创建多个实例类对象,这显然不符合单例模式的要求。

3.2.2.2 加锁实现

添加 synchronized 锁

class Singleton {
    private static Singleton singleton = null;
    private Singleton(){}
    public static sychronized Singleton getInstance() {
        // 判断对象是否创建
        if(null == singleton) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

添加 sychronized 锁虽然可以保证线程安全,但是每次调用 getInsance() 方法的时候,都会有加锁和解锁操作,同时 synchronized 锁是添加在方法上的,锁的范围过大,而单例类是全局唯一的,锁的操作会成为系统的瓶颈。因为,需要对代码再进行优化,所以就有了==“双重校验锁”==的方式。

class Singleton {
    private static Singleton singleton = null;
    private Singleton(){}
    public static Singleton getInstance() {
        // 第一次校验
        if(null == singleton) {
            sychronized(Singleton.class) {
                // 第二次校验
                if(null == singleton) {
                    // 创建对象,非原子操作
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

双重校验锁会出现指令重排的问题,所谓指令重排就是值 JVM 为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。" singleton = new Singleton() "看似原子操作,但其实并不是。

" singleton = new Singleton() "实际上剋抽象为以下几条 JVM 指令:

// 1:分配对象的内存空间
memory = allocate();
// 2:初始化对象
ctorInstance(memory);
// 3:设置 instance 指向刚分配的内存地址
singleton = memory;

上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重新排序的,经过重新排序后如下:

// 1:分配对象的内存空间
memory = allocate();
// 3:设置 instance 指向刚分配的内存地址
singleton = memory;
// 2:初始化对象
ctorInstance(memory);

可以看到,指令重排以后,singleton 指向分配好的内存放到了前面,而这段内存的初始化被排在了后面。在线程A执行这段赋值语句,在初始化分配对象之前就已经将其赋值给 singleton 引用,恰好 B 线程进入方法判断 singleton 引用不为 null,然后就将其返回使用,导致程序出错。

为了解决指令重排的问题,可以使用 volatile 关键字修饰 singleton 字段。volatile 关键字的一个语义就是禁止指令的重新排序优化,阻止 JVM 对其相关代码进行指令重排,这样就能够按照既定的顺序执行指令。

修改后的代码如下:

class Singleton {
    private static volatile Singleton singleton = null;
    private Singleton(){}
    public static Singleton getInstance() {
        if(null == singleton) {
            sychronized(Singleton.class) {
                if(null == singleton) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
3.2.3 最佳实现

静态内部类单例模式

class Singleton {
    // 私有的静态内部类,类加载器负责加锁
    private static class SingletonHolder {
        private static Singleton singleton = new Singleton();
    }
    // 私有化构造方法
    private Singleton(){}
    // 提供外界可以获得该实例的方法
    private static Singleton getInstance() {
        return SingletonHolder.singleton;
    }
}

当第一次访问类中的静态字段时,会触发类加载,并且同一个类只加载一次。静态内部类也是如此,类加载过程由类加载器负责加锁,从而保证了线程的安全。这种写法相对于双重校验锁的写法,更加简洁明了,也更加不容易出错。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值