• 单例模式--饿汉模式, 懒汉模式


    单例模式

    单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.

    单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.

    饿汉模式

    类加载的同时, 立即创建实例

    class Singleton{
        private static Singleton instance = new Singleton();
        private Singleton() {}
        public static Singleton getInstance() {
            return instance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    private static Singleton instance = new Singleton();: 带有static的属性称为类属性, 由于每个类的类对象(类名.class)是单例的, 则类对象的属性也就是单例的了. 这个代码时Singleton类被JVM加载的时候执行的.

    将_构造方法置成private_, 就可以防止该类在类外被创建的情况, 也就可以形成单例模式.

    如何创建实例?

    public class Main {
        public static void main(String[] args) {
            Singleton s1 = Singleton.getInstance();
            //Singleton s2 = new Singleton();报错
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    Singleton.getInstance得到的一直是同一个对象.

    在多线程中, 如果有多个线程同时调用getInstance方法, 他读到的都是同一个变量, 所以

    饿汉模式是线程安全的

    懒汉模式

    类加载的时候不创建实例. 第一次使用的时候才创建实例.

    一般情况下, 懒汉模式一般效率比较高.

    例如, 编辑器要打开一个超大的文件(10G), 有的编辑器会尝试把所有的内容都加载到内存中, 再显示出来; 而有的编辑器则只加载一部分内容, 其他部分, 等用户需要用到了再加载

    class SingletonLazy {
        private static SingletonLazy instance = null;
        public static SingletonLazy getInstance() {
            if(instance == null) {
                instance = new SingletonLazy();
            }
            return instance;
        }
        private SingletonLazy() {}
    }
    public class Main {
        public static void main(String[] args) {
            SingletonLazy s1 = SingletonLazy.getInstance();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    很明显, 这里是调用getInstance后才创建实例.

    那么如果多个线程同时调用getInstance, 就会很容易引发线程安全问题, 因为上述代码的赋值操作并不是原子的, 所以懒汉模式是线程不安全的

    如何解决线程安全问题

    我们可以想到:加锁, 让赋值操作变成原子性的.

    但是加锁是一个成本比较高的操作, 加锁可能会引起阻塞等待, 所以非必要不加锁. 如果无脑加锁, 就会导致程序执行效率受到影响. 所以这里该如何加锁?

    class SingletonLazy {
        private static SingletonLazy instance = null;
        public static SingletonLazy getInstance() {
            synchronized (SingletonLazy.class) {
                if(instance == null) {
                    instance = new SingletonLazy();
                }
            }
            return instance;
        }
        private SingletonLazy() {}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    如果是上述这样加锁, 那么每一次执行到这个代码的时候都会进行加锁, 但这并不是必要的, 因为引发线程不安全, 是在首次new对象的时候, 一旦把对象new好了, 就不会再出现线程不安全的问题了. 所以这样写会降低效率.

    所以只需在instance是null, 需要new对象的时候才需要进行加锁. 所以代码应该如下:

    class SingletonLazy {
        private static SingletonLazy instance = null;
        public static SingletonLazy getInstance() {
            if(instance == null) {
                synchronized (SingletonLazy.class) {
                    if(instance == null) {
                        instance = new SingletonLazy();
                    }
                }
            }
            return instance;
        }
        private SingletonLazy() {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    T1T2
    t1if(instance == null)
    t2if(instance == null)
    t3synchronized (SingletonLazy.class)阻塞等待
    t4if(instance == null)…
    t5释放锁synchronized (SingletonLazy.class)
    t6if(instance == null)

    问题又来了, T2线程一定能读到T1线程修改的值吗?

    所以这里可能又存在了一个内存可见性问题, 所以需要给instance加上一个volatile

    class SingletonLazy {
        private static volatile SingletonLazy instance = null;
        public static SingletonLazy getInstance() {
            if(instance == null) {
                synchronized (SingletonLazy.class) {
                    if(instance == null) {
                        instance = new SingletonLazy();
                    }
                }
            }
            return instance;
        }
        private SingletonLazy() {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    同时, volatile还可以解决指令重排序的问题

    instance = new SingletonLazy();这个操作可以分成三步:

    1. 给对象创造出内存空间, 得到内存地址.
    2. 在内存空间上调用构造方法, 对对象进行初始化
    3. 把内存地址复制给instance引用.

    一般的执行顺序使1->2->3, 但也可能会被优化成1->3->2. 假设T1是按照1->3->2执行的, 并且在执行完3后2之前出现了线程切换, 此时还未对对象进行初始化就调度给别的线程了. T2线程执行的时候, 判断instance非空, 于是直接返回instance, 并且后续可能会使用到instance中的一些属性和方法, 这样就可能会出现一些问题.

    给instance加上volatile之后, 针对instance的操作就不会发生指令重排序的问题了.

  • 相关阅读:
    C++程序设计期末考试复习试题及解析 3(自用~)
    内存模型与C++ 内存序
    ts面试题总结
    TELEC认证标准是什么?
    Google 开源库Guava详解(集合工具类)
    torch.roll
    selenium中定位shadow-root,以及获取shadow-root内部的数据
    出现Browse information of one xxxx解决方法
    基于安卓大学生兼职APP设计与实现
    flink1.14 sql基础语法(一) flink sql表查询详解
  • 原文地址:https://blog.csdn.net/m0_73594607/article/details/134429188