文章目录
- 1单例模式主要模式
- 1.1 饿汉模式
- 1.2 懒汉模式
- 2 单例模式安全性问题
1单例模式主要模式
在某些场景中,有些特定的类只能输出一个实例(对象),不应该创建多个实例,此时就可以使用 单例模式。
使用了单例模式后,就很难创建多个实例。
单例模式 主要介绍两种常见的方式:饿汉模式,懒汉模式。
1.1 饿汉模式
先来看一段代码。
package thread;
//饿汉模式 - 单例模式实现
//此处要保证这个类只能创建一个实例
class Singleton {
//创建实例
private static Singleton instance = new Singleton();
//如果要使用这个实例,通过 Singleton.getInstance() 方式来获取
public static Singleton getInstance() {
return instance;
}
//为了避免 Singleton 被不小心多复制出来
//把构造方法设为 private 在类的外面那就无法通过 new 的方式来创建 Singleton 这个实例了
private Singleton() {}
}
public class ThreadDemo22 {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
s1 和 s2 相等说明这是一个实例,而且此时使用 new 一个实例 程序则会报错。
如果一个饿了很久的人看到吃的,那么他就会特别急切的去吃了这个东西。
类加载阶段,就把实例创建出来了。(累加载是比较靠前的阶段)
这种效果,就会给人一种 “特别急切” 的感觉。
1.2 懒汉模式
先来看一段代码。
package thread;
class SinglentonLazy {
private static SinglentonLazy instance = null;
public static SinglentonLazy getInstance() {
if (instance == null) {
instance = new SinglentonLazy();
}
return instance;
}
private SinglentonLazy() {}
}
public class ThreadDemo23 {
public static void main(String[] args) {
SinglentonLazy s1 = SinglentonLazy.getInstance();
SinglentonLazy s2 = SinglentonLazy.getInstance();
System.out.println(s1 == s2);
}
}
s1 和 s2 相等说明这是一个实例。
懒汉模式中的实例并不是类加载的时候创建了,而是真正第一次使用的时候才会去创建。
如果不会使用,就不创建。
就好比生活中的拖延症,不到最后时刻绝不行动。
在效率方面。懒汉模式要更胜一筹。
2 单例模式安全性问题
上述讲的 饿汉模式 和 懒汉模式 如果在多线程的环境下调用 getInstance ,是否是线程·安全的。
懒汉模式有读有写。
饿汉模式这里是涉及到了 “读操作”。
这两种模式只有一种是安全的,那不安全的是哪一种呢?
注意 new 本质上也是也是2多个指令,此处暂时视为一个整体,不影响对程序的分析。
如何使用加锁让 懒汉模式 变成线程安全的?
下面这种方式是不行的。
下面这种方式是可以的。
package thread;
class SinglentonLazy {
private static SinglentonLazy instance = null;
public static SinglentonLazy getInstance() {
synchronized (SinglentonLazy.class) {
if (instance == null) {
instance = new SinglentonLazy();
}
}
return instance;
}
private SinglentonLazy() {}
}
public class ThreadDemo23 {
public static void main(String[] args) {
SinglentonLazy s1 = SinglentonLazy.getInstance();
SinglentonLazy s2 = SinglentonLazy.getInstance();
System.out.println(s1 == s2);
}
}
此处的 t2 load 得到的结果是 t1 修改过后的结果。(得到的是一个非 null 的值)
因此 t2 就不会触发 if 条件,也就不会再创建新的对象了。
而是直接返回现有对象了。
执行到此处代码还是会有问题。
每次 getInstance 都需要加锁,而加锁是要有开销的,这里就需要仔细考虑需不需要每次都加锁?
仔细想想就会发现。这里的加锁只是在 new 出对象之前加上的,是有必要的。
一旦对象 new 完了之后,后序调用 getInstance 。此时 instance 的值一定是非空的。
因此就会直接触发 return ,相当于是一个比较操作,一个是返回操作。
这两个操作都是读操作,此时不加锁也没事。
基于上面的情况,可以加上一个判定。
如果对象还没创建,才加锁;如果对象已经创建就不加锁了。
下面是部分代码
public static SinglentonLazy getInstance() {
if (instance == null) {
synchronized (SinglentonLazy.class) {
if (instance == null) {
instance = new SinglentonLazy();
}
}
}
return instance;
}
此时就不再是无脑加锁了。而是在满足了条件时候才加锁。
即便是优化到了现在这个地步也还是会有 内存可见性 问题。
假设有很多的线程,都去进行 getInstance ,这个时候,是否会有被优化的风险呢?
(只有第一次读才是真正的读了内存,后续都是读寄存器/cache)
这就是内存可见性问题。
这里还会涉及到 指令重排序问题。
instance = new Singleton(); 会被拆分成三个步骤:
- 申请内存空间。
- 调用构造方法,把这个内存空间初始化成一个合理的对象。
- 把内存空间的地址赋值给 instance 引用。
如果是正常的情况下是会按照 1 2 3 的顺序来执行的,
但是编译器为了提示效率还会进行指令重排序,也就是调整代码的执行顺序。
此时 1 2 3 就可能变成了 1 3 2 。(单线程 1 2 3 和 1 3 2 没有去区别)
但是如果是多线程环境下就回出现问题了。
假设 t1 是按照 132 的顺序来执行的。
t1 执行到 1 3 之后,执行 2 之前会被切除 CPU ,t2 来执行。
(当 t1 执行完 3 之后,t2 看起来此处的引用就非空了)
此时此刻 t2 就相当于是直接返回了 instance 引用。并且可能会尝试使用引用中的属性。
但是由于 t1 中的 2 操作还没完全执行完呢,t2 拿到的是非法的对象,还没构造完成的不完整的对象。
解决办法:volatile
两个功能:
- 解决内存可见性
- 禁止指令重排序
部分代码
class SinglentonLazy {
private volatile static SinglentonLazy instance = null;
public static SinglentonLazy getInstance() {
if (instance == null) {
synchronized (SinglentonLazy.class) {
if (instance == null) {
instance = new SinglentonLazy();
}
}
}
return instance;
}
private SinglentonLazy() {}