创建型设计模式---单例模式

一、饿汉式(线程安全)

1、普通饿汉式单例模式

       先介绍第一种饿汉式单例,在类初始化的时候立刻就实例化对象,所以是线程安全的。

	/**
	 * 饿汉式单例模式: 类一加载就会实例化
	 */
	public class HungrySingleton {
	    private static final HungrySingleton INSTANCE = new HungrySingleton();
	
	    private HungrySingleton() {
	    }
	
	    public static HungrySingleton getInstance() {
	        return INSTANCE;
	    }
	}




二、懒汉式

1、基于静态内部类的单例模式(线程安全)

       第一种懒汉式单例,借助内部类,在外部类调用getInstance()方法时,才会实例化对象。

	/**
	 * 饿汉式内部类单例模式: 类初始化的时候就会进行实例化
	 */
	public class InnerClassSingleton {
	    private InnerClassSingleton() {
	    }
	
	    private static class SingletonHolder {
	        //静态内部类没有生成它的外围类对象的引用
	        private static InnerClassSingleton INSTANCE = new InnerClassSingleton();
	    }
	
	    //JVM保证了类初始化的时候, 会显式装载SingletonHolder这个内部类, 从而实例化INSTANCE
	    public static InnerClassSingleton getInstance() {
	        return SingletonHolder.INSTANCE;
	    }
	}



2、锁住整个类的懒汉式单例模式

       第二种是最简单的懒汉式单例,将synchronized关键字加在静态方法上面,直接将整个类都锁住。

	/**
	 * 懒汉式单例: 存在线程安全问题, 这里是直接将这个类给锁住
	 */
	public class LazySimpleSingleton {
	    //懒汉式单例模式的时候不能将实例用final修饰, 因为一旦修饰这个变量就不能再指向其他的内存地址
	    private static LazySimpleSingleton INSTANCE = null;
	
	    private LazySimpleSingleton() {
	    }
	
	    //粗粒度加锁, 锁在这个静态方法上相当于把整个类都锁住了(PS: 锁在普通成员方法上只是把当前对象给锁住了)
	    public static synchronized LazySimpleSingleton getInstance() {
	        //不使用synchronized可能存在这种情况:
	        //Thread1进入了if判断, 但是还没实例化, 这时候Thread2也进来了
	        if (INSTANCE == null) {
	            INSTANCE = new LazySimpleSingleton();
	        }
	        return INSTANCE;
	    }
	}



3、使用双重检查锁的懒汉式单例模式

       第三种是细粒度加锁。采用双重同步锁解决懒汉式单例的线程安全问题。

	/**
	 * 懒汉式单例: 存在线程安全问题, 这里用双重同步锁解决线程安全问题
	 */
	public class LazyDCLSingleton {
	    //懒汉式单例模式的时候不能将实例用final修饰, 因为一旦修饰这个变量就不能再指向其他的内存地址
	    private static LazyDCLSingleton INSTANCE = null;
	
	    private LazyDCLSingleton() {
	    }
	
	    //DCL(double-checked locking) 双重检测锁保证懒汉式单例模式的线程安全问题
	    public static LazyDCLSingleton getInstance() {
	        if (INSTANCE == null) {
	            synchronized (LazyDCLSingleton.class) {
	                //如果之前就有多个线程进入了第一重if判断, 而且不判断内层的if的话, 那还是会实例化多次
	                if (INSTANCE == null) {
	                    INSTANCE = new LazyDCLSingleton();
	                }
	            }
	        }
	        return INSTANCE;
	    }
	}

       看着上面的双重检查锁,可能觉得没问题了,其实还是有问题的。因为JVM会对指令进行重排序,synchronized关键字能保证有序性的前提是锁住的变量整个交给同步块了,但是看上面的代码,INSTANCE并没有完全锁住,具体分析可以看下面的字节码。

Alt

       正确执行的时候应该如下图:
Alt

       再直观点可以看下图:
Alt

       所以只需要改一个地方,private static volatile LazyDCLSingleton INSTANCE = null;
所以当执行INSTANCE的赋值语句时候,会在当前位置加入一个写屏障,该写屏障可以保证写屏障前的指令不会重排序到写屏障之后去




三、容器式单例

1、Map式单例

       Map式单例类似Spring框架。

	/**
	 * 容器式注册单例: Spring
	 */
	public class ContainerSingleton {
	    private static ContainerSingleton INSTANCE;
	    //ConcurrentHashMap只保证往Map中存数和取数是安全的
	    private static Map<String, Object> ioc = new ConcurrentHashMap<>();
	
	    public static Object getBean(String className) {
	        synchronized (ioc) {
	            if (!ioc.containsKey(className)) {
	                Object obj = null;
	                try {
	                    Constructor<?> constructor = Class.forName(className).getDeclaredConstructor();
	                    constructor.setAccessible(true);
	                    obj = constructor.newInstance();
	                } catch (InstantiationException e) {
	                    e.printStackTrace();
	                } catch (IllegalAccessException e) {
	                    e.printStackTrace();
	                } catch (ClassNotFoundException e) {
	                    e.printStackTrace();
	                } catch (NoSuchMethodException e) {
	                    e.printStackTrace();
	                } catch (InvocationTargetException e) {
	                    e.printStackTrace();
	                }
	            }
	        }
	        return ioc.get(className);
	    }
	}



2、Enum枚举型单例

       Enum枚举型单例是非常好的单例模式。

	/**
	 * 枚举式单例: JDK从底层为枚举不被序列化、反射破坏单例提供了支持(饿汉式)
	 */
	public enum EnumSingleton {
	    //保存到JVM的内存中了
	    INSTANCE {
	        //编写枚举的对象方法
	        protected void print() {
	            System.out.println("EnumSingleton::print()...");
	        }
	    };
	
	    protected abstract void print();    //必须声明, 否则无法调用
	
	    private Object data;    //枚举对象属性
	
	    public Object getData() {
	        return data;
	    }
	
	    public void setData(Object data) {
	        this.data = data;
	    }
	
	    //JDK从底层为枚举不被序列化、反射破坏单例提供了支持
	    public static EnumSingleton getInstance() {
	        return INSTANCE;
	    }
	}



3、ThreadLocal线程单例

       ThreadLocal实现的单例,每个线程保持一个对象

	/**
	 * ThreadLocal注册式单例: 每个线程对应一份实例
	 */
	public class ThreadLocalSingleton {
	    private ThreadLocalSingleton() {
	    }
	
	    //key是当前线程对象, value是要存储的对象
	    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>() {
	        @Override
	        protected ThreadLocalSingleton initialValue() {
	            return new ThreadLocalSingleton();
	        }
	    };
	
	    //tomcat很可能就是ThreadLocal搞得, 一个用户一个request相当于开启了一个线程
	    public static ThreadLocalSingleton getInstance() {
	        return threadLocalInstance.get();
	    }
	}




四、反射攻击单例模式

1、饿汉式单例防御反射攻击

       防止反射攻击的方式:

	private HungrySingleton() {
        if (INSTANCE != null) {
            throw new RuntimeException("机制反射调用创建对象");
        }
    }

       验证代码:

	//Class clz = Class.forName(InnerClassSingleton.class.getName());
	Class clz = HungrySingleton.class;
    Constructor c = clz.getDeclaredConstructor();
    c.setAccessible(true);	//开启权限
    HungrySingleton instance = (HungrySingleton) c.newInstance();
    HungrySingleton newInstance = HungrySingleton.getInstance();
    System.out.println(instance);
    System.out.println(newInstance);
    System.out.println(instance == newInstance);

       验证结果:

Alt



2、懒汉式单例无法防御反射攻击

① 先调用单例的创建方法,再使用反射的情况。

       防止反射攻击的方式:

	private LazyDCLSingleton() {
        if (INSTANCE != null) {
            throw new RuntimeException("禁止反射调用创建对象");
        }
    }

       验证代码:

	Class clz = LazyDCLSingleton.class;
    Constructor c = clz.getDeclaredConstructor();
    c.setAccessible(true);
    LazyDCLSingleton instance = LazyDCLSingleton.getInstance();
    LazyDCLSingleton newInstance = (LazyDCLSingleton) c.newInstance();
    System.out.println(instance);
    System.out.println(newInstance);
    System.out.println(instance == newInstance);

       验证结果:

Alt


② 先使用反射技术,再调用单例的创建方法的情况。

       因为如果先通过反射调用来实例化对象,那此时INSTANCE成员变量还是空的,所以当真正调用单例的创建方法的时候,还是会实例化对象。所以尝试使用静态变量防御反射攻击。

       防止反射攻击的方式:

	private LazyDCLSingleton() {
        if (flag) {
            flag = false;
        } else {
            throw new RuntimeException("禁止反射调用创建对象");
        }
    }

       验证代码(通过反射技术直接修改变量):

 	Class clz = LazyDCLSingleton.class;
    Constructor c = clz.getDeclaredConstructor();
    c.setAccessible(true);
    LazyDCLSingleton newInstance = (LazyDCLSingleton) c.newInstance();
    Field flag = clz.getDeclaredField("flag");
    flag.setAccessible(true);
    flag.set(newInstance, true);
    LazyDCLSingleton instance = LazyDCLSingleton.getInstance();
    System.out.println(instance);
    System.out.println(newInstance);
    System.out.println(instance == newInstance);

       验证结果:

Alt



       综上,懒汉式单例无法防御反射破坏单例的攻击,而饿汉式单例可以防御反射攻击!




五、序列化和反序列化破坏单例

1、非枚举单例

       除了枚举单例,其他单例实现了序列化接口时候会破坏单例模式!

       可增加readResolve方法来解决单例被破坏的问题。但其实在JVM层面还是实例化了两次对象

	/**
	 * 序列化与反序列化会破坏单例, 添加readResolve方法可以解决
	 */
	public class SerializableSingleton implements Serializable {
	    private static final SerializableSingleton INSTANCE = new SerializableSingleton();
	
	    private SerializableSingleton() {
	        System.out.println("SerializableSingleton()...");
	    }
	
	    public static SerializableSingleton getInstance() {
	        return INSTANCE;
	    }
	
	    //增加这个方法后只是覆盖了反序列化后的对象, 之前反序列化出来的对象会被GC回收, JVM层面还是new了两次对象,
	    private Object readResolve() {
	        return INSTANCE;
	    }
	}



2、枚举单例

       枚举实现的单例模式,不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。还在JDK层面为反射创建对象保驾护航

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值