设计模式之单例模式

目标

  • 掌握单例模式的应用场景
  • 掌握IDEA环境下的多线程调试方式
  • 掌握保证线程安全的单例模式策略
  • 掌握反射暴力共计单例解决方案及原理分析
  • 序列化破坏单例的原理及解决方案
  • 掌握常见的单例模式写法

定义

  • 单例模式是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。
  • 隐藏其所有的构造方法。
  • 属于创建型模式。

单例模式的适用场景

  • 确保任何情况下都绝对只有一个实例
  • ServletContext、ServletConfig、ApplicationContext
  • DBPool

单例模式的常见写法

  • 饿汉式单例
  • 懒汉式单例
  • 注册式单例
  • ThreadLocal单例

饿汉式单例

  • 在单例类首次加载时就创建实例
// 饿汉式单例  写法1
public class HungrySingleton {
    private static final HungrySingleton hungrySingleton = new HungrySingleton();
    public HungrySingleton() {
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}
//饿汉式单例  写法2
public class HungryStaticSingleton {
	//初始化顺序  先静态后动态  先上后下 先属性后方法
    private static final HungryStaticSingleton hungrySingleton;
    static{
        hungrySingleton = new HungryStaticSingleton();
    }
    public HungryStaticSingleton() {
    }
    public static HungryStaticSingleton getInstance(){
        return hungrySingleton;
    }
}

优点:执行效率高,性能高,没有任何的锁
缺点:某些情况下,可能会造成内存浪费   大量的实例需要创建的时候需要排队等待

懒汉式单例

  • 被外部类调用时才创建实例
// 懒汉式单例  写法
public class LazySimpleSingleton {
    private static LazySimpleSingleton lazy = null;
    private LazySimpleSingleton(){}
    public static LazySimpleSingleton getInstance(){
        if(lazy == null){
            lazy = new LazySimpleSingleton();
        }
        return lazy;
    }
}
//测试
//创建线程来实例化
public class ExcutorThread implements Runnable{
    @Override
    public void run() {
        LazySimpleSingleton singleton = LazySimpleSingleton.getInstance();
        System.out.println(Thread.currentThread() + ":" +singleton);
    }
}
//测试类
public class LazySimpleSingletonTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new ExcutorThread());
        Thread t2 = new Thread(new ExcutorThread());
        t1.start();
        t2.start();
        System.out.println("Excutor End");
    }
}
打印:
结果1
Excutor End
Thread[Thread-1,5,main]:com.zq.singleton.lazy.LazySimpleSingleton@4add1757
Thread[Thread-0,5,main]:com.zq.singleton.lazy.LazySimpleSingleton@404d8bb2
结果2
Excutor End
Thread[Thread-1,5,main]:com.zq.singleton.lazy.LazySimpleSingleton@283323cb
Thread[Thread-0,5,main]:com.zq.singleton.lazy.LazySimpleSingleton@283323cb
我们会发现有时打印的类是一样的有时是不一样的

打断点调试下看看
当两个线程都进入到这个位置时
在这里插入图片描述
将其中一个线程继续跑一步
在这里插入图片描述
发现创建的实例是 @496
然后再去跑另一个线程
在这里插入图片描述
创建的是@502 这时第一个线程创建的实例被第二个线程创建的实例覆盖掉了
最终打印出的结果是相同的,线程是不安全的,在getInstance()中加synchronized可以保证线程安全

优点:节省了内存,线程安全
缺点:性能低

如何去优化呢? 一步步来

// 优化一
public class LazyDoubleCheckSingleton {
    private static LazyDoubleCheckSingleton lazy;
    private LazyDoubleCheckSingleton(){}
    public static LazyDoubleCheckSingleton getInstance(){
       synchronized(LazyDoubleCheckSingleton.class){
            if(lazy == null){
                lazy = new LazyDoubleCheckSingleton();
            }
        }
        return lazy;
    }
}
我们将锁移入到方法内部,发现并不可以,只是将之前在方法层面的阻塞转移到了方法内部。
// 优化二
public class LazyDoubleCheckSingleton {
    private static LazyDoubleCheckSingleton lazy;
    private LazyDoubleCheckSingleton(){}
    public static LazyDoubleCheckSingleton getInstance() {
        if (lazy == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                lazy = new LazyDoubleCheckSingleton();
            }
        }
        return lazy;
    }
}

我们将判断移到锁外面,反复测试会发现,当两个线程同时进入到
synchronized (LazyDoubleCheckSingleton.class) {
这里时,仍然会出现后者创建将前者创建的实例覆盖的情况
// 优化三
public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazy;
    private LazyDoubleCheckSingleton(){}
    public static LazyDoubleCheckSingleton getInstance() {
        //检查是否要阻塞
        if (lazy == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                //检查是否要创建实例
                if (lazy == null) {
                    lazy = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazy;
    }
}
volatile 解决指令重排序问题
优点:性能高了,线程安全了
缺点:if不够优雅,可读性难度增加
//懒汉式内部类写法
public class LazyStaticInnerClassSingleton {
    private LazyStaticInnerClassSingleton (){}
    private static LazyStaticInnerClassSingleton getInstance(){
        return null ;
    }
    private static class LazyHolder{
        private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
    }
}
优点:写法优雅,利用了java本身语法特点,性能高,避免了内存浪费
缺点:能够反射破坏
//反射破坏单例测试类
public class ReflectTest {

    public static void main(String[] args) {

        try {
            Class<?> clazz = LazyStaticInnerClassSingleton.class;
            Constructor c = clazz.getDeclaredConstructor(null);

            c.setAccessible(true);
            Object instance1 = c.newInstance();
            Object instance2 = c.newInstance();
            System.out.println(instance1);
            System.out.println(instance2);

            System.out.println(instance1 == instance2);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

打印:
com.zq.singleton.lazy.LazyStaticInnerClassSingleton@14ae5a5
com.zq.singleton.lazy.LazyStaticInnerClassSingleton@7f31245a
false
//优化
public class LazyStaticInnerClassSingleton {
    private LazyStaticInnerClassSingleton (){
        if(LazyHolder.INSTANCE != null ){
            throw new RuntimeException("不允许非法访问");
        }
    }
    private static LazyStaticInnerClassSingleton getInstance(){
        return null ;
    }
    private static class LazyHolder{
        private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
    }



}

枚举式单例

  • 将每一个实例都缓存到统一的容器中,使用唯一标识获取实例
// 容器式单例 1.枚举式
public enum EnumSingletion {
    INSTANCE;
    private Object data;
    public static EnumSingletion getInstance(){
        return INSTANCE;
    }
   
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    } 
}

//测试类
public class EnumSingletionTest {
    public static void main(String[] args) {
        EnumSingletion instance = EnumSingletion.getInstance();
        instance.setData(new Object());  
        try{
            Class clazz = EnumSingletion.class;
            Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
            c.setAccessible(true);
            Object a = c.newInstance();
            System.out.println(a);
        }catch (Exception e ){
            e.printStackTrace();
        }

    }
}
报错:
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.zq.singleton.lazy.EnumSingletionTest.main(EnumSingletionTest.java:24)
枚举中防止了反射获取实例
优点:写法优雅、线程安全
缺点:饿汉式大量创建对象容易造成内存浪费
// 容器式单例2
public class ContainerSingleton {

    private  ContainerSingleton(){}

    private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();

    public static Object getInstance(String className){
        Object instance = null;
        if(!ioc.containsKey(className)){
            try {
                instance = Class.forName(className).newInstance();
                ioc.put(className,instance);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return instance;
        }else{
            return ioc.get(className);
        }
    }
}
缺点:线程安全问题

//测试类
public class ContainerSingletonTest {

    public static void main(String[] args) {
        Object instance1 = ContainerSingleton.getInstance("com.zq.singleton.lazy.Pojo");
        Object instance2 = ContainerSingleton.getInstance("com.zq.singleton.lazy.Pojo");
        System.out.println(instance1==instance2);
    }
}
打印:true

序列化破坏单例

// 
public class SerializableSingleton implements Serializable {

    //序列化
    //把内存中对象的状态转换成字节码的形式
    //把字节码通过IO输出流,写到磁盘上
    //永久保存下来,持久化

    //反序列化
    //将持久化的字节码内容  通过IO输入流读到内存中来
    //转化成一个java对象
    private SerializableSingleton(){}
    
    public final static SerializableSingleton INSTANCE = new SerializableSingleton();

    public static SerializableSingleton getInstance(){
        return INSTANCE;
    }
}

//测试类
public class SerializableSingletonTest {

    public static void main(String[] args) {
        SerializableSingleton s1 = null;
        SerializableSingleton s2 = SerializableSingleton.getInstance();

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("SerializableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SerializableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SerializableSingleton) ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1==s2);

        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
打印:
false

如何防止序列化破坏单例呢?
在单例类中加入
private Object readResolve(){
        return INSTANCE;
    }
    
是如何解决的呢?
在 ois.readObject()的方法中有如下判断,判断是否重写了readResolve方法,然后反射获取该对象返回
if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
        
如果没有重写,会调用desc.newInstance()重新创建对象返回
obj = desc.isInstantiable() ? desc.newInstance() : null;

ThreadLocal单例

  • 保证线程内部的全程唯一,且天生线程安全
// 单例类
public class ThreadLocalSingleton {

    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
            new ThreadLocal<ThreadLocalSingleton>(){
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };

    private ThreadLocalSingleton(){}

    public static ThreadLocalSingleton getInstance(){
        return threadLocalInstance.get();
    }
}

//线程
public class ThreadLocalExcutorThread implements Runnable{

    @Override
    public void run() {
        ThreadLocalSingleton singleton1 = ThreadLocalSingleton.getInstance();
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread() + ":" +singleton1);
    }
}

//测试类
public class ThreadLocalSingletonTest {
    public static void main(String[] args) {
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());

        Thread t1 = new Thread(new ThreadLocalExcutorThread());
        Thread t2 = new Thread(new ThreadLocalExcutorThread());

        t1.start();
        t2.start();

        System.out.println("Excutor End");
    }
}
根据打印结果可以看出,基于线程维度是单例的

应用场景

  • AbstractFactoryBean.getObject()
  • ErrorContext

单例模式的优点

  • 在内存中只有一个实例,减少了内存开销
  • 可以避免对资源的多重占用
  • 设置全局访问点,严格控制访问

单例模式的缺点

  • 没有接口,扩展困难
  • 可以避免对资源的多重占用
  • 如果要扩展单例对象,只有修改代码,没有其他途径

总结

  • 私有化构造器
  • 保证线程安全
  • 延迟加载
  • 防止序列化和反序列化破坏单例
  • 防御反射攻击单例

本文仅作个人记录学习使用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值