设计模式之单例模式

之所以被称为单例模式,是因为整个程序有且仅有一个实例。该类负责创建自己的对象,同时确保只有一个对象被创建。具有以下特点

  1. 类构造器私有
  2. 持有自己类型的属性
  3. 对外提供获取实例的静态方法

静态内部类

当外部类Holder被加载时,内部类不会被加载。只有第一次调用getInstance方法时,虚拟机才加载 Inner 并初始化instance ,其唯一性和线程安全性都由JVM保证。

package single;

public class Holder {
    private Holder() {

    }
    
    public static Holder getInstance() {
        return InnerClass.HOLDER;
    }

    public static class InnerClass {
        private final static Holder HOLDER = new Holder();
    }
}

饿汉式

在类加载的同时创建好一个静态的对象供系统使用,其唯一性和线程安全性都由JVM保证。不过这种方法可能占用过多内存。

package single;

public class Hungry {
    private Hungry() {
    }

    private final static Hungry HUNGRY = new Hungry(); // 在类加载是被创建,所以只有一个

    public static Hungry getHungry() {
        return HUNGRY;
    }
}

懒汉式

需要时再去创建对象

package single;

public class Lazy {
    private Lazy() {

    }

    private static Lazy lazy;

    public static Lazy getInstance() {
        if (lazy == null) { // 判断没有创建过时在创建,所以只有一个(单线程下)
            lazy = new Lazy();
        }
        return lazy;
    }

}

这个程序在多线程下很有可能产生问题,执行如下程序则可发现对象被创建了多次

package single;

public class Lazy {
    private Lazy() {
        // 如果输出则说明当前线程调用了构造方法,即new了一个对象
        System.out.println(Thread.currentThread().getName() + "finish");
    }

    private static Lazy lazy;

    public static Lazy getInstance() {
        if (lazy == null) {
            lazy = new Lazy();
        }
        return lazy;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Lazy.getInstance();
            }).start();
        }
    }
}

/* 输出
Thread-0finish
Thread-2finish
Thread-1finish
*/

双重检测锁模式 (DCL懒汉)

package single;

public class Lazy {
    private Lazy() {
        // 如果输出则说明当前线程调用了构造方法,即new了一个对象
        System.out.println(Thread.currentThread().getName() + "finish");
    }

    private static Lazy lazy;

    public static Lazy getInstance() {
        if (lazy == null) {
            synchronized (Lazy.class) {
                if (lazy == null) {
                    lazy = new Lazy();
                }
            }
        }
        return lazy;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Lazy.getInstance();
            }).start();
        }
    }
}

这个程序在并发下仍然可能存在问题,因为new Lazy()并不是一个原子操作(对象加载)

  1. 分配内存空间并初始化零值
  2. 初始化
  3. 将对象指向这个空间(不属于对象加载)
    如果按照我们期望的1 -> 2 -> 3这个顺序的话,是不会有问题的,但是如果发生指令重排...
    假设有一线程A,其执行顺序是 1 -> 3 -> 2;此时又进来一个线程B,当B判断if (lazy == null)时,结果将会是false,于是它就会直接返回lazy。如果你不理解为什么为false,可以想一下:线程A先分配了内存空间,之后又把对象指向这个空间,也就是说,现在对象只是被初始化零值但是并没有初始化,但是注意,这时lazy已经不为空了!所以B线程进入的时候会以为lazy已经被创建好,所以直接返回。

要解决这个问题,需使用volatile修饰lazy实例变量

package single;

public class Lazy {
    private Lazy() {
        // 如果输出则说明当前线程调用了构造方法,即new了一个对象
        System.out.println(Thread.currentThread().getName() + "finish");
    }

    private volatile static Lazy lazy;

    public static Lazy getInstance() {
        if (lazy == null) {
            synchronized (Lazy.class) {
                if (lazy == null) {
                    lazy = new Lazy();
                }
            }
        }
        return lazy;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                Lazy.getInstance();
            }).start();
        }
    }
}

但是这个方法依旧是不安全的,使用反射可以对其进行破坏。
简单解释一下为什么反射可以破坏这个单例:正常的通过getInstance返回的对象是lazy = new Lazy()出来的,那么显然,new出来的对象被指向了lazy也就是指向了一块内存空间,那么此时lazy不为空;
反观通过反射创建的对象,是直接通过获取构造器创建对象,并不通过getInstance方法,那也就是没有检验lazy是否为空,所以可以再次创建一个新的实例。

package single;

import java.lang.reflect.Constructor;

public class Lazy {
    private Lazy() {
    }

    private volatile static Lazy lazy;

    public static Lazy getInstance() {
        if (lazy == null) {
            synchronized (Lazy.class) {
                if (lazy == null) {
                    lazy = new Lazy();
                }
            }
        }
        return lazy;
    }

    public static void main(String[] args) throws Exception {
        Lazy instance = Lazy.getInstance();
        Constructor declaredConstructor = Lazy.class.getDeclaredConstructor(null); // 通过反射拿到无参构造器
        declaredConstructor.setAccessible(true); // 破坏私有
        Lazy instance2 = declaredConstructor.newInstance(); // 新建实例

        System.out.println(instance);
        System.out.println(instance2);
    }

}

解决这种破坏方式,在构造器里添加检验。

package single;

import java.lang.reflect.Constructor;

public class Lazy {
    private Lazy() {
        synchronized (Lazy.class) {
            if (lazy != null) {
                throw new RuntimeException("请不要用反射破坏单例");
            }
        }
    }

    private volatile static Lazy lazy;

    public static Lazy getInstance() {
        if (lazy == null) {
            synchronized (Lazy.class) {
                if (lazy == null) {
                    lazy = new Lazy();
                }
            }
        }
        return lazy;
    }

    public static void main(String[] args) throws Exception {
        Lazy instance = Lazy.getInstance();
        Constructor declaredConstructor = Lazy.class.getDeclaredConstructor(null); // 通过反射拿到无参构造器
        declaredConstructor.setAccessible(true); // 破坏私有
        Lazy instance2 = declaredConstructor.newInstance(); // 新建实例

        System.out.println(instance);
        System.out.println(instance2);
    }

}

但是通过反射可以再次破坏,原因是如果第一次创建就采用反射创建,而反射直接调用构造器创建,并不会把创建的对象指向lazy,那么lazy始终为null。因此再创建第二个实例的时候也会成功。

package single;

import java.lang.reflect.Constructor;

public class Lazy {
    private Lazy() {
        synchronized (Lazy.class) {
            if (lazy != null) {
                throw new RuntimeException("请不要用反射破坏单例");
            }
        }
    }

    private volatile static Lazy lazy;

    public static Lazy getInstance() {
        if (lazy == null) {
            synchronized (Lazy.class) {
                if (lazy == null) {
                    lazy = new Lazy();
                }
            }
        }
        return lazy;
    }

    public static void main(String[] args) throws Exception {
        Constructor declaredConstructor = Lazy.class.getDeclaredConstructor(null); // 通过反射拿到无参构造器
        declaredConstructor.setAccessible(true); // 破坏私有
        Lazy instance = declaredConstructor.newInstance();
        Lazy instance2 = declaredConstructor.newInstance(); // 新建实例

        System.out.println(instance);
        System.out.println(instance2);
    }

}

我们可以通过添加一个标志位来解决这个问题

package single;

import java.lang.reflect.Constructor;

public class Lazy {
    private static boolean secret = false;
    private Lazy() {
        synchronized (Lazy.class) {
            if (secret == false) {
                secret = true;
            }
            else {
                throw new RuntimeException("请不要用反射破坏单例");
            }
        }
    }

    private volatile static Lazy lazy;

    public static Lazy getInstance() {
        if (lazy == null) {
            synchronized (Lazy.class) {
                if (lazy == null) {
                    lazy = new Lazy();
                }
            }
        }
        return lazy;
    }

    public static void main(String[] args) throws Exception {
        Constructor declaredConstructor = Lazy.class.getDeclaredConstructor(null); // 通过反射拿到无参构造器
        declaredConstructor.setAccessible(true); // 破坏私有
        Lazy instance = declaredConstructor.newInstance();
        Lazy instance2 = declaredConstructor.newInstance(); // 新建实例

        System.out.println(instance);
        System.out.println(instance2);
    }

}

但是这种写法其实还是不安全的,如果我们知道这个标识位的名字,我们还是可以通过使用反射进行破坏。在创建完实例后手动将标志位再设为false。

package single;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class Lazy {
    private static boolean secret = false;
    private Lazy() {
        synchronized (Lazy.class) {
            if (secret == false) {
                secret = true;
            }
            else {
                throw new RuntimeException("请不要用反射破坏单例");
            }
        }
    }

    private volatile static Lazy lazy;

    public static Lazy getInstance() {
        if (lazy == null) {
            synchronized (Lazy.class) {
                if (lazy == null) {
                    lazy = new Lazy();
                }
            }
        }
        return lazy;
    }

    public static void main(String[] args) throws Exception {
        Constructor declaredConstructor = Lazy.class.getDeclaredConstructor(null); // 通过反射拿到无参构造器
        declaredConstructor.setAccessible(true); // 破坏私有
        Lazy instance = declaredConstructor.newInstance();
        Field secret = Lazy.class.getDeclaredField("secret");
        secret.setAccessible(true);
        secret.set(instance, false);
        Lazy instance2 = declaredConstructor.newInstance(); // 新建实例

        System.out.println(instance);
        System.out.println(instance2);
    }

}

可能现在你就想问了,如何写才能使安全的呢?其实枚举是无法被反射破坏的(点击new Instacne()查看源码即知)。

枚举

package single;

public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance() {
        return INSTANCE;
    }
}

class TestEnumSingle {
    public static void main(String[] args) {
        EnumSingle instance = EnumSingle.INSTANCE;
        EnumSingle instance2 = EnumSingle.INSTANCE;
        System.out.println(instance);
        System.out.println(instance2);
        System.out.println(instance == instance2);

    }
}

现在我们来确认一下枚举是否真的不能被破坏。
首先去out文件夹下找到编译后的文件,发现其中有一个无参构造方法。

package single;

public enum EnumSingle {
    INSTANCE;

    private EnumSingle() {
    }

    public EnumSingle getInstance() {
        return INSTANCE;
    }
}

于是我们开心的通过反射进行破坏

package single;

import java.lang.reflect.Constructor;

public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance() {
        return INSTANCE;
    }
}

class TestEnumSingle {
    public static void main(String[] args) throws Exception {
        EnumSingle instance = EnumSingle.INSTANCE;
        System.out.println(instance);
        Constructor declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();
        System.out.println(instance2);
        System.out.println(instance == instance2);

    }
}

但是我们发现报错了,但是并不是我们期望的Cannot reflectively create enum objects,而是Exception in thread "main" java.lang.NoSuchMethodException: single.EnumSingle.()。明明编译后的文件里有无参构造器,为什么通过反射创建时又说没有呢?我们可以自己反编译查看一下,发现确实是有无参构造方法的...
javap -p EnumSingle

设计模式之单例模式_第1张图片
反编译.png

这样一来,我们就需要更专业的工具了-jad,通过jad反编译生成.java文件然后查看:jad -sjava EnumSingle.class
这次我们发现,终于不是无参构造器了
设计模式之单例模式_第2张图片
image.png

我们修改一下getDeclaredConstructor,然后再次尝试破坏

package single;

import java.lang.reflect.Constructor;

public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance() {
        return INSTANCE;
    }
}

class TestEnumSingle {
    public static void main(String[] args) throws Exception {
        EnumSingle instance = EnumSingle.INSTANCE;
        System.out.println(instance);
        Constructor declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();
        System.out.println(instance2);
        System.out.println(instance == instance2);

    }
}

这次报错Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects,是我们想要的,说明枚举无法被反射破坏。

如果想更深入的理解单例模式,可以看这片文章-传送门

你可能感兴趣的:(设计模式之单例模式)