一、背景 没有太多原由,纯粹是记录和总结自己从业以来经历和学习的点点滴滴。
本篇内容为 Java 设计模式系列的第一篇。
二、简单介绍 2.1 定义 单例模式是一种对象创建型模式,保证一个类只有一个实例,并且提供能对该实例加以访问的全局方法。
2.2 应用场景 操作系统的任务管理器
读取配置文件的类
数据库连接池
Javaweb 中的 Servlet 实例
Spring 创建的实例,默认为单例
...
三、实现方式 常用的实现方式有饿汉式、懒汉式和枚举类。
本篇文章主要讲饿汉式和懒汉式的单例模式。
共同点:将构造方法私有化,并且提供一个公共的方法访问该类的实例对象。
我们以任务管理器为例进行演示。
3.1 饿汉式 1 2 3 4 5 6 7 8 9 10 11 12 public class TaskManager { private static TaskManager tm = new TaskManager(); private TaskManager() { } public static TaskManager getInstance() { return tm; } }
优点:线程安全,不用加同步锁,因此在高并发时调用效率高。
缺点:不能懒加载,如果不使用该类的实例,浪费内存资源。
3.2 懒汉式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class TaskManager { private static TaskManager tm; private TaskManager() { } public static synchronized TaskManager getInstance() { if (tm == null) { tm = new TaskManager(); } return tm; } }
优点:实现懒加载,合理利用系统资源。
缺点:需要添加同步锁,高并发时调用效率不高。
注意点:上边的懒汉式可以通过反射机制创建多个实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Client { public static void main(String[] args) throws Exception { Class<?> clazz = Class.forName("com.light.gof.singleton.TaskManager"); Constructor<?> constructor = clazz.getDeclaredConstructor(null); // 跳过检测机制 constructor.setAccessible(true); TaskManager tm1 = (TaskManager) constructor.newInstance(); TaskManager tm2 = (TaskManager) constructor.newInstance(); System.out.println(tm1 == tm2);// 结果返回 false } }
3.3 优化方式 将饿汉式和懒汉式的优点集中起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class TaskManager { private TaskManager() { } private static class InnerTaskManager { private static final TaskManager tm = new TaskManager(); } public static TaskManager getInstance() { return InnerTaskManager.tm; } }
外部类没有静态属性,因此不会像饿汉式立即加载对象。
只有当调用公共方法(getInstance)时,才会加载静态内部类。加载内部类的过程是线程安全的。
内部类中通过 static final 确保内存中只有一个外部类的实例,因为实例变量(tm)只能被赋值一次。
四、UML 类图 类图表现如下:
五、性能比较 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class Client { public static void main(String[] args) throws Exception { // 线程数 int num = 10; // 计数器 CountDownLatch cd = new CountDownLatch(num); long t1 = System.currentTimeMillis(); for (int i = 0; i < num; i++) { new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { // 此处替换不同实现方式的单例代码进行测试 TaskManager tm = TaskManager.getInstance(); } cd.countDown(); } }).start(); } // 主线程等待 cd.await(); System.out.println("耗时:" + (System.currentTimeMillis() - t1) + "ms"); } }
测试结果:
实现方式 耗时 饿汉式 3ms 懒汉式 12ms 内部类方式 4ms
测试结果是相对的,硬件配置不同,测试结果不同,但是对于这个 3 种实现方式,它们的用时比例应该大致相同。