单例模式(上)---如何优雅地保证线程安全问题

上次帅地问的问题,让小秋学习了不少。这几天小秋刚好学习了一些设计模式的知识,这不,又跑去找帅地探讨一些问题了。

粗糙的同步

小秋:地哥,上次你问的问题,让我收获颇多,这些天我大致研究了下设计模式,帅地有什么指教的吗?

帅地:小子,行啊。那我再考考你得了。

此刻小秋聚精会神着等帅地又会抛出哪些问题…..

帅地:学过单例模式吧?单例模式有多种写法,写一种出来看看。

小秋:好啊,听说单例模式是面试中问的最多的一种模式,对于单例模式的几种的写法,我可以相当熟练哦(有点得意)。

于是,小秋甩手就写了一种懒汉模式的代码出来

public class Singleton {
    private Singleton instance = null;
    //私有构造函数
    private Singleton(){};

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

懒汉模式:就是等到有线程调用getInstance这个方法时,才来创建对象实例。与懒汉模式相反的是饿汉模式,下篇会讲到。

帅地:够熟练的你,不过你这段代码并非线程安全的,怎么办?

小秋:嘿嘿,简单,看我的。

public class Singleton {
    private Singleton instance = null;
    //私有构造函数
    private Singleton(){};

    //多了个synchronized关键字
    public   synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

注:不清楚synchronized关键字的可以看我之前写的文章:线程安全(上)—彻底搞懂synchronized(从偏向锁到重量级锁)

双重检测机制

帅地:你刚才的那种线程不安全的写法,你知道是在什么时候调用这个方法,会出现线程安全问题吗?

小秋:知道了,主要是因为,当这个实例对象还没有被创建过的时候,突然同时有几个线程来创建,就有可能会出现线程安全问题导致创建了不止一个实例。

但是,如果这个实例已经被安全着创建了之后,以后不管有再多的线程来调用,那么都不会出现线程安全的问题,因为这个if语句里面的代码永远不会被执行。

帅地:分析的很好,那么问题来了。当一个对象被创建之后,以后有线程来调用这个方法,本来可以不用进入同步块也能保证线程安全的,可是,你把synchronized声明在了方法名称前,导致之后该方法的调用都会进入同步快,这样很影响速度。

小秋:原来这样,怪不得我看书本说,不推荐这种做法,那我改一下:

public class Singleton {
    private Singleton instance = null;
    //私有构造函数
    private Singleton(){};
    public   Singleton getInstance() {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
        return instance;
    }
}

帅地:小秋,你这样其实和上面那个是几乎一样的,因为你把if(instance == null)这句话的判断放在了同步块内,以后有线程调用这个方法,还是会每次都进入同步块的。

其实,我们需要的是,当判断到instance != null时,就直接把instance返回了,而不是把这个判断放到同步块里。

小秋:我知道怎么做了。

于是,不一会,小秋劈里啪啦就写好了

public class Singleton {
    private Singleton instance = null;
    //私有构造函数
    private Singleton(){};

    //这种是什么鬼方式?
    public   Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class){
                instance = new Singleton();
            }
        }
        return instance;
    }
}

帅地:你确定你这段代码是线程安全的吗?

小秋赶紧在脑子里模拟了一下当实例对象还没有被创建时,有两个进程同时进入了if(instance == null){}代码块中,结果发现这两个对象都会成功创建新的对象实例。

于是,小秋赶紧在同步块中又加了一层if判断。

public class Singleton {
    private Singleton instance = null;
    //私有构造函数
    private Singleton(){};

    //双重检测机制
    public   Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class){
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

帅地:脑子栋的挺快嘛,这样子基本就能保证线程安全了。嗯,很棒。这种加锁的方法也叫做双重检测机制

解释说明:当instance==null时,假如有两个线程p1,p2进入了第一个if语句,之后p1进入的同步块中,成功创建了对象实例,这时候论到p2进入同步块,由于同步块还有一层if(instance==null)的判断,又因为此时instance != null了,所以p2无法再创建新的实例对象。

小秋听到帅地夸奖自己,满脸开心….

指令重排的捣蛋

帅地:不过,你这样写,还不算是绝对的线程安全,还是有可能会出现线程安全问题。你在仔细想想。

小秋:还会出现线程安全问题?(一脸懵逼)….

一阵绞尽脑汁过后….

小秋:我觉得没啥问题啊。

帅地:好吧,你已经写的挺不错了,今天就再让你涨涨知识。

其实这个线程安全的问题,主要是因为对象的创建过程并非是原子性的。在创建的过程中,由于指令重排的影响,才导致出现问题的。

所谓指令重排就是改变了指令的执行顺序,例如代码中有两行代码:int a = 10;int b = 20;由于虚拟机指令重排的影响,编译后有可能顺序被改变了,变成这样:int b = 20;int a = 10;

且听我慢慢道来:

当我们的虚拟机在执行 instance = new Singleton这句代码时,会被分解成以下三个动作来执行:

memory = allocate();//1: 给对象分配内存空间。

ctorInstance(memory);//2: 初始化对象

instance = memory; //3: 把instance变量指向刚刚分配的内存地址。

但是,这三个动作的执行顺序并非是一成不变的,有可能经过JVM和CPU的优化编译之后,这三个动作的执行顺序发生了改变,变成了这样:

memory = allocate();//1: 给对象分配内存空间。

instance = memory; //3: 把instance变量指向刚刚分配的内存地址

ctorInstance(memory);//2: 初始化对象

现在假设instance== null,且有p1, p2两个线程来调用这个方法。当p1执行完1,3但还没有执行2时,这时instance已经不再是null了。假如这个时候p2刚刚进入getInstance这个方法,然后执行if(instance == null)的判断语句,这个时候判断的结果会是false,于是p2直接把instance给返回的。

但由于p1还没有执行动作2,此时的对象还没有被初始化,但却已经被p2给返回了。此时,这个被返回的对象出现问题了。

于是,就出现了线程安全问题。

通过volatile来保证指令重排问题

小秋:又涨知识了。

帅地:问题的根源就是指令重排的影响,所以我们只要保证在创建对象的时候,不要出现指令重排就可以了。

所以说,我们可以把instance这个变量声明为volatile。代码如下:

public class Singleton {
    private volatile Singleton instance = null;
     //私有构造函数
    private Singleton(){};

    public   Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class){
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

注:不清楚volatile关键字的可以看我之前写的文章:
线程安全(上)—彻底搞懂volatile关键字

这样,只有把instance声明为volatile,那么虚拟机就会保证这三个动作按照顺序执行了,也就不会出现线程安全问题了。

小秋:哇,谢谢地哥的耐心讲解呢。我要给你点个赞。

结束语

这两次的文章都采用对话的模式来写,之后的文章可能会更多的采用文字对话漫画对话的形式讲,主要是因为现在还没找到比较喜欢的漫画人物,过几天找到了,就尝试用漫画来讲。

小伙伴们如果有好的人物推荐,可以通过加我为好友或者公众号后台发给我哦,在此感谢大家的支持。

获取更多原创文章,可以关注下我的公众号:苦逼的码农,后台回复礼包送你一份时下热门的资源大礼包。同时也感谢把文章介绍给更多需要的人。

?

原文地址:https://www.cnblogs.com/kubidemanong/p/9665339.html

时间: 2024-11-08 06:40:08

单例模式(上)---如何优雅地保证线程安全问题的相关文章

复现一个典型的线上Spring Bean对象的线程安全问题(附三种解决办法)

问题复现 假设线上是一个典型的Spring Boot Web项目,某一块业务的处理逻辑为: 接受一个name字符串参数,然后将该值赋予给一个注入的bean对象,修改bean对象的name属性后再返回,期间我们用了 Thread.sleep(300) 来模拟线上的高耗时业务 代码如下: @RestController @RequestMapping("name") public class NameController { @Autowired private NameService n

线程安全问题分析

1.为什么会出现线程安全问题 计算机系统资源分配的单位为进程,同一个进程中允许多个线程并发执行,并且多个线程会共享进程范围内的资源:例如内存地址.当多个线程并发访问同一个内存地址并且内存地址保存的值是可变的时候可能会发生线程安全问题,因此需要内存数据共享机制来保证线程安全问题. 对应到java服务来说,在虚拟中的共享内存地址是java的堆内存,比如以下程序中线程安全问题: public class ThreadUnsafeDemo { private static final ExecutorS

单例模式与线程安全问题浅析

近期看到到Struts1与Struts2的比較.说Struts1的控制器是单例的,线程不安全的:Struts2的多例的,不存在线程不安全的问题.之后又想到了之前自己用过的HttpHandler... 这些类.好像单例的线程安全问题确实是随处可见的. 可是仅仅是知道这个是不安全的,也没有认真分析过.接下来就细致分析下. 一,改动单例模式代码 首先我先写一段单例类的代码: /** * @ClassName: Sigleton * @Description: 单例类 * @author 水田 * @d

设计模式——单例模式(Java)——考虑多线程环境下的线程安全问题

设计模式--单例模式(Java)--考虑多线程环境下的线程安全问题 一:单例模式概念 单例模式是一种常用的软件设计模式.在它的核心结构中只包含一个被称为单例的特殊类.通过单例模式可以保证系统中一个类只有一个实例 二:单例模式的实现方式 特别注意,在多线程环境下,需要对获取对象实例的方法加对象锁(synchronized) 方式一:(懒汉式)程序执行过程中需要这个类的对象,再实例化这个类的对象 步骤: 1.定义静态私有对象 2.构造方法私有化保证在类的外部无法实例化该类的对象 3.定义对外开放的静

单例模式的线程安全问题小结

单例会带来什么问题? 如果多个线程同时调用这个实例,会有线程安全的问题 单例一般用在什么地方? 单例的目的是为了保证运行时只有唯一的一个实例,最常用的地方比如拿到数据库的连接,或者Spring的中创建BeanFactory操作,而这些操作都是调用他们的方法来执行某个特定的动作. 首先先来认识下两种模式:  恶汉式  懒汉式 public class MyFactory { // //饿汉式 立即创建 // private static MyFactory instance = new MyFac

双重检查锁实现单例模式的线程安全问题

一.结论 双重校验锁的单例模式代码如下: public class Singleton { private static Singleton singleton; private Singleton() {} public static Singleton getSingleton() { if (singleton == null) { // 1 synchronized (Singleton.class) { // 2 if (singleton == null) { // 3 single

线上Spark处理Bzip2引出Hadoop Bzip2线程安全问题

我们的Hadoop生产环境有两个版本,其中一个是1.0.3,为了支持日志压缩和split,我们添加了hadoop-1.2中关于Bzip2压缩的feature. 一切运行良好. 为了满足公司对迭代计算的需求(复杂HiveSQL,广告推荐算法,机器学习 etc), 我们构建了自己的Spark集群,最初是Standalone Mode,版本spark-0.9.1,支持Shark. 上线后,问题接踵而来,最为致命的是,shark在处理Hadooop bzip2文件时计算结果通常会有偏差,有时差的特别离谱

设计模式(003) 单例模式[上] 单身懒汉

 设计模式(003) 单例模式[上] 单身懒汉 什么是单例模式(What)?    GOF:"保证一个类仅有一个实例,并且提供一个访问它的全局访问点". "嗯...,GOF通常一言九鼎,单例就是这样子的." -- OO先生边思考边说. YSJIAN  :"等等,我插一句,保证应用中一个类最多只有一个实例存在,并提供一个全局访问点访问它". 只见OO先生会心一笑. 为什么用单例模式(Why)? 从What中貌似一目了然了,GOF和YSJIAN说的都

servlet 线程安全问题的解决

一,servlet容器如何同时处理多个请求. Servlet采用多线程来处理多个请求同时访问,Servelet容器维护了一个线程池来服务请求. 线程池实际上是等待执行代码的一组线程叫做工作者线程(Worker Thread),Servlet容器使用一个调度线程来管理工作者线程(Dispatcher Thread). 当容器收到一个访问Servlet的请求,调度者线程从线程池中选出一个工作者线程,将请求传递给该线程,然后由该线程来执行Servlet的service方法. 当这个线程正在执行的时候,