likes
comments
collection
share

【Java设计模式】创建型设计模式-单例模式(二)

作者站长头像
站长
· 阅读数 5

创建型设计模式

创建型设计模式:由对对象的创建过程的合理的性的思考而产生的的类型。

单例模式

单例模式适用于需要频繁创建和销毁且消耗时间较长,耗费资源过多,但又是频繁使用的对象(重量级对象)。例如:工具类对象、web开发中的必须用到的对象(servlet对象)、用于操作数据库和文件的对象(数据源对象、session工厂对象等)

注意:

  • 如果是在程序中确定绝对会使用到的实例,那么使用饿汉式的方式实现单例模式没有问题,且枚举的方式好于普通方式。
  • 如果是在程序中不确定绝对会使用到的实例,那么使用懒汉式的方式实现单例模式没有问题,且要防止反射和反序列化破坏单例模式。

饿汉式

案例一:

/**
 * 单例模式-饿汉式
 */
public class SingletonTypeDemo1 {

    // 1.私有化构造器:为了让外部不能创建该类的对象(不能 new)
    private SingletonTypeDemo1() {

    }

    // 2.本类内部创建一个该类实例
    private static final SingletonTypeDemo1 sgt = new SingletonTypeDemo1();

    // 3.对外提供一个公有的静态方法,返回实例对象
    public static SingletonTypeDemo1 getInstance() {
        return sgt;
    }

}

总结:

  • 这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

  • 在类装载的时候就完成实例化,没有达到 Lazy Loading (延迟加载)的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。这种方式基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,在单例模式中大多数都是调用 getInstance 方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 就没有达到 lazy loading 的效果。

  • 这种单例模式可用,可能造成内存浪费,注意考虑使用场景,如果是确定绝对会使用到的那么可以使用这种方式去实现单例模式。

案例二:

/**
 * 单例模式-饿汉式二
 */
public class SingletonTypeDemo2 {

    // 1.私有化构造器:为了让外部不能创建该类的对象(不能 new)
    private SingletonTypeDemo2() {

    }

    // 2.本类内部创建一个该类实例
    private static SingletonTypeDemo2 sgt;

    static {//在静态代码块中,创建单例对象
        sgt = new SingletonTypeDemo2();
    }

    // 3.对外提供一个公有的静态方法,返回实例对象
    public static SingletonTypeDemo2 getInstance() {
        return sgt;
    }
}

优缺点说明:

  • 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和静态变量是一样的。
  • 结论:这种单例模式可用,但是可能造成内存浪费。

懒汉式

案例一:

/**
 * 单例模式-懒汉式一
 */
public class SingletonTypeDemo3 {

    //1.持有一个该类的静态变量
    private static SingletonTypeDemo3 sgt;

    //2.私有化构造器
    private SingletonTypeDemo3() {

    }

    //3.提供一个静态公有方法,当使用到该方法时,才去创建sgt单例对象。即懒汉式
    public static SingletonTypeDemo3 getInstance() {
        if (sgt == null) {
            sgt = new SingletonTypeDemo3();
        }
        return sgt;
    }
}

总结:

  • 起到了 Lazy Loading (延迟加载)的效果,但是只能在单线程下使用。

  • 如果在多线程下,一个线程进入了 if (sgt == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。 所以在多线程环境下不可使用这种方式,该方式线程不安全。

  • 在实际开发中, 不要使用这种方式。

案例二:

/**
 * 单例模式-懒汉式二
 */
public class SingletonTypeDemo4 {

    //1.持有一个该类的静态变量
    private static SingletonTypeDemo4 sgt;

    //2.私有化构造器
    private SingletonTypeDemo4() {

    }

    //3.提供一个静态公有方法,加入同步处理的代码,解决线程安全问题。
    public synchronized static SingletonTypeDemo4 getInstance() {
        if (sgt == null) {
            sgt = new SingletonTypeDemo4();
        }
        return sgt;
    }
}

优缺点说明:

  • 优点:解决了 线程安全问题

  • 缺点:效率太低了,每个线程在想获得类的实例时候,执行 getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了。 方法进行同步效率太低

  • 结论:在实际开发中, 不推荐使用这种方式。

案例三:

/**
 * 单例模式-懒汉式三
 */
public class SingletonTypeDemo5 {

    //1.持有一个该类的静态变量
    private static SingletonTypeDemo5 sgt;

    //2.私有化构造器
    private SingletonTypeDemo5() {

    }

    //3.提供一个静态公有方法,加入同步处理的代码,解决线程安全问题
    public static SingletonTypeDemo5 getInstance() {
        if (sgt == null) {
            synchronized (SingletonTypeDemo5.class) {
                sgt = new SingletonTypeDemo5();
            }
        }
        return sgt;
    }
}

优缺点说明:

  • 优点:这种方式本意是想对第二种实现方式进行改进,因为使用同步方法的方式效率太低了改为同步产生实例化的代码块。

  • 缺点:但是这种同步并不能起到线程同步的作用。这种方式和第一种实现方式遇到的情形一致,假如一个线程进入了if (sgt == null) 判断语句块,CPU的时间片就切换到另一个线程了,然后另一个线程也通过了这个判断语句,这时就会产生多个实例。

  • 结论:在实际开发中, 不能使用这种方式。

案例四:

/**
 * 单例模式-懒汉式四
 */
public class SingletonTypeDemo6 {

    //1.持有一个该类的静态变量
    private static SingletonTypeDemo6 sgt;

    //2.私有化构造器
    private SingletonTypeDemo6() {

    }

    //3.提供一个静态公有方法,加入双重检查的代码,解决线程安全问题,解决懒加载问题,解决了效率问题
    public static SingletonTypeDemo6 getInstance() {
        if (sgt == null) {
            synchronized (SingletonTypeDemo6.class) {
                if (sgt == null) {
                    sgt = new SingletonTypeDemo6();
                }
            }
        }
        return sgt;
    }
}

优缺点说明:

  • 优点:Double-Check 概念是多线程开发中常使用到的,如代码中所示,我们进行了两次 if (sgt == null)检查,这样就可以保证线程安全(SingletonTypeDemo6的实例在内存中只有一个)。这样,实例化代码只用执行一次,后面再次访问时,判断 if (sgt == null),如果sgt不为null,就直接 return 实例化对象,也避免的反复进行方法同步。线程安全、 延迟加载、效率较高。

  • 缺点:在实际开发中, 应不应该使用这种方式实现单例模式呢?该单例模式有可能会出现暴力反射破解和重排序问题。

  • 暴力反射问题:我们来看下面的测试案例

/**
 * 单例模式-演示暴力反射
 */
public class SingletonTypeDemo7 {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //调用两次方法,单例模式成立
        SingletonTypeDemo6 singletonTypeDemo61 = SingletonTypeDemo6.getInstance();
        SingletonTypeDemo6 singletonTypeDemo62 = SingletonTypeDemo6.getInstance();
        System.out.println(singletonTypeDemo61 == singletonTypeDemo62);

        //使用暴力反射破解单例
        Class<?> aClass = Class.forName("com.bonc.createType.single.SingletonTypeDemo6");
        Constructor<?> constructor = aClass.getDeclaredConstructor(null);
        //将构造方法设置为允许访问
        constructor.setAccessible(true);
        SingletonTypeDemo6 instance1 = (SingletonTypeDemo6) constructor.newInstance();
        SingletonTypeDemo6 instance2 = (SingletonTypeDemo6) constructor.newInstance();
        System.out.println(instance1 == instance2);
    }
}

以上的执行结果第一个输出true,确实是证明单例模式成立,第二次通过反射暴力破解,打开了构造器进行创建对象,输出为false。

所以我们应该在这个基础上防止暴力反射的出现。

重排序问题:为什么上面的单例模式还会出现重排序问题?

  • 什么是重排序问题? ​ 编译器和处理器为了提高程序的运行效率,对指令进行的重新排序。

  • 数据的依赖性 重排序会遵循一个规则,就是重排序只会对两个没有数据依赖关系的指令进行重排序,要保证执行后的结果不会被改变。这是在一个线程内才会有效的。多线程的时候就不存在线程之间跨线程的指令重排序了。

  • 指令重排序的分类:

    • 编译器重排序
    • 处理器重排序
  • 为什么要进行指令重排序? 为了提高程序的运行效率

  • 指令重排序会造成什么影响:

    • 对于单线程环境下:只会提高程序运行的效率。
    • 对于多线程环境下:也是会提高程序运行的效率,但是,如果关键数据不做处理就会导致运行结果和预期结果不一致。
  • 竞争与同步:

    利用竞争的方式使内存中的一些代码执行顺序按照我们要求的顺序进行同步,从而避免多线程环境下因为重排序而造成的运行结果和预期结果不一致的问题。

    在本单例模式中:sgt = new SingletonTypeDemo6()时,JVM产生3个步骤:

    1. 给sgt开辟内存空间

    2. 调用Singleton的构造函数来初始化成员变量

    3. 将sgt对象指向分配的内存中的地址

      正常情况会按照1-2-3的步骤执行,但如果发生指令重排序,可能会变为1-3-2步骤执行。如果是后者,则在步骤3执行完毕,步骤2未执行前,被另外一个线程抢占了,这时sgt已经是非null了(但却没有初始化),所以线程二会直接返回sgt,然后使用,最后顺利成章的报错。

案例五:完美的懒汉式

/**
 * 单例模式-懒汉式五
 */
public class SingletonTypeDemo8 {

    //1.持有一个该类的静态变量
    //5.使用volatile防止重排序
    private volatile static SingletonTypeDemo8 sgt;

    //2.私有化构造器
    private SingletonTypeDemo8() {
        //4.加上一个判断,防止暴力反射
        if (sgt != null) {
            throw new RuntimeException();
        }
    }

    //3.提供一个静态公有方法,加入双重检查的代码,解决线程安全问题,解决懒加载问题,解决了效率问题
    public static SingletonTypeDemo8 getInstance() {
        if (sgt == null) {
            synchronized (SingletonTypeDemo8.class) {
                if (sgt == null) {
                    sgt = new SingletonTypeDemo8();
                }
            }
        }
        return sgt;
    }
}

总结:以上就是一个完美的懒汉式单例模式,解决线程安全问题,解决懒加载问题,解决效率问题,解决暴力反射问题和重排序问题。

推荐使用。

静态内部类

案例一:

/**
 * 单例模式-静态内部类一
 */
public class SingletonTypeDemo9 implements Serializable {

    private SingletonTypeDemo9() {

    }

    //写一个静态内部类,该类中有一个静态属性Singleton
    private static class SingletonInside {
        private static final SingletonTypeDemo9 SGT = new SingletonTypeDemo9();
    }

    /**
     * 提供一个静态公有方法,直接返回SingletonInside.SGT解决线程安全问题,
     * 同时解决懒加载问题。同时解决了效率问题,推荐使用
     */
    public static SingletonTypeDemo9 getInstance() {
        return SingletonInside.SGT;
    }
}

总结:

  • 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。静态内部类方式在 SingletonTypeDemo9 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance 方法,才会装载 SingletonInside 类,从而完成 SingletonTypeDemo9 的实例化。

  • 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行 初始化时,别的线程是无法进入的。避免了线程不安全,利用静态内部类特点实现延迟加载,效率高。

  • 这里是利用了静态内部类的两个特点:

    • 外部类加载的时候,不会导致内部类的加载,只有在使用到内部类的时候才会加载内部类;使用到内部类可以指的是:调用内部类的方法、访问内部类的变量、调用内部类的构造器;在本例中是在外部类SingletonTypeDemo9被加载的时候,内部类SingletonInside不会被加载,从而实现懒加载。
    • 在加载静态内部类的时候,是JVM去加载的,是线程安全的,别的线程无法进入;在本例中在加载静态内部类SingletonInside的时候,我们去实例化SingletonType9,这样就实现了线程安全。
  • 仍然存在暴力反射破解和序列化和反序列化破解问题。

我们来看如下的测试案例

/**
 * 单例模式-演示静态内部类方式的暴力反射破解和序列化破解
 */
public class SingletonTypeDemo10 {

    public static void main(String[] args) throws Exception {
        //利用反射创建对象,从而破坏单利模式的设计
        Constructor<SingletonTypeDemo9> constructor = SingletonTypeDemo9.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        SingletonTypeDemo9 instance1 = constructor.newInstance();
        SingletonTypeDemo9 instance2 = constructor.newInstance();
        System.out.println(instance1 == instance2);

        //利用反序列化可以创建多个该类的对象,从而破坏单例模式的设计
        //序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("src/com/bonc/temp.txt"));
        oos.writeObject(instance1);
        oos.close();
        //反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("src/com/bonc/temp.txt"));
        SingletonTypeDemo9 instance3 = (SingletonTypeDemo9) ois.readObject();
        System.out.println(instance1 == instance3);
    }
}

跟懒汉式一样,我们不仅需要防止暴力反射破解,还应该防止序列化破解问题。

案例二:

/**
 * 单例模式-静态内部类二
 */
public class SingletonTypeDemo11 implements Serializable {

    private SingletonTypeDemo11() {
        if (SingletonInside.SGT != null) {
            throw new RuntimeException();
        }
    }

    //写一个静态内部类,该类中有一个静态属性Singleton
    private static class SingletonInside {
        private static final SingletonTypeDemo11 SGT = new SingletonTypeDemo11();
    }

    /**
     * 提供一个静态公有方法,直接返回SingletonInside.SGT解决线程安全问题,
     * 同时解决懒加载问题。同时解决了效率问题,推荐使用
     */
    public static SingletonTypeDemo11 getInstance() {
        return SingletonInside.SGT;
    }

    /**
     * 此方法作用是在反序列化时直接返回该方法的返回对象,而无需再去创建新的对象。
     */
    private Object readResolve() {
        return SingletonInside.SGT;
    }
}

总结:以上就是一个完美的静态内部类单例模式,解决线程安全问题,解决懒加载问题,解决效率问题,解决暴力反射问题和反序列化问题。推荐使用。

枚举类

案例一:

/**
 * 单例模式-枚举类
 */
public class SingletonTypeDemo12 {

    //1.私有化构造函数
    private SingletonTypeDemo12() {

    }

    //2.定义一个静态枚举类
    static enum SingletonEnum {
        //3.创建一个枚举对象,该对象天生为单例
        INSTANCE;
        private SingletonTypeDemo12 singletonTypeDemo12;

        //4.私有化枚举的构造函数
        private SingletonEnum() {
            singletonTypeDemo12 = new SingletonTypeDemo12();
        }
        
        //5.提供一个方法用于返回枚举类中创建的对象
        public SingletonTypeDemo12 getInstance() {
            return singletonTypeDemo12;
        }
    }
}

总结:

  • 这种方式是 Effective Java 作者 Josh Bloch (乔什布洛赫)提倡的方式,这借助 JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止利用反射和反序列化重新创建新的对象去破坏单例模式。

  • 不能实现延时加载。

  • 推荐使用。