单例模式大家接触甚至都写过好多了,那么是不是所有的单例模式都堪称完美呢?呵呵,不一定。我之前写的一个单例模式就很有问题,什么问题呢?大家请看我写代码:
class SingleTon {
private static SingleTon mSingleTon;
private SingleTon() {
}
public static SingleTon getInstance() {
if (mSingleTon == null) {
mSingleTon = new SingleTon();
}
return mSingleTon;
}
...
代码省略
...
}
如果实在多线程中呢?接着分析下。假设有2个线程,线程a和线程b,线程a执行到语句9还没有执行完语句10,而这时候线程b执行到语句9,由于线程a还没有实例化mSingleTon变量,所以线程b执行到语句9的时候mSingleTon 是 null,以至于这两个线程都会实例化各自的变量。那么你的单例模式就不再是“单例”了。
那么该怎么写呢?大家都会想到想到用synchronized 同步快或同步方法。那么具体应该怎么写呢?我们改进的代码如下:
public static SingleTon getInstance() {
if (mSingleTon == null) {
synchronized (SingleTon.class) {
if (mSingleTon == null) {
mSingleTon = new SingleTon();
}
}
}
return mSingleTon;
}
分析下: 假设线程a和线程b同时执调用getInstance()方法,由于这时候mSingleTon == null,所以a和b线程都满足语句2的if语句。然后假设线程a获得了SingleTon类的锁而进入lock语句(synchronized (SingleTon.class)),此时线程b只能在lock语句外等待,直到线程a执行完synchronized 同步块的代码而释放锁。线程a执行到语句4的时候,由于此时mSingleTon 仍然是null,所以执行语句5,对mSingleTon 变量实例化。完成后,线程a释放SingleTon类的锁。线程b进入lock语句,执行语句4,由于线程a已经对mSingleTon 进行实例化了,此时mSingleTon 不再是null,从而执行语句10,返回SingleTon的实例。
大家想一想,如果没有最外面的if (mSingleTon == null)这条判断语句行不行?
public static SingleTon getInstance() {
synchronized (SingleTon.class) {
if (mSingleTon == null) {
mSingleTon = new SingleTon();
}
}
return mSingleTon;
}
答案是肯定的。分析下,当有2个线程同时调用getInstance()方法,其中一个线程获取锁而进入lock语句,另外一个线程等在lock语句外面,进入lock语句的线程对mSingleTon实例化完成后释放锁。另外一个线程进入lock语句,由于此时mSingleTon 不为null,所以直接返回mSingleTon。
既然不要嘴外层的if语句单例模式仍然是正确的,那么我们为何还要“画蛇添足”呢?
这就涉及到性能问题了,如果不要最外层的if语句,那么每次调用getInstance()方法时,都会执行synchronized (SingleTon.class),这是非常耗性能的。而加上了最外层的if语句后,那么就只有在第一次,也就是 singleTton == null 成立时的情况下执行一次锁定以实现线程同步。
好了,这篇写完了,欢迎指正。
随着后来对单例使用的更多,发现我上面写的还是有问题的,需要在这一行 private static SingleTon mSingleTon 代码加上 violate关键字。
为什么呢?
因为初始化一个对象并使一个引用指向它并不是原子操作的,导致了可能会出现引用指向了对象并未初始化好的那块堆内存,使用volatile修饰对象引用,防止重排序即可解决。
举个例子:
student = new Student(); 这行代码大致可分为如下4个步骤:
1. 栈内存开辟空间给 student
2. 堆内存开辟空间准备初始化对象
3. 初始化对象
4. 栈中引用指向这个堆内存空间地址
指令重排之后可能会是1、2、4、3;这样重排之后对单个线程来说效果是一样的,所以JVM认为是合法的重排序,但是在多线程环境下就会出问题,这里到4的时候help已经指向了一块堆内存!=null ,只是这块堆内存还没初始化就直接返回了,使用的时候抛NullPointException。使用volatile修饰对象引用,防止重排序即可解决。
推荐一种更好的实现单例的方法。
饿汉式单例类不能实现延迟加载,不管将来用不用始终占据内存;懒汉式单例类线程安全控制烦琐,而且性能受影响。而使用静态内部类的方式来实现既可以实现延迟加载,又可以保证线程安全,不影响系统性能。
例子如下:
class Singleton {
private Singleton() {
}
private static class HolderClass {
private final static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return HolderClass.instance;
}
}
由于静态内部类没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。