【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;
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值