博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
单例模式与线程安全问题浅析
阅读量:5912 次
发布时间:2019-06-19

本文共 3615 字,大约阅读时间需要 12 分钟。

           最近看到到Struts1与Struts2的比较,说Struts1的控制器是单例的,线程不安全的;Struts2的多例的,不存在线程不安全的问题。之后又想到了之前自己用过的HttpHandler。。。这些类,好像单例的线程安全问题确实是随处可见的。但是只是知道这个是不安全的,也没有认真分析过尴尬。接下来就仔细分析下。

一,修改单例模式代码

      首先我先写一段单例类的代码:

        

/** * @ClassName: Sigleton * @Description: 单例类 * @author 水田 * @date 2015年12月19日 上午10:12:55 */public class Sigleton {    private static Sigleton sigleton;    private Sigleton() {	// put the initMethod for this class    };    public static Sigleton getInstance() {	// in this demo ,we use "Lazy-load Singleton"	if (sigleton == null) {	    sigleton = new Sigleton();	}	return sigleton;    }}

   这里我使用的是延迟加载,无论是使用延迟加载还是下面的饿汉式:

public class Sigleton {    private static final Sigleton sigleton=new Sigleton();    private Sigleton() {	// put the initMethod for this class    };    public static Sigleton getInstance() {	return sigleton;    }}

       这两种情况使用哪一种,要根据实际情况来判断:到底我是要在此类还没有使用之前进行初始化,还是要在用到它去拿它的时候才初始化,还要看你的实际应用场景。比如说,我这个类超级大,这时候,你部署好了之后,就把它New了,然后放在内存中,十年八年的没人用,这不是浪费么?(情况举的比较极端,就是这个意思吧。不过你要是硬要跟我说,内存啥的越来越不值钱,或者爷有的是钱买内存,我也没办法!只能送你句,怪不得你没有女朋友!)

      仔细分析这两种方法,然后看看哪里存在线程不安全的因素。

      先来瞅瞅第一种,lazy-load方式:

      在调用getInstance的时候,先判断,是不是已经被New过了,如果没,那么我new,完了之后返回。想象下,多线程,当在一个线程内,执行到if (sigleton == null),另一个线程内,好巧啊,也执行到这里。然后两个线程同时判断发现还没这个东西,然后各自new一个。破坏了我的单实例的原则。

     相比第二种直接new的方式,这种方法显然是不安全的。但是,如果我要用到这种lazy-load方式,就要对它进行改进了。

   

     简单改进:

       

public static synchronized Sigleton getInstance()

     加个关键字。

     但是这种方法还是不好,产生问题的只有sigleton = new Sigleton();现在我锁定了整个方法,有点儿多余了。再改下:

  

public final Sigleton getInstance() {	// in this demo ,we use "Lazy-load Singleton"	if (sigleton == null) {	    synchronized (this) {		sigleton = new Sigleton();	    }	}	return sigleton;    }

      然后改完了之后,我们再从逻辑上看下是不是有漏洞:

      还是刚才的问题,俩线程,同时执行到if判空的时候,第一个线程由于调度原因,进入同步方法,执行了new操作,第二个线程判空完了之后,进不去,还等在同步方法外面。第一个线程出了同步方法,第二个线程进入了同步方法,又new了一个对象。。。。貌似确实有逻辑楼栋,再改下:

  

public final Sigleton getInstance() {	// in this demo ,we use "Lazy-load Singleton"	if (sigleton == null) {	    synchronized (this) {		if (sigleton == null){		sigleton = new Sigleton();		}	    }	}	return sigleton;    }

    当第二个线程进入同步方法之后,要不要新new一个对象,还要判断下。

二,After双重检查

    上面的写法一方面实现了Lazy-Load,另一个方面也做到了并发度很好的线程安全,一切看上很完美。 
    但是二次检查自身会存在比较隐蔽的问题,查了Peter Haggar在DeveloperWorks上的一篇文章,对二次检查的解释非常的详细: 

          “双重检查锁定背后的理论是完美的。不幸地是,现实完全不同。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因。” 

    使用二次检查的方法也不是完全安全的,原因是 java 平台内存模型中允许所谓的“无序写入”会导致二次检查失败,所以使用二次检查的想法也行不通了。 

    Peter Haggar在最后提出这样的观点:“无论以何种形式,都不应使用双重检查锁定,因为您不能保证它在任何 JVM 实现上都能顺利运行。” 

    问题在哪里? 

    假设线程A执行到了判断对象为空,于是线程A执行去初始化这个对象,但初始化是需要耗费时间的,但是这个对象的地址其实已经存在了。此时线程B也执行到了判断不为空,于是直接跳到后面去得到了这个对象。但是,这个对象还没有被完整的初始化!得到一个没有初始化完全的对象有什么用!! 

    关于这个Double-Checked Lock的讨论有很多,目前公认这是一个Anti-Pattern,不推荐使用! 
                                                                                                                                           (from 网友)

三,如何安全+单例使用

         首先说明下,饿汉模式是线程安全的;但是在某些情况下,比如,我们不得不使用lazy-load方式,可以考虑以下方法:

         1,使用volatile关键字

private volatile static Sigleton sigleton;
         
    

        有些人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。从「先行发生原则」的角度理解的话,就是对于一个 volatile变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。

 

         但是特别注意在 Java 5 以前的版本使用了volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5中才得以修复,所以在这之后才可以放心使用 volatile。

   2Initialization on Demand Holder

           
public class Sigleton {    private static class SigletonHolder {	private static final Sigleton INSTANCE = new Sigleton();    }    private Sigleton() {    };    public static final Sigleton getInstance() {	return SigletonHolder.INSTANCE;    }}
    
      在使用sigleton时候,SigletonHolder会被初始化,但是里面的INSTANCE却不会,只有当我们调用getInstance方法的时候,才会去new。既没有高大上的关键字,逻辑上也好理解。
    仔细分析,感觉还是蛮多问题的~

        

你可能感兴趣的文章
数据可视化之 Sankey 桑基图的实现
查看>>
项目实战-Api的解决方案
查看>>
前端面试题总结
查看>>
(三)从jvm层面了解线程的启动和停止
查看>>
SOA和微服务之间的区别
查看>>
IBM提出8位深度网络训练法,提速4倍同时保持高精度
查看>>
苹果发布Core ML 2
查看>>
“智能云”战略新品震撼发布,开发者如何快速上手?
查看>>
华为吴晟:分布式监控系统的设计与实现
查看>>
[deviceone开发]-do_Webview的基本示例
查看>>
亚马逊Alexa借助神经网络生成播音员声音
查看>>
比特大陆新一轮裁员50%,回应称系人员调整
查看>>
将 Measurements 和 Units 应用到物理学
查看>>
如何确定所运行的 SQL Server 2005 的版本?
查看>>
我的友情链接
查看>>
自动化安装Mysql5.6-脚本实现
查看>>
分布式事务:不过是在一致性、吞吐量和复杂度之间,做一个选择
查看>>
【云图】如何设置支付宝里的家乐福全国连锁店地图?
查看>>
对于json_lib包的使用
查看>>
scala可变长度参数(二)
查看>>