创建型设计模式(一)-单例模式简述及优化

一、前言:
  1. 单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证并发环境下中,应用该模式的一个类只有一个实例。即一个类只有一个对象实例。
  2. 常见的单例模式:
    饿汉模式: 在程序启动时即创建对象实例。
    懒汉模式:仅当程序中使用到改对象时,才回去创建对象。
二、单例模式实例:
1. 饿汉模式,程序启动,对象实例被创建 【不推荐】:

/**
 * @Des: 饿汉模式
 */
public class SingleTest01 {

    // 静态变量系统启动就会被加载
    private static SingleTest01 singleTest = new SingleTest01();

    // 私有化构造方方
    private SingleTest01(){
        System.out.println("SingleTest01 init ");
    }

    // 返回对象
    public static SingleTest01 newSingleTest(){
        return singleTest;
    }

}
优点:
  1. 代码实现简单,利用类加载机制保证线程安全。
缺点:
  1. 在程序启动时,就已经完成实例化,如果对象没有使用,会造成内存浪费。
  2. 如果对象在启动时存在一些耗时操作,会影响到我们程序启动时间
2、 懒汉模式, 当程序用到该实例时去创建对象,使用 synchronized 对获取方法进行加锁,实现并发安全【不推荐】:
/**
 * @Des: 懒汉模式
 */
public class SingleTest02 {

    // 静态变量保存对象
    private static SingleTest02 singleTest = null;

    // 私有化构造方法
    private SingleTest02(){
        System.out.println("SingleTest02 init ");
    }

    // synchronized 修饰方法保证并发安全
    public static synchronized SingleTest02 newSingleTest() {
        if (singleTest == null){
           singleTest = new SingleTest02();
        }
        return singleTest;
    }
}

优点:
  1. 就是饿汉模式的缺点
缺点:
  1. 效率太低,加锁的细粒度太大,其实仅仅在第一次创建对象时需要加锁,实例化完成之后,获取的时候完全没有必要加锁。
三、单例模式 – 懒汉模式优化:
第一种优化方案,缩小锁粒度:
  1. 使用 volatile 保证线程可见性
  2. 对象为被实例化时,通过代码快进行加锁,双重检验保证最终结果单例。

/**
 * @Des: 懒汉模式
 */
public class SingleTest03 implements Serializable {

    private static final long serialVersionUID = 1L;
	public String name;
    // 静态变量保存对象, volatile 保证每个线程读取最新的数据
    private static volatile SingleTest03 singleTest = null;

    // 私有化构造方法
    private SingleTest03(){
        System.out.println("SingleTest03 init ");
    }

    // 获取实例对象
    public static  SingleTest03 newSingleTest() {
        if (singleTest == null){
            // singleTest 为空,表示没有初始化,将当前类加锁。
            synchronized(SingleTest03.class){
                // 双重校验,避免第一个if之后有多个线程在等待。
                if (singleTest == null){
                    singleTest = new SingleTest03();
                }
            }
        }
        return singleTest;
    }
}
第二种优化方案,使用静态内部类:
  1. 利用类加载机制,保证实例化对象时仅有一个线程,内部类仅在使用时会初始化静态属性,实现了懒加载,效率高, 和第一中优化方案差不多,代码如下:

/**
 * @Des: 懒汉模式
 */
public class SingleTest04 {

    private static class SingletonInstance{
        private final static SingleTest04 SINGLETON = new SingleTest04();
    }

    private SingleTest04(){
        System.out.println("SingleTest04 init ");
    }

    // 获取实例对象
    public static SingleTest04 newSingleTest() {
        return SingletonInstance.SINGLETON;
    }
}


四、目前推荐的单例的创建方式:
  1. 上面的方式虽然都实现了单例模式,各有各自的优缺点。但是他们都有一个公共的 缺点,无法防止暴力创建对象,例如: 反射、序列化、克隆。
  2. 在 《Effective Java》作者的Josh Bloch提倡我们使用枚举的方式来创建单例对象,使用非常简单。
    关于枚举的博文: https://blog.csdn.net/zhangyong01245/article/details/103322007
  3. 代码示例:

/**
 * @Des: 枚举实现单例
 */
public enum SingletonEnum {

    // Singleton的单例对象,枚举被加载时,由JVM创建,线程安全
    INSTANCE;

    // 单例对象中的方法
    public void print(){
        System.out.println(this.hashCode());
    }
}

测试类:


class Test{
    public static void main(String[] args) {
        for (int i =0; i<10;i++){
            new Thread(new Runnable() {
                public void run() {
                    SingletonEnum singleton = SingletonEnum.INSTANCE;
                    singleton.print();
                }
            }).start();
        }

    }
}

打印结果:
在这里插入图片描述

重点:

上述的 饿汉模式 和 懒汉模式 真的就可以保证单例吗?
下面我们用 反射 和 序列化 对SingleTest01测试一下:

1、反射破坏单例:

反射生成新的对象代码如下:

public class TestReflection {

    @SneakyThrows
    public static void main(String[] args) {
        // 获取 Test01 Test03 无参构造方式
        Constructor<SingleTest01> constructorTest01 = SingleTest01.class.getDeclaredConstructor();
        // 设置为可访问
        constructorTest01.setAccessible(true);
        // 各自单例对象
        SingleTest01 singleTest01 = SingleTest01.newSingleTest();
        // 通过无参构造新的对象
        SingleTest01 newSingleTest01 = constructorTest01.newInstance();
        // 校验两个对象是否相等
        System.out.println("singleTest01 是否等于 newSingleTest01 :" + (singleTest01 == newSingleTest01));
    }
}

执行结果如下:
在这里插入图片描述
如结果所示, 我们的单例被 反射破坏了, 应对反射破坏单例的方式,我可以改造一下 SingleTest01 构造方法,改造后如下:

 private SingleTest01(){
        if (singleTest != null){
            throw new RuntimeException("单例对象不允许调用构造方法!!");
        }
        System.out.println("SingleTest01 init ");
    }

再次调用反射破坏单例执行结果如下:
在这里插入图片描述

2、序列化破坏单例:

如果我们将一个对象 序列化 到文件 然后再从文件读入到内存中,会生成一个新的对象破坏单例吗, ok,我们以SingleTest03 【类必须要实现序列化, SingleTest03代码中已经实现】试一下:

import lombok.SneakyThrows;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class TestSerializable {

    @SneakyThrows
    public static void main(String[] args) {

        SingleTest03 singleTest03 = SingleTest03.newSingleTest();

        // 写入到文件
        FileOutputStream outFile = new FileOutputStream("SingleTest03");
        ObjectOutputStream outObject = new ObjectOutputStream(outFile);
        outObject.writeObject(singleTest03);

        // 从文件中读取对象
        FileInputStream fileIn = new FileInputStream("SingleTest03");
        ObjectInputStream objIn = new ObjectInputStream(fileIn);
        SingleTest03 fileSingleTest01 = (SingleTest03) objIn.readObject();

        System.out.println("singleTest03 是否等于 fileSingleTest01: " + (singleTest03 == fileSingleTest01));
    }
}

执行结果, 如下, 单例模式再次被序列化与反序列化破坏
在这里插入图片描述
这… 单例模式再次被破坏,我们先看一下 java.io.Serializable, 注释里面有行代码:
意思: 反序列时执行序列化对象里面的这个方法获得新的对象,而不是从反序列化文件中获取

ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

在这里插入图片描述
给 SingleTest03 加入此方法后:

public class SingleTest03 implements Serializable {

    private static final long serialVersionUID = 1L;

	...........
	
    Object readResolve() throws ObjectStreamException {
        return SingleTest03.newSingleTest();
    }
}

再次执行结果如下:
在这里插入图片描述
我们可以看到,及时是反序列化生成的对象不再是从文件中获取的, 而是从readResolve() 拿到的 【写到文件中“张三” 的对象没有加载出来】

为什么? 为什么 readResolve() 方法有如此大的 能力 ~

我们从代码一步步跟进去:

1、文件输入流开始读取对象
在这里插入图片描述
2、调用 ObjectInputStream 中 readObject0 构建对象
在这里插入图片描述
3、在 readObject0()中进入 readOrdinaryObject() 中
在这里插入图片描述
4、在 readOrdinaryObject() 中判断是否有 readResolve() 方法,并反射调用该方法,生成最终的对象。
在这里插入图片描述

最后:

枚举对于上述的 反射、序列化是如何处理,并保证真正的单例 ,可以看一下我的下一遍文章,关于枚举的讲解
java能力-枚举浅析: https://blog.csdn.net/zhangyong01245/article/details/119841606

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值