- 浏览: 143932 次
- 性别:
- 来自: 北京
文章分类
最新评论
-
august_000:
很有道理,我已经亲自测试过了:
public class ...
单例模式之线程安全解析 -
Chris_bing:
一个单例有这么多名堂,最后那个内部类的解决方案很有创意啊,受教 ...
单例模式之线程安全解析
本文综合网上资料以及代码时间,对要求延迟加载和线程安全的单例模式做了如下分析。
自励共勉。
面试的时候,常常会被问到这样一个问题:请您写出一个单例模式(Singleton Pattern)吧。
单例的目的是为了保证运行时Singleton类只有唯一的一个实例,最常用的地方比如拿到数据库的连接,Spring的中创建BeanFactory这些开销比较大的操作,而这些操作都是调用他们的方法来执行某个特定的动作。
很容易,顺手写一个《Java与模式》中的第一个例子:
这种写法就是所谓的饿汉式,每个对象在没有使用之前就已经初始化了。
问题来了,问题1:单例会带来什么问题?如果这个对象很大呢?没有使用这个对象之前,就把它加载到了内存中去是一种巨大的浪费。
针对这种情况,我们可以对以上的代码进行改进,使用一种新的设计思想——延迟加载(Lazy-load Singleton)。
这种写法就是所谓的懒汉式。它使用了延迟加载来保证对象在没有使用之前,是不会进行初始化的。
通常这个时候面试官又会提问新的问题来刁难一下。他会问:这种写法线程安全吗?回答必然是:不安全。
这是因为在多个线程可能同时运行到第九行,判断instance为null,于是同时进行了初始化,出现创建多个实例的情况。
实际上使用什么样的单例实现取决于不同的生产环境,懒汉式适合于单线程程序,多线程情况下需要保护getInstance()方法,否则可能会产生多个Singleton对象的实例。
所以,这是面临的问题是如何使得这个代码线程安全?很简单,在getInstance()方法前面加一个synchronized关键字,锁定整个方法就OK了。
写到这里,面试官可能仍然会狡猾的看了你一眼,继续刁难到:这个写法有没有什么性能问题呢?答案肯定是有的!同步的代价必然会一定程度的使程序的并发度降低。
锁定整个方法的是比较耗费资源的,代码中实际会产生多线程访问问题的只有
为了降低 synchronized 块性能方面的影响,把同步的粒度降低,只在初始化对象的时候进行同步,故只锁定初始化对象语句即可。
分析这种实现方式,两个线程可以并发地进入第一次判断instance是否为空的if 语句内部,第一个线程执行new操作,第二个线程阻断,当第一个线程执行完毕之后,第二个线程没有进行判断就直接进行new操作,所以这样做也并不是安全的。
为了避免第二次进入synchronized块没有进行非空判断的情况发生,添加第二次条件判断,即一种新的设计思想——双重检查锁(Double-Checked Lock)。
这种写法使得只有在加载新的对象进行同步,在加载完了之后,其他线程在第5行就可以判断跳过锁的的代价直接到第12行代码了。做到很好的并发度。
至此,上面的写法一方面实现了Lazy-Load,另一个方面也做到了并发度很好的线程安全,一切看上很完美。
但是二次检查自身会存在比较隐蔽的问题,查了Peter Haggar在DeveloperWorks上的一篇文章,对二次检查的解释非常的详细:
“双重检查锁定背后的理论是完美的。不幸地是,现实完全不同。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因。”
使用二次检查的方法也不是完全安全的,原因是 java 平台内存模型中允许所谓的“无序写入”会导致二次检查失败,所以使用二次检查的想法也行不通了。
Peter Haggar在最后提出这样的观点:“无论以何种形式,都不应使用双重检查锁定,因为您不能保证它在任何 JVM 实现上都能顺利运行。”
问题在哪里?
假设线程A执行到了第5行,它判断对象为空,于是线程A执行到第8行去初始化这个对象,但初始化是需要耗费时间的,但是这个对象的地址其实已经存在了。此时线程B也执行到了第5行,它判断不为空,于是直接跳到12行得到了这个对象。但是,这个对象还没有被完整的初始化!得到一个没有初始化完全的对象有什么用!!
关于这个Double-Checked Lock的讨论有很多,目前公认这是一个Anti-Pattern,不推荐使用!
那么有没有什么更好的写法呢?
有!这里又要提出一种新的模式——Initialization on Demand Holder. 这种方法使用内部类来做到延迟加载对象,在初始化这个内部类的时候,JLS(Java Language Sepcification)会保证这个类的线程安全。这种写法最大的美在于,完全使用了Java虚拟机的机制进行同步保证,没有一个同步的关键字。
上面的方式是值得借鉴的,在ResourceFactory中加入了一个私有静态内部类ResourceHolder ,对外提供的接口是 getResource()方法,也就是只有在ResourceFactory .getResource()的时候,Resource对象才会被创建,
这种写法的巧妙之处在于ResourceFactory 在使用的时候ResourceHolder 会被初始化,但是ResourceHolder 里面的resource并没有被创建,
这里隐含了一个是static关键字的用法,使用static关键字修饰的变量只有在第一次使用的时候才会被初始化,而且一个类里面static的成员变量只会有一份,这样就保证了无论多少个线程同时访问,所拿到的Resource对象都是同一个。
值得注意的是,饿汉式的实现方式虽然貌似开销比较大,但是不会出现线程安全的问题,也是解决线程安全的单例实现的有效方式。
所以本文提出的第一个例子(也是《Java与模式》中的例子),也是使用单例模式的有效方法之一。这种方式没有使用同步,并且确保了调用static getInstance()方法时才创建Singleton的引用(static 的成员变量在一个类中只有一份)。
附:
饿汉式单例类可以在Java语言实现,但不易在C++内实现,因为静态初始化在C++里没有固定的顺序,因而静态的instance变量的初始化与类的加载顺序没有保证,可能会出问题。这就是为什么GoF在提出单例类的概念时,举的例子是懒汉式的。他们的书影响之大,以致Java语言中单例类的例子也大多是懒汉式的。实际上,本书认为饿汉式单例类更符合Java语言本身的特点。
——《Java与模式》作者
参考资料:
Double-Checked Lock:http://en.wikipedia.org/wiki/Double-checked_locking
Initialzation on Demand Holder: http://en.wikipedia.org/wiki/Initialization_on_demand_holder_idiom
线程安全的单例模式http://blog.sina.com.cn/s/blog_75247c770100yxpb.html
线程安全的单例模式http://hi.baidu.com/snbrskt/item/e8b12c16bc62b407d0d66d03
双重检查锁定及单例模式http://www.ibm.com/developerworks/cn/java/j-dcl.html#author
Lazy Loading Singletonshttp://blog.crazybob.org/2007/01/lazy-loading-singletons.html
自励共勉。
面试的时候,常常会被问到这样一个问题:请您写出一个单例模式(Singleton Pattern)吧。
单例的目的是为了保证运行时Singleton类只有唯一的一个实例,最常用的地方比如拿到数据库的连接,Spring的中创建BeanFactory这些开销比较大的操作,而这些操作都是调用他们的方法来执行某个特定的动作。
很容易,顺手写一个《Java与模式》中的第一个例子:
public final class Singleton { private static Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } }
这种写法就是所谓的饿汉式,每个对象在没有使用之前就已经初始化了。
问题来了,问题1:单例会带来什么问题?如果这个对象很大呢?没有使用这个对象之前,就把它加载到了内存中去是一种巨大的浪费。
针对这种情况,我们可以对以上的代码进行改进,使用一种新的设计思想——延迟加载(Lazy-load Singleton)。
public final class Singleton{ private static Singleton instance = null; private Singleton(){} public static Singleton getInstance(){ if(instance == null){ instance = new Singleton(); } return instance; } }
这种写法就是所谓的懒汉式。它使用了延迟加载来保证对象在没有使用之前,是不会进行初始化的。
通常这个时候面试官又会提问新的问题来刁难一下。他会问:这种写法线程安全吗?回答必然是:不安全。
这是因为在多个线程可能同时运行到第九行,判断instance为null,于是同时进行了初始化,出现创建多个实例的情况。
实际上使用什么样的单例实现取决于不同的生产环境,懒汉式适合于单线程程序,多线程情况下需要保护getInstance()方法,否则可能会产生多个Singleton对象的实例。
所以,这是面临的问题是如何使得这个代码线程安全?很简单,在getInstance()方法前面加一个synchronized关键字,锁定整个方法就OK了。
public final class Singleton{ private static Singleton instance=null; private Singleton(){} public static synchronized Singleton getInstance(){ if(instance==null){ instance=new Singleton(); } return instance; } }
写到这里,面试官可能仍然会狡猾的看了你一眼,继续刁难到:这个写法有没有什么性能问题呢?答案肯定是有的!同步的代价必然会一定程度的使程序的并发度降低。
锁定整个方法的是比较耗费资源的,代码中实际会产生多线程访问问题的只有
instance = new Singleton();
为了降低 synchronized 块性能方面的影响,把同步的粒度降低,只在初始化对象的时候进行同步,故只锁定初始化对象语句即可。
public final Singleton getInstance(){ if(instance == null){ synchronize(this){ instance = new Singleton(); } } return instance; }
分析这种实现方式,两个线程可以并发地进入第一次判断instance是否为空的if 语句内部,第一个线程执行new操作,第二个线程阻断,当第一个线程执行完毕之后,第二个线程没有进行判断就直接进行new操作,所以这样做也并不是安全的。
为了避免第二次进入synchronized块没有进行非空判断的情况发生,添加第二次条件判断,即一种新的设计思想——双重检查锁(Double-Checked Lock)。
public final class Singleton{ private static Singleton instance=null; private Singleton(){} public static Singleton getInstance(){ if(instance == null){ synchronize(this){ if(instance == null){ instance = new Singleton(); } } } return instance; } }
这种写法使得只有在加载新的对象进行同步,在加载完了之后,其他线程在第5行就可以判断跳过锁的的代价直接到第12行代码了。做到很好的并发度。
至此,上面的写法一方面实现了Lazy-Load,另一个方面也做到了并发度很好的线程安全,一切看上很完美。
但是二次检查自身会存在比较隐蔽的问题,查了Peter Haggar在DeveloperWorks上的一篇文章,对二次检查的解释非常的详细:
“双重检查锁定背后的理论是完美的。不幸地是,现实完全不同。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因。”
使用二次检查的方法也不是完全安全的,原因是 java 平台内存模型中允许所谓的“无序写入”会导致二次检查失败,所以使用二次检查的想法也行不通了。
Peter Haggar在最后提出这样的观点:“无论以何种形式,都不应使用双重检查锁定,因为您不能保证它在任何 JVM 实现上都能顺利运行。”
问题在哪里?
假设线程A执行到了第5行,它判断对象为空,于是线程A执行到第8行去初始化这个对象,但初始化是需要耗费时间的,但是这个对象的地址其实已经存在了。此时线程B也执行到了第5行,它判断不为空,于是直接跳到12行得到了这个对象。但是,这个对象还没有被完整的初始化!得到一个没有初始化完全的对象有什么用!!
关于这个Double-Checked Lock的讨论有很多,目前公认这是一个Anti-Pattern,不推荐使用!
那么有没有什么更好的写法呢?
有!这里又要提出一种新的模式——Initialization on Demand Holder. 这种方法使用内部类来做到延迟加载对象,在初始化这个内部类的时候,JLS(Java Language Sepcification)会保证这个类的线程安全。这种写法最大的美在于,完全使用了Java虚拟机的机制进行同步保证,没有一个同步的关键字。
public class ResourceFactory{ private static class ResourceHolder{ public static Resource resource = new Resource(); } public static Resource getResource() { return ResourceFactory.ResourceHolder.resource; } }
上面的方式是值得借鉴的,在ResourceFactory中加入了一个私有静态内部类ResourceHolder ,对外提供的接口是 getResource()方法,也就是只有在ResourceFactory .getResource()的时候,Resource对象才会被创建,
这种写法的巧妙之处在于ResourceFactory 在使用的时候ResourceHolder 会被初始化,但是ResourceHolder 里面的resource并没有被创建,
这里隐含了一个是static关键字的用法,使用static关键字修饰的变量只有在第一次使用的时候才会被初始化,而且一个类里面static的成员变量只会有一份,这样就保证了无论多少个线程同时访问,所拿到的Resource对象都是同一个。
值得注意的是,饿汉式的实现方式虽然貌似开销比较大,但是不会出现线程安全的问题,也是解决线程安全的单例实现的有效方式。
所以本文提出的第一个例子(也是《Java与模式》中的例子),也是使用单例模式的有效方法之一。这种方式没有使用同步,并且确保了调用static getInstance()方法时才创建Singleton的引用(static 的成员变量在一个类中只有一份)。
附:
饿汉式单例类可以在Java语言实现,但不易在C++内实现,因为静态初始化在C++里没有固定的顺序,因而静态的instance变量的初始化与类的加载顺序没有保证,可能会出问题。这就是为什么GoF在提出单例类的概念时,举的例子是懒汉式的。他们的书影响之大,以致Java语言中单例类的例子也大多是懒汉式的。实际上,本书认为饿汉式单例类更符合Java语言本身的特点。
——《Java与模式》作者
参考资料:
Double-Checked Lock:http://en.wikipedia.org/wiki/Double-checked_locking
Initialzation on Demand Holder: http://en.wikipedia.org/wiki/Initialization_on_demand_holder_idiom
线程安全的单例模式http://blog.sina.com.cn/s/blog_75247c770100yxpb.html
线程安全的单例模式http://hi.baidu.com/snbrskt/item/e8b12c16bc62b407d0d66d03
双重检查锁定及单例模式http://www.ibm.com/developerworks/cn/java/j-dcl.html#author
Lazy Loading Singletonshttp://blog.crazybob.org/2007/01/lazy-loading-singletons.html
评论
2 楼
august_000
2012-10-17
很有道理,我已经亲自测试过了:
结果:
public class A { private A(){ System.out.println("init a...."); } private static class ResourceHolder{ private static A a =new A(); } public static A getInst(){ System.out.println("getInst..."); A a = ResourceHolder.a; return a; } }
结果:
getInst... init a....
1 楼
Chris_bing
2012-10-17
一个单例有这么多名堂,最后那个内部类的解决方案很有创意啊,受教了!
发表评论
-
(转)Spring声明式事务配置详解
2013-11-11 16:45 1780Spring声明式事务功能应该是大家应用Spring中使用的最 ... -
(转)Java几款性能分析工具的对比
2013-11-01 14:26 973来源:http://www.javaweb.cc/langua ... -
HashMap的遍历
2013-11-01 14:15 8201、新建一个HashMap Map<Integer , ... -
java将汉子转换成汉语拼音
2013-11-01 13:51 897import net.sourceforge.pinyin4j ... -
Spring加载properties文件的两种方式
2013-10-31 10:05 0在spring中可以通过下面的方式将配置文件中的项注入到配置中 ... -
动态加载spring .xml中的bean对象
2013-10-30 15:27 0在java中如何取得spring.xml中加载的bean对象? ... -
(转)重述——组合/聚合复用原则
2013-10-30 09:10 998组合/聚合复用原则(Com ... -
(转)重述——迪米特法则
2013-10-29 10:51 1230迪米特法则(Law of Demeter) 又叫最 ... -
(转)重述——依赖倒置原则
2013-10-29 10:50 778依赖倒置原则(Dependence Inversion Pri ... -
(转)重述——里氏替换原则
2013-10-29 10:46 1398里氏替换原则(Liskov Substitution Prin ... -
(转)重述——开放封闭原则
2013-10-29 10:41 742开发封闭原则(Open-Closed Principle OC ... -
(转)重述——单一职责原则
2013-10-29 10:37 799单一职责原则(Single Respo ... -
(转)Spring mvc+hibernate+freemarker(实战)
2013-10-28 19:40 918http://zz563143188.iteye.com/bl ... -
(转)Java之美[从菜鸟到高手演变]系列之博文阅读导航
2013-10-28 17:00 1661Java之美[从菜鸟到高手演变]系列之博文阅读导航 http: ... -
(转)openssl 制作证书和签名java方法
2013-10-28 15:03 0Win32OpenSSL_Light-0_9_8k.exe ... -
(转)面向接口编程详解
2013-10-25 12:34 5老文章,自己学习。 面向接口编程详解(一) http://w ... -
(转)细说业务逻辑
2013-10-25 12:30 488前篇 http://www.cnblogs.com/leoo2 ... -
(转)Java架构师之路:JAVA程序员必看的15本书
2013-10-08 18:24 0作为Java程序员来说,最 ... -
Java获取CPU ID和磁盘ID
2013-09-25 20:18 0原文链接:http://www.rgagnon.com/jav ... -
Java编程之备用程序段
2013-09-25 14:57 0记录一些程序段,供日常备用,持续增加中…… 1. Java中 ...
相关推荐
单例模式 Singleton 单例模式线程安全问题和拓展
本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。
今天我们要讲的是单例模式 定义 确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例 使用场景 确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源 某个类型的对象只应该有一个 ...
串口管理类放到线程中,这样不会影响界面的流程,并且串口管理类用到了单例的设计模式,这个平时做项目积攒下了的经验,并且对3字节的数据进行了解析。用到了联合体,更方便的将16进制数转换成浮点数,发送数据的...
C中实现单例模式7.4 子类化Singleton7.5 线程安全7.6 在Cocoa Touch框架中使用单例模式7.6.1 使用UIApplication类7.6.2 使用UIAccelerometer类7.6.3 使用NSFileManager类7.7 总结第三部分 接口适配第8章 适配器8.1 ...
本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。
单例问题与线程安全性深入解析.mp4 理解自旋锁,死锁与重入锁.mp4 深入理解volatile原理与使用.mp4 JDK5提供的原子类的操作以及实现原理.mp4 Lock接口认识与使用.mp4 手动实现一个可重入锁.mp4 ...
28.4.1 线程安全的问题 28.4.2 性能平衡 28.5 最佳实践 第29章 桥梁模式 29.1 我有一个梦想…… 29.2 桥梁模式的定义 29.3 桥梁模式的应用 29.3.1 桥梁模式的优点 29.3.2 桥梁模式的应用 29.3.3 桥梁模式的注意事项 ...
本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。
您将了解如何正确创建对象实例、访问实例的成员变量和方法、实现对象的拷贝(包括浅拷贝和深拷贝)、判断对象相等性、管理对象的生命周期、实现线程安全的单例模式等。此外,我们还探讨了对象的哈希码、重写equals()...
本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。
本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。
本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。
本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。
本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。
本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。
本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。
本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。
本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。
本专栏主要为Java程序设计(基础)实验报告和Java程序设计(进阶)...进阶篇有反射、泛型、注解、网络编程、多线程、序列化、数据库、Servlet、JSP、XML解析、单例模式与枚举。本专栏主要为Java入门者提供实验参考。