你确定你的单例模式真的用对了?

需求不明确
• 阅读 1604

一、什么是单例模式

单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。该类负责创建自己的对象,同时确保只有一个对象被创建。一般常用在工具类的实现或创建对象需要消耗资源的业务场景。

单例模式的特点

1.类构造器私有

2.持有自己类的引用

3.对外提供获取实例的静态方法

我们先用一个简单示例了解一下单例模式的用法。

public class SimpleSingleton {
    //持有自己类的引用
    private static final SimpleSingleton INSTANCE = new SimpleSingleton();
​
    //私有的构造方法
    private SimpleSingleton() {
​
    }
    //对外提供获取实例的静态方法
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }
​
    public static void main(String[] args) {
        System.out.println(SimpleSingleton.getInstance().hashCode());
        System.out.println(SimpleSingleton.getInstance().hashCode());
    }
}

打印结果:

1639705018

我们看到两次获取SimpleSingleton实例的hashCode是一样的,说明两次调用获取到的是同一个对象。

可能很多朋友平时工作当中都是这么用的,但是我要说的是这段代码其实是有问题的。

你确定你的单例模式真的用对了?

private static final SimpleSingleton INSTANCE = new SimpleSingleton();

一开始就实例化对象了,如果实例化过程非常耗时,并且最后这个对象没有被使用,不是白白造成资源浪费吗?

这个时候你也许会想到,如果在真正使用的时候再实例化不就可以了?这就是我接下来要介绍的 懒汉模式

二、饿汉模式与懒汉模式

什么是饿汉模式?

实例在初始化的时候就已经建好了,不管你有没有用到,都先建好了再说。好处是没有线程安全的问题,坏处是浪费内存空间。代码如下:

public class SimpleSingleton {
    //持有自己类的引用
    private static final SimpleSingleton INSTANCE = new SimpleSingleton();
​
    //私有的构造方法
    private SimpleSingleton() {
​
    }
    //对外提供获取实例的静态方法
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }
​
    public static void main(String[] args) {
        System.out.println(SimpleSingleton.getInstance().hashCode());
        System.out.println(SimpleSingleton.getInstance().hashCode());
    }
}

什么是懒汉模式?

顾名思义就是实例在用到的时候才去创建,“比较懒”,用的时候才去检查有没有实例,如果有则返回,没有则新建。代码如下:

public class SimpleSingleton2 {
​
​
    private static SimpleSingleton2 INSTANCE;
​
    private SimpleSingleton2() {
​
    }
​
    public static SimpleSingleton2 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SimpleSingleton2();
        }
        return INSTANCE;
    }
    public static void main(String[] args) {
        System.out.println(SimpleSingleton2.getInstance().hashCode());
        System.out.println(SimpleSingleton2.getInstance().hashCode());
    }
}
​

示例中的INSTANCE对象一开始是空的,在调用getInstance方法才会真正实例化。

如果代码可能有些朋友在使用,但是还是有问题。

有什么问题呢?

假如有多个线程中都调用了getInstance方法,那么都走到 if (INSTANCE == null) 判断时,可能同时成立,因为INSTANCE初始化时默认值是null。这样会导致多个线程中同时创建INSTANCE对象,即INSTANCE对象被创建了多次,违背了一个INSTANCE对象的初衷。

要如何改进呢?

最简单的办法就是使用synchronized关键字,改进后的代码如下:

public class SimpleSingleton3 {
​
    private static SimpleSingleton3 INSTANCE;
​
    private SimpleSingleton3() {
​
    }
​
    public synchronized static SimpleSingleton3 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SimpleSingleton3();
        }
        return INSTANCE;
    }
    public static void main(String[] args) {
        System.out.println(SimpleSingleton3.getInstance().hashCode());
        System.out.println(SimpleSingleton3.getInstance().hashCode());
    }
}

这样总可以了吧?

不好意思,还是有问题。

你确定你的单例模式真的用对了?

有什么问题?

使用synchronized关键字会消耗性能,我们应该判断INSTANCE为空时才加锁,而不为空不应该加锁,需要直接返回。这就需要使用双重检查锁。

饿汉模式 和 懒汉模式 各有什么优缺点?

饿汉模式:好处是没有线程安全的问题,坏处是浪费内存空间。

懒汉模式:好处是没有内存空间浪费的问题,但是控制不好实际不是单例。

三、双重检查锁

双重检查锁顾名思义会检查两次,在加锁之前检查一次是否为空,加锁之后再检查一次是否为空。代码如下:

public class SimpleSingleton4 {
​
​
    private static SimpleSingleton4 INSTANCE;
​
    private SimpleSingleton4() {
​
    }
​
    public static SimpleSingleton4 getInstance() {
        if (INSTANCE == null) {
            synchronized (SimpleSingleton4.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SimpleSingleton4();
                }
            }
        }
        return INSTANCE;
    }
​
    public static void main(String[] args) {
        System.out.println(SimpleSingleton4.getInstance().hashCode());
        System.out.println(SimpleSingleton4.getInstance().hashCode());
    }
}

在加锁之前判断是否为空,可以确保INSTANCE不为空的情况下,不用加锁,可以直接返回。

为什么在加锁之后,还需要判断INSTANCE是否为空呢?

其实,是为了防止在多线程并发的情况下,比如:线程a 和 线程b同时调用

getInstance,同时判断INSTANCE为空,则同时进行抢锁。假如线程a先抢到锁,开始执行synchronized关键字包含的代码,此时线程b处于等待状态。线程a创建完新实例了,释放锁了,此时线程b拿到锁,进入synchronized关键字包含的代码,如果没有再判断一次INSTANCE是否为空,则可能会重复创建实例。

不要以为这样就完了,还有问题呢?

你确定你的单例模式真的用对了?

有啥问题?

  public static SimpleSingleton4 getInstance() {
        if (INSTANCE == null) {//1
            synchronized (SimpleSingleton4.class) {//2
                if (INSTANCE == null) {//3
                    INSTANCE = new SimpleSingleton4();//4
                }
            }
        }
        return INSTANCE;//5
    }

getInstance方法的这段代码,我是按1、2、3、4、5这种顺序写的,希望也按这个顺序执行。但是java虚拟机实际上会有一些优化,对一些代码指令进行重排。重排之后的顺序可能就变成了:1、3、2、4、5,这样在多线程的情况下同样会创建多次实例。重排之后的代码可能如下:

public static SimpleSingleton4 getInstance() {
        if (INSTANCE == null) {//1
           if (INSTANCE == null) {//3
               synchronized (SimpleSingleton4.class) {//2
                    INSTANCE = new SimpleSingleton4();//4
                }
            }
        }
        return INSTANCE;//5
    }

原来如此,那有什么办法可以解决呢?

可以在定义INSTANCE是加上volatile关键字,代码如下:

public class SimpleSingleton7 {
​
​
    private volatile static SimpleSingleton7 INSTANCE;
​
    private SimpleSingleton7() {
​
    }
​
    public static SimpleSingleton7 getInstance() {
        if (INSTANCE == null) {
            synchronized (SimpleSingleton7.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SimpleSingleton7();
                }
            }
        }
        return INSTANCE;
    }
​
    public static void main(String[] args) {
        System.out.println(SimpleSingleton7.getInstance().hashCode());
        System.out.println(SimpleSingleton7.getInstance().hashCode());
    }
}

volatile 关键字可以保证多个线程的可见性,但是不能保证原子性。同时它也能禁止指令重排。

双重检查锁的机制既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。

除了上面的单例模式之外,还有没有其他的单例模式?

四、静态内部类

静态内部类顾名思义是通过静态的内部类来实现单例模式的。

public class SimpleSingleton5 {
​
    private SimpleSingleton5() {
    }
​
    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }
​
    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
    }
​
    public static void main(String[] args) {
        System.out.println(SimpleSingleton5.getInstance().hashCode());
        System.out.println(SimpleSingleton5.getInstance().hashCode());
    }
}

我们看到在SimpleSingleton5类中定义了一个静态的内部类Inner,SimpleSingleton5类的getInstance方法返回的是内部类Inner的实例INSTANCE。

只有第一次调用getInstance方法时,虚拟机才加载 Inner 并初始化INSTANCE ,只有一个线程可以获得对象的初始化锁,其他线程无法进行初始化,保证对象的唯一性。

五、枚举

枚举是天然的单例,每一个实例只有一个对象,这是java底层内部机制保证的。

public enum  SimpleSingleton6 {
    INSTANCE;
​
    public void doSameThing() {
​
    }
}

但是实际情况下,枚举的单例用的并不多,因为它不好理解。

六、总结​

本文主要介绍了:

饿汉模式、懒汉模式、双重检查锁、静态内部类 和 枚举 这5种单例模式,各有优缺点,静态内部类是所有单例模式中最推荐的模式。

如果您看了这篇文章觉得有所收获,帮忙关注一下我的公众账号:苏三说技术。

点赞
收藏
评论区
推荐文章
3A网络 3A网络
2年前
Golang 常见设计模式之单例模式
之前我们已经看过了Golang常见设计模式中的装饰和选项模式,今天要看的是Golang设计模式里最简单的单例模式。单例模式的作用是确保无论对象被实例化多少次,全局都只有一个实例存在。根据这一特性,我们可以将其应用到全局唯一性配置、数据库连接对象、文件访问对象等。Go语言实现单例模式的方法有很多种,下面我们就一起来看一下。饿汉式饿汉式实现单例模式非
Wesley13 Wesley13
3年前
java设计模式1
1:单例模式简介  单例模式是一种常用的软件设计模式,它确保某个类只有一个实例,而且自行实例化并向整个系统提供唯一的实例。总而言之就是在系统中只会存在一个对象,其中的数据是共享的  特点:    单例类只能有一个实例,所以一般会用static进行修释。    单例类必须自己创建自己的唯一实例。也就是在类中要new一个自己。    单例类必
Wesley13 Wesley13
3年前
java 23种设计模式(五、单例模式)
作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。单例模式的结构  单例模式的特点:单例类只能有一个实例。单例类必须自己创建自己的唯一实例。单例类必须给所有其他对象提供这一实例。  饿汉式单例类publicclassEagerSingleton
Wesley13 Wesley13
3年前
JAVA设计模式之单例设计模式
    单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例。  在JAVA中实现单例,必须了解JAVA内存机制,JAVA中实例对象存在于堆内存中,若要实现单例,必须满足两个条件:  1.限制类实例化对象。即只能产生一个对象。
Wesley13 Wesley13
3年前
Java单例模式
什么是单例模式  单例模式是在程序中,一个类保证只有一个实例,并提供统一的访问入口。为什么要用单例模式节省内存节省计算如对象实例中的一样的,那就不用每次都创建一个对象方便管理因为单例提供一个统一的访问入口,不需要创建N多个对象,很多工具类都用了单例实现,如日志、字符串工具类
Wesley13 Wesley13
3年前
C#单例
单例模式:步骤:1.定义静态私有对象2.构造函数私有化3.定义一个静态的,返回值为该类型的方法,一般以Getinstance/getInit为方法名称单例模式有懒汉和饿汉,最好使用饿汉1.饿汉式先实例化publicclassSingleton{privatestati
Wesley13 Wesley13
3年前
PHP单例模式(精讲)
首先我们要明确单例模式这个概念,那么什么是单例模式呢?单例模式顾名思义,就是只有一个实例。作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类我们称之为单例类。单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例
Stella981 Stella981
3年前
C#设计模式(1)——单例模式(Singleton)
单例模式即所谓的一个类只能有一个实例,也就是类只能在内部实例一次,然后提供这一实例,外部无法对此类实例化。单例模式的特点:1、只能有一个实例;2、只能自己创建自己的唯一实例;3、必须给所有其他的对象提供这一实例。普通单例模式(没有考虑线程安全)  ///<summary///单例模式
Wesley13 Wesley13
3年前
(面试常问)4种单例设计模式的总结(内含代码以及分析)
单例设计模式:  单例模式,是一种常见的软件设计模式.在它的核心结构中只包含了一个被称为单例的特殊类.通过单例模式可以保证系统中只有该类的一个实例对象.优点:  实例控制:单例模式会阻止其它对象实例化其自己的单例对象的副本,从而确保所有对象都访问的是唯一的实例   灵活性:因为类控制了实例化过程,所以类可以很灵活的更改实
Wesley13 Wesley13
3年前
Java设计模式:Singleton(单例)模式
概念定义Singleton(单例)模式是指在程序运行期间,某些类只实例化一次,创建一个全局唯一对象。因此,单例类只能有一个实例,且必须自己创建自己的这个唯一实例,并对外提供访问该实例的方式。单例模式主要是为了避免创建多个实例造成的资源浪费,以及多个实例多次调用容易导致结果出现不一致等问题。例如,一个系统只能有一个窗口管理器或文件系统,一个程
Wesley13 Wesley13
3年前
23种设计模式(1):单例模式
定义:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。类型:创建类模式类图:!23种设计模式(1):单例模式第1张|快课网(http://static.oschina.net/uploads/img/201407/05200605_0dij.gif"23种设计模式(1):单例模式