设计模式-单例模式

一、单例模式定义

1. 什么是单例模式?

  单例模式 (Singleton Pattern )是指确保一个类在任何情况下有且仅有一个实例,并提供一个全局访问点。这就要求我们不能使用常规的构造器,而是提供一种机制来保证一个类只有一个实例。单例模式是创建型模式。J2EE 标准中的 ServletContext 、ServletContextConfig 等、Spring 框架应用中的ApplicationContext、数据库的连接池等也都是单例形式。单例模式在现实生活中应用也非常广泛,例如,公司 CEO、部门经理等。

2. 单例类的特点

  • 只能有一个实例
  • 隐藏其所有构造方法,构造方法应该由private修饰(私有),也就是自己创建自己的实例
  • 提供一个全局访问点,给其他对象提供这一实例

3. 单例模式常见写法

  1. 饿汉式单例
  2. 懒汉式单例
  3. 注册式单例
  4. ThreadLocal单例

二、实现单例模式的几种方式及使用场景

1. 饿汉式单例模式

饿汉式单例:在单例类首次加载时就创建实例。

  饿汉式单例模式在类加载的时候就会立即初始化,并且创建单例对象。它是线程安全的,在线程还没出现以前就实例化了,不会存在访问安全问题。

下面,我们来看一下饿汉式单例模式的标准代码:

public class HungrySingleton {

    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    /**
     * 将构造方法隐藏起来,也就是构造方法私有化。这样,在外部就无法通过构造方法进行实例化
     */
    private HungrySingleton() {
    }

    /**
     * 提供一个全局访问点
     */
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }

}

还有另外一种写法,利用静态代码块机制:

/**
 *类加载优先于类实例化对象
 * 类加载:程序从上到下,加载静态的初始化语句,初始化块和构造方法
 * 类创建对象:程序从上到下,加载非静态的初始化语句,初始化块和构造方法
 */
public class HungrySingleton {

    private static final HungrySingleton hungrySingleton;

    static {
        hungrySingleton = new HungrySingleton();
    }

    private HungrySingleton() {
    }

    public HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

适用场景:饿汉式单例模式适用于单例对象较少的情况。

优缺点:这样写可以保证绝对线程安全,而且在单例类首次加载时就创建实例,执行效率比较高,同时没有任何锁,性能较高。但是它的缺点也很明显,就是所有对象在类加载的时候就实例化,这样一来,如果系统中有大批量的单例对象存在,那系统初始化就会导致大量的内存浪费。也就是说不管对象用与不用都占着空间,浪费了内存。

那有没有更优的写法呢?下面我们来继续分析。

2. 懒汉式单例模式

懒汉式单例:被外部类调用时才创建实例。

2.1. 懒汉式单例模式的简单实现

  为了解决饿汉式单例可能带来的内存浪费问题,于是就出现了懒汉式单例的写法,懒汉式单例模式的特点是:单例对象在被使用时才会初始化,下面是懒汉式单例模式的简单实现:

public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton() {
    }

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

但这样写又带来了一个新的问题,如果在多线程环境下,就会出现线程安全问题。我们通过实验来验证一下,写一个线程类ThreadExecut:

public class ThreadExecut implements Runnable{
    
    @Override
    public void run() {
        LazySingleton instance = LazySingleton.getInstance();
        System.out.println("线程" + Thread.currentThread().getName() + "创建的对象" + ":" + instance);

    }
}

客户端测试代码:

public class LazySingletonTest {

    public static void main(String[] args) {
        ThreadExecut threadExecut = new ThreadExecut();
        Thread t1 = new Thread(threadExecut);
        Thread t2 = new Thread(threadExecut);
        t1.start();
        t2.start();
        System.out.println("-----------------------------------");
    }
}

客户端测试代码,运行结果如下图所示:

从结果中可以看到,上面的代码有一定概率出现两种不同结果,这意味着上面的单例存在线程安全隐患。由于存在线程安全问题,所以会产生两种不同的结果,如下:

1. 两个线程获取到的是同一个实例,但获取到的同一个实例,又分为两种情况:

  ①两个线程按正常顺序先后执行;
  ②两个线程同时进入了LazySingleton中的if判断条件,由于抢占CPU资源问题,跑在前面的线程创建了对象之后,在执行打印语句之前,后面的另一个线程也创建了对象(instance =  new LazySingleton()),将前者创建的对象覆盖,这样的话,控制台最终输出的是后者创建的对象。即使我们从执行的结果中看到两个线程获取到的对象相同,但线程安全隐患依旧存在,因为LazySingleton被实例化了两次;

2. 两个线程获取到的是不同的实例:两个线程同时进入了LazySingleton中的if判断条件,由于抢占CPU资源问题,跑在前面的线程创建了实例对象,并执行了输出语句之后,后面的另一个线程才继续执行,这个时候不仅会覆盖第一个线程创建的对象,执行的输出语句也会打印出不同的地址值,从结果中可以看到两个线程创建的对象不同;

下面,我们通过调试运行再具体看一下。这里,我们使用线程模式进行调试,通过手动控制线程的执行顺序来跟踪内存的变化。先在ThreadExecut类中打上断点,使用鼠标右键单击断点,切换为Thread模式,如下图所示:

设计模式-单例模式_第1张图片

设计模式-单例模式_第2张图片

然后给LazySingleton打上断点,同样标记为Thread模式

设计模式-单例模式_第3张图片

切回客户端测试代码,同样也打上断点,同时也改为Thread模式,如下图所示

设计模式-单例模式_第4张图片

以"Debug"模式运行之后,会看到"Debug"控制台可以自由切换Thread的运行状态,如下图所示

设计模式-单例模式_第5张图片

RUNNING:表示线程已经抢到CPU资源,开始运行

通过不断切换线程,并观测其内存状态,我们发现线程安全问题确实存在。在线程环境下,有时LazySingleton被实例化了两次,而我们得到的运行结果却可能是相同的两个对象,实际上是被后面执行的线程创建的对象覆盖了,我们看到的相同的对象,有可能是一个假象,线程安全隐患依旧存在。

2.2. 使用synchronized关键字,将获取实例的方法变成同步方法

那么,我们如何来优化代码,使得懒汉式单例模式在线程环境下安全呢?来看下面的代码,给 getlnstance()加上 synchronized 关键字,使这个方法变成线程同步方法:

public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton() {
    }

    /**
     * 加synchronized关键字,使getInstance()变成同步方法
     */
    public synchronized static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

我们再来调试:当其中一个线程执行并调用 getlnstance()方法时,另一个线程再调用 getInstance()方法,线程的状态由 RUNNING 变成了 MONITOR,出现阻塞。直到第一个线程执行完getlnstance()方法,第二个线程才恢复到 RUNNING 状态继续调用 getlnstance()方法,如下图所示

设计模式-单例模式_第6张图片

上图展现了 synchronized 监视锁的运行状态,线程安全的问题得到了解决。但是,用synchronized 加锁时,在线程数量比较多的情况下,如果CPU分配压力上升,则会导致大批线程阻塞从而导致程序性能大幅下降。那还有没有其它的方式,不会导致大批线程阻塞?我们接着往下分析

2.3.  使用synchronized关键字,修饰代码块

既然使用synchronized关键字,将获取实例的方法变成同步方法时在线程数量比较多的情况下,如果CPU分配压力上升,会导致大批线程阻塞从而导致程序性能大幅下降。那么,我们可以尝试缩小加锁的范围,使其在if判断里面生效:使用同步代码块,将创建实例部分放到同步代码块中。实现如下:

public class LazySingleton {

    private static LazySingleton instance;

    private LazySingleton() {
    }

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

我们继续使用线程模式进行调试运行(2.1中有线程模式调试的详细步骤),发现运行结果和 2.1 中的产生的结果相同,也是会产生两种不同的结果。说明采用这种方式,也存在线程安全问题。那该怎么办呢?到底有没有一种更好的方式,既能兼顾线程安全又能提升程序性能呢?答案是肯定的,我们来看双重检查锁的单例模式。

2.4. DCL(Double Check Lock)单例模式

public class LazyDoubleCheckSingleton {

    private volatile static LazyDoubleCheckSingleton instance;

    private LazyDoubleCheckSingleton() {
    }

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

        }
        return instance;
    }
}

你可能感兴趣的:(设计模式,java,单例模式)