单例模式(Singleton Pattern)

一、单例模式的经典实现方式  

  单例模式分为饿汉式(立即加载)和懒汉式(延迟加载),其中懒汉式又可以分为双重检查锁、静态内部类和枚举三种情况。;

  单例模式使用不当,则会产生线程安全问题:

    饿汉式不会产生线程安全问题,但是它一般不使用,因为他会浪费内存空间;

    懒汉式会合理的使用内存空间,因为只有第一次被加载的时候才会真正的创建对象;但是这种方式存在线程安全问题

  创建单例模式的三要素:私有静态成员变量(对象本身)、私有的构造函数、公共的获取静态变量方法

1、饿汉式

  类加载的时候,JVM内部保证了整个过程的线程安全。类加载包括静态成员变量的初始化。

  所以饿汉式在类加载时,就会加载静态变量,从而对静态属性进行初始化,这个操作是受JVM线程保护的,不会出现线程安全问题。

package com.lcl.galaxy.design.pattern.singleton;

public class HungrySingleAnimal {
    private static HungrySingleAnimal singleAnimal = new HungrySingleAnimal();

    private HungrySingleAnimal(){

    }

    public static HungrySingleAnimal getSingleAnimal(){
        return singleAnimal;
    }

}

2、懒汉式

  懒汉式的最优写法就是使用双重检查锁的方式实现,实现代码如下:

package com.lcl.galaxy.design.pattern.singleton;

import java.io.Serializable;

public class DoubleCheckSingleAnimal implements Serializable {private static volatile DoubleCheckSingleAnimal singleAnimal = null;

    private DoubleCheckSingleAnimal(){

    }

    public static DoubleCheckSingleAnimal getSingleAnima (){
        if(singleAnimal == null){
            synchronized (DoubleCheckSingleAnimal.class){
                if(singleAnimal == null){
                    singleAnimal = new DoubleCheckSingleAnimal();
                }
            }
        }
        return singleAnimal;
    }

    private Object readResolve(){
        return singleAnimal;
    }
}

  为什么要这么实现呢,我们可以一步步的分析

  最简单的实现方式:

    private static volatile LazySingleAnimal1 singleAnimal = null;

    private LazySingleAnimal1(){

    }

    /**
     * 不安全
     * @return
     */
    public static LazySingleAnimal1 getSingleAnimal(){
        if(singleAnimal == null){
            singleAnimal = new LazySingleAnimal1();
        }
        return singleAnimal;
    }

  这种实现方式会存在线程安全问题:当存在并发时,如果对象还未创建,则都会执行创建对象语句。

  针对上述问题的优化如下

    public static synchronized LazySingleAnimal1 getSingleAnima2(){
        if(singleAnimal == null){
            singleAnimal = new LazySingleAnimal1();
        }
        return singleAnimal;
    }

  优化方式是将方法增加一个synchronized同步锁,这样的问题就在于,会非常影响单例的使用性能

  针对上述内容进一步优化

    public static LazySingleAnimal1 getSingleAnima3 (){
        if(singleAnimal == null){
            synchronized (LazySingleAnimal1.class){
                singleAnimal = new LazySingleAnimal1();
            }
        }
        return singleAnimal;
    }

  这种实现,只有在对象为空的时候才加锁,但是仍然存在问题:即如果存在并发,对象为空时,都能绕过为空判断,虽然在创建对象语句的执行上有锁,但是仍然会以串行方式创建多个对象。

  继续上述问题优化

    public static LazySingleAnimal1 getSingleAnima4 (){
        if(singleAnimal == null){
            synchronized (LazySingleAnimal1.class){
                if(singleAnimal == null){
                    singleAnimal = new LazySingleAnimal1();
                }
            }
        }
        return singleAnimal;
    }

  这种情况已经控制住了,只有一个线程可以进入第二层为空判断,且创建了对象,释放锁后,后续竞争锁成功的线程判断时,对象已不为空,那么就不会在创建对象;但是该种实现仍然存在问题:JVM会对代码进行指令重排序,因此可能存在一个对象虽然已经创建,但是还未赋值的情况下,就被其他线程所使用,进而导致错误产生。因此需要对私有属性加volatile修饰。

  这里需要说明一个指令重排序和volatile关键字的作用:

    对象的创建流程:1、new关键字开辟空间;2、对象空间初始化;3、将内存地址赋值给栈空间的变量进行保存

    指令重排序:JIT即时编译器,会对指令做重排序,上述创建顺序有可能不是按照123执行的,而是按照132执行的,那么就有个问题,先将引用对象做了保存,但是没有对对象的空间做初始化,那么下一个线程拿到的引用就会有问题

    并发编程的三大特性:1、原子性:狭义上指CPU指令的原子操作,广义上指字节码指令的原子性;2、有序性:CPU指令有序性;3、可见性:CPU工作内存种的数据存在多核之间的不可见问题

    volatile作用:解决有序性问题(禁止指令重排);解决可见性问题(强制刷新告诉缓存到内存中,当多个CPU对同一个数据进行操作时,一旦有数据有写操作,那么必须先等它写完数据之后,写入主内存)

  这里还有一个问题,就是单例模式有可能被破坏,被破坏主要有反射破坏和序列化破坏,再最优的单例模式中增加了一个readResolve方法,就是为了防止使用序列化破坏。

3、静态内部类

  静态内部类,主要是利用了静态类加载受JVM保护,在调用公共的get方法时,会去访问静态内部类的私有属性,此使就会触发静态内部类的加载,那么就初始化了静态内部类中的私有属性,同时又因为静态类的加载受JVM保护,因此静态内部类也是一个很好的单例实现方式。

package com.lcl.galaxy.design.pattern.singleton;

public class StaticInnerClass {

    private static class SingletonHandler{
        private static final StaticInnerClass STATIC_INNER_CLASS = new StaticInnerClass();
    }

    private StaticInnerClass(){}

    public StaticInnerClass getStaticInnerClass(){
        return SingletonHandler.STATIC_INNER_CLASS;
    }
}

4、枚举

  先上代码,具体原因后面说。

package com.lcl.galaxy.design.pattern.singleton;

public enum EnumSingleton {
    INSTANCE;

    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

二、破坏单例

1、反射攻击

  使用反射获取对象的无参构造,然后使用构造函数的newInstance方法进行创建对象。

    @Test
    public void reflectAttackTest() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
        Constructor constructor = DoubleCheckSingleAnimal.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        DoubleCheckSingleAnimal a1 = (DoubleCheckSingleAnimal) constructor.newInstance();
        DoubleCheckSingleAnimal a2 = (DoubleCheckSingleAnimal) constructor.newInstance();

        a1.setName("lcl");
        a2.setName("lcl");
        log.info("a1============={}==========",a1);
        log.info("a2============={}==========",a2);
        log.info("a1.equals(a2)============={}==========",a1.equals(a2));
    }

2、序列化攻击

  使用深拷贝的方式,创建两个对象。其实就是先使用序列化,将对象写到一个文件内,然后再将文件中的内容反序列化为对象。

    @Test
    public void serializationAttackTest() throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("aaa"));
        DoubleCheckSingleAnimal a1 = DoubleCheckSingleAnimal.getSingleAnima();
        oos.writeObject(a1);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("aaa"));
        DoubleCheckSingleAnimal a2 = (DoubleCheckSingleAnimal) ois.readObject();

        a1.setName("lcl");
        a2.setName("lcl");
        log.info("a1============={}==========",a1);
        log.info("a2============={}==========",a2);
        log.info("a1.equals(a2)============={}==========",a1.equals(a2));
    }

 三、攻击防御

1、反射攻击防御

  对于反射攻击,private属性简直形同虚设,同时反射是基于对象的构造函数进行创建对象的,那么就可以在构造函数中加入判断,如果对象不为空,则抛出异常

package com.lcl.galaxy.design.pattern.singleton;

public class StaticInnerClass {

    private static class SingletonHandler{
        private static final StaticInnerClass STATIC_INNER_CLASS = new StaticInnerClass();
    }

    private StaticInnerClass(){}

    public StaticInnerClass getStaticInnerClass() throws Exception {
        if(SingletonHandler.STATIC_INNER_CLASS != null){
            throw new Exception("单例被破坏");
        }
        return SingletonHandler.STATIC_INNER_CLASS;
    }
}

2、反序列化攻击防御

  这里可以看一下序列化时调用的ois.readObject方法,在该方法的源码中,会判断是否存在readResolve方法,如果存在,则调用该方法进行处理,如果不存在,则反序列化对象,因此,在上面提到,双重检查锁的单例模式,需要加入readResolve方法,这样就可以避免通过序列化攻击单例的情况(上面代码示例的readResolve方法)。

package com.lcl.galaxy.design.pattern.singleton;

import java.io.Serializable;

public class DoubleCheckSingleAnimal implements Serializable {

    private static volatile DoubleCheckSingleAnimal singleAnimal = null;

    private DoubleCheckSingleAnimal() throws Exception {
    }

    public static DoubleCheckSingleAnimal getSingleAnima () throws Exception {
        if(singleAnimal == null){
            synchronized (DoubleCheckSingleAnimal.class){
                if(singleAnimal == null){
                    singleAnimal = new DoubleCheckSingleAnimal();
                }
            }
        }
        return singleAnimal;
    }

    private Object readResolve(){
        return singleAnimal;
    }

}

3、枚举单例防御

  JVM对枚举类做了特殊的处理,既保证了线程安全,又防止了序列化攻击,同时也防止了反射攻击。

public abstract class Singleton extends Enum

  保证线程安全:查看枚举类编译后的文件(如上述代码)可以发现,枚举类是用static修饰的类;JVM在加载static类的时候,会保证线程安全

  防止序列化攻击:JVM对枚举类做了特殊处理,在序列化时,只保存了属性名称和引用地址,当反序列化时,使用的是引用地址和名称进行处理,因此拿到的对象仍然是同一个对象。

  防止反射攻击:枚举编译后的文件是抽象类,因此不能使用反射进行破坏。

原文地址:https://www.cnblogs.com/liconglong/p/14174496.html