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;
    }
}

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

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

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

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 什么什么ant(初级会计的职称是什么)

    2019独角兽企业重金招聘Python工程师标准>>>…

  • 面试题(状态压缩dp)

    面试题(状态压缩dp)题解状态压缩dp,f[i][j]代表第i行状态为j的方案数#include<bits/stdc++.h>using namespace std;#define x first#define y second#define send string::npos#define lowbit(x) (x&(-x))#define left(x) x<<1#define right(x) x<<1|1#define transformu(s) tr..

  • ICA独立成分分析去除脑电伪影「建议收藏」

    ICA独立成分分析去除脑电伪影「建议收藏」点击上面"脑机接口社区"关注我们更多技术干货第一时间送达关于脑电图EEG,Rose分享过很多,可以查看《什么是EEG以及如何解释EEG?》《EEG数据、伪影的查看与清洗》…

  • 用python实现关机程序_python实现重启关机程序

    用python实现关机程序_python实现重启关机程序python实现重启关机程序发布于2014-08-2523:12:16|595次阅读|评论:0|来源:网友投递Python编程语言Python是一种面向对象、解释型计算机程序设计语言,由GuidovanRossum于1989年底发明,第一个公开发行版发行于1991年。Python语法简洁而清晰,具有丰富和强大的类库。它常被昵称为胶水语言,它能够把用其他语言制作的各种模块…

  • Java环境变量配置详细步骤

    Java环境变量配置详细步骤引言很多初学Java的小伙伴可能都会听别人说想要编译运行Java程序需要配置环境变量,所以在这里我就手把手教给你如何配置Java环境变量;再多说一句,可能会有小伙伴想:我编译运行Java程序干嘛要配置环境变量呢,直接用IDEA等开发工具不好嘛;其实对于Java初学者,学习Java最好开始不要使用这些开发工具,因为这些工具功能实在是太强大了,并不适合开始学习Java,不利于打好基础;所以开始最好还是老老实实用DOS编译运行Java程序吧!;注:电脑系统是win10下载JDK至于什么是JDK还有到底有

  • 《Android应用开发揭秘》内容简介「建议收藏」

    《Android应用开发揭秘》内容简介「建议收藏」关于本博客《Android应用开发揭秘》分类中的文章,欢迎转载。     最近,本博客关于Android的文章更新速度慢了不少,这几个月以来在写一本关于Android应用开发的书籍——《Android应用开发揭秘》,经过三四个月的努力,本书终于定稿,现在已交由机械工业出版社华章公司进行出版,从本书的策划编辑处得到消息,预计本书于12月15日印刷完毕。所以很快就会和大家见面了,期待…

发表回复

您的电子邮箱地址不会被公开。

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