单例模式简介

单例模式是设计模式中最简单的形式之一。这一模式的目的是使得类的一个对象成为系统中的唯一实例。要实现这一点,可以从客户端对其进行实例化开始。因此需要用一种只允许生成对象类的唯一实例的机制,“阻止”所有想要生成对象的访问。使用工厂方法来限制实例化过程。这个方法应该是静态方法(类方法),因为让类的实例去生成另一个唯一实例毫无意义。

单例模式存在的原因

一些对象的存在只需要唯一的一个,例如,缓存池和线程池。如果线程池存在多例的话,会导致资
源使用过量,缓存多个的话,会导致数据不一致

这种情况是否可以用 java 的静态变量达成共用一份?

用静态变量就依赖于程序员之间的互相沟通,并且,静态变量也有问题,总不能我新建一个静态变
量,就告诉所有人,再新建一个再告诉所有人,比较麻烦,还有一点,就是程序一开始,你就要创建好
这个对象,如果这个对象的创建时间非常长,并且创建完成后,很长时间没有用,救会造成很大的浪费

单例模式的实现方式

  • 懒汉式—线程不安全:最基础的实现方式,线程上下文单例,不需要共享给所有线程,也不需要加synchronize之类的锁,以提高性能。
  • 懒汉式—线程安全:加上synchronize之类保证线程安全的基础上的懒汉模式,相对性能很低,大部分时间并不需要同步
  • 饿汉方式。指全局的单例实例在类装载时构建。
  • 双检锁式。在懒汉式基础上利用synchronize关键字和volatile关键字确保第一次创建时没有线程间竞争而产生多个实例,仅第一次创建时同步,性能相对较高
  • 枚举。java中枚举类本身也是一种单例模式

单例模式的优点

  1. 在内存中只有一个对象,节省内存空间
  2. 避免频繁的创建销毁对象,可以提高性能
  3. 避免对共享资源的多重占用,简化访问
  4. 为整个系统提供一个全局访问点

代码案例

饿汉式:

单线程单例模式立即创建(饿汉式):创建类Hungry

public class Hungry {
    /**
     * 创建一个单例对象
     */
    private static Hungry hungry = new Hungry();

    /**
     * 不能让外界直接创建对象,所以设置私有构造器
     */
    private Hungry() {
    }

    /**
     * 单例对象的全局访问点
     * @return
     */
    public static Hungry getInstance() {
        return hungry;
    }
}
懒汉式

单线程单例模式延迟创建(懒汉式):创建类Lazy

public class Lazy {
    /**
     * 创建单例对象
     */
    private static volatile Lazy lazy = null;

    /**
     * 不能让外界直接创建对象,所以设置私有构造
     */
    private Lazy() {

    }

    /**
     * 单例全局访问点
     *
     * @return
     */
    public static Lazy getInstance() {
        if (lazy == null) {
            lazy = new Lazy();
        }
    return lazy;
    }
}
多线程下测试两种模式存在哪些问题

先测试饿汉式,代码如下:

public class Demo2 extends Thread {
    @Override
    public void run() {
        Hungry hungry = Hungry.getInstance();
        System.out.println(hungry);
    }

    public static void main(String[] args) {
        for (int x = 0; x < 10; x++) {
            Demo2 demo = new Demo2();
            demo.start();
        }
    }
}

看一下结果:饿汉式输出结果

com.ywt.springboot.demo.Hungry@671b8c4d
com.ywt.springboot.demo.Hungry@671b8c4d
com.ywt.springboot.demo.Hungry@671b8c4d
com.ywt.springboot.demo.Hungry@671b8c4d
com.ywt.springboot.demo.Hungry@671b8c4d
com.ywt.springboot.demo.Hungry@671b8c4d
com.ywt.springboot.demo.Hungry@671b8c4d
com.ywt.springboot.demo.Hungry@671b8c4d
com.ywt.springboot.demo.Hungry@671b8c4d
com.ywt.springboot.demo.Hungry@671b8c4d

输出的都是同一个对象,没有线程安全问题,但是不能延迟加载
测试懒汉式,修改 run 方法创建对象

@Override
    public void run() {
        Lazy lazy = Lazy.getInstance();
        System.out.println(lazy);
    }

运行测试:懒汉式输出结果

com.ywt.springboot.demo.Lazy@4985c093
com.ywt.springboot.demo.Lazy@4985c093
com.ywt.springboot.demo.Lazy@4985c093
com.ywt.springboot.demo.Lazy@4985c093
com.ywt.springboot.demo.Lazy@4985c093
com.ywt.springboot.demo.Lazy@279260e5
com.ywt.springboot.demo.Lazy@4985c093
com.ywt.springboot.demo.Lazy@2ea64cd8
com.ywt.springboot.demo.Lazy@279260e5
com.ywt.springboot.demo.Lazy@bc35b81

控制台输出了多个对象,可见,懒汉式存在线程安全问题

解决懒汉式线程安全问题

解决方式一:在方法上加上 synchronized,保证对临界资源的同步互斥访问

public synchronized static Lazy getInstance() {
        if (lazy == null) {
         	lazy = new Lazy();
        }
        return lazy;
    }

这种方式虽然解决了问题,但是当有线程在执行方法时,不管有没有创建对象,其他线程都会在外等候
里面的线程执行完毕,有没有一种方式可以解决这个问题,提高代码执行效率

解决方式二:在方法中创建对象部分设置同步块

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

代码分析:

当还没有创建对象的时候,如果被多个线程同时进入方法执行,比如线程 1,线程 2 进来执行方 法,两个线程同时判断外层 if 时,都为
true ,都进入外层 if 执行,当走到同步块时,比如线程 1 进入 同步块执行代码,线程 2 只能在外面等着,当线程 1
创建完对象执行完同步块后,线程 2 再进入同步块 执行代码,在同步块中,线程 2 判断内层 if ,结果为 false
,不用再创建对象,再有别的线程来执行方法,就不会再一直等待前一个线程了,因为外层 if 已经永远为 false 。

两种模式的简单对比

饿汉式:线程安全,调用率高,但是不能延迟加载
懒汉式:线程安全,调用率不高,可以延迟加载
其他的单例模式:
双重检测锁式(由于JVM底层内部模型原因,偶尔会出问题,不建议使用)
静态内部类式(线程安全,调用效率高。但是,可以延时加载)
枚举式(线程安全,调用率高,不能延时加载)

那么对比五种模式,如何选择使用?

单例对象占用资源少,不需要延迟加载,枚举式比饿汉式好
单例对象占用资源大,需要延迟加载,静态内部类式比懒汉式好