精讲23种设计模式-006:深入研究单例底层实现原理

1 深入研究单例底层实现原理课程安排

课程内容

  1. 单例模式基本与设计思想概念
  2. 单例模式的应用场景
  3. 单例模式多种写法
  4. 懒汉式为什么需要双重检验锁
  5. Java创建对象有哪些方式
  6. 如何防止单例被反射、序列化破解
  7. 反序列化创建对象底层实现原理

2 单例模式基本概念与优缺点

单例基本概念
在Jvm中只会存在一个实例

单例优缺点
优点:

  1. 不会频繁创建对象,节约服务器内存;
  2. 只有一个实例实现复用,不需要再创建对象,访问速度比较快;

缺点:

  1. 因为该对象只有一个实例,多个线程同时共享同一个单例对象的时候,可能存在线程安全问题;
  2. 单例对象一般创建加static存放在永久区,不会被垃圾回收机制回收,会一直占用服务器内存,如果创建过多可能导致服务器内存溢出;

单例应用场景

  1. 项目中定义的配置文件都是单例;
  2. SpringIOC容器创建对象默认单例(复用);
  3. 线程池和数据库连接池;
  4. Servlet对象默认单例,线程不安全、SpringMVC基于Servlet封装也不安全;
  5. Jvm内置缓存框架,底层基于HashMap封装+淘汰策略,hashMap对象单例;
  6. 枚举、常量
  7. 网站计数器

3 手写单例模式

懒汉式概念:当真正需要该对象的时候才会创建该对象。
注意:需要将构造函数私有化

手写懒汉式(线程不安全)

public class Singleton01 {

    private static Singleton01 singleton01 = null;

    /**
     * 私有化
     */
    private Singleton01() {
        System.out.println("singleton01");
    }

    public static Singleton01 getInstance() {
        if (singleton01 == null) {
            singleton01 = new Singleton01();
        }
        return singleton01;
    }

    public static void main(String[] args) {
        Singleton01 instance1 = Singleton01.getInstance();
        Singleton01 instance2 = Singleton01.getInstance();
        System.out.println(instance1 == instance2);
    }
}

运行结果:
请添加图片描述
该写法:如果多个线程同时访问,有可能会造成线程不安全,创建多个对象,违背单例唯一原则。
线程安全问题:多个线程同时共享同一个全局变量,做写的操作的时候,可能会产生线程安全问题。

手写懒汉式(线程安全)

public class Singleton02 {

    private static Singleton02 singleton02;
    
    private Singleton02(){
        
    }

    // synchronized加在方法上,读取的时候也会上锁,效率低
    public static synchronized Singleton02 getInstance() {
        if (singleton02 == null) {
            singleton02 = new Singleton02();
        }
        return singleton02;
    }

    public static void main(String[] args) {
        Singleton02 instance1 = Singleton02.getInstance();
        Singleton02 instance2 = Singleton02.getInstance();
        System.out.println(instance1 == instance2); // true
    }
}

该写法:虽然能保证线程安全问题,但只要该对象创建以后,后期获取对象的时候都需要获取锁,效率非常低。

懒汉式双重检验锁

public class Singleton03 {
    private static Singleton03 singleton03;

    private Singleton03() {
    }

    public static Singleton03 getInstance() {
        // 第一次判断 上锁 作用是保证仅在创建对象的时候会上锁,获取对象不会上锁
        if (singleton03 == null) {
            synchronized (Singleton03.class) {
                // 第二次判断 保证后面代码处理逻辑单线程,防止创建多个对象
                if (singleton03 == null) {
                    singleton03 = new Singleton03();
                }
            }

        }
        return singleton03;
    }

    public static void main(String[] args) {
        Singleton03 instance1 = Singleton03.getInstance();
        Singleton03 instance2 = Singleton03.getInstance();
        System.out.println(instance1 == instance2);
    }
}

该写法:既能解决线程安全问题(两次判断保证创建对象唯一),效率也比较高(获取对象的时候不会进入锁)

懒汉式与饿汉式区别
懒汉式:当真正需要该对象才会创建,相对节约内存空间使用。缺点:先天性线程不安全,需要后期通过代码保证线程安全问题;
饿汉式:在class文件被加载的时候就会创建该对象,先天性线程安全(只会加载一次),缺点是比较浪费内存空间。

饿汉式实现单例(线程安全)

public class Singleton04 {

    // 当class文件被加载的时候就会创建该对象
//    private static Singleton04 singleton04 = new Singleton04();
    // 创建常量写法
    public static final Singleton04 singleton04 = new Singleton04();

    private Singleton04() {
        System.out.println("singleton04");
    }

    public static Singleton04 getInstance() {
        return singleton04;
    }

    public static void main(String[] args) {
//        Singleton04 instance1 = Singleton04.getInstance();
//        Singleton04 instance2 = Singleton04.getInstance();
//        System.out.println(instance1 == instance2);
        System.out.println(Singleton04.getInstance() == Singleton04.getInstance());
    }
}

项目中定义的常量都是饿汉式形式,如果定义常量过多,可能导致内存溢出。

基于静态代码块实现单例

public class Singleton05 {

    private static Singleton05 singleton05 = null;

    private Singleton05() {
    }

    static {
        System.out.println("当前class被加载");
        singleton05 = new Singleton05();
    }

    public static Singleton05 getInstance() {
        return singleton05;
    }

    public static void main(String[] args) {
//        Singleton05 instance1 = Singleton05.getInstance();
//        Singleton05 instance2 = Singleton05.getInstance();
//        System.out.println(instance1 == instance2);
    }
}

饿汉式写法,当class被加载即初始化。
请添加图片描述

4 使用反射机制破解单例&如何防御

Jvm中可以通过哪些方式创建对象?
反射机制、序列化、new、克隆技术

public class Test001 {
    public static void main(String[] args) throws Exception {
        Singleton01 instance1 = Singleton01.getInstance();
        // 使用反射机制创建对象
        Class<?> aClass = Class.forName("com.mayikt.Singleton01");
//        Constructor<?> constructor = aClass.getConstructor(); // 获取所有的构造函数,包括父类Object的,报错
        Constructor<?> constructor = aClass.getDeclaredConstructor();// 获取当前类的无参构造函数,不包含父类
        constructor.setAccessible(true);
        Singleton01 instance2 = (Singleton01) constructor.newInstance();
        System.out.println(instance1 == instance2);
    }
}

运行结果:
请添加图片描述
如何防御反射破解单例

public class Singleton01 {

    private static Singleton01 singleton01 = null;

    /**
     * 私有化
     */
    private Singleton01() throws Exception {
        if (singleton01 != null) {
            throw new Exception("该对象已经创建过,不能重复创建");
        } else {
            singleton01 = this;
        }
        System.out.println("singleton01");
    }

    public static Singleton01 getInstance() throws Exception {
        if (singleton01 == null) {
            singleton01 = new Singleton01();
        }
        return singleton01;
    }

    public static void main(String[] args) throws Exception {
        Singleton01 instance1 = Singleton01.getInstance();
        Singleton01 instance2 = Singleton01.getInstance();
        System.out.println(instance1 == instance2);
    }
}

运行结果:
请添加图片描述
如果没有else部分,则当反射创建对象在getInstance()创建对象之前调用,防御无效。

5 使用序列化破坏单例&如何防御

序列化:将对象转换成二进制形式直接存放在硬盘持久化;
反序列化:从本地文件读取二进制文件转换成对象;

public class Test003 {
    public static void main(String[] args) throws Exception {
        // 1.需要将该对象序列化到本地存放
        FileOutputStream fos = new FileOutputStream("d:/code/singleton.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        Singleton01 instance1 = new Singleton01();
        oos.writeObject(instance1);
        oos.close();
        fos.close();
        //2.从硬盘中反序列化对象到内存中
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/code/singleton.txt"));
        Singleton01 instance2 = (Singleton01) ois.readObject();
        System.out.println(instance1 == instance2);
    }
}

运行结果:
请添加图片描述
序列化创建对象违背单例设计原则

疑问:

  1. 序列化创建对象为什么没有走无参构造函数却创建新对象?
  2. 如何防御序列化创建新的对象

如何防御序列化创建新的对象

public class Singleton01 implements Serializable {

    private static Singleton01 singleton01 = null;

    /**
     * 私有化
     */
    Singleton01() throws Exception {
        if (singleton01 != null) {
            throw new Exception("该对象已经创建过,不能重复创建");
        } else {
            singleton01 = this;
        }
        System.out.println("singleton01");
    }

    public static Singleton01 getInstance() throws Exception {
        if (singleton01 == null) {
            singleton01 = new Singleton01();
        }
        return singleton01;
    }

    public static void main(String[] args) throws Exception {
        Singleton01 instance1 = Singleton01.getInstance();
        Singleton01 instance2 = Singleton01.getInstance();
        System.out.println(instance1 == instance2);
    }

    /**
     * 序列号生成回调方法,通过该方法实现反序列化生成单例对象
     * 方法名称一定是固定写死的 readResolve
     * @return
     */
    public Object readResolve() {
        return singleton01;
    }
}

运行结果:
请添加图片描述

6 序列化如何生成一个新的对象

序列化如何生成一个新的对象
通过序列化的形式创建对象不会走序列化类无参构造函数,判断序列化类是否有实现Serializable接口,如果实现该接口的情况下,则调动该类的父类(没有实现Serializable接口)无参构造函数,通过父类实例化该对象。如果父类也实现了Serializable接口,通过Object类初始化该对象。

public class Singleton01 extends UserEntity implements Serializable {

    private static Singleton01 singleton01 = null;

    /**
     * 私有化
     */
    Singleton01() throws Exception {
        if (singleton01 != null) {
            throw new Exception("该对象已经创建过,不能重复创建");
        } else {
            singleton01 = this;
        }
        System.out.println("singleton01");
    }

    public static Singleton01 getInstance() throws Exception {
        if (singleton01 == null) {
            singleton01 = new Singleton01();
        }
        return singleton01;
    }

    public static void main(String[] args) throws Exception {
        Singleton01 instance1 = Singleton01.getInstance();
        Singleton01 instance2 = Singleton01.getInstance();
        System.out.println(instance1 == instance2);
    }

    /**
     * 序列号生成回调方法,通过该方法实现反序列化生成单例对象
     * 方法名称一定是固定写死的 readResolve
     * @return
     */
    public Object readResolve() {
        return singleton01;
    }
}
public class UserEntity extends Object implements Serializable {
    public UserEntity() {
        System.out.println("父类开始初始化");
    }
}

运行结果:
请添加图片描述
readResolve方法执行的原理
序列化在创建对象的时候,判断序列化类中是否存在readResolve方法,如果存在的情况下,则直接走反射调用序列化类中的readResolve方法返回单例对象。

7 枚举是最安全的单例

序列化、反射破解单例不能在根本底层上解决
枚举 最安全的单例:

  1. 先天性 通过序列化生成的对象默认保证单例
  2. 反射无法初始化到枚举
public enum Singleton06 {

    INSTANCE;

    public void addUser(){
        System.out.println("枚举类,先天性安全");
    }

    Singleton06(){
        System.out.println(">>无参构造函数执行");
    }
}
public class Test004 {
    public static void main(String[] args) throws Exception {
//        Singleton06 instance1 = Singleton06.INSTANCE;
//        Singleton06 instance2 = Singleton06.INSTANCE;
//        System.out.println(instance1 == instance2);
//
//        Class<?> aClass = Class.forName("com.mayikt.Singleton06");
//        aClass.newInstance();  // 报错

        // 1.需要将该对象序列化到本地存放
        FileOutputStream fos = new FileOutputStream("d:/code/enum.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        Singleton06 instance1 = Singleton06.INSTANCE;
        oos.writeObject(instance1);
        oos.close();
        fos.close();
        //2.从硬盘中反序列化对象到内存中
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/code/enum.txt"));
        Singleton06 instance2 = (Singleton06) ois.readObject();
        System.out.println(instance1 == instance2);
    }
}

运行结果:
请添加图片描述
源码下载地址(mayikt_designPattern_6.zip):
链接:https://pan.baidu.com/s/1wWKZN1MbXICZVW1Vxtwe6A
提取码:fire

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值