单例模式

我是研究僧i 提交于 2020-01-15 00:27:55

单例模式

单例模式(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值
  • 静态成员不参与序列化
  • 每个类可以实现readObjectwriteObject方法实现自己的序列化策略,即使是transient修饰的成员变量也可以手动调用ObjectOutputStreamwriteInt等方法将这个成员变量序列化。
  • 任何一个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 }

 

单例模式总结

单例模式的主要优点

  • 单例模式提供了对唯一实例的受控访问。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式可以提高系统的性能。
  • 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。

单例模式的主要缺点

  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了 “单一职责原则”。
  • 如果实例化的共享对象长时间不被利用,系统可能会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。

适用场景

  • 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
  • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!