单例模式
单例模式
前言
单例模式算是设计模式中比较好理解,也比较重要的一种设计模式。
单例模式属于创造型的设计模式。主要用于类的实例化。
很多资料吧单例模式分为懒汉式,饿汉式.. 等等各种变化,这里可以已这为基础举一些简单的例子,好理解
单例模式主要我们可以分为三步
- 私有构造函数
- 初始化.可以提前初始化,可以在使用需要时初始化。
- 开放一个方法来获取唯一实例
优点:
- 一个类只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例
- 避免对共享资源的多重占用
缺点:
- 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
- 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。
饿汉式
什么叫饿汉式,其实就是提前加载。看代码
1 | class A{ |
饿汉式是怎么做的?很简单,就是初始化的时候是提前初始化。这里初始化我只一个简单的new了一个A对象,实际中如果A对象比较重,可以写在静态代码块中。
这种写法是天然支持并发操作的。因为在类装载时便完成了初始化操作。实际就是在调用getInstance
之前完成了A的初始化操作。所以getInstance
实际已经没有了并发的危险
懒汉式
什么叫懒汉式,其实就是懒加载。看代码
1 | class A{ |
上面的代码,讲初始化的操作搬到了getInstance
中了。在单线程中可以使用,但是如果涉及到并发,可能就不行了。
因为很简单 a==null
这一步,在并发的时候,单线程1进入a==null
,还没来得及初始化a。可能其他线程也进入a==null
的判断。所以这不是线程安全的。
怎么改进?
最简单的方法加锁
1 | class A{ |
这种synchronized加法比较简单,但是效率太差。比如100个线程要获取a,实际上a已经初始化好了。那么为什么还有100个线程去抢锁了?
改进一下,缩小锁的颗粒度
1 | class A{ |
这种锁的颗粒度是小了,但是问题在于标记1的位置,很可能一次性进入很多个线程,实际上导致刚开始每个线程拿到的都是不同的对象。
再改进一下
1 | class A{ |
这就是我们常见的加锁时使用的双重检测,即可以保证性能,又可以保证稳定性,这中代码习惯很多地方都会使用。
但是其实上诉代码可不是完全可靠的。主要是因为指令重排序的问题。
首先我们先了解一下a=new A()
这句代码实际做了什么。
- 分配一块内存 M
- 在内存M上初始化A的数据
- 将M的内存地址给a
实际上a == null
是针对第3步。但是由于指令重排序,在实际情况下,可以是123,可以是132 毕竟在单线程中都不影响。
来讲一下实际情况。
线程A调用getInstance
方法,进入了a=new A(),然后按132的步骤初始化a,初始化到2的时候。线程B调用getInstance
方法。发现a!=null
(此时A虽然不等于null,但是a还没有初始化数据,直接使用可能出现异常)。直接返回了一个还没有初始化好的a对象。这样就会造成线程B拿到的a是有问题的。
怎么解决这个问题?
通过添加volatile
关键字来解决,volatile
只要是保证a的可见性,同时也禁止指令重排序。所有123步骤是永远按123步骤走的。
实际上volatile
经常和双重检测一起使用。
1 | class A{ |
其他
单例模式除了以上这些写法还可以通过内部类和枚举来使用,利用java的类加载来保证其安全性,也是推荐使用的
1 | class A { |
总结
实际代码中用什么可以自己去考虑一下,比如有些类在项目中是肯定使用的,可以直接使用饿汉式,有些不确定使用性,可以考虑使用饿汉式,双重检测和内部类使用都可以。
原文作者: duteliang
原文链接: http://yoursite.com/2019/02/13/javabase/shejimoshi/单例模式/
版权声明: 转载请注明出处(必须保留原文作者署名原文链接)