单例模式
单例模式(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