设计模式——单例模式
HeadFirst中对单例模式的定义:单例模式确保一个类只有一个实例,并只提供一个全局访问点。
单例模式的应用:任务管理器、回收站、项目的配置文件、日志文件等等
单例模式的特点:单例模式只有一个实例,减少了系统的开销,当一个对象的产生需要很多资源时,就可以通过在启动时来创建一个实例永久的驻存。
可以在全局设置访问点,优化资源的访问。
单例模式的常见实现方式:
饿汉式:线程安全,效率高,不能延时加载。
懒汉式:线程安全,效率低,可以延时加载。
双重检测锁:线程安全,效率高,可以延时加载,但是只有在java1.5之后才支持,并且由于JVM底层模型的原因容易出问题。
静态内部类:线程安全,效率高,可以延时加载。
枚举:线程安全,效率高,不能延时加载,可以天然的防止反射和反序列化漏洞。
一.饿汉式
1.静态初始化是天然的线程安全的。
2.效率比较高
3.一开始就创建,没有延时加载,如果一直没有用到这个单例对象的话就浪费了资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | package com.wxisme.singleton; /** * 饿汉式单例模式 * @author wxisme * */ public class SingletonOne { //静态初始化时就new出单例对象 private static final SingletonOne instance = new SingletonOne(); //私有构造器 private SingletonOne() { } //返回单例对象 public static SingletonOne getInstance() { return instance; } } |
二.懒汉式
1.线程安全,但是效率较低。
2.延时加载,不会造成资源的浪费。
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | package com.wxisme.singleton; import java.io.Serializable; /** * 懒汉式实现单例模式 * @author wxisme * */ public class SingletonTow implements Serializable { //在获取的时候创建此处不能加final private static SingletonTow instance; private SingletonTow() { //防止反射破解 /* if(instance != null) { throw new RuntimeException(); } */ } //必须要手动加锁,达到线程安全的目的。 public synchronized static SingletonTow getInstance() { if (instance == null ) { instance = new SingletonTow(); } return instance; } //防止反序列化破解 /* private Object readResolve() { return instance; } */ //防止反射破解 /* private static Class getClass(String classname) throws ClassNotFoundException { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); if(classLoader == null) classLoader = SingletonTow.class.getClassLoader(); return (classLoader.loadClass(classname)); } */ } |
三.双重检验锁
1.对懒汉式进行改进,只需要在第一次调用getInstance()方法的时候枷锁,提高了效率。
2.由于JVM底层模型问题,这种方式偶尔会出问题。在JDK1.5之后才能支持。
volatile 用来确保线程安全。
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 | package com.wxisme.singleton; /** * 双重检验锁实现单例模式 * @author wxisme * */ public class SingletonThree { //volatile指令关键字确保实例是线程安全的 private volatile static SingletonThree instance; private SingletonThree() { } //双重检验锁实现 线程安全&延时加载 public static SingletonThree getInstance() { if (instance == null ) { synchronized (SingletonThree. class ) { if (instance == null ) { instance = new SingletonThree(); } } } return instance; } } |
四.静态内部类
1.静态初始化,天然的线程安全,效率高。
2.实现延时加载
3.但是能用反射机制和序列化破解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | package com.wxisme.singleton; /** * 静态内部类实现单例模式 * @author wxisme * */ public class SingletonFour { //静态内部类 private static class Inner { private static final SingletonFour instance = new SingletonFour(); } private SingletonFour() {} //只有显示调用getInstance方法时才会加载内部类 public static final SingletonFour getInstance() { return Inner.instance; } } |
五.枚举
1.天然的线程安全,效率高
2.代码简洁。
3.防止反射和反序列化破解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | package com.wxisme.singleton; /** * 枚举实现单例模式 * @author wxisme * */ public enum SingletonFive { INSTANCE; public void getInstance() { } } |
存在的问题:以上方法中除了枚举的方式之外,都可以通过反射和反序列化的方式来破解。
来破解一下。
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | package com.wxisme.singleton; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Constructor; /** * 反射和反序列化破解单例模式(除枚举之外的都可以破解) * @author wxisme * */ @SuppressWarnings ( "all" ) public class SingletonBreak { /* * 通过反射破解,以懒汉式为例 * 应对策略:在私有构造器中手动抛出异常 */ public static void test1() throws Exception { SingletonTow st1 = SingletonTow.getInstance(); SingletonTow st2 = SingletonTow.getInstance(); System.out.println(st1==st2); //反射破解 Class<SingletonTow> clazz = (Class<SingletonTow>) Class.forName( "com.wxisme.singleton.SingletonTow" ); //获取构造器 Constructor<SingletonTow> c = clazz.getDeclaredConstructor( null ); c.setAccessible( true ); //跳过权限的检查,可以访问私有构造器 SingletonTow st3 = c.newInstance(); SingletonTow st4 = c.newInstance(); System.out.println(st3==st4); } /* * 反序列化破解 以懒汉式为例 (被反序列化的类必须实现Serializable接口) * 应对策略:在类中定义一个readResolve()方法,当反序列化时直接返回已经存在的对象 */ public static void test2() throws IOException, ClassNotFoundException { SingletonTow st1 = SingletonTow.getInstance(); SingletonTow st2 = SingletonTow.getInstance(); System.out.println(st1==st2); //序列化st1对象 FileOutputStream fos = new FileOutputStream( "e:/a.txt" ); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(st1); oos.close(); fos.close(); //反序列化创建对象 ObjectInputStream ois = new ObjectInputStream( new FileInputStream( "e:/a.txt" )); SingletonTow st3 = (SingletonTow) ois.readObject(); ois.close(); System.out.println(st1==st3); } public static void main(String[] args) throws Exception { test1(); System.out.println( "---------------" ); test2(); } } |
不过这种破解可以防止,但是比较繁琐。
防止反射破解的方法:
在私有构造器中手动抛出异常
1 2 3 4 5 6 7 8 9 | private SingletonTow() { //防止反射破解 if (instance != null ) { throw new RuntimeException(); } } |
添加一个getClass()方法
1 2 3 4 5 6 7 8 | private static Class getClass(String classname) throws ClassNotFoundException { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); if (classLoader == null ) classLoader = SingletonTow. class .getClassLoader(); return (classLoader.loadClass(classname)); |
防止反序列化破解的方法:
添加一个readRsolve()方法
1 2 3 | private Object readResolve() { return instance; } |
总结:通过以上可以得出结论:如果需要延时加载,静态内部类好于懒汉式,不需要延时加载则枚举好于饿汉式。双重检验锁是对饿汉式的优化但是不推荐使用。如果没有特别的安全要求静态内部类式是最好的,如果需要还可以防止破解。懒汉式也不错。
PS.在多线程环境下测试每种方式的执行效率。(感谢高淇老师的视频:))
必须在除main线程执行其他所有的线程执行完之后才能计时,用到了CountDownLatch类来控制。
Demo:
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 32 33 34 35 36 37 38 39 | package com.wxisme.singleton; import java.util.concurrent.CountDownLatch; /** * 在多线程环境下测试单例模式的效率 * @author wxisme * */ public class TestEfficiency { public static void main(String[] args) { long start = System.currentTimeMillis(); int thread = 0 ; //内部类方法生命周期和全局变量不一致,需要加final final CountDownLatch count = new CountDownLatch(thread); for ( int i= 0 ; i< 100 ; i++) { new Thread( new Runnable() { @Override public void run() { for ( int j= 0 ; j< 10000 ; j++) { Object o = SingletonOne.getInstance(); } count.countDown(); //一个线程执行完计数器减一。 } }).start(); } try { count.await(); } catch (InterruptedException e) { e.printStackTrace(); } //阻塞main线程,直到所有的线程执行完,线程计数器减为零。 long end = System.currentTimeMillis(); } } |








【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架