单例模式:
饿汉模式/懒汉模式
单例模式是一种常见的设计模式,类似于棋谱,把一些对局中常见场景 ,整理总结,让别人来研究学习。
(
设计模式有多种,以后有些工作经验再学习 ,有了一定的经验才能明白其中的道理
)
为什么会有设计模式
代码领域里程序猿水平参差不齐,
于是就有大佬们,根据一些常见的需求场景, 整理出来了一些应对的解决办法,
这个东西就是"设计模式"。
能够提高程序猿的“下限”按照设计模式来写代码,不说代码能写多好,但是不会很差。
饿汉模式:
类加载的同时, 创建实例。
懒汉模式:
效率更高
类加载的时候不创建实例. 第一次使用的时候才创建实例
如果整个代码后续没人调用getInstance, 与
饿汉模式
相比,这样就把构造实例的过程省去了,
或者即使有代码后续调用getInstance,但是调用的时机比较晚,这个时候创建实例的时机也就较晚,就
和其他的耗时操作错开了,使系统执行更加高效。(一般程序刚启动的时候,要初始化的东西很多,系统资源紧张)
饿汉模式线程安全,懒汉不安全
饿汉模式:线程安全
这个操作只是单纯的“读数据" ,不涉及到修改。
懒汉模式:线程不安全
此时懒汉模式会创建多个实例不符合单例模式要求出现BUG
如何解决懒汉模式线程不安全问题?
(1)加锁:synchronized,保证原子性
(2)关键字:Volatile,内存可见性,禁止指令重排序
加锁
通过
synchronized
加锁方式,线程安全问题解决了。
但是又引入了新的问题,
线程不安全,也不是一直都不安全。只是实例创建之前(首轮调用的时候)
才会触发线程不安全问题,在第一次
实例创建好之后,instance不为null也就不涉及修改操作,此时线程也就安全了。
由于
,
加锁开销其实较大
,加锁可能就会涉及到用户态->内核态之间的切换,这样的切换成本是比较高的。因此
加锁绝对不是“无脑加”,在我们
只在需要时加锁。
加锁代价比较大,因此该加锁枷锁,不该加就去掉。
只需要在加锁前面加一个判断,判断是否需要加锁
Volatile关键字:
指令重排序(编译器进行代码优化操作):造成线程不安全
new操作本质上也分成三个步骤:
1.申请内存,得到内存首地址。
2.调用构造方法来初始化实例。
3.把内存的首地址赋值给instance引用。
上述步骤可能编译器会进行“指令重排序”的优化,
在单线程的角度下, 2和3是可以调换顺序的。
(单线程的情况下,此时2和3先执行谁,后执行谁效果样)
设想:
假设此处触发指令重排序,
并且按照1 3 2的顺序执行的。
有可能t1执行了1和3之后,instance得到了不完全的对象,只是有内存,但是内存上的数据是无效的。接下来t2线程执行调用getInstance方法,判断instance不为null,直接返回instance出现了BUG,造成线程不安全。
因此就要用Volatile关键字禁止指令重排序
完整单例模式的懒汉实现:
另一方面,再想想,
如果线程更多的话,
有的线程在读Instance,有的线程在修改,
也存在内存可见性问题,
加上volatile也是应当。
简单完整单例模式代码如下:
class SingletonLazy{
private volatile static SingletonLazy instance = null;
//类加载的时候不创建实例. 第一次使用的时候才创建实例
public static SingletonLazy getInstance(){
if(instance == null){//判断是否加锁
//给SingletonLazy单例类加锁
synchronized (SingletonLazy.class){
if(instance == null){//判断是否创建实例
instance = new SingletonLazy();
}
}
}
return instance;
}
//将此类构造方法设置为private,此时就无法在此类外创建实例对象
private SingletonLazy(){ }
}
public class Singleton {
public static void main(String[] args) {
SingletonLazy instance = SingletonLazy.getInstance();
}
}