单例模式中的线程安全(延迟加载)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式中的线程安全(延迟加载)相关的知识,希望对你有一定的参考价值。

    设计模式中常用的单例模式,在jvm中可以保证该对象只有一个实例存在。这样对于一些特别大的对象,可以很好的节省资源。由于省去了new,所以节省了gc消耗。同时,对于一些核心系统逻辑,可以能要一个对象实例来控制会省去很多麻烦。

     单例模式,如果不考虑多线程,则可以如下创建

public class Singleton {
	/* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */
	private static Singleton instance = null;
	/* 私有构造方法,防止被实例化 */
	private Singleton() {
	}
	/* 静态工程方法,创建实例 */
	public static Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
	/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
	public Object readResolve() {
		return instance;
	}
}

     上面提到延迟加载,所谓延迟加载,就是指当实际用到该对象时才加载对应类,否则只是声明,并未实际花费资源去初始化对象。

      最好的理解方式,将上面代码中如下语句,此时为非延迟加载,加载该单例类时,也会初始化该静态类的静态成员变量,并调用new来创建实例。而采用延迟加载,则需要当调用getInstance()方法 时,才会通过new初始化实例。

private static Singleton instance = new Singleton();

      以上是单例模式中延迟加载的解释,但是上面的示例是不考虑多线程下的单例模式。如果多线程下进行延迟加载,上述单利模式是否有问题? 答案是有。

      如果多个线程同时调用getInstance,则可能会调用多次new,会产生问题。如何避免呢?我们很容易想到使用锁、synchronized等方法。

      synchronized:

     如下,该方法可以保证类每次调用getInstance时,都只有一个线程在使用。但是问题也来了,每次调用都会锁住这个对象,因为synchronized用在方法上时,锁住的是整个类对象(ps:如果一个对象有多个synchronized方法,某一时刻某个线程已经进入到了某个synchronized方法,那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized方法的。注意这时候是给对象上锁,如果是不同的对象,则各个对象之间没有限制关系。但如果是静态方法的情况(方法加上static关键字),即便是向两个线程传入不同的对象(同一个类),这两个线程仍然是互相制约的,必须先执行完一个,再执行下一个)我们不希望这样子,因为这样子在并发处理中会损失性能。

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

     根据上面,我们做了改动,将synchronized关键字放在代码块中,这样锁定的就不是整个对象,而是方法块,synchronized块比synchronized方法更加细粒度地控制了多个线程的访问,只有synchronized块中的内容不能同时被多个线程所访问,方法中的其他语句仍然可以同时被多个线程所访问(包括synchronized块之前的和之后的。如下

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

       这个时候,貌似解决了问题。但是,我想说的是,从网上看到的资料里面有如下问题情况:jvm优化使得new singleton()的操作实际上不是原子操作,包括分配内存和初始化对象两个过程,很有可能第一个线程进入分配内存后,就已经将instance分配内存地址,此时不为null,但是还没有分对象初始化,此时另一个线程发现instance不等于null,就直接开始使用对象的其他方法,就会报错,类似classnotdefine或者noclass之类的异常报错。

       最后,不得不使用如下的代码解决:

private static class SingletonFactory{         
        private static Singleton instance = new Singleton();         
    }         
    public static Singleton getInstance(){         
        return SingletonFactory.instance;         
    }

      以上为SingletonFactory为内部类,放在Singleton类中。JVM有个特性:一个类被加载时,该类是线程互斥的,且只会被加载一次。所以如上代码,当调用getInstance()时(该方法可以多线程同时访问),就可以实现加载类了(第一个线程访问就会加载SingletonFactory,并创建Class对象。其他线程要等他创建完成后才能访问SingletonFactory,且不会再new)。


       以上方法可以解决问题,同时也有人参考上面的思路来做了另一种,因为上面主要就是让访问可以多线程同时,对象获取只能单线程互斥,那把static class SingletonFactory换成一个synchronized的方法应该也可以吧,如下代码:

public class SingletonTest {
	private static SingletonTest instance = null;
	private SingletonTest() {
	}
	private static synchronized void syncInit() {
		if (instance == null) {
			instance = new SingletonTest();
		}
	}
	public static SingletonTest getInstance() {
		if (instance == null) {
			syncInit();
		}
		return instance;
	}
}

     可能一眼看起来和synchronized块的方法类似,这个是否也会发生类似的问题?(我的疑虑,不过貌似很难测者中情况,也许synchronized方法时会保证方法中的变量都创建对象并赋值)。


本文出自 “开心一杯茶” 博客,请务必保留此出处http://gugw9handsome.blog.51cto.com/1187812/1776025

以上是关于单例模式中的线程安全(延迟加载)的主要内容,如果未能解决你的问题,请参考以下文章

多线程单例模式之延迟加载(懒汉模式)

单例模式

设计模式-单例模式总结

菜鸟成长日记:Java基础篇3 --- 单例模式

idiom - Initialization-on-demand holder 延迟加载的单例模式

多线程下的立即加载与延迟加载