单例设计模式

作用:

对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级的对象而言,是非常可观的一笔系统开销。由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。
所以对于系统的关键组件和被频繁操作的对象,使用单例模式便可以有效地改善系统性能。

下面来说说单例模式的几种写法

(1)饿汉式单例模式
/**
 * 饿汉式(Java)
 */

public class SingletonDemo {

    private static SingletonDemo singletonDemo = new SingletonDemo();

    private SingletonDemo(){}

    public static SingletonDemo getInstance(){
        return singletonDemo;
    }
}

/**
 * 饿汉式(Kotlin)
 */
object SingletonDemo {
    val instance: SingletonDemo = SingletonDemo
}

以上是饿汉式单例的标准写法,分析结果如下:

  • 由于SingletonDemo实例的创建时静态的,所以当SingletonDemo加载的时候就会创建实例;
  • 当SingletonDemo中的getInstance方法被执行的时候,SingletonDemo将被加载,从而new了一个SingletonDemo实例;
  • 但是当SingletonDemo类中有其他成员的时候,假如多了一个A方法,首先执行A方法后,SingletonDemo实例也会被创建,也就是说,在不需要SingletonDemo对象的时候依然创建了SingletonDemo实例,造成资源不必要的浪费,那么有没有办法做到当使用的时候再创建实例呢?

想要避免以上弊端就不能让SingletonDemo静态加载,我们改写代码:

/**
 * 懒汉式(java)
 */
public class SingletonDemo {

    private static SingletonDemo instance;

    private SingletonDemo(){}

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

/**
 * 懒汉式(Kotlin)
 */
object SingletonDemo {
    var instance: SingletonDemo? = null
        get() {
            if (field == null) {
                field = SingletonDemo
            }
            return field
        }
        private set
}

采用延迟加载(懒汉式)的方式解决以上弊端,但是这样解决方式并不是完美的,我们不仅要考虑实例单一,还要考虑线程安全,很显然以上代码会带来线程安全危机,当多个线程同时调用getInstance方法时,有可能会new多个实例,接下来我们来解决线程安全问题。

(2)懒汉式单例模式
/**
 * 线程安全的懒汉式(Java)
 */

public class SingletonDemo {

    private static SingletonDemo instance;

    private SingletonDemo(){}

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

/**
 * 线程安全的懒汉式(Kotlin)
 */
object SingletonDemo {
    @get:Synchronized
    var instance: SingletonDemo? = null
        get() {
            if (field == null) {
                field = SingletonDemo
            }
            return field
        }
        private set
}

以上是线程安全的懒汉式的标准写法。

添加synchronized锁,使在同一时间内,只能有一个线程执行getInstance方法,这样虽然决绝了线程安全的问题,但是效率严重降低,那么怎么才能提高效率呢?

(3)双检查锁机制(DCL:double checked locking)
/**
 * 双重校验锁(Java)
 */
public class SingletonDemo {

    private static SingletonDemo instance;

    private SingletonDemo(){}

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

/**
 * 双重校验锁(Kotlin)
 */
object SingletonDemo {
    var instance: SingletonDemo? = null
        get() {
            if (instance == null) {
                synchronized(SingletonDemo::class.java) {
                    if (instance == null) {
                        instance = SingletonDemo
                    }
                }
            }
            return instance
        }
        private set
}

这里有两处判空的地方,如果第一次判断的时候不为空,则跳过synchronized,使效率提高。

这个方案当时被认为是单例模式的最佳方案,但是由于jvm存在乱序执行功能,DCL也会出现线程不安全的情况,jdk1.6之后,只要定义为private volatile static SingleTon instance 就可解决DCL失效问题, volatile确保instance每次均在主内存中读取,这样虽然会牺牲一点效率,但也无伤大雅,volatile可以保证即使java虚拟机对代码执行了指令重排序,也会保证它的正确性。(volatile:防止编译器对代码进行优化)

/**
 * 双重校验锁(Java)
 */
public class SingletonDemo {

    private volatile static SingletonDemo instance;

    private SingletonDemo(){}

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

/**
 * 双重校验锁(Kotlin)
 */
object SingletonDemo {
    @Volatile
    var instance: SingletonDemo? = null
        get() {
            if (instance == null) {
                synchronized(SingletonDemo::class.java) {
                    if (instance == null) {
                        instance = SingletonDemo
                    }
                }
            }
            return field
        }
        private set
}

以上代码才是DCL的标准写法,一定要添加volatile修饰才能保证绝对安全性。

所谓 指令重排 的概念可以举例说明:

创建一个对象需要3条指令:
    指令1:开辟内存
    指令2:对象初始化(内存填值)
    指令3:将指针指向该内存

volatile 是一个指令关键字。
如果不添加 volatile ,那么在多线程的情况下,99.99%的概率是线程安全的,但是有0.01%的概率是不安全的。
如果一旦触发这个0.01%的概率,发生了`指令重排`,那么创建对象的指令可能变成:

    指令1:开辟内存
    指令3:将指针指向该内存 ---- 此时对象已经不为空了
    指令2:对象初始化(内存填值)

这样会导致对象没有初始化完成。
(4)静态内部类单例模式
/**
 * 静态内部类(Java)
 */

public class SingletonDemo {

    private SingletonDemo(){}

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

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

/**
 * 静态内部类(Kotlin)
 */
object SingletonDemo {
    val instance: SingletonDemo
        get() = SingleHolder.instance

    internal object SingleHolder {
        var instance: SingletonDemo = SingletonDemo
    }
}

或者

/**
 * 静态内部类(Kotlin)
 */
class SingletonDemo {
    companion object {
        val instance: CrashHandler
            get() = CrashHandlerHolder.instance

        internal object CrashHandlerHolder {
            var instance: CrashHandler = CrashHandler()
        }
    }
}

分析一下这种写法:
(1)当调用SingletonDemo类中的其他成员时,SingletonDemo加载之后并没有创建实例,所以解决了饿汉式的弊端;(只有SingleHolder内部类被加载的时候才会创建SingletonDemo实例)
(2)由于静态的特性,没有线程安全问题,所以解决了懒汉式的弊端;
(3)由于反射机制无法侵入静态内部类,从而加深了安全性;

(5)用枚举实现单例模式

其实,前四种设计模式存在一个共同的弊端,那就是一旦序列化之后就不再单例,具体原因可以查询其他资料。

public class SingletonDemo {

    private String A;

    private String B;

    public enum  MyEnumSingleton {

        INSTANCE;

        private SingletonDemo singletonDemo;

        private MyEnumSingleton(){
            singletonDemo = new SingletonDemo();
        }

        public SingletonDemo getInstance(){
            return singletonDemo;
        }
    }

    public static SingletonDemo getInstance(){
        return MyEnumSingleton.INSTANCE.getInstance();
    }
}

以上代码就是枚举单例了。

    SingletonDemo singletonDemo1 = SingletonDemo.getInstance();
    System.out.println(String.valueOf(singletonDemo1.hashCode()));
    SingletonDemo singletonDemo2 = SingletonDemo.getInstance();
    System.out.println(String.valueOf(singletonDemo2.hashCode()));
    SingletonDemo singletonDemo3 = SingletonDemo.getInstance();
    System.out.println(String.valueOf(singletonDemo3.hashCode()));

打印hashcode发现是一致的,说明singletonDemo1 、singletonDemo2 、singletonDemo3 是同一个对象。

枚举单例的优点:
  • 线程安全
  • 代码简洁
  • 序列化之后依然是单例
  • 反射机制无法侵入
枚举单例的缺点:
  • 消耗的内存大概是静态内部类单例的两倍
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 什么是设计模式? 是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。一些开发的套路,用于解决某一些特...
    Spring618阅读 3,858评论 0 0
  • 单例模式(SingletonPattern)一般被认为是最简单、最易理解的设计模式,也因为它的简洁易懂,是项目中最...
    成热了阅读 9,894评论 4 34
  • 1.应用场景: 当需要保证类在内存中的对象唯一性,可以使用单例模式,不想创建多个实例浪费资源,或者避免多个实例由于...
    发光的鱼阅读 1,810评论 0 0
  • 设计模式在软件开发人员中非常流行。设计模式是一种通用软件问题的精妙解决方案。单例模式是Java创建型设计模式中的一...
    唐先僧阅读 4,430评论 2 21
  • 单例设计模式(Singleton Pattern)是Java开发人员了解设计模式的第一种,也是最容易理解的,在平时...
    Michaelhbjian阅读 1,756评论 0 2

友情链接更多精彩内容