单例模式
//一.单例模式的简要概述
1.单例模式被破坏的情况
暴力反射:在私有构造方法中,判非空时,直接抛出异常。禁止使用构造
序列化和反序列化:添加一个readResolve方法,返回单例对象
Unsafe破坏:目前没有解决策略
2.枚举单例是一种饿汉式的单例,不会受反序列化破坏,不会被暴力反射破坏,但unsafe束手无策。
3.普通懒汉式不能保证多线程安全
尝试在获取实例的整个方法上加synchronized关键字,但是在第一次获取之后的效率较低。
4.双重检验锁(DCL)
两次判空分别保证高效和单例、线程安全
volatile修饰单例对象保证 多线程下变量的可见性和有序性,能够有效地防止指令重排(Jvm的优化手段)
5.静态内部类
//进化史——普通饿汉式、枚举饿汉式、普通懒汉式、双检锁懒汉式、静态内部类懒汉式
二.jdk中的单例模式
System类中调用exit、gc方法本质都是调用了Runtime类。而Runtime是一个饿汉式单例对象
System类中的成员变量Console对象——双检锁懒汉式
Collections集合工具类中有很多单例对象
//三.五种单例模式的实现
public class ExplainSingleton {
public static void main(String[] args) {
//测试枚举类实现单例模式
System.out.println(SingletonObject5.INSTANCE==SingletonObject5.INSTANCE);
SingletonObj1 singletonObj1 = SingletonObj1.getInstance();
new Thread(){//匿名内部类是局部内部类的一种简化形式.本质上是一个对象,是实现了该接口或继承了该抽象类的子类对象.
@Override
public void run() {
SingletonObj1 singletonObj11 = SingletonObj1.getInstance();
System.out.println(singletonObj1==singletonObj11);
}
}.start();
SingletonObject2 singletonObject2 = SingletonObject2.getSingletonObject2();
new Thread(() -> {
SingletonObject2 singletonObject21 = SingletonObject2.getSingletonObject2();
System.out.println(singletonObject21==singletonObject2);
}).start();
}
}
//1.懒汉式——保证线程安全
class SingletonObj1 {
//私有化成员变量,禁止外类直接通过类获取,然后对该变量进行引用的修改
private static SingletonObj1 singletonObj1;
//覆盖默认的构造方法,提供私有构造方法
private SingletonObj1(){
}
//静态方法
public synchronized static SingletonObj1 getInstance(){
// 静态方法可以被调用多次,每次调用都会执行方法体中的代码。但是,如果一个类中有静态代码块(static block),
// 那么这个代码块只会在类加载的时候执行一次,并且在所有对象中全局共享。静态代码块通常用来初始化一些静态变量或者做一些只需要执行一次的操作
// 总之,静态方法可以被多次调用,但是静态代码块只会被执行一次。
if(singletonObj1==null){
singletonObj1= new SingletonObj1();
}
return singletonObj1;
}
}
//2.饿汉式
//饿汉式单例的写法适用于单例对象较少的情况,这样写可以保证绝对的线程安全,执行效率比较高。
// 但是缺点也很明显,饿汉式会在类加载的时候就将所有单例对象实例化,这样系统初始化的时候会造成大量的内存浪费.
// 从而导致系统的内存不可控,换句话说就是不管对象用不用,对象都已存在,占用内存。
class SingletonObject2{
//加载类的时候,旧产生单例对象.由于类加载只会有一次,故只会产生唯一的一个单例对象
private static SingletonObject2 singletonObject2=new SingletonObject2();
//构造私有化后,就不允许通过new产生对象,故只能通过 类获取,所以相关的成员变量和方法都要有 static关键字
private SingletonObject2(){
}
public static SingletonObject2 getSingletonObject2(){
return singletonObject2;
}
}
//3.双重校验锁.volatile 和 synchronized
//双重判空
//特点为 懒初始化、线程安全、实现难度复杂、在多线程情况下能保持高性能。
class SingletonObject3{
/*
当一个变量被声明为 volatile 时,意味着它的值可能会在程序的执行过程中被意外地修改,
例如在多线程或中断处理程序中。这样,编译器就不能对该变量进行优化,以免导致代码的行为与预期不符。
volatile 关键字通常被用于以下情况:
1.访问硬件寄存器或设备状态,这些状态可能会随时被修改。
2.在多线程环境中共享变量,以确保线程间的可见性。
3.在信号处理程序中访问变量,以避免编译器对变量的优化导致的问题。
需要注意的是,虽然使用 volatile 可以避免编译器的优化,但它并不能保证线程安全和数据一致性,
因此在多线程环境中,还需要使用其他同步机制(CAS、Unsafe、AtomicInteger)来确保数据的正确性。
*/
//当一个变量被声明为 volatile 时,volatile 变量的写操作会立即刷新到主内存,
// 而读操作会从主内存中读取最新值,而非从本地线程缓存中读取。
//因此,当多个线程同时访问一个共享的 volatile 变量时,它们总是能够看到最新的值。
//需要注意的是,volatile 变量并不能完全保证线程安全,因为它只保证了可见性和有序性,而不保证原子性。
private volatile static SingletonObject3 singletonObject3;
private SingletonObject3(){
}
public static SingletonObject3 getSingletonObject3(){
if(singletonObject3==null){
//在多线程的竞争下,如果第一次没有其他线程进行修改。则使用重量级锁synchronized创建单例对象。
//synchronized 的使用可以保证线程安全,避免多个线程同时对共享资源进行修改而导致的数据不一致问题。
//需要注意的是,synchronized 的过度使用可能会导致程序性能下降,因此在使用时应该考虑到性能和实际需求。
synchronized (SingletonObject3.class){
if(singletonObject3==null){
singletonObject3=new SingletonObject3();
}
}
}
return singletonObject3;
}
}
//4.静态内部类
//实现懒加载,内部类只加载一次,线程安全
//静态内部类的加载是在程序中调用静态内部类的时候加载的,和外部类的加载没有必然关系,
//但是在加载静态内部类的时候 发现外部类还没有加载,那么就会先加载外部类.
//加载完外部类之后,再加载静态内部类(初始化静态变量和静态代码块etc)
//如果在程序中单纯的使用外部类,并不会触发静态内部类的加载
//类的加载时机:(暂时的认知里是四种)
//1.new 一个类的时候,
//2.调用类内部的 静态变量,
//3.调用类的静态方法,
//4.调用类的 静态内部类
class SingletonObject4{
private SingletonObject4(){
//为了防止暴力反射破坏私有构造方法,可以直接在获取构造方法时,直接抛出异常
throw new RuntimeException("不允许调用构造方法");
}
public SingletonObject4 getInstance(){
//外部类可以直接访问内部类的
return SingletonObjectInner.instance;
}
private static final class SingletonObjectInner{
private static final SingletonObject4 instance=new SingletonObject4();
}
}
//5.枚举式
//JAVA规定:不允许通过反射调用构造方法的类---枚举
//枚举类有以下特点:
//1.枚举常量在枚举类中是唯一的,并且是不可改变的。
//2.枚举常量可以具有属性、方法和构造函数。
//3.枚举常量可以作为参数传递给方法或构造函数。
//4.枚举常量可以使用switch语句进行比较。
//5.枚举可以实现接口,但是不能继承其他类。
//6.枚举可以定义在类内部或外部,但是不能在方法内部定义。
//7.枚举常量通常使用大写字母表示,并且使用下划线分隔单词。
//8.枚举类可以具有静态方法和静态属性。
//9.枚举类可以实现Serializable和Comparable接口。
//10.枚举类可以使用valueOf()方法将一个字符串转换为枚举常量。
enum SingletonObject5 {
//1.枚举是一种特殊的类,每个枚举常量都是该类的一个实例。
//枚举实际上是一种语法糖,枚举类的每一个实例static final修饰的,只会在类加载时初始化一次,因此为单例,并且由JVM来保证线程安全。
// 枚举类实现单例模式的优势是:
// - 简洁易读,不需要额外的代码来保证单例。
// - 可以防止懒汉模式下的双重检查锁定(DCL)问题,因为枚举类型在类加载时就已经初始化了。
//2.枚举为什么是单例
// 因为当一个类为enum的时候,其会被编译为public static final T extends Enum,
// 因为是final修饰的,所以其首先是不能被继承的。其次,Enum类中只有一个构造,其源码的注释解释:唯一的构造函数,程序员无法调起此构造函数(protected修饰)。
// 它只能供编译器响应枚举类型声明发出的代码使用。
INSTANCE;
//3.枚举实现单例模式特点之——天然不受序列化影响
// 首先,枚举都是默认继承自java中Enum类的,枚举类实现Serializable接口,但在枚举类中禁用了readObject等一系列方法(通过直接抛异常)。
// 我们知道,如果一个类实现Serializable接口,那么就不可能是单例,因为每次调用readObject方法都会返回一个新的实例。
// 所以,完全可以通过序列化来破坏单例,但是枚举类有其自己的一套序列化方式,因此其禁用readObject方法。所以,不会因为序列化而破坏单例。
//4.枚举实现单例模式特点之——天然禁止暴力反射
// 枚举避免了反射破坏单例,因为枚举类型没有构造方法,且来自父类Enum类的构造方法无法继承,故无法被反射创建。
//5.枚举类为什么没有公有的构造方法
// 枚举没有构造方法,是因为枚举类是一种特殊的类,它的实例是固定的,并且由JVM在类加载时创建。
// 如果枚举有公有的构造方法,那么就可以在外部创建新的枚举实例,这样就破坏了枚举的唯一性和不变性。
// 所以,枚举类只能有私有的构造方法,这样就保证了只有在枚举类内部才能创建枚举实例。
public void whateverMethod() {
//do something
}
}
//单例模式的扩展——序列化问题
//创建完单例对象之后,有时候我们会使用序列化将对象写入磁盘,当下次使用时再从磁盘中反序列化转化为内存对象,这样也会破坏单例模式。
//那么如何保证在序列化的情况下保证单例呢?很简单,只需要增加readResolve方法。
//在jdk源码中,规定了这个方法已经约定方法名称readResolve.这时候执行类中的readResolve方法,直接返回已经创建的实例。
//即在源码中,只要实现序列化接口的单例类拥有一个名为readResolve的返回单例对象的私有方法,那么反序列化后的结果变成了在单例类中已创建的实例对象。
class HungrySingleton implements Serializable {
private static final HungrySingleton instance;
static {
instance = new HungrySingleton();
}
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return instance;
}
private Object readResolve(){
return instance;
}
}