需求
程序中标识某个东西只会存在一个的时候,就会有 "只能创建一个实例" 的需求。 例子:应用相关的配置文件,防止系统创建多个实例对象,同时存在多份配置文件的内容,浪费内存资源
类图
定义:保证一个类仅有一个实例,并且提供了一个访问它的全局访问点
关键点: 私有构造函数保证了不能通过构造函数来创建对象实例
只能通过公共静态函数返回唯一的私有静态变量
Singleton类的构造函数是 private 的,禁止从Singleton类外部调用构造函数。如果从Singleton类以外的代码中调用构造函数new Singleton(),就会出现编译错误- 定义了用于获取唯一一个实例的
static方法,同时,为了防止不小心使用 new 关键字创建实例,还将构造函数设置为private getInstacne()方法:一个全局唯一访问这个类实例的访问当。以便程序从Singleton类外部获取 Singleton 类唯一的示例。建议方法名为getInstance。作为获取唯一实例的方法,通常情况下会以这样为其命名
极简单例
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 在装在饿汉式实现的单例类的时候就会去创建一个类的实例。
优缺点
优点:
单例模式提供对唯一实例的受控访问,在系统内存中只存一个对象,能节约资源、提高频繁创建和销毁对象时的系统性能,还可在其基础上扩展出双例、多例模式。
缺点:
- 单例的职责过重,单个类代码过于臃肿,一定程度上违背
单一职责 - 如果实例化的对象长时间不被利用,会被认为是垃圾而被回收
练习
不考虑高并发情形下简单应用 单例模式
题目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);
}