学习设计模式之单例模式

一、定义

单例模式是整个设计中比较简单的模式,即使没有看过设计模式的相关资料,也会经常用在实际业务的编码开发中。

因为在编程开发中经常会遇到这种场景——需要保证一个类只有一个实例,哪怕多线程同时访问,而且需要提供一个全局访问此实例的点。可以总结出一条经验,单例模式主要解决的是一个全局使用的类,被频繁地创建与销毁,从而提升代码的整体性能,如图所示,孙悟空可以用猴毛实例化变出很多只猴子。
在这里插入图片描述

二、问题背景

单例模式适用的场景非常简单,是在日常开发中能遇到的,如数据库的连接池不会反复创建,Spring中一个单例模式Bean的生成和使用,代码中需要设置全局的一些属性并保存。

三、七种单例模式实现方式

单例模式的实现方式比较多,主要分为在实现上是否支持懒汉模式,是否支持在线程安全中运用各项技巧。

当然,也有一些场景不需要考虑懒汉模式的情况,会直接使用static静态类或属性和方法的方式,供外部调用。

(1)静态类使用
这种静态类方式在日常的业务开发中很常见,它可以在第一次运行时直接初始化Map类,同时也不需要直到延迟加载再使用。在不需要维持任何状态的情况下,仅仅用于全局访问,使用静态类方式更加方便。在需要被继承及维持一些特定状态的情况下,适合使用单例模式。

public class Singleton_00 {

    public static Map<String,String> cache = new ConcurrentHashMap<String, String>();

}

(2)懒汉模式(线程不安全)
单例模式有一个特点是不允许外部直接创建,也就是 new Singleton_01(),因此这里在默认的构造函数上添加了私有属性private。虽然采用此种方式的单例满足了懒汉模式,但是如果有多个访问者同时获取对象实例,就会造成多个同样的实例并存,没有达到单例的要求。

public class Singleton_01 {

    private static Singleton_01 instance;

    private Singleton_01() {
    }

    public static Singleton_01 getInstance(){
        if (null != instance) return instance;
        return new Singleton_01();
    }

}

(3)懒汉模式(线程安全)
此种模式虽然是安全的,但由于把锁加到方法中后,所有的访问因为需要锁占用,导致资源浪费。除非在特殊情况下,否则不建议用此种方式实现单例模式。

public class Singleton_02 {

    private static Singleton_02 instance;

    private Singleton_02() {
    }

    public static synchronized Singleton_02 getInstance(){
        if (null != instance) {
            return instance;
        }
        return new Singleton_02();
    }

}

(4)饿汉模式(线程安全)
这种方式与开头的第一个实例化 Map 基本一致,在程序启动时直接运行加载,后续有外部需要使用时获取即可。这种方式并不是懒加载,也就是说无论程序中是否用到这样的类,都会在程序启动之初进行创建。这种方式造成的问题就像一款游戏软件,可能游戏地图还没有打开,但是程序已经将这些地图全部实例化。在手机上最明显的体验就是打开游戏提示内存满了,造成手机卡顿。

public class Singleton_03 {

    private static Singleton_03 instance = new Singleton_03();

    private Singleton_03() {
    }

    public static Singleton_03 getInstance() {
        return instance;
    }

}

(5)使用类的内部类(线程安全)
使用类的静态内部类实现的单例模式,既保证了线程安全,又保证了懒汉模式,同时不会因为加锁而降低性能。这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是一个类的构造方法在多线程环境下可以被正确地加载。这也是推荐使用的一种单例模式。

public class Singleton_04 {

    private static class SingletonHolder {
        private static Singleton_04 instance = new Singleton_04();
    }

    private Singleton_04() {
    }

    public static Singleton_04 getInstance() {
        return SingletonHolder.instance;
    }

}

(6)双重锁校验(线程安全)
双重锁的方式是方法级锁的优化,减少了获取实例的耗时。同时,这种方式也满足了懒汉模式。

public class Singleton_05 {

    private static Singleton_05 instance;

    private Singleton_05() {
    }

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

}

(7)CAS“AtomicReference”(线程安全)
Java 并发库提供了很多原子类支持并发访问的数据安全性

如:AtomicInteger、AtomicBoolean、AtomicLong 和AtomicReference。AtomicReference 可以封装引用一个V实例上面支持并发访问的单例模式就是利用了这种特性。

使用CAS的好处是不需要使用传统的加锁方式,而是依赖CAS的忙等算法、底层硬件的实现保证线程安全。

相对于其他锁的实现,没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发。当然,CAS也有一个缺点就是忙等,如果一直没有获取到,会陷于死循环。

public class Singleton_06 {

    private static final AtomicReference<Singleton_06> INSTANCE = new AtomicReference<Singleton_06>();

    private static Singleton_06 instance;

    private Singleton_06() {
    }

    public static final Singleton_06 getInstance() {
        for (; ; ) {
            Singleton_06 instance = INSTANCE.get();
            if (null != instance) {
                return instance;
            }
            INSTANCE.compareAndSet(null, new Singleton_06());
            return INSTANCE.get();
        }
    }

    public static void main(String[] args) {
        System.out.println(Singleton_06.getInstance()); // org.itstack.demo.design.Singleton_06@2b193f2d
        System.out.println(Singleton_06.getInstance()); // org.itstack.demo.design.Singleton_06@2b193f2d
    }
}

(8)Effective Java作者推荐的枚举单例(线程安全)
这种写法虽然在功能上与共有域的方法接近,但是它更简洁。即使在面对复杂的串行化或反射攻击时,也无偿地提供了串行化机制,绝对防止对此实例化。虽然这种方式还没有被广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

public enum Singleton_07 {

    INSTANCE;
    public void test(){
        System.out.println("hi~");
    }

}

同时,我们也要知道在存在继承的场景下,此种方式是不可用的

因为使用enum关键字定义的枚举类默认继承了java.lang.Enum类,因此不能再继承其他类
否则会爆:No extends clause allowed for enum

五、总结

虽然单例模式只是一个很平常的模式,但在各种的实现上却需要用到Java的基本功,包括懒汉模式、饿汉模式、线程是否安全、静态类、内部类、加锁和串行化等。

在日常开发中,如果可以确保此类是全局可用的,则不需要懒汉模式,那么直接创建并给外部调用即可。

但如果有很多的类,有些需要在用户触发一定的条件后才显示,那么一定要用懒汉模式。

对于线程的安全,可以按需选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南淮北安

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值