共计 2530 个字符,预计需要花费 7 分钟才能阅读完成。
目录
- 一、设计模式
-
- 1.1 单例模式
-
- 1.1.1 饿汉模式
- 1.1.2 懒汉模式
- 1.2 线程安全问题
- 1.3 懒汉模式线程安全问题的解决方法
-
- 1.3.1 原子性问题解决
- 1.3.2 解决效率问题
- 1.3.3 解决内存可见性问题和指令重排序问题
一、设计模式
在讲解案例前,先介绍一个概念设计模式:就是大佬们把一些经典问题整理出来,针对这些场景,大佬们总结出固定的套路,来解决这些问题。就类似棋谱一样的概念。
1.1 单例模式
单例模式:就是强制要求,某个类在某个程序中,只能有一个实例,只能 new 一个对象。比如在开发中一个类存有很大的数据量,new 两三次就把空间占满了,这时就可以使用单例模式了。
1.1.1 饿汉模式
饿:指尽早创建对象。
饿汉模式:类对象在类中使用 static 修饰为静态成员,并将构造方法使用 private 修饰私有化。
下面写一个简单饿汉模式代码:
class SingletonHunger {
private static SingletonHunger instance = new SingletonHunger();
public static SingletonHunger getInstance() {
return instance;
}
private SingletonHunger() {
}
}
1.1.2 懒汉模式
懒:指尽量晚创建对象,甚至不创建。
懒汉模式:类对象在类中使用 static 修饰为静态成员,并赋值为 null,并将构造方法使用 private 修饰私有化,只不过是在 get 方法中去实例化。
下面写一个简单懒汉模式代码:
class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if(instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() {
}
}
1.2 线程安全问题
在多线程代码中我们要考虑上诉两中模式是否存在线程安全问题。
- 我们看饿汉模式中在类创建的同时直接就将对象实例化好了,后续就一个 return 操作,相当于只是读取操作,而读取操作是不涉及线程安全问题的,所以 饿汉模式不存在线程安全问题。
- 我们看懒汉模式中,是先进行一次判断操作,在进行实例化,那这样就涉及到不是原子性的了,所以 懒汉模式存在线程安全问题。
1.3 懒汉模式线程安全问题的解决方法
1.3.1 原子性问题解决
这样的问题我们使用 synchronized
加锁操作就行。
- 可以加在 get 方法上,以当前类对象作为锁对象;
- 也可以将 if 包含起来。
class SingletonLazy {
private static SingletonLazy instance = null;
private static Object block = new Object();
public static SingletonLazy getInstance() {
synchronized(block){
if(instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
private SingletonLazy() {
}
}
1.3.2 解决效率问题
在我们上面加了锁之后,创建完对象之后每次在调用 get 方法的时候,还是会加锁,这就会导致产生锁竞争,线程阻塞问题影响效率。
这种解决方式就是在这之前在判断一次对象是否为空就行了。
class SingletonLazy {
private static SingletonLazy instance = null;
private static Object block = new Object();
public static SingletonLazy getInstance() {
if(instance == null) {
synchronized (block) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {
}
}
这可能两个相同的判空语句放在一起,感觉会有点别扭,但是其实两者的作用是天差地别的:
- 第一个语句是防止加了锁之后,在竞争锁导致效率低;
- 第二个语句是为了保证判断和实例是原子的。
1.3.3 解决内存可见性问题和指令重排序问题
编译器是否会进行优化导致内存可见性问题的出现,是不一定的,人为也不好预测。所以在对象前面加上 volatile
修饰就好。
指令重排序:是编译器对代码执行的指令的顺序进行调整,以达到优化的目的。
就像去买东西一样,先买什么后买什么的顺序也会影响买东西花费的时间。
而在上诉代码中 instance = new SingletonLazy();
这个语句就有可能触发指令重排序问题。
这条语句主要执行三条主要指令;
- 申请内存空间;
- 在空间上构造对象,也就是实例化对象;
- 将内存空间的 ” 首地址 ” 赋值给引用变量。
正常的执行顺序是 1 ->2->3,但是由于指令重排序会出现 1 ->3-> 2 的情况,
这样先执行了 3 操作,该对象就不为 null 了,其它线程就可以对这个还没有实例化的对象进行操作了。这也引发了线程不安全问题。
这个问题的解决方法也是在对象前面加上 volatile
修饰就好。
volatile 主要作用是:
- 确保从内存中读取数据,避免内存可见性问题;
- 确保读取和修改操作不会触发指令重排序问题。
代码:
class SingletonLazy {
private volatile static SingletonLazy instance = null;
private static Object block = new Object();
public static SingletonLazy getInstance() {
if (instance == null) {
synchronized (block) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {
}
}
原文地址: 【JavaEE】【多线程】单例模式