【Monica的android学习之路】单例模式
1. 懒汉型
1.1 双检锁+volatile
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)第一层检查去掉,同样也能实现正确的单例模式,但是会带来性能损耗。因为每次调用getInstance时,均会进入synchronized执行锁机制。
加上第一层检查后,除了第一次为instance赋值,且两个线程同时到达第一层检查处时,才会都通过第一层检查的判空,执行锁机制。以后均会被第一层检查挡在外面,锁机制不会触发。
(2)当两个线程同时通过第一层检查后,只有一个线程获得锁,另一个线程处于等待状态。当线程正常给instance赋值并释放锁后,第二个线程获得锁,进行第二层检查,发现instance已赋值,就不会再次实例化了
为什么必须配合volatile使用呢?
下面的代码在多线程环境下不是原子执行的。
instance=new DoubleCheckSingleton();
正常的底层执行顺序会转变成三步:
(1) 给DoubleCheckSingleton类的实例instance分配内存
(2) 调用实例instance的构造函数来初始化成员变量
(3) 将instance指向分配的内存地址
无论在A线程当前执行到那一步骤,对B线程来说可能看到A的状态只能是两种1,2看到的都是null,3看到的非null
线程A在重排序的情况下,上面的执行顺序会变成1,3,2。现在假设A线程按1,3,2三个步骤顺序执行,当执行到第二步的时候。B线程开始调用这个方法,那么在第一个null的检查的时候,就有可能看到这个实例不是null,然后直接返回这个实例开始使用,但其实是有问题的,因为对象还没有初始化,状态还处于不可用的状态,故而会导致异常发生。
volatile如何发挥作用:
通过volatile关键词来避免指令重排序,这里相比可见性问题主要是为了避免重排序问题。如果使用了volatile修饰成员变量,那么在变量赋值之后,会有一个内存屏障。也就说只有执行完1,2,3步操作后,读取操作才能看到,读操作不会被重排序到写操作之前。这样以来就解决了对象状态不完整的问题。
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,
也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
总结:
volatile的效果有:
原子性(不保证,与读写操作有关)
可见性(保证)
有序性(部分保证,涉及该变量的读写操作可以保证有序)
详细介绍见《【Monica的android学习之路】线程同步volatile、Synchronized和lock》
1.2 静态类加载
1.2.1 类加载顺序
首先回顾一下类加载顺序:
public class Main {
private static String tag = "1";
private String name = "2";
static {
System.out.println("this is static{}");
tag += "3";
}
{
System.out.println("this is {}");
}
Main() {
System.out.println("this is Main()");
}
public static void main(String[] args) throws InterruptedException {
System.out.println("this is main()");
System.out.println(tag);
fun();
System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
Main instance = new Main();
instance.fun2();
System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
Main instance2 = new Main();
instance2.fun2();
}
private static void fun() {
System.out.println("this is fun()");
System.out.println("tag: " + tag);
}
private void fun2() {
System.out.println("this is fun2()");
name += "4";
System.out.println("name: " + name);
}
}
执行上述代码,输出:
上面主要为了证明静态方法在程序初始化时进行初始化,但是不会执行方法体
只有在调用时才执行方法体
总结:
程序开始运行时,分为两个阶段
(1)开始执行生命周期时,进入初始化阶段。
在程序生命周期会且仅会执行一次,将依次初始化以下部分:
父类静态变量
父类静态代码块
子类静态变量
子类静态代码块
注:静态方法无初始化过程,调用时直接执行
(2)初始化完成进入main函数,执行调用过程,当用到某个类,会首先进行类加载
该阶段每进行一次new操作,会执行一遍如下过程,返回一个新的对象:
父类的非静态变量
父类的非静态代码块
父类的构造方法
子类的非静态变量
子类的非静态代码块
子类的构造方法
注:静态方法和非静态方法均在调用时执行
静态方法和非静态方法均只存在一份,无论有多少个对象存在,调用的方法是同一个。
1.2.2 类加载实现天然线程安全
public class BaseServiceBinder extends IBaseService.Stub {
private BaseServiceBinder() {
}
private static class ServiceBinderLoader {
private static final BaseServiceBinder binder = new BaseServiceBinder(); //静态变量在初始化(程序启动时)时就进行了初始化
}
public static BaseServiceBinder getInstance() { //静态方法在调用时执行
return ServiceBinderLoader.binder;
}
}
2. 饿汉型
2.1 静态变量
public class Singleton {
private static Singleton instance = new Singleton(); //程序初始化时会实例化,仅执行一次
public static Singleton getInstance() {
return instance;
}
private Singleton() {
}
}
2.2 枚举
public enum EnumSingleton {
SINGLETON;
}