设计模式01——单例模式

2022-10-25 15:51:53 浏览数 (2)

设计模式01——单例模式

  • 一、分类(Group Of Four/GOF 23)
  • 二、单例模式

一、分类(Group Of Four/GOF 23)

设计模式一定要运用到具体应用中。 创建型模式:单例模式、工厂模式、抽象工厂模式、创建者模式、原型模式。 结构性模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。 行为型模式:模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、责任链模式、访问者模式。

设计模式的要点: 面向接口设计; 复合 has a,继承 is a; 隔离变化:发现变化部分,把它封装起来。

二、单例模式

  • 作用 保证一个类只有一个对象,并且提供一个访问该实例的全局访问点。
  • 应用场景 任务管理器、配置文件、日志管理、数据库连接池、OS文件系统、Servlet中的Application对象、Spring中的Bean。
  • 优点 内存占用和系统开销小。
  • 实现 主要: (1)饿汉式 构造器私有,类初始化时(天然线程安全过程)就加载这个对象(static)。 线程安全、调用效率高,不能延时加载。
代码语言:javascript复制
/**
 * 饿汉式单例模式
 */
class SingletonPattern01 {

    // 类加载时对象就创建了
    private static /*final*/ SingletonPattern01 instance = new SingletonPattern01();

    private SingletonPattern01() {
    }

    public static SingletonPattern01 getInstance() {
        return instance;
    }
}

(2)懒汉式 懒加载,需要时才加载,但是需要考虑同步synchronized。 线程安全、资源利用率高,延时加载、并发效率低。

代码语言:javascript复制
/**
 * 懒汉式单例模式
 */
class SingletonPattern02 {

    private static SingletonPattern02 instance;

    private SingletonPattern02() {
    }

    // 延时加载
    public static synchronized SingletonPattern02 getInstance() {
        if (instance == null) {
            instance = new SingletonPattern02();
        }
        return instance;
    }
}

其他: (3)双重检测锁式(由于JVM底层内部模型原因,偶尔会出问题,不推荐) 同步方法优化为两个同步块,放在if内部。 隐患:实力对象过程可分为1.分配内存空间;2.初始化对象;3.将对象指向刚分配的内存空间。有些编译器为了性能问题可能将2、3步重排序,导致一个线程访问已分配内存空间的对象,还对象没有完成初始化的情况发生。 解决:给对象添加volatile关键词,将对象存放在内存中而非缓存。

代码语言:javascript复制
/**
 * 双重检测锁式单例模式
 */
class SingletonPattern03 {

    // 添加volatile关键字
    private volatile static SingletonPattern03 instance;

    private SingletonPattern03() {

    }

    public static SingletonPattern03 getInstance(){
        // 提高检测效率
        if (instance == null) {
            synchronized (SingletonPattern03.class) {
                // 可能有多线程同时进入同步块内
                if (instance == null) {
                    instance = new SingletonPattern03();
                }
            }
        }
        return instance;
    }
}

(4)静态内部类式(线程安全、调用效率高,延时加载) 优化注意:线程安全、调用效率高、懒加载 静态内部类的加载不需要依附外部类,在使用时才加载,不过在加载静态内部类的过程中也会加载外部类。

代码语言:javascript复制
/**
 * 静态内部类式单例模式
 */
class SingletonPattern04 {

    // 静态内部类在使用时加载,天然线程安全
    private static class SingletonPatternClassInstance {
        // 保证只有一个,且只能赋值一次
        private static final SingletonPattern04 instance = new SingletonPattern04();
    }

    public static SingletonPattern04 getInstance() {
        return SingletonPatternClassInstance.instance;
    }

    private SingletonPattern04() {
    }
}

问题:即使构造器私有了,也可以通过反射来调用。

(5)枚举单例(线程安全、调用效率高,不能延时加载) 可以天然防止反射和反序列化,基于JVM底层实现的。4、5较优。

代码语言:javascript复制
/**
 * 枚举式单例模式
 */
enum SingletonPattern05 {

    // 枚举元素本身就是单例的,INSTANCE就是SingletonPattern05的一个单例
    INSTANCE;

    // 添加需要的操作
    public void singletonPatternOperation() {

    }
}
  • 反射破解 通过c.setAccessible(true);跳过私有构造器保护。
代码语言:javascript复制
public static void main(String[] args) throw Exception {
	Class<SingletonPattern01> clazz = (Class<SingletonPattern01>) Class.forName("com.ustc.designpattern.SingletonPattern01");
	Constructor<SingletonPattern01> c = clazz.getDeclaredConstructor(null);
	c.setAccessible(true);
	SingletonPattern01 s3 = c.newInstance();
	SingletonPattern01 s4 = c.newInstance();
}

解决:在私有构造器时添加判断,反射操作时抛出异常。

代码语言:javascript复制
private SingletonPattern01() {
 	if (instance != null) {
        throw new RuntimeException();
    }
}
  • 反序列化破解
代码语言:javascript复制
public static void main(String[] args) throw Exception {
	FileOutputStream fos = new FileOutputStream("${url}");
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    oos.writeObject(s1);
    oos.close();
    fos.close();

    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("${url}"));
    SingletonPattern01 s5 = (SingletonPattern01) ois.readObject();
}

解决:定义readResolve()方法,直接返回此方法中的反序列化对象,而不需要新创建

代码语言:javascript复制
private Object readResolve() throw ObjectStreamException {
	return instance;
}
  • 性能比较

CountDownLatch: 同步辅助类,在完成一组正在其他线程执行的操作之前,它允许一个或多个线程一直等待。 coutDown()当前线程调用此方法,则计数减一,放在finally里执行; await(),调用此方法会一直阻塞当前进程,知道计时器的值为0。

0 人点赞