• JavaEE 第8节 单例模式详解


    目录

    概念

    饿汉模式

    懒汉模式

    懒汉模式在多线程环境下的优化

    1.线程安全问题

    2.效率问题

    3.指令重排序导致的问题

    1)为什么要进行指令重排序?

    2)指令重排序在上述代码为什么会构成问题?


    导读:

    单例模式是一种设计模式

    简单来讲设计模式就类似于下棋的棋谱,在特定的场景下使用这种模式(固定套路),可以让程序达到一个不错的效果。设计模式也是和编程语言相关的,有些设计模式是在给一些语言的语法填坑,而有些语言又不太依赖设计模式。

    设计模式适合具有一定编程经验之后再去主要学习,如果缺乏变成经验,难以理解,别人这么设计的好处。

    概念

    单例模式的概念很简单,顾名思义,既在一个线程中一个类只包含一个对应的实例化对象

    多线程程序中,有些场景就是要求只能创建一个实例化对象。

    比如JDBC的设置数据源:

    一个数据库,对应的MySQL服务器只有一份,DataSoruce这个类就没有必要new多份。

    当然JDBC这块知识不了太解没关系,主要是告诉你,单例模式在多线程程序中其实是非常重要的

    单例模式的写法有很多,这里介绍两个最常用、最主流的写法:饿汉模式懒汉模式

    饿汉模式

    饿汉的饿,其实突出的是实例的创建时间比较早是在类被加载的时候就创建了(可以近似的理解为在程序启动时创建)

    为SingleL类写一个单例模式,用饿汉模式:

    1. class SingleL{
    2. private static SingleL singleL=new SingleL();//直接new一个
    3. public static SingleL getSingleL(){
    4. return singleL;
    5. }
    6. }

    懒汉模式

    懒汉的懒,其实突出的是实例的创建时间比较晚

    这里的晚,指的是,程序在需要这个类的时候才去实例化它:

    1. class SingleL{
    2. private static SingleL instance=null;//先置为空,要的时候才实例化
    3. public static SingleL getInstance(){
    4. if(instance==null){//没有创建,先创建,有就直接返回
    5. instance=new SingleL();
    6. }
    7. return instance;
    8. }
    9. }

    懒汉模式有一个优点,就是效率高,在计算机中其实是一个褒义词,勤快反而是一个贬义词。

    为什么这么说呢?

    最典型的场景就是打开一个内存比较大的文档,为了有一个更好的用户体验,响应速度因该是越快越好的,如果程序加载很“勤快”(提前加载完所有文档内容),打开文档程序所需的时间势必会变长,用户体验感就会变差。

    但是如果程序加载比较的“懒”(先只加载几页,之用户想要看那一页,在加载那一页),响应速度就变得快了,用户体验感也会不错。

    懒汉模式在多线程环境下的优化

    1.线程安全问题

    刚才的懒汉模式的代码在多线程环境下,肯定会造成线程安全问题,因为程序中不仅对变量进行了修改,而且读取和修改操作不是原子性的。        

    1. class SingleL{
    2. private static Object lock=new Object();
    3. private static SingleL instance=null;//先置为空,要的时候才实例化
    4. public static SingleL getInstance(){
    5. synchronized(lock){
    6. /*注意读写操作都要放到同步块中*/
    7. if(instance==null){
    8. instance=new SingleL();
    9. }
    10. }
    11. return instance;//返回之加不加到同步块中都无所谓,因为线程安全问题已经解决
    12. }
    13. }

    2.效率问题

    这个问题是由上面解决了线程安全问题诱发的新的问题。

    1. public static SingleL getInstance(){
    2. synchronized(lock){
    3. /*注意读写操作都要放到同步块中*/
    4. if(instance==null){
    5. instance=new SingleL();
    6. }
    7. }
    8. return instance;//返回之加不加到同步块中都无所谓,因为线程安全问题已经解决
    9. }

    假如说由多个线程都要调用getInstance()那么就很可能导致多次的上锁和解锁,因为每次都要去判断有没有创建这个单例对象,这是非常消耗时间的。

    解决办法也很简单,就是在线程安全的情况下,再次判断instance是否为null:

    1. class SingleL{
    2. private static Object lock=new Object();
    3. private static SingleL instance=null;//先置为空,要的时候才实例化
    4. public static SingleL getInstance(){
    5. if(instance==null){
    6. synchronized(lock){
    7. /*注意读写操作都要放到同步块中*/
    8. if(instance==null){
    9. instance=new SingleL();
    10. }
    11. }
    12. }
    13. return instance;//返回之加不加到同步块中都无所谓,因为线程安全问题已经解决
    14. }
    15. }

    这就极大避免了多次上锁的情况了,你细品,两个if(instance==null)都不是多余的!

    3.指令重排序导致的问题

    1)为什么要进行指令重排序?

    指令重排序和内存可见性一样都是编译器为了优化程序而引入的。

    假如说有1、2、3条指令。这三条指令如果顺序执行可能是不经济的。例如执行1指令的时候需要和某个其他的指令同时争抢某一个资源导致冲突,但是如果先执行2,然后执行1就可以避免这种情况发生。

    再比如这个形象的例子,老妈让你出去菜市场买三样东西:葱、姜、蒜:

    为了节省时间继续打游戏,当然先去姜蒜两个摊位把东西买了,然后最后去葱这个摊位买啊。

    2)指令重排序在上述代码为什么会构成问题?

    在优化后的代码中new SingleL在编译时,可以大致分解成三个指令:

    1、给对象分配内存空间。

    2、调用构造函数初始化对象

    3、将instance引用指向分配内存的空间

    通过指令重排序后,可能先执行1,然后直接执行3,最后执行2。

    这样就会出现一个不安全的时机,就是1、3都执行完了,但是2还没有执行,此时instance引用指向的是一个无效的内存,因为还没有初始化好对象。

    然后我们回到代码中来,假如说有两个线程,他们都刚开始执行,单例对象还没有创建:

    3)问题的解决办法

    指令重排序和内存可见性问题解决方式是一样的,用volatile关键字修饰变量。

    volatile的作用:
    1、保证变量可见性:一个线程对volatile变量修改,另一个线程可以立马看到。
    2、禁止指令重排序:防止编译器对volatile变量的读/写操作进行指令重排序。

    优化后的代码:

    1. class SingleL{
    2. private static Object lock=new Object();
    3. private static volatile SingleL instance=null;//先置为空,要的时候才实例化,最后volatile禁止指令重排序
    4. public static SingleL getInstance(){
    5. if(instance==null){
    6. /*在同步块中执行*/
    7. synchronized(lock){
    8. if(instance==null){
    9. instance=new SingleL();
    10. }
    11. }
    12. }
    13. return instance;
    14. }
    15. }

  • 相关阅读:
    基于HTML5的消灭星星网页小游戏设计
    STC51单片机38——按键控制舵机连续运动,稳定不抖动
    tr 命令
    进程的初识
    数据分析技能点-概括性度量
    MySQL进阶之性能优化与调优技巧
    我用PYQT5做的第一个实用的上位机项目(四)
    Qt——窗口
    SpringBoot注册web组件
    BCD码 (二进码十进数)
  • 原文地址:https://blog.csdn.net/2301_80636143/article/details/140682839