Android单例模式详解

一.什么是单例

单例对象的类必须保证只有一个实例存在

对单例的实现可以分为两大类——懒汉式和饿汉式,他们的区别在于:

懒汉式:指全局的单例实例在第一次被使用时构建。

饿汉式:指全局的单例实例在类装载时构建。

懒汉式单例

1.简单版本

缺点:当多线程工作的时候,如果有多个线程同时运行到if (instance == null),都判断为null,那么两个线程就各自会创建一个实例——这样一来,就不是单例了

改进 ...

加上synchronized关键字

缺点:虽然解决了多个线程多个实例的问题 但是当一个线程执行到getInstance()时其他线程就要进入等待状态 实际上会对程序的执行效率造成负面影响

继续改进 ..

双重检查

实际上就是合并了上面两个解决办法

第一个if (instance == null),其实是为了解决Version2中的效率问题,只有instance为null的时候,才进入synchronized的代码段——大大减少了几率。

第二个if (instance == null),则是跟Version2一样,是为了防止可能出现多个实例的情况

其实 还是有小概率会出现问题  主要是设计 原子操作 和 指令重排的概念

主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

1. 给 singleton 分配内存

2. 调用 Singleton 的构造函数来初始化成员变量,形成实例

3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错

再稍微解释一下,就是说,由于有一个『instance已经不为null但是仍没有完成初始化』的中间状态,而这个时候,如果有其他线程刚好运行到第一层if (instance == null)这里,这里读取到的instance已经不为null了,所以就直接把这个中间状态的instance拿去用了,就会产生问题。

这里的关键在于——线程T1对instance的写操作没有完成,线程T2就执行了读操作

继续改进....

终极版本 volatile关键字

对于上面的版本虽然已经很大程度上改善了 但是毕竟还是有几率会出现问题的

解决方案就是给instance生明加上volatile关键字

volatile关键字的一个作用是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障(什么是内存屏障?),这样,在它的赋值完成之前,就不用会调用读操作。

注意:volatile阻止的不singleton = new Singleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))。

到了这个版本 确实没什么问题了 但是就是太复杂了。。。。

上面介绍了 懒汉式的一些基本写法 和改进后的写法 下面介绍一些饿汉式 相比懒汉式就简单多了

饿汉式单例实现

注:开头说过 饿汉式单例是指全局的单例实例在类装载时构建的实现方式

由于类装载的过程是由类加载器(ClassLoader)来执行的,这个过程也是由JVM来保证同步的,所以这种方式先天就有一个优势能够免疫许多由多线程引起的问题

当然万事万物没有完美的 如果非要对上面挑点毛病出来就是 由于INSTANCE的初始化是在类加载时进行的 而类的加载是由ClassLoader来做的 所以开发者很难把握它的初始化时机

1.可能由于初始化太早造成资源浪费

2.如果初始化本身依赖于一些其他数据,那么也就很难保证其他数据会在它初始化之前准备好。

一些其他的实现方式:

《Effective Java 1》  —— 静态内部类

这种写法非常巧妙:

对于内部类SingletonHolder,它是一个饿汉式的单例实现,在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个真·单例。

同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。

——它利用了ClassLoader来保证了同步,同时又能让开发者控制类加载的时机。从内部看是一个饿汉式的单例,但是从外部看来,又的确是懒汉式的实现

《Effective Java 2》 —— 枚举

由于创建枚举实例的过程是线程安全的,所以这种写法也没有同步的问题。

但是在需要继承的场景,它就不适用了

总结任何一种方法都有他的用武之地 只是需要考虑一个度的问题 就需要开发者在开发中选择自己适合的单例就好了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容