DCL之单例模式_实现一个单例模式

DCL之单例模式_实现一个单例模式DoubleCheckLock

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全家桶1年46,售后保障稳定

所谓的DCL 就是 Double Check Lock,即双重锁定检查,在了解DCL在单例模式中如何应用之前,我们先了解一下单例模式。单例模式通常分为“饿汉”和“懒汉”,先从简单入手

饿汉

所谓的“饿汉”是因为程序刚启动时就创建了实例,通俗点说就是刚上菜,大家还没有开始吃的时候就先自己吃一口。

public class Singleton { 
   
    private static final Singleton singleton = new Singleton();
    private Singleton(){ 
   }
    public static Singleton getInstance(){ 
   
        return singleton;
    }
}

Jetbrains全家桶1年46,售后保障稳定

第3行 通过一个私有构造方法限制了创建此类对象的途径(反射忽略)。这种方法很安全,但从某种程度上有点浪费资源,比方说从一开始就创建了Singleton实例,但很少去用它,这就造成了方法区资源的浪费,因此出现了另外一种单例模式,即懒汉单例模式

懒汉

之所以叫“懒汉”是因为只有真正叫它的时候,才会出现,不叫它它就不理,跟它没关系。也就是说真正用到它的时候才去创建实例,并不是一开始就创建实例。如下代码所示:


public class Singleton { 
   
    private static Singleton singleton = null;
    private Singleton(){ 
   }
    public static Singleton getInstance(){ 
   
        if(null == singleton){ 
   
            singleton = new Singleton();
        }
        return singleton;
    }
}

看似很简单的一段代码,但存在一个问题,就是线程不安全的问题。例如,现在有1000个线程,都需要这一个Singleton的实例,验证一下是否拿到同一个实例,代码如下所示:

public class Singleton { 
   
    private static Singleton singleton = null;
    private Singleton(){ 
   }
    public static Singleton getInstance(){ 
   
        if(null == singleton){ 
   
            try { 
   
                Thread.sleep(1);//象征性的睡了1ms
            } catch (InterruptedException e) { 
   
                e.printStackTrace();
            }
            singleton = new Singleton();
        }
        return singleton;
    }

    public static void main(String[] args) { 
   
        for (int i=0;i<1000;i++){ 
   
            new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
        }
    }
}

部分运行结果,乱七八糟:

944436457
1638599176
710946821
67862359

为什么会这样?第一个线程过来了,执行到第7行,睡了1ms,正在睡的同时第二个线程来了,第二个线程执行到第5行时,结果肯定为空,因此接下来将会有两个线程各自创建一个对象,这必然会导致Singleton.getInstance().hashCode()结果不一致。可以通过给整个方法加上一把锁改进如下:

改进1

public class Singleton { 
   
    private static Singleton singleton = null;
    private Singleton(){ 
   }
    public static synchronized Singleton getInstance(){ 
   
        if(null == singleton){ 
   
            try { 
   
                Thread.sleep(1);
            } catch (InterruptedException e) { 
   
                e.printStackTrace();
            }
            singleton = new Singleton();
        }
        return singleton;
    }

    public static void main(String[] args) { 
   
        for (int i=0;i<1000;i++){ 
   
            new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
        }
    }
}

通过给getInstance()方法加上synchronized来解决线程一致性问题,结果分析虽然显示所有实例的hashcode都一致,但是synchronized的粒度太大了,即锁的临界区太大了,有点影响效率,例如如果第4行和第5行之间有业务处理逻辑,不会涉及共享变量,那么每次对这部分业务逻辑加锁必然会导致效率低下。为了解决粗粒度的问题,可以对代码进一步改进:

改进2

public class Singleton { 
   
    private static Singleton singleton = null;
    private Singleton(){ 
   }
    public static Singleton getInstance(){ 
   
        /* 一堆业务处理代码 */
        if(null == singleton){ 
   
            synchronized(Singleton.class){ 
   //锁粒度变小
                try { 
   
                    Thread.sleep(1);
                } catch (InterruptedException e) { 
   
                    e.printStackTrace();
                }
                singleton = new Singleton();
            }
        }
        return singleton;
    }

    public static void main(String[] args) { 
   
        for (int i=0;i<1000;i++){ 
   
            new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
        }
    }
}

部分运行结果 :

391918859
391918859
391918859
1945023194

通过分析运行结果发现,虽然锁的粒度变小了,但线程不安全了。为什么会这样呢?因为有种情况,线程1执行完if判断后还没有拿到锁的时候时间片用完了,此时线程2来了,执行if判断时发现对象还是空的,继续往下执行,很顺利的拿到锁了,因此线程2创建了一个对象,当线程2创建完之后释放掉锁,这时线程1激活了,顺利的拿到锁,又创建了一个对象。所以代码还需要再一步的改进。

改进3

public class Singleton { 
   
    private static Singleton singleton = null;
    private Singleton(){ 
   }
    public static Singleton getInstance(){ 
   
        /* 一堆业务处理代码 */
        if(null == singleton){ 
   
            synchronized(Singleton.class){ 
   //锁粒度变小
                if(null == singleton){ 
   //DCL
                    try { 
   
                        Thread.sleep(1);
                    } catch (InterruptedException e) { 
   
                        e.printStackTrace();
                    }
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    public static void main(String[] args) { 
   
        for (int i=0;i<1000;i++){ 
   
            new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
        }
    }
}

通过在第10行又加了一层if判断,也就是所谓的Double Check Lock。也就是说即便拿到锁了,也得去作一步判断,如果这时判断对像不为空,那么就不用再创建对象,直接返回就可以了,很好的解决了“改进2”中的问题。但这里第8行是不是可以去了,我个人觉得都行,保留第8行的话,是为了提升效率,因为如果去了,每个线程过来就直接抢锁,抢锁本身就会影响效率,而if判断就几ns,且大部分线程是不需要抢锁的,所以最好保留。
到这DCL 单例的原理就介绍完了,但是还是有一个问题。就是需要考虑指令重排序的问题,因此得加入volatile来禁止指令重排序。继续分析代码,为了分析方便这里将Singleton代码简化:

public class Singleton { 
   
    int a = 5;//考虑指令重排序的问题
}

singleton = new Singleton()的字节码如下:

  0: new    #2           // class com/reasearch/Singleton
  3: dup
  4: invokespecial #3   // Method com/reasearch/Singleton."<init>":()V
  7: astore_1

先不管dup指令。这里补充一个知识点,创建对象的时候,先分配空间,类里面的变量先有一个默认值,等调用了构造方法后才给变量赋值。例如int a = 5 刚开始的时候 a = 0。字节码指令执行过程如下,

  1. new 分配空间,a=0
  2. invokespecial 构造方法 a=5
  3. astore_1将对象赋给singleton

这是理想的状态,2和3语义和逻辑上没有什么关联,因此jvm可以允许这些指令乱序执行,即先执行3再执行2 。回到改进3,假如线程1再执行第16行代码时,指令的执行顺序是1,3,2,当执行完3时,时间片用完了,此时a=0,也就是说初始化到一半时就挂起了。这时线程2 来了,第8行判断,singleton肯定不为空,因此直接返回一个Singleton的对象,但其实这个对象是一个问题对象,是一个半初始化的对象,即a=0 。这就是指令重排序造成的,因此为了防止这种现象的发生加上关键字volatile就可以了。因而,最终DCL之单例模式的代码完整版如下:

完整版

public class Singleton { 
   
    private volatile static Singleton singleton = null;//加上volatile 
    private Singleton(){ 
   }
    public static Singleton getInstance(){ 
   
        /* 一堆业务处理代码 */
        if(null == singleton){ 
   
            synchronized(Singleton.class){ 
   //锁粒度变小
                if(null == singleton){ 
   //DCL
                    try { 
   
                        Thread.sleep(1);
                    } catch (InterruptedException e) { 
   
                        e.printStackTrace();
                    }
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

至此,可以告一段落了,相信很多小伙伴都会写单例,但是了解其中的原理还是有一定的难度,大家一起加油!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/234215.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • httprunner(3)用脚手架快速搭建项目「建议收藏」

    httprunner(3)用脚手架快速搭建项目「建议收藏」前言如何快速搭建一个httprunner项目呢?我们可以使用脚手架,脚手架就是自动地创建一些目录,形成一个项目的架构,不需要我们再手动的去创建查看创建新项目的命令先来查看一下帮助命令httpr

    2022年7月31日
    7
  • PHP $_SERVER[‘HTTP_REFERER’] 获取前一页面的 URL 地址

    PHP $_SERVER[‘HTTP_REFERER’] 获取前一页面的 URL 地址

    2021年9月24日
    48
  • 产品经理必会知识:万字长文 | 史上最全的付费会员体系分析

    产品经理必会知识:万字长文 | 史上最全的付费会员体系分析1如何理解付费会员体系?老生常谈的AARRR模型中,会员体系在活跃、留存、收入端是最常见的运营手段,它有两种类型: 付费会员体系:通过付费,购买高价值、差异化的权益。 例:88VIP、京东PLUS、腾讯视频VIP、知乎盐选会员。 成长会员体系:通过特定用户行为带来的积累,获得等级成长及差异化权益(一般为低价值)。 例:大众点评会员、滴滴橙长会员、支付宝会员。 会员体系的核心目标,是拉升「付费用户比例」、「用户生命周期」、「ARPU」;在用户价值层面体现为…

    2022年6月16日
    52
  • 【微信小程序-0基础入门】项目发布完整流程

    【微信小程序-0基础入门】项目发布完整流程写在前面:上一节讲述了小程序的相关介绍以及账号注册,这一节讲述小程序发布的具体流程。目录?安装开发者工具?小程序项目发布流程?小程序代码的构成-项目结构?1.了解项目的基本组成结构?2.小程序页面的组成部分?小程序代码的构成-JSON配置文件?1.JSON配置文件的作用?2.app.json文件?3.project.config.json文件个人主页:个人主页系列专栏:系列专栏【微信小程序】?安装开发者工具微信开发

    2025年5月24日
    5
  • Android ORM 框架之 greenDAO

    Android ORM 框架之 greenDAO

    2021年9月30日
    43
  • MySQL 字符集 注意事项

    MySQL 字符集 注意事项utf8 unicode ci 与 utf8 general ci 区别 utf8 unicode ci 和 utf8 general ci 对中英文来说没有实质的差别 utf8 general ci 校对速度快 但准确度稍差 utf8 unicode ci 准确度高 但校对速度稍慢 若数据库中有德语 法语或者俄语需求 需使用 utf8 unicode ci 其他情况用 utf8 general ci 即可 如果你想使用 gb2312 编码 那么建议你使用 latin1 作为数据表的默认字符集 这样就能直

    2025年12月12日
    5

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号