文章目录
单例介绍
单例是指仅仅被实例化一次的类。
弊端:使客户端测试变的困难。
双重检查锁单例模式演变(Double check)
如下列举 饱汉模式(懒汉模式)下经过两次优化变为 double check。
并且解决synchronized为什么这么添加以及volatile问题。
1.单线程情况下:饱汉模式(懒汉模式)
public class Singleton {
private static Singleton instance;
//私有构造方法,禁止外部通过构造方法创建对象。
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
2.多线程下:
可以直接给方法加锁:
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
3.优化:
双重检查:
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if(instance == null){
synchronized (Singleton.class){
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
4.再优化:禁止指令重排
我们可以在代码块上加锁,而不是在方法上。
synchronized (Singleton.class){ //3
if (instance == null) { //4
instance = new Singleton(); //5
}
}
在进入到代码块之前,只有在对象未实例情况下,才需要走上面的代码块。
if(instance == null){ //2
synchronized (Singleton.class){ //3
if (instance == null) { //4
instance = new Singleton(); //5
}
}
}
由此变成:
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() { //1
if(instance == null){ //2
synchronized (Singleton.class){ //3
if (instance == null) { //4
instance = new Singleton(); //5
}
}
}
return instance;
}
}
上面有两个问题:
- 1.为什么synchronized代码块需要加在
if (instance == null) { //4
外面?
- 2.指令重排序问题。
4.1. 为什么synchronized代码块需要加在外面?
未加空的时候是这样的。
把4去掉。
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() { //1
if (instance == null) { //2
synchronized (Singleton.class) { //3
instance = new Singleton(); //5
}
}
return instance;
}
}
例如2个线程A,B。A,B都 走过了1-2。
A 进入3 创建了instance。
B 进入3 也创建了instance。
4.2.指令重排序问题。
编译器为了优化性能,有时候会改变程序中语句的先后顺序。
new操作会进行指令重排:
我们以为的new操作:
- 分配一块内存 M;
- 在内存 M 上初始化 Singleton 对象;
- 然后 M 的地址赋值给 instance 变量。
实际上优化后的执行路径:
- 分配一块内存 M;
- 将 M 的地址赋值给 instance 变量;
- 最后在内存 M 上初始化 Singleton 对象。
导致的空指针问题:
线程A 先执行 getInstance() 方法,执行指令2时,发生线程切换,切换为B。
B执行 getInstance() 方法,B执行 第一个instance == null时会发现不等于空。直接返回了instance。而此时instance是没有初始化过的,导致空指针。
添加volatile
保证程序的可见性,及时刷新cpu内存中的值。
禁止了指令重新排序。
volatile和synchronized区别在于只保证可见性,而不保证原子性。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if(instance == null){
synchronized (Singleton.class){
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
其他的单例模式:
1.枚举方式实现单例
该方式也是可以在多线程下面保证线程安全的,并且防止了序列化和反射。《Effect Java》中也推荐使用此方式来实现单例。
并不是使用枚举就不需要保证线程安全,只不过线程安全的保证不需要我们关心。
public enum Singleton {
INSTANCE;
public void doSomething(){
System.out.println("do sth.");
}
}
2.饿汉式
顾名思义,在成员变量的时候就会初始化。
缺点:
类加载时就初始化,浪费内存,容易产生垃圾对象。
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() { //1
return instance;
}
}
既然类加载时候初始化,我们可以放在代码块中:
3.饿汉式(变种)
public class Singleton {
private static Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() { //1
return instance;
}
}
那么我们放到静态内部类中也是可以的,也就是holder模式。
4.静态内部类模式(holder模式)
public class Singleton {
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() { //1
return SingletonHolder.instance;
}
}