单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
介绍
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
应用实例:
- 1、一个班级只有一个班主任。
- 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
- 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
优点:
- 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 2、避免对资源的多重占用(比如写文件操作)。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
- 1、要求生产唯一序列号。
- 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
角色
Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的 getInstance()
工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例
类内部定义了一个 Singleton
类型的静态对象,作为外部共享的唯一实例。
1.饿汉式
顾名思义,饿汉式,就是使用类的时候不管用的是不是类中的单例部分,都直接创建出单例类,看一下饿汉式的写法:
1 public class EagerSingleton 2 { 3 private static EagerSingleton instance = new EagerSingleton(); 4 5 private EagerSingleton() 6 { 7 8 } 9 10 public static EagerSingleton getInstance() 11 { 12 return instance; 13 } 14 }
优点:简单,使用时没有延迟;在类装载时就完成实例化,天生的线程安全
缺点:没有懒加载,启动较慢;如果从始至终都没使用过这个实例,则会造成内存的浪费。
2.饿汉式变种
1 public class Singleton { 2 3 private static Singleton instance; 4 5 static { 6 instance = new Singleton(); 7 } 8 9 private Singleton() {} 10 11 public static Singleton getInstance() { 12 return instance; 13 } 14 }
将类实例化的过程放在了静态代码块中,在类装载的时执行静态代码块中的代码,初始化类的实例。优缺点同上。
3.懒汉式
同样,顾名思义,这个人比较懒,只有当单例类用到的时候才会去创建这个单例类,看一下懒汉式的写法:
1 public class LazySingleton 2 { 3 private static LazySingleton instance = null; 4 5 private LazySingleton() 6 { 7 8 } 9 10 public static LazySingleton getInstance() 11 { 12 if (instance == null) 13 instance = new LazySingleton(); 14 return instance; 15 } 16 }
优点:懒加载,启动速度快、如果从始至终都没使用过这个实例,则不会初始化该实力,可节约资源
缺点:多线程环境下线程不安全。if (singleton == null)
存在竞态条件,可能会有多个线程同时进入 if 语句
,导致产生多个实例
线程A初次调用getInstance()方法,代码走到第12行,线程此时切换到线程B,线程B走到12行,看到instance是null,就new了一个LazySingleton出来,这时切换回线程A,线程A继续走,也new了一个LazySingleton出来。这样,单例类LazySingleton在内存中就有两份引用了,这就违背了单例模式的本意了。
可能有人会想,CPU分的时间片再短也不至于getInstance()方法只执行一个判断就切换线程了吧?问题是,万一线程A调用LazySingleton.getInstance()之前已经执行过别的代码了呢,走到12行的时候刚好时间片到了,也是很正常的。
我们通过程序来验证这个问题:
1 public class LazySingleton { 2 3 private static LazySingleton lazySingleton; 4 5 private LazySingleton() { 6 } 7 8 public static LazySingleton getInstance() { 9 if (lazySingleton == null) { 10 11 try { 12 Thread.sleep(5000); // 模拟线程在这里发生阻塞 13 } catch (InterruptedException e) { 14 e.printStackTrace(); 15 } 16 17 lazySingleton = new LazySingleton(); 18 } 19 return lazySingleton; 20 } 21 }
测试类:
1 public class MainTest { 2 3 public static void main(String[] args) throws InterruptedException { 4 MyThread myThread = new MyThread(); 5 MyThread myThread1 = new MyThread(); 6 myThread.start(); 7 myThread1.start(); 8 } 9 10 public static class MyThread extends Thread { 11 12 public void run() { 13 LazySingleton layLazySingleton = LazySingleton.getInstance(); 14 System.out.println(layLazySingleton); 15 } 16 17 } 18 19 }
输出的结果如下:
1 com.boiin.testdemo.LazySingleton@297ffb 2 com.boiin.testdemo.LazySingleton@914f6a
从以上结果可以看出,输出两个实例并且实例的hashcode值不相同,证明了我们获得了两个不一样的实例。我们生成了两个线程同时访问getInstance()方法,在程序中我让线程睡眠了5秒,是为了模拟线程在此处发生阻塞,当第一个线程t1进入getInstance()方法,判断完singleton为null,接着进入if语句准备创建实例,同时在t1创建实例之前,另一个线程t2也进入getInstance()方法,此时判断singleton也为null,因此线程t2也会进入if语句准备创建实例,这样问题就来了,有两个线程都进入了if语句创建实例,这样就产生了两个实例。
4.懒汉式变种
1 // 线程安全,效率低 2 public class Singleton { 3 4 private static Singleton singleton; 5 6 private Singleton() {} 7 8 public static synchronized Singleton getInstance() { 9 if (singleton == null) { 10 singleton = new Singleton(); 11 } 12 return singleton; 13 } 14 }
优点:解决了上一种实现方式的线程不安全问题
缺点:synchronized 对整个 getInstance()
方法都进行了同步,每次只有一个线程能够进入该方法,并发性能极差
5.双重检查锁
既然懒汉式是非线程安全的,那就要改进它。最直接的想法是,给getInstance方法加锁不就好了,但是我们不需要给方法全部加锁啊,只需要给方法的一部分加锁就好了。基于这个考虑,引入了双检锁(Double Check Lock,简称DCL)的写法:
1 public class DoubleCheckLockSingleton 2 { 3 private static DoubleCheckLockSingleton instance = null; 4 5 private DoubleCheckLockSingleton() 6 { 7 8 } 9 10 public static DoubleCheckLockSingleton getInstance() 11 { 12 if (instance == null) 13 { 14 synchronized (DoubleCheckLockSingleton.class) 15 { 16 if (instance == null) 17 instance = new DoubleCheckLockSingleton(); 18 } 19 } 20 return instance; 21 } 22 }
优点:线程安全;延迟加载;效率较高。
线程A初次调用DoubleCheckLockSingleton.getInstance()方法,走12行,判断instance为null,进入同步代码块,此时线程切换到线程B,线程B调用DoubleCheckLockSingleton.getInstance()方法,由于同步代码块外面的代码还是异步执行的,所以线程B走12行,判断instance为null,等待锁。结果就是线程A实例化出了一个DoubleCheckLockSingleton,释放锁,线程B获得锁进入同步代码块,判断此时instance不为null了,并不实例化DoubleCheckLockSingleton。这样,单例类就保证了在内存中只存在一份。注意在同步块中,我们再次判断了instance是否为空,下面解释下为什么要这么做。假设我们去掉这个判断条件,有这样一种情况,当两个线程同时进入if语句,第一个线程A获得线程锁执行实例创建语句并返回一个实例,接着第二个线程B获得线程锁,如果这里没有实例是否为空的判断条件,B也会执行下面的语句返回另一个实例,这样就产生了多个实例。因此这里必须要判断实例是否为空,如果已经存在就直接返回,不会再去创建实例了。这种方式既保证了线程安全,也改善了程序的执行效率。
似乎解决了之前提到的问题,将synchronized关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在instance为null,并创建对象的时候才需要加锁,性能有一定的提升。但是,这样的情况,还是有可能有问题的,看下面的情况:在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例。这样就可能出错了,我们以A、B两个线程为例:
a>A、B线程同时进入了第一个if判断
b>A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton();
c>由于JVM内部的优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。
d>B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。
e>此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。
所以程序还是有可能发生错误,其实程序在运行过程是很复杂的,从这点我们就可以看出,尤其是在写多线程环境下的程序更有难度,有挑战性。我们对该程序做进一步优化:
1 public class Singleton { 2 // 注意:这里有 volatile 关键字修饰 3 private static volatile Singleton singleton; 4 5 private Singleton() {} 6 7 public static Singleton getInstance() { 8 if (singleton == null) { 9 synchronized (Singleton.class) { 10 if (singleton == null) { 11 singleton = new Singleton(); 12 } 13 } 14 } 15 return singleton; 16 } 17 }
volatile 关键字的作用:
- 保证了不同线程对这个变量进行操作时的可见性
- 禁止进行指令重排序
6.静态内部类
1 public class Singleton { 2 3 /* 私有构造方法,防止被实例化 */ 4 private Singleton() { 5 } 6 7 /* 此处使用一个内部类来维护单例 */ 8 private static class SingletonFactory { 9 private static Singleton instance = new Singleton(); 10 } 11 12 /* 获取实例 */ 13 public static Singleton getInstance() { 14 return SingletonFactory.instance; 15 } 16 17 /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */ 18 public Object readResolve() { 19 return getInstance(); 20 } 21 }
优点:避免了线程不安全,延迟加载,效率高。
实际情况是,单例模式使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证
instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。
其实说它完美,也不一定,如果在构造函数中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况,选择最适合自己应用场景的实现方法。
7.枚举
1 public enum Singleton { 2 INSTANCE; 3 public void whateverMethod() { 4 } 5 }
优点:通过JDK1.5中添加的枚举来实现单例模式,写法简单,且不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
单例模式的安全性
单例模式的目标是,任何时候该类都只有唯一的一个对象。但是上面我们写的大部分单例模式都存在漏洞,被攻击时会产生多个对象,破坏了单例模式。
序列化攻击
通过Java的序列化机制来攻击单例模式
1 public class HungrySingleton { 2 private static final HungrySingleton instance = new HungrySingleton(); 3 private HungrySingleton() { 4 } 5 public static HungrySingleton getInstance() { 6 return instance; 7 } 8 9 public static void main(String[] args) throws IOException, ClassNotFoundException { 10 HungrySingleton singleton = HungrySingleton.getInstance(); 11 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file")); 12 oos.writeObject(singleton); // 序列化 13 14 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file")); 15 HungrySingleton newSingleton = (HungrySingleton) ois.readObject(); // 反序列化 16 17 System.out.println(singleton); 18 System.out.println(newSingleton); 19 System.out.println(singleton == newSingleton); 20 } 21 }
结果
1 com.singleton.HungrySingleton@ed17bee 2 com.singleton.HungrySingleton@46f5f779 3 false
Java 序列化是如何攻击单例模式的呢?我们需要先复习一下Java的序列化机制
Java 序列化机制
java.io.ObjectOutputStream
是Java实现序列化的关键类,它可以将一个对象转换成二进制流,然后可以通过 ObjectInputStream
将二进制流还原成对象。具体的序列化过程不是本文的重点,在此仅列出几个要点。
Java 序列化机制的要点:
- 需要序列化的类必须实现
java.io.Serializable
接口,否则会抛出NotSerializableException
异常 - 若没有显示地声明一个
serialVersionUID
变量,Java序列化机制会根据编译时的class自动生成一个serialVersionUID
作为序列化版本比较(验证一致性),如果检测到反序列化后的类的serialVersionUID
和对象二进制流的serialVersionUID
不同,则会抛出异常 - Java的序列化会将一个类包含的引用中所有的成员变量保存下来(深度复制),所以里面的引用类型必须也要实现
java.io.Serializable
接口 - 当某个字段被声明为
transient
后,默认序列化机制就会忽略该字段,反序列化后自动获得0或者null值 - 静态成员不参与序列化
- 每个类可以实现
readObject
、writeObject
方法实现自己的序列化策略,即使是transient
修饰的成员变量也可以手动调用ObjectOutputStream
的writeInt
等方法将这个成员变量序列化。 - 任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例
-
每个类可以实现private Object readResolve()方法,在调用readObject方法之后,如果存在readResolve方法则自动调用该方法,readResolve将对readObject的结果进行处理,而最终readResolve的处理结果将作为readObject的结果返回。readResolve的目的是保护性恢复对象,其最重要的应用就是保护性恢复单例、枚举类型的对象
Serializable
接口是一个标记接口,可自动实现序列化,而Externalizable
继承自Serializable
,它强制必须手动实现序列化和反序列化算法,相对来说更加高效
序列化破坏单例模式的解决方案
根据上面对Java序列化机制的复习,我们可以自定义一个 readResolve
,在其中返回类的单例对象,替换掉 readObject
方法反序列化生成的对象,让我们自己写的单例模式实现保护性恢复对象
1 public class HungrySingleton implements Serializable { 2 private static final HungrySingleton instance = new HungrySingleton(); 3 private HungrySingleton() { 4 } 5 public static HungrySingleton getInstance() { 6 return instance; 7 } 8 9 private Object readResolve() { 10 return instance; 11 } 12 13 public static void main(String[] args) throws IOException, ClassNotFoundException { 14 HungrySingleton singleton = HungrySingleton.getInstance(); 15 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file")); 16 HungrySingleton newSingleton = (HungrySingleton) ois.readObject(); 17 18 System.out.println(singleton); 19 System.out.println(newSingleton); 20 System.out.println(singleton == newSingleton); 21 } 22 }
结果
1 com.singleton.HungrySingleton@24273305 2 com.singleton.HungrySingleton@24273305 3 true
注意:自己实现的单例模式都需要避免被序列化破坏
反射攻击
在单例模式中,构造器都是私有的,而反射可以通过构造器对象调用 setAccessible(true)
来获得权限,这样就可以创建多个对象,来破坏单例模式了
1 public class HungrySingleton { 2 private static final HungrySingleton instance = new HungrySingleton(); 3 4 private HungrySingleton() { 5 } 6 7 public static HungrySingleton getInstance() { 8 return instance; 9 } 10 11 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 12 HungrySingleton instance = HungrySingleton.getInstance(); 13 Constructor constructor = HungrySingleton.class.getDeclaredConstructor(); 14 constructor.setAccessible(true); // 获得权限 15 HungrySingleton newInstance = (HungrySingleton) constructor.newInstance(); 16 17 System.out.println(instance); 18 System.out.println(newInstance); 19 System.out.println(instance == newInstance); 20 } 21 }
结果:
1 com.singleton.HungrySingleton@3b192d32 2 com.singleton.HungrySingleton@16f65612 3 false
反射攻击解决方案
反射是通过它的Class对象来调用构造器创建新的对象,我们只需要在构造器中检测并抛出异常就可以达到目的了
1 private HungrySingleton() { 2 // instance 不为空,说明单例对象已经存在 3 if (instance != null) { 4 throw new RuntimeException("单例模式禁止反射调用!"); 5 } 6 }
注意,上述方法针对饿汉式单例模式是有效的,但对懒汉式的单例模式是无效的,懒汉式的单例模式是无法避免反射攻击的!
为什么对饿汉有效,对懒汉无效?因为饿汉的初始化是在类加载的时候,反射一定是在饿汉初始化之后才能使用;而懒汉是在第一次调用 getInstance() 方法的时候才初始化,我们无法控制反射和懒汉初始化的
先后顺序,如果反射在前,不管反射创建了多少对象,instance都将一直为null,直到调用 getInstance()。
事实上,实现单例模式的唯一推荐方法,是使用枚举类来实现。
为什么推荐使用枚举单例
写下我们的枚举单例模式
1 package com.singleton; 2 3 import java.io.*; 4 import java.lang.reflect.Constructor; 5 import java.lang.reflect.InvocationTargetException; 6 7 public enum SerEnumSingleton implements Serializable { 8 INSTANCE; // 单例对象 9 private String content; 10 11 public String getContent() { 12 return content; 13 } 14 15 public void setContent(String content) { 16 this.content = content; 17 } 18 19 private SerEnumSingleton() { 20 } 21 22 public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 23 SerEnumSingleton singleton1 = SerEnumSingleton.INSTANCE; 24 singleton1.setContent("枚举单例序列化"); 25 System.out.println("枚举序列化前读取其中的内容:" + singleton1.getContent()); 26 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj")); 27 oos.writeObject(singleton1); 28 oos.flush(); 29 oos.close(); 30 31 FileInputStream fis = new FileInputStream("SerEnumSingleton.obj"); 32 ObjectInputStream ois = new ObjectInputStream(fis); 33 SerEnumSingleton singleton2 = (SerEnumSingleton) ois.readObject(); 34 ois.close(); 35 System.out.println(singleton1 + "\n" + singleton2); 36 System.out.println("枚举序列化后读取其中的内容:" + singleton2.getContent()); 37 System.out.println("枚举序列化前后两个是否同一个:" + (singleton1 == singleton2)); 38 39 Constructor<SerEnumSingleton> constructor = SerEnumSingleton.class.getDeclaredConstructor(); 40 constructor.setAccessible(true); 41 SerEnumSingleton singleton3 = constructor.newInstance(); // 通过反射创建对象 42 System.out.println("反射后读取其中的内容:" + singleton3.getContent()); 43 System.out.println("反射前后两个是否同一个:" + (singleton1 == singleton3)); 44 } 45 }
运行结果,序列化前后的对象是同一个对象,而反射的时候抛出了异常
1 枚举序列化前读取其中的内容:枚举单例序列化 2 INSTANCE 3 INSTANCE 4 枚举序列化后读取其中的内容:枚举单例序列化 5 枚举序列化前后两个是否同一个:true 6 Exception in thread "main" java.lang.NoSuchMethodException: com.singleton.SerEnumSingleton.<init>() 7 at java.lang.Class.getConstructor0(Class.java:3082) 8 at java.lang.Class.getDeclaredConstructor(Class.java:2178) 9 at com.singleton.SerEnumSingleton.main(SerEnumSingleton.java:39)
当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。
那么,为什么推荐使用枚举单例呢?
1. 枚举单例写法简单
2. 线程安全&懒加载
代码中 INSTANCE 变量被 public static final
修饰,因为static类型的属性是在类加载之后初始化的,JVM可以保证线程安全;且Java类是在引用到的时候才进行类加载,所以枚举单例也有懒加载的效果。
3. 枚举自己能避免序列化攻击
为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制,因
此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 我们看一下Enum类的valueOf方法:
1 ublic static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { 2 T result = enumType.enumConstantDirectory().get(name); 3 if (result != null) 4 return result; 5 if (name == null) 6 throw new NullPointerException("Name is null"); 7 throw new IllegalArgumentException( 8 "No enum constant " + enumType.getCanonicalName() + "." + name); 9 }
从代码中可以看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()
方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的
enumConstantDirectory属性。所以,JVM对序列化有保证。
4. 枚举能够避免反射攻击,因为反射不支持创建枚举对象
Constructor
类的 newInstance
方法中会判断是否为 enum,若是会抛出异常
1 @CallerSensitive 2 public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { 3 if (!override) { 4 if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { 5 Class<?> caller = Reflection.getCallerClass(); 6 checkAccess(caller, clazz, null, modifiers); 7 } 8 } 9 // 不能为 ENUM,否则抛出异常:不能通过反射创建 enum 对象 10 if ((clazz.getModifiers() & Modifier.ENUM) != 0) 11 throw new IllegalArgumentException("Cannot reflectively create enum objects"); 12 ConstructorAccessor ca = constructorAccessor; // read volatile 13 if (ca == null) { 14 ca = acquireConstructorAccessor(); 15 } 16 @SuppressWarnings("unchecked") 17 T inst = (T) ca.newInstance(initargs); 18 return inst; 19 }
单例模式在Java中的应用及解读
Runtime是一个典型的例子,看下JDK API对于这个类的解释"每个Java应用程序都有一个Runtime类实例,使应用程序能够与其运行的环境相连接,可以通过getRuntime方法获取当前运行时。应用程序不能创建自己的Runtime类实例。",这段话,有两点很重要:
1、每个应用程序都有一个Runtime类实例
2、应用程序不能创建自己的Runtime类实例
只有一个、不能自己创建,是不是典型的单例模式?看一下,Runtime类的写法:
public class Runtime { private static Runtime currentRuntime = new Runtime(); /** * Returns the runtime object associated with the current Java application. * Most of the methods of class <code>Runtime</code> are instance * methods and must be invoked with respect to the current runtime object. * * @return the <code>Runtime</code> object associated with the current * Java application. */ public static Runtime getRuntime() { return currentRuntime; } /** Don't let anyone else instantiate this class */ private Runtime() {} ... }
后面的就不黏贴了,到这里已经足够了,看到Runtime使用getRuntime()方法并让构造方法私有保证程序中只有一个Runtime实例且Runtime实例不可以被用户创建。
spring AbstractFactoryBean
AbstractFactoryBean 类
1 public final T getObject() throws Exception { 2 if (this.isSingleton()) { 3 return this.initialized ? this.singletonInstance : this.getEarlySingletonInstance(); 4 } else { 5 return this.createInstance(); 6 } 7 } 8 9 private T getEarlySingletonInstance() throws Exception { 10 Class<?>[] ifcs = this.getEarlySingletonInterfaces(); 11 if (ifcs == null) { 12 throw new FactoryBeanNotInitializedException(this.getClass().getName() + " does not support circular references"); 13 } else { 14 if (this.earlySingletonInstance == null) { 15 // 通过代理创建对象 16 this.earlySingletonInstance = Proxy.newProxyInstance(this.beanClassLoader, ifcs, new AbstractFactoryBean.EarlySingletonInvocationHandler()); 17 } 18 return this.earlySingletonInstance; 19 } 20 }
Mybatis ErrorContext ThreadLocal
ErrorContext 类,通过 ThreadLocal 管理单例对象,一个线程一个ErrorContext对象,ThreadLocal可以保证线程安全
1 public class ErrorContext { 2 private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>(); 3 private ErrorContext() { 4 } 5 6 public static ErrorContext instance() { 7 ErrorContext context = LOCAL.get(); 8 if (context == null) { 9 context = new ErrorContext(); 10 LOCAL.set(context); 11 } 12 return context; 13 } 14 //... 15 }
单例模式总结
单例模式的主要优点
- 单例模式提供了对唯一实例的受控访问。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式可以提高系统的性能。
- 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。
单例模式的主要缺点
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了 “单一职责原则”。
- 如果实例化的共享对象长时间不被利用,系统可能会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
适用场景
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
来源:https://www.cnblogs.com/xiaojiesir/p/11065151.html