单例模式你真的会了吗(上篇)?

单例模式相信是不少程序员接触最多的了,也是面试过程当中考察最频繁的一个了,不知道你有没有被问过这道面试题?欢迎留言讨论。java

今天咱们来重点讨论一下单例的几个问题,及如何正确的实现一个单例,而后你再来回顾一下,你以前的回答或者使用方式是否正确。程序员

为什么要使用单例

单例很是简单,一个类只容许建立一个对象或者实例,这个类就是一个单例类。这种设计模式就叫作单例设计模式,是建立型的第一种设计模式,简称单例模式。面试

单例模式何时使用呢?又或者说这种状况下为何要使用单例?设计模式

  1. 解决资源冲突问题
    好比说,咱们如今的java处理程序中是使用打印机,而咱们的服务端程序是多线程的,可是打印机只有一个,不能重复建立打印机资源啊。固然咱们也能够定义普通类,在调用打印添加synchronized关键字。
  2. 全局惟一类
    有时候,咱们作业务设计时,有些数据在系统中只应该保留一份,这时候就应该设计为单例。
    好比配置信息类,系统的配置文件应该只有一份,加载到内存以后以对象的形式存在,理所应当只有一份。
    再好比说,咱们设计一个抽奖系统,每点击一次生成一个抽奖序号,能够设计一个单例,内部存储好全部的序号,每次随机取出一个序号。若是使用普通类对象的话,那就须要经过共享内存共享全部抽奖序号。安全

    单例应该怎么写?

    学习任何东西,由于大脑的容量是有限的,首先咱们要理解概念,知道为何,来后追求怎么作,怎么实现,作的过程可能很复杂,好比有一二三四五步骤,但咱们要化繁为简,归纳精简。多线程

单例须要考虑如下几个问题:并发

  • 构造函数要是private的,这样才能避免外部经过new建立实例嘛,否则怎么叫单例,别人能够随便经过new来建立啊。
  • 多线程建立时是否有线程安全问题。
  • 支持延迟加载吗?
  • getInstance()性能高吗?

单例典型实现方式

饿汉式

经过这种形容方式,能够直观的理解一下,饿汉一直担忧本身吃不饱,因此先吃了再说,也就是说实例是事先初始化好的,也就没有办法延迟加载了。
不支持懒加载,有人就说这种方式很差,说我都没有使用单例,你都给我加载了,浪费啊。可是有坏处也有好处,提早把类加载进来,提早暴露问题,这样若是类的设计有问题,在程序启动时就会报错,而不是等到程序运行中才暴露出来。分布式

public class SingleTon {
    private static final SingleTon instance = new SingleTon();
    
    private SingleTon() {}
    
    public static SingleTon getInstance() {
        return instance;
    }
    
    public void method() {}
}

懒汉式

所谓懒汉式,那就是支持延迟加载喽。整体思路相似,但在类内部并非默认就把instance实例化好。函数

public class SingleTon {
    private static SingleTon instance;

    private SingleTon() {}

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

    public void method() {}
}

为何要加synchronized呢?若是是多线程同时调用getInstance(),会有并发问题啊,多个线程可能同时拿到instance == null的判断,这样就会重复实例化,单例就不是单例。因此为了解决多线程并发的问题,这里牺牲了性能,变成了严格的串行制。多线程下性能很低。性能

双重检测懒汉式

饿汉方式不支持延迟加载。
懒汉方式,多线程下性能低下,那怎么修改呢,就是改进的懒汉方式,又叫双重检测。
具体怎么作呢?

public class SingleTon {
    private static volatile SingleTon instance;

    private SingleTon() {}

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

    public void method() {}
}

这个类里的volatile十分关键,若是没有volatile关键字修饰instance变量,若是线程1执行到instance = new SingleTon();的时候,线程2此时判断instance已经不等于null了,会直接返回instance,但此时instance并未初始化完毕,为何这么说呢?由于对象的初始化分为三步:

  • 分配内存
  • 内存初始化
  • 对象指向新分配的内存地址

既然是分为三步,那就不是原子操做,并且可能会发生指令重排,也就是说可能先执行第三步,这时候其余线程判断instance也就不是null了。加上volatile关键字,能够禁止机器指令重排,就不会有这个问题了。

静态内部类

这种方式,避免双重检测,利用java静态内部类,相似饿汉方式,又作到延迟加载。

public class SingleTon {

    private SingleTon() {}
    
    private static class SingleTonHolder {
        private static final SingleTon instance = new SingleTon();
    }

    public static SingleTon getInstance() {
        return SingleTonHolder.instance;
    }

    public void method() {}
}

是否是以为很简洁?推荐你们使用这种方式,类SingleTon加载时,并不会加载SingleTonHolder类,只要调用getInstance方法时,SingleTonHolder才会被加载,并建立instance,这些都是由JVM来保证的。

枚举方式

还有一种更简单的,可是理解起来可能有点费解,枚举的构造函数默认就是私有的。java的枚举类型自己就保证了线程安全性和实例惟一性。
只须要简单几行,就可使用枚举单例INSTANCE的方法了。

public enum SingleTon {
    INSTANCE;
    
    public void method() {}
}

可是单例模式真的就好吗?下面咱们会讨论一下为何不推荐单例模式?如何替代,以及如何作到集群下的分布式单例模式?

程序员的小伙伴们,学习之路,同行的人越多才能够走的更远,加入公众号[程序员之道],一块儿交流沟通,走出咱们的程序员之道!
扫码加入吧!

相关文章
相关标签/搜索