锁定老帖子 主题:单例模式与双重检测
精华帖 (1) :: 良好帖 (10) :: 新手帖 (2) :: 隐藏帖 (6)
|
|
---|---|
作者 | 正文 |
发表时间:2010-04-25
最后修改:2010-04-29
首先要解释一下什么是延迟加载,延迟加载就是等到真真使用的时候才去创建实例,不用时不要去创建。
从速度和反应时间角度来讲,非延迟加载(又称饿汉式)好;从资源利用效率上说,延迟加载(又称懒汉式)好。
下面看看几种常见的单例的设计方式:
第一种:非延迟加载单例类 public class Singleton { private Singleton() {} private static final Singleton instance = new Singleton(); public static Singleton getInstance() { return instance; } }
第二种:同步延迟加载 public class Singleton { private static Singleton instance = null; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
第三种:双重检测同步延迟加载 public class Singleton { private volatile static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) {// 1 if (instance == null) {// 2 instance = new Singleton();// 3 } } } return instance; } } 双重检测锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是失败的一个主要原因。
无序写入:
为展示此事件的发生情况,假设代码行 instance =new Singleton(); 执行了下列伪代码:
如果真像这篇文章:http://dev.csdn.net/author/axman/4c46d233b388419e9d8b025a3c507b17.html所说那样的话,1.2或以后的版本就不会有问题了,但这个规则是JMM的规范吗?谁能够确认一下。 但是从JAVA2以后,JMM发生了根本的改变,分配空间,初始化,调用构造方法只会在线程的工作存储区完成,在没有
另一篇详细分析文章:http://www.iteye.com/topic/260515
第四种:使用ThreadLocal修复双重检测
借助于ThreadLocal,将临界资源(需要同步的资源)线程局部化,具体到本例就是将双重检测的第一层检测条件 if (instance == null) 转换为了线程局部范围内来作。这里的ThreadLocal也只是用作标示而已,用来标示每个线程是否已访问过,如果访问过,则不再需要走同步块,这样就提高了一定的效率。但是ThreadLocal在1.4以前的版本都较慢,但这与volatile相比却是安全的。
public class Singleton { private static final ThreadLocal perThreadInstance = new ThreadLocal(); private static Singleton singleton ; private Singleton() {} public static Singleton getInstance() { if (perThreadInstance.get() == null){ // 每个线程第一次都会调用 createInstance(); } return singleton; } private static final void createInstance() { synchronized (Singleton.class) { if (singleton == null){ singleton = new Singleton(); } } perThreadInstance.set(perThreadInstance); } } public class Singleton { private Singleton() {} public static class Holder { // 这里的私有没有什么意义 /* private */static Singleton instance = new Singleton(); } public static Singleton getInstance() { // 外围类能直接访问内部类(不管是否是静态的)的私有变量 return Holder.instance; } }
单例测试
下面是测试单例的框架,采用了类加载器与反射。 public class Singleton { private Singleton() {} public static class Holder { // 这里的私有没有什么意义 /* private */static Singleton instance = new Singleton(); } public static Singleton getInstance() { // 外围类能直接访问内部类(不管是否是静态的)的私有变量 return Holder.instance; } } class CreateThread extends Thread { Object singleton; ClassLoader cl; public CreateThread(ClassLoader cl) { this.cl = cl; } public void run() { Class c; try { c = cl.loadClass("Singleton"); // 当两个不同命名空间内的类相互不可见时,可采用反射机制来访问对方实例的属性和方法 Method m = c.getMethod("getInstance", new Class[] {}); // 调用静态方法时,传递的第一个参数为class对象 singleton = m.invoke(c, new Object[] {}); c = null; cl = null; } catch (Exception e) { e.printStackTrace(); } } } class MyClassLoader extends ClassLoader { private String loadPath; MyClassLoader(ClassLoader cl) { super(cl); } public void setPath(String path) { this.loadPath = path; } protected Class findClass(String className) throws ClassNotFoundException { FileInputStream fis = null; byte[] data = null; ByteArrayOutputStream baos = null; try { fis = new FileInputStream(new File(loadPath + className.replaceAll("\\.", "\\\\") + ".class")); baos = new ByteArrayOutputStream(); int tmpByte = 0; while ((tmpByte = fis.read()) != -1) { baos.write(tmpByte); } data = baos.toByteArray(); } catch (IOException e) { throw new ClassNotFoundException("class is not found:" + className, e); } finally { try { if (fis != null) { fis.close(); } if (fis != null) { baos.close(); } } catch (Exception e) { e.printStackTrace(); } } return defineClass(className, data, 0, data.length); } } class SingleTest { public static void main(String[] args) throws Exception { while (true) { // 不能让系统加载器直接或间接的成为父加载器 MyClassLoader loader = new MyClassLoader(null); loader .setPath("D:\\HW\\XCALLC16B125SPC003_js\\uniportal\\service\\AAA\\bin\\"); CreateThread ct1 = new CreateThread(loader); CreateThread ct2 = new CreateThread(loader); ct1.start(); ct2.start(); ct1.join(); ct2.join(); if (ct1.singleton != ct2.singleton) { System.out.println(ct1.singleton + " " + ct2.singleton); } // System.out.println(ct1.singleton + " " + ct2.singleton); ct1.singleton = null; ct2.singleton = null; Thread.yield(); } } }
声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2010-04-26
楼主能详细说明第二种加载和第三种的区别吗?
我认为第二种方法同步方法加锁对像是this,而第三种加载方式是同步当前类的类对像。所以单从范围上面来讲只是if (instance == null)这一句的区别。 |
|
返回顶楼 | |
发表时间:2010-04-26
dominic6988 写道 楼主能详细说明第二种加载和第三种的区别吗?
我认为第二种方法同步方法加锁对像是this,而第三种加载方式是同步当前类的类对像。所以单从范围上面来讲只是if (instance == null)这一句的区别。 第二种加锁的对象不是this,其实也是 Singleton.class 锁对象,以前我测试过。为什么说是Singleton.class而不是this呢,最简单的理由就是该方法是静态的,这就很明显了:静态方法是不能访问this的。 其实第二也等同于下在面: public class Singleton { private volatile static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } return instance; } } 第二种与第三种唯一的区别就是第三种多了“instance == null”的条件,但该条件的检测不是放在同步块中的,正是因为这一点,导致了双重检测失效! |
|
返回顶楼 | |
发表时间:2010-04-26
谢谢楼主的回复。
正如你所说的用ThreadLocal能解决这个问题,担是我认为你的代码里面的写法是不是有问题。 public class Singleton { private static final ThreadLocal perThreadInstance = new ThreadLocal(); private static Singleton singleton ; private Singleton() {} public static Singleton getInstance() { if (perThreadInstance.get() == null){ // 每个线程第一次都会调用 createInstance(); } return singleton; } private static final void createInstance() { synchronized (Singleton.class) { if (singleton == null){ singleton = new Singleton(); } } perThreadInstance.set(perThreadInstance); } } 倒数第三行perThreadInstance.set(perThreadInstance);应该写成perThreadInstance.set(singleton); 除此之外每一个线程都会有一个singleton的副本这样一样会造成资源浪费。本来单态模式就是想节省资源的,这样与模式的初衷不相符吧。 还有这种ThreadLocal方式的解决方案能不能应用在分布式情形,不同的JVM,ClassLoader? |
|
返回顶楼 | |
发表时间:2010-04-26
另外一个问题是如果用了关键字volatile就能解决双重检测的问题吗?
这样的写法是能避免无序写入的问题。因为别的线程进入不了方法体,除非当前线程释放锁。这样就能确保实例化完成。我的理解对吗? public class Singleton { private volatile static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } return instance; } } |
|
返回顶楼 | |
发表时间:2010-04-26
我就想问下,为什么第三种方式,//3没执行完(也就是没有new完整),它为什么会释放锁?
|
|
返回顶楼 | |
发表时间:2010-04-26
fengsky491 写道
我就想问下,为什么第三种方式,//3没执行完(也就是没有new完整),它为什么会释放锁?
第三步可以看作一个赋值语句,只不过是调用构造函数初始化在付值语句之后。另外一个线程得到锁后就看到当前的instence已经不是null了就直接返回了,这个时候有可能第一个线程初始化工作做了一半,或者没有做。这样后面的线程得到的对像就会有问题。我感觉是这样的。 |
|
返回顶楼 | |
发表时间:2010-04-26
恩,楼上说的没错
是因为这样 |
|
返回顶楼 | |
发表时间:2010-04-26
其实个人认为用单例模式主要还是看所创建的对像是不是有状态的,如果是没有状态的就是多创建一个也不会对系统照成大的影响,但是如果是有状态的比如连接(本来是一个连接的结果变成了两个)或者计数器之类的所有线程要共享的这样的情况就不用能ThreadLocal这种方式了,这种方式是每个线程都捅有一个对像的副本且不会相互影响的,如果是这种情况就可以用关键字volatile来解决,或者通过静态内部类(第五种)的方式保证只加载一次来实现单态。
总之楼主总结的不错。能用的这几种方式都总结出来了。 |
|
返回顶楼 | |
发表时间:2010-04-26
最后修改:2010-04-26
dominic6988 写道
fengsky491 写道
我就想问下,为什么第三种方式,//3没执行完(也就是没有new完整),它为什么会释放锁?
第三步可以看作一个赋值语句,只不过是调用构造函数初始化在付值语句之后。另外一个线程得到锁后就看到当前的instence已经不是null了就直接返回了,这个时候有可能第一个线程初始化工作做了一半,或者没有做。这样后面的线程得到的对像就会有问题。我感觉是这样的。
还有new的时候它为什么不会初始化完整了才,释放锁?
照这样理解,我是不是也可以认为第二种,在new的时候,只初始化一半,就释放了锁,其他线程进来不是一样看到的instance也不是null,而照样返回一个不完整的实例? |
|
返回顶楼 | |