草庐IT

单例模式也能玩出花

累成一条狗 2023-03-28 原文

一、单例模式

1、什么是单例模式

(1)单例模式

【单例模式(Singleton Pattern):】
定义:
    Ensure a class has only one instance, and provide a global point of access to it.
    直译:确保一个类只有一个实例,并提供对它的全局访问点(只允许通过全局访问点获取实例对象)。

 

(2)单例模式实现要点

一般情况下,访问类中某变量、方法:
    可以通过 new 进行对象实例化,再通过 "对象名.变量名"、"对象名.方法名" 的形式获取。
    可以通过 static 修饰(全局)变量、方法,再通过 "类名.变量名"、"类名.方法名" 的形式获取。可避免使用 new 进行对象实例化。

为了保证一个类只存在一个实例,应该保证其有且只有 一次 实例化 机会:
    应该保证其构造方法不能在该类以外的地方被调用(防止使用 new 进行对象实例化)。
    构造方法只能在该类中被调用一次。
注:
    对象实例化常见方式: new、序列化、克隆、反射。
    
基本实现要点:
    构造方法私有化(防止使用 new 进行对象实例化)。
    在类的内部进行一次实例化(构造方法只能在该类中被调用一次)。
    对外提供一个全局访问点(全局变量、全局方法等),可以通过 "类名.变量名" 或者 "类名.方法名" 的形式获取实例对象(避免使用 new 进行对象实例化)。
注:
    反射会破坏 构造方法的私有化,需要注意,后面会介绍。
    序列化、克隆 等操作可能会破坏单例模式。需要注意。

 

(3)使用场景
  当频繁创建、销毁某个对象时,可以考虑单例模式。
  当创建对象消耗资源过多时,但又经常使用时,可以考虑单例模式。

 

2、常见单例模式实现方式

(1)实现方式

【饿汉式:】
    静态变量
    静态代码块
    枚举(推荐)

【懒汉式:】
    静态方法
    synchronized 同步方法
    synchronized 同步代码块
    双重检查
    静态内部类

 

(2)饿汉式、懒汉式 区别

【基本区别:】
    懒汉式 在需要使用对象的时候才进行实例化操作。
    饿汉式 在类加载时完成实例化操作,可能暂时还不用该对象(占用内存)。

【饿汉式:】
核心:
    饿汉式借助 JVM 的类加载机制,在 类加载的初始化阶段 完成 实例化操作。
    类初始化阶段 只会执行一次,从而保证实例的唯一性 以及 线程安全。
    当类被主动使用时,才会导致类的初始化。而被动使用时,不会导致类的初始化。
主动使用类的方式:
    类的 main 方法被调用时。
    执行 new 实例化操作时。
    访问静态变量、静态方法时。
    实例化子类时(先触发父类初始化)。
    反射调用某类时。
JVM 类加载过程可参考: https://www.cnblogs.com/l-y-h/p/13496969.html#_label1_5

【懒汉式:】
核心:
    在需要使用对象的时候才进行实例化操作。
    多线程环境下,多个线程可能同时使用对象,需要考虑线程安全问题,防止并发访问生成多个实例。

 

二、饿汉式

1、实现

(1)基本说明

【核心思路:】
    使用 static 关键字,借助类加载过程,进行实例的初始化。
    使用 private 修饰 构造方法,保证构造方法私有化。
    提供一个全局访问点(类名.变量名 或者 类名.方法名)获取对象。    
 
【可用方式:】
    静态变量
    静态方法
    静态代码块

【优点:】
    在类加载的初始化阶段完成了实例化,仅加载一次。保证对象的唯一性 以及 线程安全。
    
【缺点:】
    在类加载的初始化阶段完成了实例化,没有实现懒加载(Lazy Loading),可能造成内存的浪费(在不需要使用的时候被创建)。

 

(2)代码实现(静态变量)
  public 修饰变量,直接通过 "类名.变量名" 的方式获取对象。

class HungrySingleton {
    // 提供一个全局访问点,通过 "类名.变量名" 访问
    public static HungrySingleton singleton = new HungrySingleton();

    // 构造器私有化(防止通过new创建实例对象)
    private HungrySingleton() {
    }
}

public class Test {
    public static void main(String[] args) {
        HungrySingleton singleton = HungrySingleton.singleton;
        HungrySingleton singleton2 = HungrySingleton.singleton;
        System.out.println(singleton == singleton2);  // true,为同一个对象
    }
}

 

(3)代码实现(静态方法)
  private 修饰变量,不允许通过 "类名.变量名" 的形式访问。
  public 修饰方法,通过 "类名.方法名" 的方式获取对象。

class HungrySingleton {
    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static HungrySingleton singleton = new HungrySingleton();

    // 构造器私有化(防止通过new创建实例对象)
    private HungrySingleton() {
    }

    // 提供一个全局访问点,通过 "类名.变量名" 访问
    public static HungrySingleton getInstance() {
        return singleton;
    }
}

public class Test {
    public static void main(String[] args) {
        HungrySingleton singleton = HungrySingleton.getInstance();
        HungrySingleton singleton2 = HungrySingleton.getInstance();
        System.out.println(singleton == singleton2);  // true,为同一个对象
    }
}

 

(4)代码实现(静态代码块)
  静态代码块,只是将实例化操作 移动到 静态代码块中进行实现。

class HungrySingleton {
    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static HungrySingleton singleton;

    // 在 static 代码块中进行实例化,同样在 类加载初始化阶段 执行
    static {
        singleton = new HungrySingleton();
    }

    // 构造器私有化(防止通过new创建实例对象)
    private HungrySingleton() {
    }

    // 提供一个全局访问点,通过 "类名.变量名" 访问
    public static HungrySingleton getInstance() {
        return singleton;
    }
}

public class Test {
    public static void main(String[] args) {
        HungrySingleton singleton = HungrySingleton.getInstance();
        HungrySingleton singleton2 = HungrySingleton.getInstance();
        System.out.println(singleton == singleton2);  // true,为同一个对象
    }
}

 

(5)这就完了吗?
  当然不是了,这样写只是防止了通过 new 实例化对象。
  对象实例化的方式还有 反射、序列化、克隆 等操作。
  这些操作是否会破坏单例模式?需要思考一下。

 

2、反射破坏

(1)类主动使用时,才会进行类的初始化
  类只有主动使用时,才会进行初始化操作。并不一定使用到类,就会触发初始化操作。
比如:
  进行反射获取私有构造方法时,并不会触发 类加载过程。
  如下代码执行后,静态代码块中的 "start..." 不会输出。

import java.lang.reflect.Constructor;

class HungrySingleton {
    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static HungrySingleton singleton;

    // 在 static 代码块中进行实例化,同样在 类加载初始化阶段 执行
    static {
        System.out.println("start...");
        singleton = new HungrySingleton();
    }

    // 构造器私有化(防止通过new创建实例对象)
    private HungrySingleton() {
    }

    // 提供一个全局访问点,通过 "类名.变量名" 访问
    public static HungrySingleton getInstance() {
        return singleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class;
        Constructor<HungrySingleton> hungrySingletonConstructor = hungrySingletonClass.getDeclaredConstructor();
        hungrySingletonConstructor.setAccessible(true);
    }
}

 

(2)反射破坏
  如下代码所示,反射调用构造方法时,会进行类加载过程(输出 "start..." ),然后构建一个实例。
  此时的实例对象是通过 构造方法重新创建的对象。与类加载过程中创建的对象不同。
  即 反射对 单例模式造成了破坏。

import java.lang.reflect.Constructor;

class HungrySingleton {
    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static HungrySingleton singleton;

    // 在 static 代码块中进行实例化,同样在 类加载初始化阶段 执行
    static {
        System.out.println("start...");
        singleton = new HungrySingleton();
    }

    // 构造器私有化(防止通过new创建实例对象)
    private HungrySingleton() {
    }

    // 提供一个全局访问点,通过 "类名.变量名" 访问
    public static HungrySingleton getInstance() {
        return singleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class;
        Constructor<HungrySingleton> hungrySingletonConstructor = hungrySingletonClass.getDeclaredConstructor();
        hungrySingletonConstructor.setAccessible(true); // 此时,还不会触发 类加载过程
        HungrySingleton singleton = hungrySingletonConstructor.newInstance(); // 此时,触发 类加载过程,并创建一个实例
        HungrySingleton singleton2 = HungrySingleton.getInstance();
        System.out.println(singleton == singleton2);  // false,不为同一个对象
     }
}

 

(3)防止反射破坏(未必会生效)
  在构造方法中,判断实例是否已经被创建。
  类初始化过程中,会创建一个实例。即使通过反射调用构造方法,也会在实例创建之后再去调用,所以在 构造方法中进行判断,实例存在则会抛出异常。从而防止反射破坏(未必会生效,后续序列化破坏中有提到)。

import java.lang.reflect.Constructor;

class HungrySingleton {
    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static HungrySingleton singleton;

    // 在 static 代码块中进行实例化,同样在 类加载初始化阶段 执行
    static {
        System.out.println("start...");
        singleton = new HungrySingleton();
    }

    // 构造器私有化(防止通过new创建实例对象)
    private HungrySingleton() {
        if (singleton != null) {
            throw new RuntimeException("实例已存在,不允许重复创建");
        }
    }

    // 提供一个全局访问点,通过 "类名.变量名" 访问
    public static HungrySingleton getInstance() {
        return singleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class;
        Constructor<HungrySingleton> hungrySingletonConstructor = hungrySingletonClass.getDeclaredConstructor();
        hungrySingletonConstructor.setAccessible(true);
        HungrySingleton singleton = hungrySingletonConstructor.newInstance();
        HungrySingleton singleton2 = HungrySingleton.getInstance();
        System.out.println(singleton == singleton2);  // true,为同一个对象
    }
}

 

3、反序列化破坏

(1)序列化、反序列化
  序列化对象,并再次读取对象时(反序列化),会创建一个新的对象。
注:
  序列化就是把实体对象状态按照一定的格式写入到有序字节流。
  反序列化就是从有序字节流重建对象,恢复对象状态。

【反序列化核心代码:】
ObjectInputStream 中的 readOrdinaryObject() 方法

private Object readOrdinaryObject(boolean unshared) throws IOException {
    Object obj;
    try {
        obj = desc.isInstantiable() ? desc.newInstance() : null;
    } catch (Exception ex) {
        throw (IOException) new InvalidClassException(desc.forClass().getName(), "unable to create instance").initCause(ex);
    }

    if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
        Object rep = desc.invokeReadResolve(obj);
        if (rep != obj) {
            handles.setObject(passHandle, obj = rep);
        }
    }
    return obj;
}

【关注点一:(调用构造函数实例化)】
desc.isInstantiable()
    如果一个 serializable/externalizable 的类可以在运行时被实例化,那么该方法就返回true。

desc.newInstance()
    通过反射调用无参构造创建一个对象。
注:
    此处调用的无参构造,与类本身的无参构造方法有差别。
    从实际效果上看,此处仅触发了类加载,并未触发类的构造函数。与前面提到的反射有区别。
    没有深入研究,有兴趣的可以帮忙解答一下。

【关注点二:(自定义对象生成策略)】
desc.hasReadResolveMethod()
    如果一个 serializable/externalizable 接口的类中包含 readResolve() 方法,则返回 true。

desc.invokeReadResolve(obj)
    通过反射的方式调用要被反序列化的类的 readResolve() 方法。

handles.setObject(passHandle, obj = rep)
    如果 readResolve() 返回的实例与构造方法创建的不同,则以 readResolve() 方法创建的实例为准。

 

(2)反序列化破坏
  如下代码所示,通过反序列化创建了个对象。
  从实际代码执行结果看,反序列化仅触发了类加载过程(此时调用了构造函数),反序列化中 newInstance() 未主动触发类的构造函数,所以此处构造方法中的判断 无法防止 反序列化中反射的行为。
  即 反序列化对 单例模式造成了破坏。

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;

class HungrySingleton implements Serializable {
    private static final long serialVersionUID = 42L;

    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static HungrySingleton singleton;

    static {
        System.out.println("start...");  // start...
        singleton = new HungrySingleton();
    }

    // 构造器私有化(防止通过new创建实例对象)
    private HungrySingleton() {
        if (singleton != null) {
            throw new RuntimeException("实例已存在,不允许重复创建");
        }
    }

    // 提供一个全局访问点,通过 "类名.变量名" 访问
    public static HungrySingleton getInstance() {
        return singleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
//        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test"));
//        oos.writeObject(HungrySingleton.getInstance());

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test"));
        HungrySingleton singleton = (HungrySingleton) ois.readObject();

        HungrySingleton singleton2 = HungrySingleton.getInstance();
        System.out.println(singleton == singleton2); // false,不为同一对象
    }
}

 

(3)防止反序列化破坏
  通过 readResolve() 可以返回一个实例对象,保证此对象为类加载过程中创建的实例对象,即可防止 反序列化破坏。

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;

class HungrySingleton implements Serializable {
    private static final long serialVersionUID = 42L;

    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static HungrySingleton singleton;

    static {
        System.out.println("start...");  // start...
        singleton = new HungrySingleton();
    }

    // 构造器私有化(防止通过new创建实例对象)
    private HungrySingleton() {
        if (singleton != null) {
            throw new RuntimeException("实例已存在,不允许重复创建");
        }
    }

    // 提供一个全局访问点,通过 "类名.变量名" 访问
    public static HungrySingleton getInstance() {
        return singleton;
    }

    // 定义 readResolve() 方法,返回类加载过程中创建的实例对象(反序列化时返回此对象)
    private Object readResolve() {
        return singleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
//        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test"));
//        oos.writeObject(HungrySingleton.getInstance());

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test"));
        HungrySingleton singleton = (HungrySingleton) ois.readObject();

        HungrySingleton singleton2 = HungrySingleton.getInstance();
        System.out.println(singleton == singleton2); // true,为同一对象
    }
}

 

4、克隆破坏

(1)克隆破坏
  如下代码所示,通过克隆创建了个对象。
  调用了 Object 的 clone 方法(native 方法),与反序列化类似,也没有触发 类的构造方法(应该是直接从内存中 copy 了一份)。创建了一个新的对象。
  即 克隆对 单例模式造成了破坏。

class HungrySingleton implements Cloneable {
    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static HungrySingleton singleton;

    static {
        System.out.println("start...");  // start...
        singleton = new HungrySingleton();
    }

    // 构造器私有化(防止通过new创建实例对象)
    private HungrySingleton() {
        if (singleton != null) {
            throw new RuntimeException("实例已存在,不允许重复创建");
        }
    }

    // 提供一个全局访问点,通过 "类名.变量名" 访问
    public static HungrySingleton getInstance() {
        return singleton;
    }

    // 重写 clone() 方法,返回 clone 对象
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        HungrySingleton singleton = HungrySingleton.getInstance();
        HungrySingleton singleton2 = (HungrySingleton) singleton.clone();
        System.out.println(singleton == singleton2); // false,不是同一对象
    }
}

 

(2)防止克隆破坏
  保证 clone() 方法返回的对象为类加载过程中创建的实例对象,即可防止 克隆破坏。

class HungrySingleton implements Cloneable {
    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static HungrySingleton singleton;

    static {
        System.out.println("start...");  // start...
        singleton = new HungrySingleton();
    }

    // 构造器私有化(防止通过new创建实例对象)
    private HungrySingleton() {
        if (singleton != null) {
            throw new RuntimeException("实例已存在,不允许重复创建");
        }
    }

    // 提供一个全局访问点,通过 "类名.变量名" 访问
    public static HungrySingleton getInstance() {
        return singleton;
    }

    // 重写 clone() 方法,返回 clone 对象
    @Override
    public Object clone() throws CloneNotSupportedException {
        return singleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        HungrySingleton singleton = HungrySingleton.getInstance();
        HungrySingleton singleton2 = (HungrySingleton) singleton.clone();
        System.out.println(singleton == singleton2); // true,是同一对象
    }
}

 

5、枚举

(1)基本说明

【基本说明:】
    写个简单的 enum 类,然后反编译一下 javap -c xx.class。
    可以看到底层就类似于 饿汉式 静态代码块 的写法。在类加载的初始化阶段完成实例化操作。

【优点:】
    在类加载的初始化阶段完成了实例化,仅加载一次。保证对象的唯一性 以及 线程安全。
    可以防止 克隆、反序列化、反射 破坏单例模式。
    写法简单。

 

(2)反编译一下 enum 类

【EnumSingleton】
enum EnumSingleton {
    INSTANCE;
}

【javap -c EnumSingleton.classfinal class pattern.sington.EnumSingleton extends java.lang.Enum<pattern.sington.EnumSingleton> {
  public static final pattern.sington.EnumSingleton INSTANCE;

  public static pattern.sington.EnumSingleton[] values();
    Code:
       0: getstatic     #1                  // Field $VALUES:[Lpattern/sington/EnumSingleton;
       3: invokevirtual #2                  // Method "[Lpattern/sington/EnumSingleton;".clone:()Ljava/lang/Object;
       6: checkcast     #3                  // class "[Lpattern/sington/EnumSingleton;"
       9: areturn

  public static pattern.sington.EnumSingleton valueOf(java.lang.String);
    Code:
       0: ldc           #4                  // class pattern/sington/EnumSingleton
       2: aload_0
       3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
       6: checkcast     #4                  // class pattern/sington/EnumSingleton
       9: areturn

  static {};
    Code:
       0: new           #4                  // class pattern/sington/EnumSingleton
       3: dup
       4: ldc           #7                  // String INSTANCE
       6: iconst_0
       7: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
      10: putstatic     #9                  // Field INSTANCE:Lpattern/sington/EnumSingleton;
      13: iconst_1
      14: anewarray     #4                  // class pattern/sington/EnumSingleton
      17: dup
      18: iconst_0
      19: getstatic     #9                  // Field INSTANCE:Lpattern/sington/EnumSingleton;
      22: aastore
      23: putstatic     #1                  // Field $VALUES:[Lpattern/sington/EnumSingleton;
      26: return
}

【等价于:】
public final class EnumSingleton extends Enum< EnumSingleton> {
    public static final EnumSingleton INSTANCE;
    public static EnumSingleton[] values();
    public static EnumSingleton valueOf(String s);
    static {
        INSTANCE = new EnumSingleton(name, ordinal);
    };
}

 

(3)防止反射破坏
  枚举类型的类,没有无参构造。默认继承 Enum 的有参构造。

【代码实现:】
import java.lang.reflect.Constructor;

enum EnumSingleton {
    INSTANCE;
}

public class Test {
    public static void main(String[] args) throws Exception {
        Class<EnumSingleton> enumSingletonClass = EnumSingleton.class;
        Constructor<EnumSingleton> enumSingletonConstructor = enumSingletonClass.getDeclaredConstructor(String.class, int.class);
        enumSingletonConstructor.setAccessible(true);
        // newInstance 会出现异常,java.lang.IllegalArgumentException: Cannot reflectively create enum objects
        EnumSingleton enumSingleton = enumSingletonConstructor.newInstance();  

        EnumSingleton enumSingleton2 = EnumSingleton.INSTANCE;

        System.out.println(enumSingleton == enumSingleton2);
    }
}

【原因分析:】
newInstance() 方法中进行判断,若为枚举类型,则抛异常。

@CallerSensitive
public T newInstance(Object ... initargs) throws IllegalArgumentException
{
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
}

 

(4)防止克隆破坏
  枚举类型的类,无法重写 clone() 方法。其父类 Enum 中定义 clone() 方法为 final 类型,不能被子类重写。

protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
}

 

(5)防止序列化破坏
  序列化返回的是同一个对象,无需定义 readResolve() 方法。其执行的是另一个逻辑。

【代码实现:】
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

enum EnumSingleton {
    INSTANCE;
}

public class Test {
    public static void main(String[] args) throws Exception {
        EnumSingleton enumSingleton = EnumSingleton.INSTANCE;

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test"));
        oos.writeObject(enumSingleton);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test"));
        EnumSingleton enumSingleton2 = (EnumSingleton) ois.readObject();

        System.out.println(enumSingleton == enumSingleton2); // true,是同一个对象
    }
}

【反序列化核心代码:】
ObjectInputStream 中的 readEnum() 方法。
读入并返回枚举常量,如果枚举类型不可解析,则返回 nullprivate Enum<?> readEnum(boolean unshared) throws IOException {

    ObjectStreamClass desc = readClassDesc(false);

    int enumHandle = handles.assign(unshared ? unsharedMarker : null);
    
    String name = readString(false);
    Enum<?> result = null;
    Class<?> cl = desc.forClass();
    if (cl != null) {
        try {
            @SuppressWarnings("unchecked")
            Enum<?> en = Enum.valueOf((Class)cl, name);
            result = en;
        } catch (IllegalArgumentException ex) {
            throw (IOException) new InvalidObjectException(
                "enum constant " + name + " does not exist in " +
                cl).initCause(ex);
        }
        if (!unshared) {
            handles.setObject(enumHandle, result);
        }
    }

    handles.finish(enumHandle);
    passHandle = enumHandle;
    return result;
}

 

三、懒汉式

1、实现

(1)基本说明

【核心思路:】
    使用 private 修饰 构造方法,保证构造方法私有化。
    提供一个静态的公共方法,在调用该方法时,才去创建实例对象。(全局访问点,通过 "类名.方法名" 获取对象)。    
 
【可用方式:】
    静态方法
    synchronized 同步方法
    synchronized 同步代码块
    双重检查
    静态内部类

【优点:】
    懒加载,需要使用对象时才会去实例化操作,提高内存利用率。

【缺点:】
    多线程环境下,多个线程可能同时使用对象,需要考虑线程安全问题,防止并发访问生成多个实例。

 

(2)代码实现(静态方法)
  如下代码所示,只允许通过 "类名.方法名" 的方式获取对象。

class FullSingleton {
    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static FullSingleton fullSingleton;

    // 构造器私有化(防止通过new创建实例对象)
    private FullSingleton () {
    }

    // 提供一个全局访问点,通过 "类名.变量名" 访问
    public static FullSingleton getInstance() {
        if (fullSingleton == null) {
            fullSingleton = new FullSingleton();
        }
        return fullSingleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        FullSingleton fullSingleton = FullSingleton.getInstance();
        FullSingleton fullSingleton2 = FullSingleton.getInstance();
        System.out.println(fullSingleton == fullSingleton2); // true,是同一个对象
    }
}

 

(3)这就完了吗?
  当然不是了,这样写只是在单线程环境下正常执行。多线程操作下,会出现多个实例。
比如:
  线程 A 与线程 B 并发执行到 if (fullSingleton == null),此时两个线程的 fullSingleton 均为 null,则均会进入方法,执行 new 实例化操作,此时便会产生多个实例对象。
  如下代码所示,代码执行多次可以发现,两个线程输出的对象并不一致。
  此时单例模式被破坏,线程不安全。

class FullSingleton {
    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static FullSingleton fullSingleton;

    // 构造器私有化(防止通过new创建实例对象)
    private FullSingleton () {
    }

    // 提供一个全局访问点,当调用该方法时,才去检查并创建一个实例对象。通过 "类名.方法名" 访问
    public static FullSingleton getInstance() {
        if (fullSingleton == null) {
            fullSingleton = new FullSingleton();
        }
        return fullSingleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@f3f9f4b
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@26af6bb1
            }
        });

        thread.start();
        thread2.start();
    }
}

 

(4)代码实现(synchronized 同步方法)
  为了保证线程安全,可以使用 synchronized 关键字实现同步。
注:
  synchronized 保证同一个时刻,只有一个线程可以执行某个方法或者某个代码块。
  如下代码所示,在方法上添加一个 synchronized,代码执行多次可以发现,两个线程输出的对象始终一致。

class FullSingleton {
    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static FullSingleton fullSingleton;

    // 构造器私有化(防止通过new创建实例对象)
    private FullSingleton () {
    }

    // 提供一个全局访问点,当调用该方法时,才去检查并创建一个实例对象。通过 "类名.方法名" 访问
    public static synchronized FullSingleton getInstance() {
        if (fullSingleton == null) {
            fullSingleton = new FullSingleton();
        }
        return fullSingleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@40788638
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@40788638
            }
        });

        thread.start();
        thread2.start();
    }
}

 

(5)这就完了吗?
  当然不是了,虽然使用 synchronized 保证线程安全,但是这种方式锁粒度太大,可能会导致执行效率低。

(6)代码实现(synchronized 同步代码块)
  如下代码所示,为了缩小 synchronized 影响范围,可以在方法内部使用同步代码块的方式实现。

class FullSingleton {
    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static FullSingleton fullSingleton;

    // 构造器私有化(防止通过new创建实例对象)
    private FullSingleton () {
    }

    // 提供一个全局访问点,当调用该方法时,才去检查并创建一个实例对象。通过 "类名.方法名" 访问
    public static FullSingleton getInstance() {
        if (fullSingleton == null) {
            synchronized(FullSingleton.class) {
                fullSingleton = new FullSingleton();
            }
        }
        return fullSingleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@5eb39c2b
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@401a5cff
            }
        });

        thread.start();
        thread2.start();
    }
}

 

(7)这就完了吗?
  当然不是了,这样写又回到了 静态方法中 提到的 线程不安全的问题上了。
比如:
  线程 A 与线程 B 并发执行到 if (fullSingleton == null),此时两个线程的 fullSingleton 均为 null,则均会进入方法,遇到 synchronized,同步执行后,仍会执行 new 操作,产生多个实例对象。
  此时单例模式被破坏,线程不安全。双重检查可以解决这个问题。

 

2、双重检查

(1)代码实现
  如下代码所示,双重检查,在 synchronized 同步代码块 的基础上,再添加一个判断。

class FullSingleton {
    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static FullSingleton fullSingleton;

    // 构造器私有化(防止通过new创建实例对象)
    private FullSingleton () {
    }

    // 提供一个全局访问点,当调用该方法时,才去检查并创建一个实例对象。通过 "类名.方法名" 访问
    public static FullSingleton getInstance() {
        if (fullSingleton == null) {
            synchronized(FullSingleton.class) {
                if (fullSingleton == null) {
                    fullSingleton = new FullSingleton();
                }
            }
        }
        return fullSingleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance());
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance());
            }
        });

        thread.start();
        thread2.start();
    }
}

 

(2)这就完了吗?
  当然不是了。这样写看上去是保证了线程安全,但是有个细节需要思考一下(指令重排)。
  如下所示,反编译一下代码,可以看到实例化操作的相关指令。

【Test.java】
public class Test {
    public static void main(String[] args) {
        Test test = new Test();
    }
}

【javap -c Test.classpublic class pattern.sington.Test {
  public pattern.sington.Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class pattern/sington/Test
       3: dup
       4: invokespecial #3                  // Method "<init>":()V
       7: astore_1
       8: return
}

【关注 main 函数:】
    new 指令在 堆内存中为 Test 对象分配内存空间。
    invokespecial 指令,执行实例初始化操作。
    astore_1 指令,将栈顶引用类型值存入变量(即 使对象指向 堆内存空间)。
即分为三步:
    1、分配内存空间。
    2、实例初始化
    3、实例指向内存空间
注:
    按照常理说,1、2、3 是按照顺序执行的。
    但是 JVM 会根据处理器特性,对指令进行优化(指令重排序),从而提高性能。
    指令重排,意味着指令可能不会按照指定顺序执行。
    
【回到上例的 双重检查的代码:】
    发生指令重排,new 实例化操作按照 1、3、2 的顺序执行。
假设线程 A 执行完 1、3,但 2 还未执行完,即对象已指向内存空间,但是还没有初始化。
此时线程 B 执行 getInstance() 代码,由于对象已指向内存空间,判断对象是否为 null 时返回 false, 跳过 synchronized 代码块。
此时线程 B 拿到的实例对象,由于初始化并未完成,使用对象将可能出现错误(引用逃逸)。
注: synchronized 并非原子性操作,可能发生指令重排。 使用 voliate 可以通过 内存屏障 禁止指令重排序。

 

(3)代码实现(voliate )
  使用 voliate 修饰 变量,禁止指令重排序。

class FullSingleton {
    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    // volatile 防止指令重排
    private static volatile FullSingleton fullSingleton;

    // 构造器私有化(防止通过new创建实例对象)
    private FullSingleton () {
    }

    // 提供一个全局访问点,当调用该方法时,才去检查并创建一个实例对象。通过 "类名.方法名" 访问
    public static FullSingleton getInstance() {
        if (fullSingleton == null) {
            synchronized(FullSingleton.class) {
                if (fullSingleton == null) {
                    fullSingleton = new FullSingleton();
                }
            }
        }
        return fullSingleton;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@79af4c1c
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@79af4c1c
            }
        });

        thread.start();
        thread2.start();
    }
}

 

3、静态内部类

(1)基本说明
  静态内部类是一种结合了 饿汉模式、懒汉模式 优点的实现方式。

【核心思路:】
    使用 private 修饰 构造方法,保证构造方法私有化。
    在类的内部定义一个静态内部类(只有被调用时,才会被加载),并在内部类中实例化对象。
    提供一个静态的公共方法,在调用该方法时,调用静态内部类。(全局访问点,通过 "类名.方法名" 获取对象)。    
 
【优点:】
    定义内部类,只有在用到的时候才回去加载,实现懒加载。
    使用 static 定义内部类,利用 JVM 类加载机制保证 线程安全。

 

(2)代码实现
  如下代码所示,定义一个静态内部类。

class FullSingleton {
    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static FullSingleton fullSingleton;

    // 构造器私有化(防止通过new创建实例对象)
    private FullSingleton () {
    }

    // 提供一个全局访问点,当调用该方法时,才去调用静态内部类。通过 "类名.方法名" 访问
    public static FullSingleton getInstance() {
        return InnerInstance.INSTANCE;
    }

    // 定义静态内部类
    private static class InnerInstance {
        public static final FullSingleton INSTANCE = new FullSingleton();
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@2a75ae17
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(FullSingleton.getInstance()); // pattern.sington.FullSingleton@2a75ae17
            }
        });

        thread.start();
        thread2.start();
    }
}

 

(3)这就完了吗?
  当然不是了。反序列化破坏、反射破坏、克隆破坏 的问题同样存在。
  解决方式与 饿汉模式的解决方式类似。

(4)防止序列化破坏
  重写 readResolve() 方法,返回实例对象。

import java.io.*;

class FullSingleton implements Serializable {
    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static FullSingleton fullSingleton;

    // 构造器私有化(防止通过new创建实例对象)
    private FullSingleton () {
    }

    // 提供一个全局访问点,当调用该方法时,才去调用静态内部类。通过 "类名.方法名" 访问
    public static FullSingleton getInstance() {
        return InnerInstance.INSTANCE;
    }

    // 定义静态内部类
    private static class InnerInstance {
        public static final FullSingleton INSTANCE = new FullSingleton();
    }

    private Object readResolve() {
        return getInstance();
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        FullSingleton fullSingleton = FullSingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test"));
        oos.writeObject(fullSingleton);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test"));
        FullSingleton fullSingleton2 = (FullSingleton) ois.readObject();
        System.out.println(fullSingleton == fullSingleton2);
    }
}

 

(5)防止克隆破坏
  重写 clone() 方法,返回实例对象。

class FullSingleton implements Cloneable {
    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static FullSingleton fullSingleton;

    // 构造器私有化(防止通过new创建实例对象)
    private FullSingleton () {
    }

    // 提供一个全局访问点,当调用该方法时,才去调用静态内部类。通过 "类名.方法名" 访问
    public static FullSingleton getInstance() {
        return InnerInstance.INSTANCE;
    }

    // 定义静态内部类
    private static class InnerInstance {
        public static final FullSingleton INSTANCE = new FullSingleton();
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
        return getInstance();
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        FullSingleton fullSingleton = FullSingleton.getInstance();
        FullSingleton fullSingleton2 = (FullSingleton) fullSingleton.clone();
        System.out.println(fullSingleton == fullSingleton2);
    }
}

 

(6)防止反射破坏
  在构造方法中,新增一个判断。

import java.lang.reflect.Constructor;

class FullSingleton {
    // 私有化变量,不可以通过 "类名.变量名" 的形式访问
    private static FullSingleton fullSingleton;

    // 构造器私有化(防止通过new创建实例对象)
    private FullSingleton () {
        if (getInstance() != null) {
            throw new RuntimeException("实例已存在,创建失败");
        }
    }

    // 提供一个全局访问点,当调用该方法时,才去调用静态内部类。通过 "类名.方法名" 访问
    public static FullSingleton getInstance() {
        return InnerInstance.INSTANCE;
    }

    // 定义静态内部类
    private static class InnerInstance {
        public static final FullSingleton INSTANCE = new FullSingleton();
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Class<FullSingleton> fullSingletonClass = FullSingleton.class;
        Constructor<FullSingleton> fullSingletonConstructor = fullSingletonClass.getDeclaredConstructor();
        fullSingletonConstructor.setAccessible(true);
        FullSingleton fullSingleton = fullSingletonConstructor.newInstance();

        FullSingleton fullSingleton2 = FullSingleton.getInstance();
        System.out.println(fullSingleton == fullSingleton2);
    }
}

 

四、举例

1、JDK中的单例模式举例(Runtime)

(1)部分源码
  如下代码所示,就是 饿汉模式的 实现。

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    public static Runtime getRuntime() {
        return currentRuntime;
    }
    private Runtime() {} 
}

 

有关单例模式也能玩出花的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  4. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  5. ruby-on-rails - environment.rb 中设置的常量在开发模式中消失 - 2

    了解Rails缓存如何工作的人可以真正帮助我。这是嵌套在Rails::Initializer.runblock中的代码:config.after_initializedoSomeClass.const_set'SOME_CONST','SOME_VAL'end现在,如果我运行script/server并发出请求,一切都很好。然而,在我的Rails应用程序的第二个请求中,一切都因单元化常量错误而变得糟糕。在生产模式下,我可以成功发出第二个请求,这意味着常量仍然存在。我已通过将以上内容更改为以下内容来解决问题:config.after_initializedorequire'some_cl

  6. Ruby:标准递归模式 - 2

    我经常迷上ruby​​的一件事是递归模式。例如,假设我有一个数组,它可能包含无限深度的数组作为元素。所以,例如:my_array=[1,[2,3,[4,5,[6,7]]]]我想创建一个方法,可以将数组展平为[1,2,3,4,5,6,7]。我知道.flatten可以完成这项工作,但这个问题是作为我经常遇到的递归问题的一个例子-因此我试图找到一个更可重用的解决方案。简而言之-我猜这种事情有一个标准模式,但我想不出任何特别优雅的东西。任何想法表示赞赏 最佳答案 递归是一种方法,它不依赖于语言。您在编写算法时要考虑两种情况:再次调用函数的情

  7. ruby - 在 Ruby 中查找多个正则表达式匹配的模式和位置 - 2

    这应该是一个简单的问题,但我找不到任何相关信息。给定一个Ruby中的正则表达式,对于每个匹配项,我需要检索匹配的模式$1、$2,但我还需要匹配位置。我知道=~运算符为我提供了第一个匹配项的位置,而string.scan(/regex/)为我提供了所有匹配模式。如果可能,我需要在同一步骤中获得两个结果。 最佳答案 MatchDatastring.scan(regex)do$1#Patternatfirstposition$2#Patternatsecondposition$~.offset(1)#Startingandendingpo

  8. ruby - sinatra 框架的 MVC 模式 - 2

    我想开始使用“Sinatra”框架进行编码,但我找不到该框架的“MVC”模式。是“MVC-Sinatra”模式或框架吗? 最佳答案 您可能想查看Padrino这是一个围绕Sinatra构建的框架,可为您的项目提供更“类似Rails”的感觉,但没有那么多隐藏的魔法。这是使用Sinatra可以做什么的一个很好的例子。虽然如果您需要开始使用这很好,但我个人建议您将它用作学习工具,以对您来说最有意义的方式使用Sinatra构建您自己的应用程序。写一些测试/期望,写一些代码,通过测试-重复:)至于ORM,你还应该结帐Sequel其中(imho

  9. ruby-on-rails - Rails 如何创建数据模式种子数据 - 2

    有没有一种方法可以自动生成种子数据文件并创建种子数据,就像您在下面链接中的Laravel中看到的那样?LaravelDatabaseMigrations&Seed我在另一个应用程序上看到在Rails的db文件夹下创建了一些带有时间戳的文件,其中包含种子数据。创建它的好方法是什么? 最佳答案 我建议你使用Fabrication的组合gem和Faker.Fabrication允许您编写一个模式来构建您的对象,而Faker为您提供虚假数据,如姓名、电子邮件、电话号码等。这是制造商的样子:Fabricator(:user)dousernam

  10. ruby-on-rails - Ruby on Rails 应用程序的只读模式 - 2

    我有一个交互式RubyonRails应用程序,我想在特定时间将其置于“只读模式”。这将允许用户读取他们需要的数据,但阻止他们执行写入数据库的操作。执行此操作的一种方法是在数据库中放置一个true/false变量,该变量在进行任何写入之前进行检查。我的问题。有没有更优雅的解决方案来解决这个问题? 最佳答案 如果你真的想阻止任何数据库写入,我能想到的最简单的方法是覆盖readonly?始终返回true的模型方法,无论是在选定模型中还是对于所有ActiveRecord模型。如果模型设置为只读(通常通过调用#readonly!来完成),任何

随机推荐