一、背景

没有太多原由,纯粹是记录和总结自己从业以来经历和学习的点点滴滴。

本篇内容为 Java 设计模式系列的第一篇。

二、简单介绍

# 2.1 定义

单例模式是一种对象创建型模式,保证一个类只有一个实例,并且提供能对该实例加以访问的全局方法。

# 2.2 应用场景

  1. 操作系统的任务管理器

  2. 读取配置文件的类

  3. 数据库连接池

  4. Javaweb 中的 Servlet 实例

  5. Spring 创建的实例,默认为单例

...

三、实现方式

常用的实现方式有饿汉式、懒汉式和枚举类。

本篇文章主要讲饿汉式和懒汉式的单例模式。

共同点:将构造方法私有化,并且提供一个公共的方法访问该类的实例对象。

我们以任务管理器为例进行演示。

# 3.1 饿汉式

  1. public class TaskManager {
  2. private static TaskManager tm = new TaskManager();
  3. private TaskManager() {
  4. }
  5. public static TaskManager getInstance() {
  6. return tm;
  7. }
  8. }

优点:线程安全,不用加同步锁,因此在高并发时调用效率高。

缺点:不能懒加载,如果不使用该类的实例,浪费内存资源。

# 3.2 懒汉式

  1. public class TaskManager {
  2. private static TaskManager tm;
  3. private TaskManager() {
  4. }
  5. public static synchronized TaskManager getInstance() {
  6. if (tm == null) {
  7. tm = new TaskManager();
  8. }
  9. return tm;
  10. }
  11. }

优点:实现懒加载,合理利用系统资源。

缺点:需要添加同步锁,高并发时调用效率不高。

注意点:上边的懒汉式可以通过反射机制创建多个实例。

  1. public class Client {
  2. public static void main(String[] args) throws Exception {
  3. Class<?> clazz = Class.forName("com.light.gof.singleton.TaskManager");
  4. Constructor<?> constructor = clazz.getDeclaredConstructor(null);
  5. // 跳过检测机制
  6. constructor.setAccessible(true);
  7. TaskManager tm1 = (TaskManager) constructor.newInstance();
  8. TaskManager tm2 = (TaskManager) constructor.newInstance();
  9. System.out.println(tm1 == tm2);// 结果返回 false
  10. }
  11. }

# 3.3 优化方式

将饿汉式和懒汉式的优点集中起来。

  1. public class TaskManager {
  2. private TaskManager() {
  3. }
  4. private static class InnerTaskManager {
  5. private static final TaskManager tm = new TaskManager();
  6. }
  7. public static TaskManager getInstance() {
  8. return InnerTaskManager.tm;
  9. }
  10. }

外部类没有静态属性,因此不会像饿汉式立即加载对象。

只有当调用公共方法(getInstance)时,才会加载静态内部类。加载内部类的过程是线程安全的。

内部类中通过 static final 确保内存中只有一个外部类的实例,因为实例变量(tm)只能被赋值一次。

四、UML 类图

类图表现如下:

五、性能比较

  1. public class Client {
  2. public static void main(String[] args) throws Exception {
  3. // 线程数
  4. int num = 10;
  5. // 计数器
  6. CountDownLatch cd = new CountDownLatch(num);
  7. long t1 = System.currentTimeMillis();
  8. for (int i = 0; i < num; i++) {
  9. new Thread(new Runnable() {
  10. @Override
  11. public void run() {
  12. for (int i = 0; i < 10000; i++) {
  13. // 此处替换不同实现方式的单例代码进行测试
  14. TaskManager tm = TaskManager.getInstance();
  15. }
  16. cd.countDown();
  17. }
  18. }).start();
  19. }
  20. // 主线程等待
  21. cd.await();
  22. System.out.println("耗时:" + (System.currentTimeMillis() - t1) + "ms");
  23. }
  24. }

测试结果:

实现方式耗时
饿汉式3ms
懒汉式12ms
内部类方式4ms

测试结果是相对的,硬件配置不同,测试结果不同,但是对于这个 3 种实现方式,它们的用时比例应该大致相同。