概述
单例模式,它能保证我们始终如一的使用同一个对象,我们平时经常会去用它,因为可以避免重复制造对象,减少内存隐患,我们也都可以写个常见的单例出来。
这里要讲下单例到底应该怎么写,既能避免线程不安全,也能保证性能。
内容
1、一个最简单的单例模式
public class GirlFriend {
//静态变量记录唯一实例
private static GirlFriend instance;
//构造器私有化
private GirlFriend() {
}
//静态实例化方法公有,保证不重复创建对象
public static GirlFriend getInstance() {
if (instance == null) {
instance = new GirlFriend();
}
return instance;
}
}
以上代码片,给出了单例模式的最基本的三要素:
- 静态变量记录唯一实例;
- 构造器私有化;
这样能保证外界不会通过构造器构造对象。 - 静态实例化方法公有化,保证不重复创建对象
void test() {
GirlFriend girlFriend = GirlFriend.getInstance();
}
当你在第一次调用getInstance()方法时,会对instance进行初始化,以后再次调用该方法,则不会重复创建对象,而是直接使用已经被实例化的instance对象。
但是呢,这个单例不是线程安全的,假设有两个线程,都在调用getInstance()方法,其中线程A执行到了“if (instance == null) ”,而另一个线程B执行到“instance = new GirlFriend();”,那么当B中的instance实例初始化完成前,线程A就已经满足条件即将进入再次初始化instance,这样一来,instance实例被初始化了两次,且A、B两个线程得到的并不是同一个实例对象,如果同时一百个线程这么执行,将构造一百次GirlFriend类型变量,所以多线程使用以上单例就需要谨慎了。
###2、同步机制
如果想要线程安全,一种最便捷的方式是使用同步“synchronized”,就像这样:
public class GirlFriend {
private static GirlFriend instance;
private GirlFriend() {
}
public static synchronized GirlFriend getInstance() {
if (instance == null) {
instance = new GirlFriend();
}
return instance;
}
}
使用synchronized关键字修饰getInstance()方法,会使得,在多个线程执行此方法时,必须等待其他线程执行完毕后,才能进入该方法,且每次只能有一个线程进入getInstance()方法。
但是,频繁并发同步时,会意味着,你多个线程总是要等来等去,一个接一个执行被synchronized关键字修饰的代码,这样会影响程序效率、性能。我们只是需要在第一次进入getInstance()方法时,同步线程避免多次实例化对象。
###双重检查加锁
为了避免多次同步,使用双重检查加锁,如下:
public class GirlFriend {
private volatile static GirlFriend instance;
private GirlFriend() {
}
public static GirlFriend getInstance() {
if (instance == null) {
synchronized (GirlFriend.class) {
if (instance == null) {
instance = new GirlFriend();
}
}
}
return instance;
}
}
分析一下,如果同时线程A和线程B几乎同时执行getInstance()方法,它们都经历第一次条件判断,假设A拿到同步锁,执行初始化instance的代码,线程B等候A释放了同步锁后,经历第二次判断,显然instance已经不为空了,所以会释放锁,执行“return instance”,避免了重复初始化instance。这样以后再调用getInstance()方法,第一次条件判断为“假”,则避免了synchronized语句,极大的优化了性能。
上面那段话横竖没有提到“volatile”关键字,但是为神马要用它呢?
计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:
i = i + 1;
当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。
在多核CPU中,每个线程都有自己的高速缓存,这就意味着,在多个线程并发读写同一个变量时,这种读写只是作用于高速缓存中内容。比如以上那句代码,当线程A和线程B分别从主内存中读取i的值到自己的CPU高速缓存中,A中的“i=i+1”,则“i”值为“1”,这个值还在A线程CPU高速缓存中,没有同步到主内存,而此时线程B正好执行了“i=i+1”,但因为“i”存在于B线程CPU高速缓存中的值仍然是0,执行“i=i+1”的结果仍然是“i”值为“1”,最后两线程同步到主内存中的i的值是“1”。但实际上我们预测的是“i加1然后再加1“,结果是“2”。
volatile:它是一个类型修饰符(type specifier),就像大家更熟悉的const一样,它是被设计用来修饰被不同线程访问和修改的变量。volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。
这就意味着,被volatile修饰的变量,值已经修改,会马上同步到主内存。所以多个线程每次得到的被volatile修饰的变量,它的值都是经过最近更新的。
所以这里我们用volatile修饰instance变量,就是为了让instance的值和主内存同步。