单例模式可以说是最容易理解的模式了,也是应用最广的模式之一,先看看定义吧。
定义
确保单例类只有一个实例,并且这个单例类提供一个函数接口让其他类获取到这个唯一的实例。
什么时候需要使用单例模式呢:如果某个类,创建时需要消耗很多资源,即new出这个类的代价很大;或者是这个类占用很多内存,如果创建太多这个类实例会导致内存占用太多。
关于单例模式,虽然很简单,无需过多的解释,但是这里还要提个醒,其实单例模式里面有很多坑。我们去会会单例模式。最简单的单例模式如下:
public class TestSingle {
private static TestSingle instance = null;
private TestSingle(){}
public static TestSingle getInstance(){
if (instance== null){
instance = new TestSingle();
}
return instance;
}
}
如果是单线程下的系统,这么写肯定没问题。可是如果是多线程环境呢?这代码明显不是线程安全的,存在隐患:某个线程拿到的
instance
可能是
null
,可能你会想,这有什么难得,直接在
getInstance()
函数上加
sychronized
关键字不就好了。可是你想过没有,每次调用
getInstance()
时都要执行同步,这带来没必要的性能上的消耗。注意,在方法上加
sychronized
关键字时,一个线程访问这个方法时,其他线程无法同时访问这个类其他
sychronized
方法。的我们看看另外一种实现:
public class TestSingle {
private static TestSingle instance = null;
private TestSingle() {
}
public static TestSingle getInstance() {
if (instance == null) {
synchronized (TestSingle.class) {
if (instance == null) {
instance = new TestSingle();
}
}
}
return instance;
}
}
为什么需要2次判断是否为空呢?第一次判断是为了避免不必要的同步,第二次判断是确保在此之前没有其他线程进入到sychronized块创建了新实例。这段代码看上去非常完美,但是,,,
却有隐患!问题出现在哪呢?主要是在
instance=new Singleton();
这段代码上。这段代码会编译成多条指令,大致上做了3件事:
(1)给Singleton实例分配内存
(2)调用Singleton()构造函数,初始化成员字段
(3)将instance对象指向分配的内存(此时instance就不是null啦~)
上面的(2)和(3)的顺序无法得到保证的,也就是说,JVM可能先初始化实例字段再把instance
指向具体的内存实例,也可能先把instance
指向内存实例再对实例进行初始化成员字段。考虑这种情况:一开始,第一个线程执行instance=new Singleton();
这句时,JVM先指向一个堆地址,而此时,又来了一个线程2,它发现instance
不是null
,就直接拿去用了,但是堆里面对单例对象的初始化并没有完成,最终出现错误~ 。
看看另外一种方式:
public class TestSingle {
private volatile static TestSingle instance = null;
private TestSingle() {
}
public static TestSingle getInstance() {
if (instance == null) {
synchronized (TestSingle.class) {
if (instance == null) {
instance = new TestSingle();
}
}
}
return instance;
}
}
相比前面的代码,这里只是对
instance
变量加了一个
volatile
关键字
volatile
关键字的作用是:线程每次使用到被
volatile
关键字修饰的变量时,都会去堆里拿最新的数据。换句话说,就是每次使用instance时,保证了instance是最新的。注意:
volatile
关键字并不能解决并发的问题,关于
volatile
请查看其它相关文章。但是
volatile
能解决我们这里的问题。