品设计模式 - (创建型) 单例模式 Singleton

110 阅读9分钟
这里写图片替代文字

需求


程序中标识某个东西只会存在一个的时候,就会有 "只能创建一个实例" 的需求。 例子:应用相关的配置文件,防止系统创建多个实例对象,同时存在多份配置文件的内容,浪费内存资源

类图


定义:保证一个类仅有一个实例,并且提供了一个访问它的全局访问点

关键点: 私有构造函数保证了不能通过构造函数来创建对象实例

只能通过公共静态函数返回唯一的私有静态变量

  • Singleton 类的构造函数是 private 的,禁止从 Singleton 类外部调用构造函数。如果从 Singleton类以外的代码中调用构造函数 new Singleton(),就会出现编译错误
  • 定义了用于获取唯一一个实例的 static方法,同时,为了防止不小心使用 new 关键字创建实例,还将构造函数设置为 private
  • getInstacne()方法:一个全局唯一访问这个类实例的访问当。以便程序从 Singleton 类外部获取 Singleton 类唯一的示例。建议方法名为 getInstance。作为获取唯一实例的方法,通常情况下会以这样为其命名

image.png

极简单例

public class Singleton {

    private static Singleton singleton = new Singleton();

    private Singleton() {
        System.out.println("生成一个实例")
    }

    public static Singleton getInstance() {
        return singletion;
    }
    
}

测试

public class Main {

    public static void main(String[] args) {
        Singleton obj1 = Singleton.getInstance();
        Singleton obj2 = Singleton.getInstance();

        if (obj1 == obj2) {
            System.out.println("obj1 与 obj2 是相同的实例");
        } else {
            System.out.println("不相同");
        }
        
        /*
         输出:
            生成一个实例。
            obj1 与 obj2 是相同的实例
         */
    }
}

核心角色

只有 Singleton 这一角色。Singlton 角色中有一个返回唯一实例的 static方法。总是会返回同一个实例。

何时生成这个唯一的实例?

在第一次调用 getInstance() 方法的时候,Singleton 类会被初始化。这个时候 static字段 singleton被初始化,生成了唯一的一个实例。

作用范围

一个 ClassLoader 及其子类 ClassLoader 的范围。因为一个 ClassLoader 在装在饿汉式实现的单例类的时候就会去创建一个类的实例。

优缺点

优点:

单例模式提供对唯一实例的受控访问,在系统内存中只存一个对象,能节约资源、提高频繁创建和销毁对象时的系统性能,还可在其基础上扩展出双例、多例模式。

缺点:

  1. 单例的职责过重,单个类代码过于臃肿,一定程度上违背 单一职责
  2. 如果实例化的对象长时间不被利用,会被认为是垃圾而被回收

练习

不考虑高并发情形下简单应用 单例模式

题目01

目前有一位售票员 TicketMaker 类,起始售票序号为 1000, 每卖出一张票则序号 + 1;请你修改以下代码,运用 Singleton 模式确保只能生成一个该类的实例

public class TicketMaker {
    private int ticket = 100;
    public int getNextTicketNumber() {
        return ticket++;
    }
}

答案

public class TicketMakerDemo {

    public static void main(String[] args) {
        TicketMaker ticketMaker01 = TicketMaker.getInstance();
        TicketMaker ticketMaker02 = TicketMaker.getInstance();

        //测试
        System.out.println(ticketMaker01.getNextTicketNumber());
        System.out.println(ticketMaker02.getNextTicketNumber());
        System.out.println(ticketMaker01.getNextTicketNumber());
        System.out.println(ticketMaker02.getNextTicketNumber());
        
        /*
         输出:
            1000
            1001
            1002
            1003
         */
    }
}

class TicketMaker {

    private static TicketMaker ticketMaker = new TicketMaker();

    public static TicketMaker getInstance() {
        return ticketMaker;
    }

    private int ticket = 1000;

    public int getNextTicketNumber() {
        return ticket++;
    }
}

题目 02

编写 Triple 类,实现最多只能生成 3 个 Triple 类的实例,实例编号分别为 0, 1, 2 且可以通过 getInstance(int id) 来获取该编号对应的实例

答案

public class Triple {
    private static Triple triple = new Triple();
    private int no = 0;

    private static int instancesCnt = 0;

    public int getNo() {
        return no++;
    }

    public static Triple getInstance() {
        instancesCnt++;
        if (instancesCnt > 3) {
            throw new RuntimeException("生成超过3个实例!");
        }
        return triple;
    }
}


public class TripleClient {

    public static void main(String[] args) {

        Triple instance01 = Triple.getInstance();
        Triple instance02 = Triple.getInstance();
        Triple instance03 = Triple.getInstance();
        System.out.println(instance01.getNo());
        System.out.println(instance02.getNo());
        System.out.println(instance03.getNo());

        Triple.getInstance();

        /*
         输出:
         0
         1
         2
         Exception in thread "main" java.lang.RuntimeException: 生成超过3个实例!
            at com.jools.designpattern.singleton.Triple.getInstance(Triple.java:22)
            at com.jools.designpattern.singleton.TripleClient.main(TripleClient.java:20)
        */
    }
}

题目03

以下 Singleton 类并非严格的 Singleton 模式,请问是为什么?

public class Singleton {
    private static Singleton singleton = null;

    private Singleton() {
        System.out.println("生成一个实例");
    }

    public static Singleton getInstance() {
        if(singleton == null) {
            sinlgeton = new Singleton();
        }
        return singleton;
    }
}

简答: 多线程环境下是不完全的,如果多个线程能够同时进入 if(single == null),可能导致多次实例化

额外练习

题目来源 - 卡码网之设计模式单例练习

题目描述

小明去了一家大型商场,拿到了一个购物车,并开始购物。请你设计一个购物车管理器,记录商品添加到购物车的信息(商品名称和购买数量),并在购买结束后打印出商品清单。(在整个购物过程中,小明只有一个购物车实例存在)。

输入输出样例

代码实现 (仅供参考)

import java.util.*;

public class Main {
    
    private static Cart cart;
    
    public static void main(String[] args) {
        
        cart = Cart.getInstance();
        
        //接收输入
        Scanner scanner = new Scanner(System.in);
        
        //添加购物物品
        while(scanner.hasNext()) {
            String[] inputs = scanner.nextLine().split("\\s+");
            cart.getProducts().add(new String[]{inputs[0], inputs[1]});
        }
        
        //输出
        List<String[]> products = cart.getProducts();
        for(String[] p : products) {
            System.out.println(p[0] + " " + p[1]);
        }
    }
}

class Cart {
    
    //存储购物物品
    private List<String[]> products;
    
    //构造器私有化    
    private Cart() {
        this.products = new ArrayList<>();
    }
    

    private static volatile Cart cartInstance;
    
    //双检索校验保证单例
    public static Cart getInstance() {
        if(cartInstance == null) {
            synchronized(Cart.class) {
                if(cartInstance == null) {
                    cartInstance = new Cart();
                }
            }
        }
        return cartInstance;
    }
    
    public List<String[]> getProducts() {
        return this.products;
    }
}

实现方式总结

实现方法实现方式原理优点缺点
饿汉式依赖 JVM 类加载机制创建单例利用类加载机制安全单例创建时机不可控
枚举类型枚举元素作为静态常量,通过静态代码块初始化借助 JVM 对枚举的处理机制线程安全、自由序列化、实现简单简洁单例创建时机不可控(类加载时自动创建)
懒汉式类加载时不创建,需要时手动创建按需加载单例按需加载单例、节约资源线程不安全(多线程下不适用)
同步锁(懒汉式改进)用同步锁锁住创建单例的方法防止多个线程同时调用创建方法线程安全造成过多的同步开销
双重校验锁(懒汉式改进)两次校验锁控制单例创建第一次校验若已创建则返回,第二次校验防止多次创建线程安全、节省资源实现复杂,易出错
静态内部类在静态内部类中创建单例,按需加载JVM 加载静态内部类保证单例唯一性线程安全、节省资源、实现简单无明显缺点

懒汉式 - 线程不安全

时间换空间

  • 私有静态变量 singleton 在首次使用时才被实例化,这样在未使用该类时不会浪费资源。
  • 但在多线程环境中,这种实现可能有问题,因为多个线程可能同时通过 if (single == null) 的检查,导致多次实例化。
  • 这种方式体现了“延迟加载”的思想,即资源只有在需要时才加载。
  • 同时也体现了“缓存”的概念,对于频繁使用的资源或数据,先尝试从内存中获取,若不存在则获取后再存入内存,以便下次快速访问。
public class LazySingletonObject {
    //缓存实例
    private static LazySingletonObject singleton;

    private LazySingletonObject() {
    }

    //缓存的实现
    public static LazySingletonObject getSingleton() {
        if (singleton == null) {	//体现延迟加载
            singleton = new LazySingletonObject();
        }
        return singleton;
    }
}

饿汉式 — 线程安全

空间换时间

采用直接实例化 singleton

但是直接实例化的方式丢失了延迟实例化带来的节约资源的好处

public class SingletonThreadUnsafe {

    private static SingletonThreadUnsafe singleton = new LazySingletonThreadUnsafe();

    private SingletonThreadUnsafe() {
    }

    public static SingletonThreadUnsafe getSingleton() {
        return singleton;
    }
}

可以配合 Lombok 简化

public class SingletonThreadUnsafe {

    @Getter
    private static SingletonThreadUnsafe singleton = new SingletonThreadUnsafe();

    private SingletonThreadUnsafe() {
    }

}
public class TestSingletonObjClient {

    public static void main(String[] args) {

        SingletonThreadUnsafe singleton = SingletonThreadUnsafe.getSingleton();
        SingletonThreadUnsafe newSingleton = SingletonThreadUnsafe.getSingleton();
        Assert.assertEquals(singleton, newSingleton);

        System.out.println(singleton);
        System.out.println(newSingleton);

        /*
        输出: 
        com.jools.designpattern.singleton.SingletonThreadUnsafe@5fd0d5ae
        com.jools.designpattern.singleton.SingletonThreadUnsafe@5fd0d5ae
         */
    }
}

懒汉式 — 线程安全

对获取单例实例对象的方法使用 synchronized 加锁

一个时间点只能有一个线程能够进入该方法,从而避免了多次实例化问题

但是当一个线程进入该方法之后,其他视图进入该方法的线程都必须等待,性能上有损耗

public class LazySingletonThreadSafe {

    private static LazySingletonThreadSafe singleton;

    private LazySingletonThreadSafe() {
    }

    public static synchronized LazySingletonThreadSafe getInstance() {
        if (singleton == null) {
            singleton = new LazySingletonThreadSafe();
        }
        return singleton;
    }
}

双重校验锁 - 线程安全

  • singleton只需要被实例化一次,之后就可以直接使用了。
  • 只有当 singleton 没有被实例化的时候,才需要进行加锁
  • 双重锁先判断 singleton是否被实例化,如果没有被实例化,那么才对实例化语句进行加锁
  • 但是不推荐
public class DoubleLockSingleton {

    private volatile static DoubleLockSingleton singleton;

    private DoubleLockSingleton() {

    }

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

如果在 getInstance()内仅使用一次 if

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

如果两个线程同时执行 if 语句,那么两个线程就会同时进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 singleton = new DoubleLockSingleton(); 这条语句,只是先后的问题,那么就会进行两次实例化,从而产生了两个实例。因此必须使用**双重校验锁**,也就是需要使用两个 if 语句。

因此采用 volatile关键字修饰也是很有必要的

private volatile static DoubleLockSingleton singleton;

静态内部类实现 — 线程安全

静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getInstance() 方法从而触发 SingletonHolder.INSTANCE 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例。

  • 静态内部类相当于其外部类的成员,只有在第一次被使用的时候才会被装载
  • 采用静态初始化器的方式,它可以由 JVM 来保证线程安全性。只要不使用到这个静态内部类,不会创建对象实例
public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton() {
    }

    private static class SingletonHolder {
        /**
         * 静态初始化器,由JVM来保证线程安全
         */  
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

测试

    @Test
    public void testStaticInnerClassSingleton() {
        StaticInnerClassSingleton instance01 = StaticInnerClassSingleton.getInstance();
        StaticInnerClassSingleton instance02 = StaticInnerClassSingleton.getInstance();

        Assert.assertEquals(instance01, instance02);
        System.out.println(instance01);
        System.out.println(instance02);
        /*
        com.jools.designpattern.singleton.StaticInnerClassSingleton@4ec6a292
        com.jools.designpattern.singleton.StaticInnerClassSingleton@4ec6a292
         */
    }

枚举类实现 —— 线程安全 [最佳]

这是单例模式的最佳实践,它实现简单,并且在面对复杂的序列化或者反射攻击的时候,能够防止实例化多次。

public enum Singleton {

    /**
     * 枚举类 - 实现单例模式
     */
    INSTANCE;
}
    @Test
    public void testEnum() {
        Singleton instance01 = Singleton.INSTANCE;
        Singleton instance02 = Singleton.INSTANCE;
        Assert.assertEquals(instance01, instance02);
    }

参考


创建型-单例模式

单例模式-简书