Loading

单例模式

单例模式

饿汉式单例模式

类一旦加载就创建一个单例(程序已启动就加载这个类),在调用 getInstance()方法之前单例已经存在了。

缺点:及其浪费空间,因为在还未调用getInstance()单例就以及创建了,这期间可能包含了一些及其占用空间的操作。

public class Hungry {
    private Hungry() {

    }

    private final static Hungry HUNGRY = new Hungry();

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

懒汉式单例

// 懒汉式
public class LazyMan {

    private LazyMan() {
    }

    private volatile static LazyMan lazyMan;

    public static LazyMan getInstance() {
        if (lazyMan == null) {
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}

上述这种懒汉式在单线程情况下是安全的,但在多线程情况下是不安全的,多个线程同时执行lazy = new LazyMan(),这样就会创建多个对象。所以我们需要使用synchronized关键字来保证同步。

// 懒汉式
public class LazyMan {

    private LazyMan() {
    }

    private volatile static LazyMan lazyMan;

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

上面的这种懒汉式在多线程的情况下大体来说是安全的,此时即使多个线程进入第一个if语句块,也只有一个线程能拿到锁,等第一个线程创建完毕后才会释放锁,然后第二个线程拿到锁,进入第二个if语句块,但在第一个线程的运行中,lazyMan已经创建了,所以此时第二个线程不会创建新的对象。

但上述代码在多线程的情况下还是有些许问题,在实例化对象的过程中,这个操作不是一个原子性的,可能会发生指令重排。

实例化一个对象操作如下:

  1. 分配内存空间
  2. 执行构造代码块
  3. 把这个对象指向分配空间

正常步骤是1-2-3,当发生指令重排的时候,可能会变成1-3-2,这种情况就会出现一些问题。A线程成功拿到当前类的锁,先执行步骤1,然后执行步骤3,最后执行步骤2(步骤2还未执行完,B线程来了)。B线程在A线程执行步骤2的时候进入,发现lzayMan此时不为空,因为此时已经分配了内存空间B线程直接返回lazyMan,导致初始化的内容不是完整的,所有我们要使用volatile避免指令重排。

// 懒汉式
public class LazyMan {

    private LazyMan() {
    }

    private volatile static LazyMan lazyMan;

    // 双重检测锁    DCL懒汉式
    public static LazyMan getInstance() {
        if (lazyMan == null) {
            synchronized (LazyMan.class) {
                if (lazyMan == null) {
                    /*
                     * 由于实例化对象不是一个原子性操作,这里可能发生指令重排
                     * 1. 分配内存空间
                     * 2. 执行构造代码块
                     * 3. 把这个对象指向分配的空间
                     * 若发生指令重排,可能会:
                     *   A线程成功拿到当前类的锁,先执行步骤1,然后执行步骤3,最后执行步骤2
                     *   B线程在A线程执行步骤2的时候进入,发现lzayMan此时不为空,因为此时已经分配了内存空间
                     *   B线程直接返回lazyMan,导致初始化的内容不是完整的
                     * 所有要使用volatile避免指令重排
                     * */
                    lazyMan = new LazyMan();
                }
            }
        }
        return lazyMan;
    }
}

静态内部类

​ 当外部内被访问时,并不会加载内部类,所以只要不访问InnerClass 这个内部类, private static final Holder HOLDER = new Holder() 就不会实例化,这就相当于实现懒加载的效果,只有当InnerClass.HOLDER 被调用时访问内部类的属性,此时才会将对象进行实例化,这样既解决了饿汉模式下可能造成资源浪费的问题,也避免了了懒汉模式下的并发问题。

// 静态内部类
public class Holder {

    private Holder() {
    }

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

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

枚举

// 枚举,枚举本身也是一个Class类
public enum EnumSingleton {
    INSTANCE;

    public EnumSingleton getInstance() {
        return INSTANCE;
    }

}

字节码文件

package designPattern.singletonPattern;

public enum EnumSingleton {
    INSTANCE;

    private EnumSingleton() {
    }

    public EnumSingleton getInstance() {
        return INSTANCE;
    }
}

在字节码中显示,该类有一个空参的构造方法,我们可以通过反射来获取对象

class Test {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingleton instance = EnumSingleton.INSTANCE;
        // 编译器显示没有这个类的空参的构造方法
        Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        EnumSingleton instance1 = declaredConstructor.newInstance();
        System.out.println(instance);
        System.out.println(instance1);
    }
}

但此时却没有成功的创建出对象。说明字节码中实际上是没有空参的构造方法的。

image-20210322100125827

使用javap -p还原字节码文件,我们发现还原后的代码中,仍然有空参的构造方法,说明这里得到的结果也是不准确的。

image-20210322100504579

通过jad最终反编译的代码

D:\IdeaProjects\utils\Code\target\classes\designPattern\singletonPattern>jad -sjava EnumSingleton.class
Parsing EnumSingleton.class... Generating EnumSingleton.java
package designPattern.singletonPattern;


public final class EnumSingleton extends Enum
{

    public static EnumSingleton[] values()
    {
        return (EnumSingleton[])$VALUES.clone();
    }

    public static EnumSingleton valueOf(String name)
    {
        return (EnumSingleton)Enum.valueOf(designPattern/singletonPattern/EnumSingleton, name);
    }

    private EnumSingleton(String s, int i)
    {
        super(s, i);
    }

    public EnumSingleton getInstance()
    {
        return INSTANCE;
    }

    public static final EnumSingleton INSTANCE;
    private static final EnumSingleton $VALUES[];

    static 
    {
        INSTANCE = new EnumSingleton("INSTANCE", 0);
        $VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}

在上述代码中,我们终于发现了一个不是空参的构造方法,于是我们可以使用这个构造方法,来通过放射获取枚举类的实例对象。

class Test {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingleton instance = EnumSingleton.INSTANCE;
        // 编译器显示没有这个类的空参的构造方法
        Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        EnumSingleton instance1 = declaredConstructor.newInstance();
        System.out.println(instance);
        System.out.println(instance1);
    }
}

image-20210322101234355

得到以上运行结果,这也和反射中构造类规定的一样,不允许使用反射的方式去获取一个枚举类。

image-20210322101332594

posted @ 2021-03-22 10:17  nuoxin  阅读(42)  评论(0)    收藏  举报