单例模式及破解单例模式

单例模式的介绍

单例模式:一个类自己负责创建自己的对象,同时确保系统中该对象只会被创建一个!

特点:

  • 在整个系统中只能有一个该类的实例(必须确保构造器私有,避免其他类new出该类的对象)
  • 他必须自行创建这个实例(在代码对应的类中自己编写实例化逻辑)
  • 它必须自行向整个系统提供这个实例(对外提供实例化方法,外面的类想要这个实例的对象随时可以调用)

单例模式的讲解

单例模式的饿汉式

public class Person {
    private String name;
    private int age;

    // 很饿,上来就要创建对象,要面包吃
    private final static Person instance = new Person();
    // 构造器私有化,保证外部不能通过new创建这个person对象
    private Person() {
    }

    public static Person getPerson(){

        return instance;
    }
}

饿汉式的局限性:
静态成员变量对象实例 随着类加载器将该类的加载而分配了内存。随着项目的运行,整个期间该对象实例一直被引用着,永远不会被垃圾回收,只能等着在类的卸载(结束项目运行)之后,随着类的内存空间释放而释放。如果是大对象,且一直迟迟不需要用到这样的对象,就会白白的浪费jvm的内存!

相比较懒汉式而言,懒汉式不但在我们使用的时候才创建这样的对象,而且我们还会手动的设置remove方法再对象不需要使用的时候释放内存空间!

单例模式的懒汉式

public class Person {
    private String name;
    private int age;


    private static Person instance = null;
    // 构造器私有化,保证外部不能通过new创建这个person对象
    private Person() {
        System.out.println("创建了person");
    }

    public static Person getPerson(){
        if (instance == null){
            Person person = new Person();
            instance = person;
        }
        return instance;
    }
}

缺点,多线程模式下起不到单例的效果,如果两个线程同时进入if语句内,那么他们两个的if条件都通过了就都进入了下面的代码创建了person对象,使得单例模式失效

优化1:在方法中加synchronized;或在方法里加个同步代码块,将所有业务都放在同步代码块中
缺点:锁加载方法上粒度太大,有可能获取单例的同时下面还有一堆业务逻辑,对于加锁来说,代码能锁的少就尽量锁少一些。因此可以对锁进行更加细粒度的优化

 public static synchronized Person getPerson(){
        if (instance == null){
            Person person = new Person();
            instance = person;
        }
        return instance;
}
 public static Person getPerson(){
        synchronized (Person.class){
            if (instance == null){
                Person person = new Person();
                instance = person;
            }
        }
   return instance;
 }

继续优化:把同步代码块放在if里面呢?

public static Person getPerson1() {
  if (instance == null) {
    synchronized (Person.class){
      Person person = new Person();
      instance = person;
    }
  }
  return instance;
}

当 instance 为 null 时,此时还没有对象进行创建,两个线程都有机会进入if语句内,进去了以后其中一个初始化创建了对象,另一个被阻断,当第一个线程初始化创建完了person并释放锁,第二个线程依然可以创建person。导致单例模式失效,这依然是存在多线程问题的。简单点理解:在多线程场景下可能会有几个线程进入if里面了,那进去if语句里面的每一个线程肯定都会创建person对象。

最终优化方法:双重检查锁 + 内存可见性volatile

双重检查锁定是为了避免对除第一次调用外的所有调用都实行同步。

假设两个线程都有机会进入第一个if语句中来(此时还没person对象):

第一个线程进入同步代码块,第二个线程被阻塞中… ,第一个线程继续判断进入了第二个if中,并创建了person对象,锁被释放。第二个线程进入同步代码块,判断:哦,有person对象了,那就不执行后面的代码了。那最终肯定只会有一个对象。

好处

  1. 从根本上解决了线程不安全的问题
  2. 减少锁竞争,提高效率。因为大部分情况下后面线程进入第一个instance == null语句就结束了,不会参与锁竞争

完整代码如下:

public class Person {
    private String name;
    private int age;

    private volatile static Person instance = null;

    // 构造器私有化,保证外部不能通过new创建这个person对象
    private Person() {
        System.out.println("创建了person");
    }

    public static Person getPerson() {
        if (instance == null) {
            synchronized (Person.class) {
                if (instance == null) {
                    Person person = new Person();
                    instance = person;
                }
            }
        }
        return instance;
    }

}

在instance变量加上volatile的作用:

  1. 避免指令重排。
    因为cpu读取内存的速度要比cpu计算的速度慢100倍以上,cpu不会在前一个指令读取完数据再计算完之后才执行下一个指令,cpu为了提高效率就会产生指令重排。

    因为new一个对象的时候是三步,第一步申请内存赋默认值,第二步调用构造器初始化成员变量赋初始值,第三步是将这块内存的赋值给引用。如果发生指令重拍,恰好第二步和第三步调换。那么这个对象中的值就全都是默认值。并不是我们期望的值。且后面获得的所有实例都是成员变量为默认值的。会对系统产生很多意想不到的问题。

    在这里插入图片描述
    从字节码中可以看出,如果发生指令重排,比如:astore_1跑到了invokespecial前面,那么对象就会带着成员变量的默认值赋值给对象的引用。如下图:(需要对 “对象在内存中的存储布局” 有所了解)
    在这里插入图片描述

  2. 可见性

    可见性指的是在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以会马上反应在其它线程的读取操作中。顺便一提,工作内存和主内存可以近似理解为实际电脑中的高速缓存和主存,工作内存是线程独享的,主存是线程共享的

单例模式的破解

使用序列化和反序列化的破解

可以通过序列化和反序列化这两步实现单例模式的破解!

首先说下什么是序列化和反序列化:

  • 序列化:将对象转换成字节序列的过程,通过ObjectOutputStream
  • 反序列化: 将字节序列转换恢复成对象的过程,通过ObjectInputStream

因此我们可以通过对象的序列化,将对象保存在磁盘文件中,并通过读取磁盘文件序列化的对象,生成一个新的对象

对象序列化包括如下步骤:

  • 创建一个对象输出流ObjectOutputStream,它可以包装一个其他类型的目标输出流,如文件输出流;
  • 通过对象输出流的writeObject()方法将对象序列化写出。

对象反序列化的步骤如下:

  • 创建一个对象输入流ObjectInputStream,它可以包装一个其他类型的源输入流,如文件输入流;
  • 通过对象输入流的readObject()方法读取对象。

具体实现如下

  public static void main(String[] args) {
        // 使用反序列化破解单例模式
        Person person = Person.getInstance();
        person.setName("Tom");
        person.setAge(18);
        System.out.println(person);
        Person person1 = null;
        FileOutputStream fileOutputStream = null;
        ObjectOutputStream objectOutputStream = null;
        FileInputStream fileInputStream = null;
        ObjectInputStream objectInputStream = null;
        try {
            fileOutputStream = new FileOutputStream("single.txt");
            objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(person);

            fileInputStream = new FileInputStream("single.txt");
            objectInputStream = new ObjectInputStream(fileInputStream);
            person1 = (Person) objectInputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (fileOutputStream != null){
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (objectOutputStream!= null){
                try {
                    objectOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fileInputStream !=null){
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (objectInputStream != null){
                try {
                    objectInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
        System.out.println(person = person1);
    }
}

在这里插入图片描述

在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。

那么如何避免被破解呢?
只有实现了Serializable接口的类才能被序列化,但是在实际开发中我们的实体类上必须要实现Serializable接口的,具体pojo类为什么要实现接口请看:java中POJO类为什么要实现序列化
加入readResolve方法即可

 private Object readResolve() throws ObjectStreamException{
        return instance;
    }

使用反射暴力破解

public class SingleTon {
    public static void main(String[] args) throws Exception{
        // 使用反射暴力破解
        Person person = Person.getInstance();
        person.setName("Tom");
        person.setAge(18);
        System.out.println(person);
        Class<Person> clazz = Person.class;
        Constructor<Person> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        Person person1 = constructor.newInstance();
        System.out.println(person1);
        System.out.println(person == person1);
    }
}

在这里插入图片描述

如何避免?
在构造器中加入判断,如果该对象已被创建,就不能继续调用私有构造器

private Person() {
        if (instance != null){
            throw new RuntimeException("不能通过反射创建新的对象!");
        }
    }

在这里插入图片描述

最终版的最终版的完美版写法

class Person implements Serializable {
    private String name;
    private Integer age;

    private Person() {
        if (instance != null){
            throw new RuntimeException("不能通过反射创建新的对象!");
        }
    }

    private volatile static Person instance = null;

    public static Person getInstance() {
        if (instance == null) {
            synchronized (Person.class) {
                if (instance == null)
                    instance = new Person();
            }
        }
        return instance;
    }
    private Object readResolve() throws ObjectStreamException{
        return instance;
    }
}

单例模式的使用场景

在这里插入图片描述
在这里插入图片描述

  • 多线程中的线程池。
  • 数据库的连接池。
    像线程池,数据库连接池,在整个系统中必须只能有一个,不可能有多个
  • 系统环境信息。
  • 上下文(ServletContext)。

单例模式面试题

  • 系统环境信息(System.getProperties())?
  • Spring中怎么保持组件单例的?
  • ServletContext是什么(封装Servlet的信息)?是单例吗?怎么保证?
  • ApplicationContext是什么?是单例吗?怎么保证?
  • ApplicationContext: tomcat:一个应用(部署的一个war包)会有一个应用上下文
  • ApplicationContext: Spring:表示整个IOC容器(怎么保证单例的)。ioc容器中有很多组件(怎么保证单例)
  • 数据库连接池一般怎么创建出来的,怎么保证单实例?

总结:单例模式虽然只是短短的几句小代码,但是放大来看它是一种思想,它有着数不清的应用场景和实现!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值