目录

1.什么是单例模式?

2.如何保证单例?

3.两种写法

(1)饿汉模式(早创建)

 (2)懒汉模式(缓执行,可能不执行)

4.应用场景

🔥5.多线程中的单例模式

(1)加锁

(2)双重if

(3)volatie

6.指令重排序(小概率出现问题)

(1)什么是指令重排序?

 (2)以Instance = new SingletonLazy( );为例分析

(3)解决上述指令重排序问题

7.延伸:了解

🔥8.常见考察


1.什么是单例模式?

单例模式是最常见的设计模式之⼀

Q:啥是设计模式?

A:编程中典型场景的解决方案,设计模式好⽐象棋中的”棋谱”.红⽅当头炮,⿊⽅⻢来跳.针对红⽅的⼀些⾛法,⿊⽅应招的时候有⼀些固定的套路.按照套路来⾛局势就不会吃亏

单例模式即某个类在进程中又能有唯一实例

  • 有且只有一个对象,不会new出来多个对象,这样的对象就是“单例”

2.如何保证单例?

(1)保证单例:instance只有唯一一个,初始化也只是执行一次的

  • 保证单例:instance只有唯一一个,初始化也只是执行一次的
    static修饰的,其实是“类属性”,就是在“类对象”上的,每个类的类对象在JVM中只有一个,里面的静态成员,只有一份
  • 后续需要使用这个类的实例,就可以直接通过getInstance来获取已经new好的这个,而不是重新new

(2) 核心操作:private:禁止外部代码来创建该类的实例

  • 怎么操作?类之外的代码,尝试new的时候,势必就要调用构造方法,由于构造方法私有,无法调动,就会编译出错

3.两种写法

(1)饿汉模式(早创建)

唯一实例创建时机非常早,类似于饿了很久的人,​看到了吃的就赶紧开始吃(急迫)

package thread;

//单例,饿汉模式
//唯一实例创建时机非常早,类似于饿了很久的人,看到了吃的就赶紧开始吃(急迫)
class Singleton {
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }

    private Singleton() {

    }
}
public class Demo27 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);

        //Singleton s3 = new Singleton();
    }
}

 (2)懒汉模式(缓执行,可能不执行)

懒是提高效率,节省开销的体现 啥时候调用,就啥时候创建,如果不调用,就不创建了

package thread;

//单例模式,懒汉模式的实现
class SingletonLazy {
    private static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }

    private SingletonLazy (){

    }
}

public class Demo28 {
    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);

        //SingletonLazy s3 = new SingletonLazy();
    }
}

Q:如果有多个线程同时调用getInstance,是否会产生线程安全问题?

A

  • 饿汉模式不存在线程安全问题,而懒汉模式存在
     
  • 饿汉模式:实例早就有了,每个线程getInstance,就是读取上面的静态变量,多个线程读取同一个变量,是线程安全的
  • 懒汉模式:instance = new SingletonLazy,赋值操作就是修改,而且操作不是原子的,肯定就是线程不安全的

图解:懒汉模式:非原子性操作

  • 还可能存在其他执行顺序,t2再次new,可能创建多个实例
  • instance只是一个引用,instance中地址指向的那个对象可能就是一个大对象,上述代码会出现覆盖,第二个对象的地址覆盖了第一个,进一步第一个对象没有引用指向了,就会被GC回收(但是这个创建时间的开销,是客观存在的)

4.应用场景

(1)写的服务器,要从硬盘上加载100G的数据到内存(加载到若干个哈希表中),肯定要写一个类,封装上述加载操作,并且获取一些获取、处理数据的业务逻辑

  • 代码中的有些对象,本身就不应该是多个实例的,从业务角度就应该是单个实例
    一个实例就管理100G的内存数据
  • 搞多个实例,就是N*100G的内存数据,机器肯定吃不消,没必要

(2)MySQL的配置文件,专门管理配置,需要加载配置数据到内存中提供其他代码使用,这样的类也是单例的

  • 如果是多个实例,就存储了多份数据,如果一样还罢了,如果不一样,以哪个为准?

🔥5.多线程中的单例模式

懒汉模式的线程安全问题如何解决呢?

(1)加锁

Q:这样加锁线程就安全了吗?

public static SingletonLazy getInstance() {
        if (instance == null) {
            synchronized (locker) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }

A: 没有,t2拿到锁后,还在直接执行new操作

图解:

应该把if和new操作打包成一个原子操作

public static SingletonLazy getInstance() {
 
       synchronized (locker) {
            if(instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }

(2)双重if

 加锁之后的问题Q:懒汉模式只有最开始调用getInstance会存在线程安全问题,一旦把实例创建好了,后续再调用,就是只读操作,就不存在线程安全问题了,但是上述代码,只要一调用getInstance方法,就需要先加锁,再执行后续操作(后续没有线程安全问题,还要加锁,有开销

真正的解决A:再加一层判断

连续两次一样的条件判断(单线程中无意义,但是多线程含义就不一样)

第一层if:判定是否要加锁(new之前要加锁,new之后就不用加了)

第二层if:判断是否要创建对象

 指令级理解

 t2拿到锁,这个时候instance已经被t1修改了

(3)volatie

private static volatile SingletonLazy instance = null;

为了保证第一次线程修改,后续线程一定会读到,加上volatile【避免内存可见性+指令重排序问题

综上:懒汉模式的线程安全版

//单例模式,懒汉模式的实现
class SingletonLazy {
    private static volatile SingletonLazy instance = null;

    private static Object locker = new Object();
    public static SingletonLazy getInstance() {
        if (instance == null) {
            synchronized (locker) {
                if(instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    private SingletonLazy (){

    }
}

6.指令重排序(小概率出现问题)

编译器的优化策略有很多: ​

把读内存优化到读寄存器,指令重排序,循环展开,条件分支预测…

(1)什么是指令重排序?

编译器会在保证逻辑是等价的情况下,调整二进制指令的执行顺序,从而提高效率

  • 正常来说写的代码,最终会编译成为一系列的二进制指令,CPU会按照顺序,一条一条执行

 (2)以Instance = new SingletonLazy( );为例分析

  • 这行代码可以细分为三个步骤
    • 申请内存空间
    • 调用构造方法(对内存空间进行优化)
    • 把此时内存空间的地址,赋值给Instance引用
  • 在指令重排序的策略下,上述执行的过程,不一定是123,也可能是132(但是1一定先执行)
    • 132这样的执行顺序,就可能存在线程安全问题
  • 指令级理解

    • 3一旦执行完,就意味着instance就非null,但是指向的对象其实是一个未初始化的对象(里面的成员都是0)
    • 执行到t2的时候,instance已经非null了,这里的条件无法进行,直接返回未初始完毕的instance
    • 后续如果t2中还有其他逻辑,就会对未初始完毕的对象进行操作,这样存在严重的问题

(3)解决上述指令重排序问题

  • 加上volatile,主要是针对某个对象的读写过程中,不会出现重排序
    • 很多地方都能重排序,但是只是针对这一过程中
    • 这样t2线程读到的数据,一定是t1已经构造完毕的完整对象了(一定是123执行的对象)

7.延伸:了解

(1)单例模式要确保反射下安全,即使动用反射也无法破坏单例特性

(2)单例模式要确保序列化下安全,即使动用Java标准库的序列化机制,也无法破坏单例特性

  • enum类型的实例天然支持序列化和反序列化
  • 序列化:把对象转为二进制字符串

🔥8.常见考察

(1)为什么说饿汉式单例天生就是线程安全的?

实例早就有了,每个线程getInstance,就是读取上面的静态变量,多个线程读取同一个变量,是线程安全的

(2)传统的懒汉式单例为什么是非线程安全的?

instance = new SingletonLazy,赋值操作就是修改,而且操作不是原子的,肯定就是线程不安全的

(3)怎么修改传统的懒汉式单例,使其线程变得安全?

1.线程不安全的版本

2.加锁版本

3.加上双重if

4.最后加上volatile

(4)线程安全的单例的实现还有哪些,怎么实现?

静态内部类单例

public class StaticInnerClassSingleton {
    private StaticInnerClassSingleton() {}

    private static class SingletonHolder {
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

 静态内部类在外部类加载时不会被加载,只有在调用 getInstance 方法时才会加载类,从而创建实例。类加载过程是线程安全的,所以这种方式实现了线程安全的单例,并且具有延迟加载的特性

(5)双重检查模式、Volatile关键字在单例模式中的应用

1.双重检查模式:

第一层if:判定是否要加锁(new之前要加锁,new之后就不用加了)

第二层if:判断是否要创建对象

2.Volatile关键字:避免内存可见性+指令重排序问题

(6)ThreadLocal在单例模式中的应用

public class ThreadLocalSingleton {
    private static final ThreadLocal threadLocalInstance = 
        new ThreadLocal() {
            @Override
            protected ThreadLocalSingleton initialValue() {
                return new ThreadLocalSingleton();
            }
        };

    private ThreadLocalSingleton() {}

    public static ThreadLocalSingleton getInstance() {
        return threadLocalInstance.get();
    }
}

ThreadLocal 会为每个使用该实例的线程都提供一个独立的副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。在单例模式中使用 ThreadLocal,可以保证每个线程都有自己的单例实例,适用于需要在每个线程中维护单例状态的场景 

(7)枚举式单例

public enum EnumSingleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("Doing something...");
    }
}

 枚举式单例是实现单例模式的最佳方式之一。它是线程安全的,因为枚举类型的实例创建是由 JVM 保证线程安全的。而且枚举类型可以防止反序列化和反射攻击,因为 Java 规范中规定,枚举类型的 clone()、readObject()、readResolve() 等方法都不会破坏单例的唯一性。使用时可以直接通过 EnumSingleton.INSTANCE 来获取单例实例

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。