单例模式基本介绍及两种实现方式详解

时间:2024-04-11 14:33:19

1. 什么是单例模式

1.1 什么是模式

        这里所说的模式,指的是代码的设计模式,这属于程序猿的高阶技能.对于普通的程序猿,写出来的代码没有bug,能跑就行,不用过多地在乎,实际上也很难去设计代码的模式.                                              但是有些大佬对这一方面十分得心应手,能够将具体的代码抽象为一种模式,让其他程序猿照着这个模式写代码就可以了,属实是造福他人.

1.2 什么是单例模式

        单例模式是设计模式的一种,我们约定在单例模式中类实例化出的对象只能存在一个,也就是说每当我们要去获取这个类实例的时候,无论获取多少次,什么时候获取,获取到的类实例必须是同一个.

1.3 为什么要使用单例模式

        有的读者可能会思考:为什么我们一定要使用单例模式呢?我多创建几个实例,我只用一个不行吗?实际上,这样的想法没有什么问题.

但是,存在即合理,不要忘了,我们创建一个实例是需要消耗时间和空间资源的,现在我们这个类中没有放很多成员变量/方法,但是试想一下,如果这个实例中存放了百万条数据,我们创建一个实例需要的资源开销就大了!!!在这种情况下,我们还能心中毫无波澜地创建一个个实例吗?显然是不可以的.

2. 单例模式的两种设计方法

对于单例模式来说,有许多设计方式,我们这里只介绍两种:饿汉模式和懒汉模式

2.1 饿汉模式

实例创建时机

啥叫饿汉?就是饿到一有吃的就抢着开吃,体现一种"迫不及待"的感觉.我们刚才提到单例模式中只能有一个类的实例,针对饿汉模式,就是在加载类的时候(main线程中代码还未开始执行)直接创建出实例.

如何保证实例的唯一性

这里提供了一个private修饰的构造方法,确保外部代码无法调用这个构造方法,导致在new一个实例时就会编译报错.

对应代码:
class Singleton{
    private static Singleton instance = new Singleton();
    public Singleton getInstance(){
        return instance;
    }
    private Singleton(){

    }
}

2.2 懒汉模式

实例创建时机

啥叫懒?我想许多读者肯定认为懒时一个贬义词,但是我们思考一下:是不是人类为了懒而创造出了许多东西,推动了科技的进步与时代的发展.在计算机中,懒是一个褒义词,是"高效"的代名词.那么对应懒汉模式来说,啥时候创建这样一个类实例呢?

因为"懒",所以在要使用时我再创建;如果一直不用,我就一直不创建.

这样我们也可以满足只创建一个类实例的条件,其他部分其实同饿汉模式没有很大的区别,我们这里直接给出代码

对应代码:
class SingletonLazy {
    public static SingletonLazy instance = null;
    public SingletonLazy getInstance() {
        if(instance==null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
    private SingletonLazy() {

    }
}

3. 线程安全问题分析

在上述两种模式中,我们可以分析一下其中存在的线程安全问题:

对于饿汉模式,其实并不会存在线程安全问题,因为在饿汉模式中,类实例创建的时机是在java进程启动时(此时还没有开始执行main线程),因此即使是在多线程,也不会创建多个类实例十分安全.

但是,在懒汉模式中,可能会存在线程安全问题:

3.1 可能创建多个实例

因为是在多线程环境下,不同线程可能创建了多个类的实例,对于这个问题以及它的解决方法我们十分熟悉,只要加锁就可以了.于是,改进后的代码如下:

class SingletonLazy {
    public static SingletonLazy instance = null;
    public Object locker = new Object();
    public SingletonLazy getInstance() {
        synchronized (locker) {
            if(instance==null) {
                instance = new SingletonLazy();
            }
            return instance;
        }
    }
    private SingletonLazy() {

    }
}
3.2 可能进行错误判断

先问大家一个问题,在单线程中,连续两次条件一样的if判断得到的结果是不是一样的?这不用思考,结果必定是一样的.但是,在多线程环境下,两次看似一样的if判断中间隔着的也许就是"沧海桑田",因此为了保险起见,我们需要进行双重if判断.

双重if加在锁里吗?当然不是.是一层if加在锁外,一层if加在锁里.改进后的代码如下:

class SingletonLazy {
    public static SingletonLazy instance = null;
    public Object locker = new Object();
    public SingletonLazy getInstance() {
        if(instance==null) {
            synchronized (locker) {
                if(instance==null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy() {

    }
}

3.3 可能发生指令重排序

也就是instance这个引用的对象可能在还没有被赋值时(默认的引用对象为null),被其他的线程错误判断,从而创建一个类实例,这也会导致创建多个类实例.我们可以使用volatile修饰instance,来解决这个问题.关于指令重排序详细介绍可以看上一篇博客-->引发线程安全问题的原因及解决方法-CSDN博客

这里给出最终的代码:

class SingletonLazy {
    public static volatile SingletonLazy instance = null;
    public Object locker = new Object();
    public SingletonLazy getInstance() {
        if(instance==null) {
            synchronized (locker) {
                if(instance==null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy() {

    }
}