设计模式中常用的单例模式,在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方法时会保证方法中的变量都创建对象并赋值)。