zoukankan      html  css  js  c++  java
  • 单例模式

    单例模式

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

    单例模式总结

    单例模式的主要优点

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

    单例模式的主要缺点

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

    适用场景

    • 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
    • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
  • 相关阅读:
    0802作业1替换文本文件内容

    看病
    爬山
    作业1
    超市(未完成)
    图片复制
    替换
    文件
    英文字母和中文汉字在不同字符集编码下的字节数
  • 原文地址:https://www.cnblogs.com/xiaojiesir/p/11065151.html
Copyright © 2011-2022 走看看