单例模式的奇幻漂流
单例模式这个题目可以关系到很多知识点。比如线程安全、类加载机制、synchronized的原理、volatile的原理、指令重排与内存屏障、枚举的实现、反射与单例模式、序列化如何破坏单例、CAS、CAS的ABA问题、Threadlocal等。
1 2 3 4 5 6 7 8 9 10 11 12 | public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null ) { instance = new Singleton(); } return instance; } } |
线程安全版本的懒汉模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class LazySingleton { private static volatile LazySingleton instance= null ; //保证 instance 在所有线程中同步 private LazySingleton(){} //private 避免类在外部被实例化 public static <strong> synchronized LazySingleton getInstance</strong>() { //getInstance 方法前加同步 if (instance== null ) { instance= new LazySingleton(); } return instance; } } |
注意:如果编写的是多线程程序,则不能删除代码中的关键字 volatile 和 synchronized,否则将存在线程非安全的问题。如果不删除这两个关键字就能保证线程安全,但是每次访问时都要同步,会影响性能,且消耗更多的资源,这是懒汉式单例的缺点。
1 2 3 4 5 6 7 | public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; } } |
饿汉模式单例的变种:
1 2 3 4 5 6 7 8 9 10 | public class Singleton { private Singleton instance = null ; static { instance = new Singleton(); } private Singleton (){} public static Singleton getInstance() { return this .instance; } } |
饿汉式的创建方式在一些场景中将无法使用:比如 Singleton 实例的创建是依赖参数或者配置文件的,在getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class DoubleCheckSingleton{ private static volatile DoubleCheckSingleton instance; //静止指令重排,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作 private DoubleCheckSingleton(){} public static DoubleCheckSingleton getInstance() { if (instance == null ) { <strong> synchronized (Singleton. class )</strong> { if (instance == null ) { instance = new DoubleCheckSingleton(); } } } return instance; } } |
这样写有好处: 检查变量是否被初始化(不去获得锁),如果已被初始化则立即返回, 如果没有获取锁,再次检查变量是否已经被初始化,如果还没被初始化就初始化一个对象。如果多个线程同时了通过了第一次检查,并且其中一个线程首先通过了第二次检查并实例化了对象,那么剩余通过了第一次检查的线程就不会再去实例化对象。 除了初始化的时候会出现加锁的情况,后续的所有调用都会避免加锁而直接返回,解决了性能消耗的问题,解决了上述的懒汉单例的缺点。
1 2 3 4 5 6 7 8 9 | public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } } |
静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
-
遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
-
使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
-
当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
-
当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
-
当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
1 2 3 4 5 | public enum Singleton { INSTANCE; public void whateverMethod() { } } |
枚举其实底层是依赖Enum类实现的,这个类的成员变量都是static类型的,并且在静态代码块中实例化的,和饿汉有点像, 所以天然是线程安全的。
枚举在java中与普通类一样,都能拥有字段与方法,而且枚举实例创建是线程安全的,在任何情况下,它都是一个单例。可直接以 SingleTon.INSTANCE的方式调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class Singleton { private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>(); private Singleton() {} public static Singleton getInstance() { for (;;) { Singleton singleton = INSTANCE.get(); if ( null != singleton) { return singleton; } singleton = new Singleton(); //大量的对象被创建,很容易造成OOM if (INSTANCE.compareAndSet( null , singleton)) { return singleton; } } } } |
ThreadLocal的理解:ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。对于多线程资源共享的问题,同步机制(synchronized)采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。同步机制仅提供一份变量,让不同的线程排队访问,而ThreadLocal为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class Singleton { private static final ThreadLocal<Singleton> singleton = new ThreadLocal<Singleton>() { @Override protected Singleton initialValue() { return new Singleton(); } }; public static Singleton getInstance() { return singleton.get(); } private Singleton() {} } |
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 IdGenerator { private AtomicLong id = new AtomicLong( 0 ); private static IdGenerator instance; private static SharedObjectStorage storage = FileSharedObjectStorage(); private static DistributedLock lock = new DistributedLock(); private IdGenerator() {} public synchronized static IdGenerator getInstance() if (instance == null ) { lock.lock(); instance = storage.load(IdGenerator. class ); } return instance; } public synchroinzed void freeInstance() { storage.save( this , IdGeneator. class ); instance = null ; //释放对象 lock.unlock(); } public long getId() { return id.incrementAndGet(); } } // IdGenerator使用举例 IdGenerator idGeneator = IdGenerator.getInstance(); long id = idGenerator.getId(); IdGenerator.freeInstance(); |
上面是一段伪代码,我们把单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。多个不同进程之间的访问通过分布式锁来控制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /** * 使用双重校验锁方式实现单例 */ public class Singleton implements Serializable{ private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null ) { synchronized (Singleton. class ) { if (singleton == null ) { singleton = new Singleton(); } } } return singleton; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public class SingletonTest { public static void main(String[] args) { Singleton singleton = Singleton.getSingleton(); try { Class<Singleton> singleClass = (Class<Singleton>)Class.forName( "com.dev.interview.Singleton" ); Constructor<Singleton> constructor = singleClass.getDeclaredConstructor( null ); constructor.setAccessible( true ); Singleton singletonByReflect = constructor.newInstance(); System.out.println( "singleton : " + singleton); System.out.println( "singletonByReflect : " + singletonByReflect); System.out.println( "singleton == singletonByReflect : " + (singleton == singletonByReflect)); } catch (Exception e) { e.printStackTrace(); } } } 输出为: singleton : com.dev.interview.Singleton @55d56113 singletonByReflect : com.dev.interview.Singleton @148080bb singleton == singletonByReflect : false |
如上,通过发射的方式即可获取到一个新的单例对象,这就破坏了单例。
1 2 3 4 5 | private Singleton() { if (singleton != null ) { throw new RuntimeException( "Singleton constructor is called... " ); } } |
这样,在通过反射调用构造方法的时候,就会抛出异常:
Caused by: java.lang.RuntimeException: Singleton constructor is called...
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 | public class SingletonTest { public static void main(String[] args) { Singleton singleton = Singleton.getSingleton(); //Write Obj to file ObjectOutputStream oos = null ; try { oos = new ObjectOutputStream( new FileOutputStream( "tempFile" )); oos.writeObject(singleton); //Read Obj from file File file = new File( "tempFile" ); ObjectInputStream ois = new ObjectInputStream( new FileInputStream(file)); Singleton singletonBySerialize = (Singleton)ois.readObject(); //判断是否是同一个对象 System.out.println( "singleton : " + singleton); System.out.println( "singletonBySerialize : " + singletonBySerialize); System.out.println( "singleton == singletonBySerialize : " + (singleton == singletonBySerialize)); } catch (Exception e) { e.printStackTrace(); } } } 输出结果如下: singleton : com.dev.interview.Singleton @617faa95 singletonBySerialize : com.dev.interview.Singleton @5d76b067 singleton == singletonBySerialize : false |
如上,通过先序列化再反序列化的方式,可获取到一个新的单例对象,这就破坏了单例。
1 2 3 | private Object readResolve() { return getSingleton(); } |
为什么增加readResolve就可以解决序列化破坏单例的问题了呢?
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 字符编码:从基础到乱码解决
· 提示词工程——AI应用必不可少的技术