目录
今日良言:关关难过关关过,步步难行步步行
一、单例模式
1.饿汉模式
2.懒汉模式
二、工厂模式
首先来解释一下,什么是单例模式。
单例模式也就是单个实例(对象)。在有些场景中,只能创建出一个实例,不应该创建多个实例。
单例模式,就是针对上述的需求场景进行了个更强制的保证,通过巧用 java 现有的语法,达成了某个类 只能被创建出一个实例,这样的效果.(当程序猿不小心创建了多个实例,就会编译报错)。
单例模式最常见的两种就是:饿汉模式和懒汉模式。
代码如下:
class Singleton{
// 在此处就把实例给创建出来了
private static Singleton instance = new Singleton();
// 如果需要使用这个唯一的实例,统一通过这个方法获取
public static Singleton getInstance() {
return instance;
}
// 为了避免Singleton 类不小心被赋值出多份,将构造方法设置成private.
// 此时就无法通过new 的方式来创建这个Singleton 实例了。
private Singleton(){}
}
public class Exercise {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
在类加载阶段就将实例创建好了,这种效果就给人一种“特别急切”的感觉。
被static修饰表示这个属性和实例无关,而是和类相关。
Java 代码中的每个类,都会在编译完成后得到.class 文件,JVM 运行时就会加载这个.class文件,读取其中的二进制指令,并且在内存中构造出对应过的类对象(形如:Singleton.class),具体的类加载可以阅读博主之前写的博客:
深度剖析JVM三个面试常考知识点_程序猿小马的博客-CSDN博客
由于 类对象在一个 Java 进程中是唯一的,因此这个类对象的内部的类属性也是唯一的。
static 在这里的作用有两个:
1)static 保证这个实例唯一。
2)static 保证这个实例确实在一定的时机被创建出来。
static 属于这个实现方式中的灵魂角色。
代码实现:
class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
// 构造方法设置成私有的
private SingletonLazy(){}
}
public class Exercise {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}
这个实例并非是类加载阶段创建,而是真正第一次使用的时候才去创建, 如果不用就不创建了。
上述写的饿汉模式和懒汉模式是在单线程情况下的代码,如果在多线程下调用getInstance 是否是线程安全的呢?
答案是:一个是线程安全的,一个是线程不安全的。
饿汉模式是线程安全的,因为饿汉模式的 getInstance 方法只涉及到“读操作”。
懒汉模式是线程不安全的,因为懒汉模式的 getInstance 方法既有读操作又有写操作。
线程安全问题的详细解释,博主在之前的博客中有提到:
线程安全问题_程序猿小马的博客-CSDN博客
这里如果在多线程情况下调用懒汉模式的 getInstance 方法,会发生多次 new 操作,显然就不是单例了。
那么,如何让上述懒汉模式能够成为线程安全的呢?
加锁!!!
上述线程安全问题本质上是 修改操作不是原子的,因此,需要保证这个修改操作是原子的。
修改代码如下:
此时,把锁加到外面,保证了读操作和修改操作是一个整体。
但是,代码写到这里,还有问题,上述这种写法,就导致了每次 getInstance 都需要加锁,加锁操作都是有开销的,仔细考虑一下,这里真的需要每次都加锁吗?
显然不是,这里的加锁只是在new出对象之前加上是有必要的,一旦对象 new 完以后,后续调用 getInstance ,此时 instance 一定是非空的,因此会直接 return。
基于上述讨论,就可以给刚才的代码加上一个判定:
如果对象还没创建才加锁,如果对象已经创建过了,就不加锁了。
修改代码如下:
此时,这里就不再是无脑加锁了,而是满足了特定条件之后,才真正加锁。
注意:
这两个if 的作用不一样,第一个if 判断是否要加锁,第二个if 判断是否要创建对象。
加锁操作可能会引起线程阻塞,当执行到锁结束之后,执行到第二个 if 的时候,第二个 if 和第一个 if 之间可能已经隔了很久的时间了,instance 变量可能已经被别的线程给修改过了,所以需要第二次 if 判断当前线程是否需要创建对象。
上述代码其实还存在问题: 内存可见性问题以及指令重排序
关于这个问题,博主之前的博客也有详细介绍:
线程安全问题_程序猿小马的博客-CSDN博客
内存可见性问题:
假设有很多线程都去进行 getInstance ,这个时候,可能会存在被优化的风险(只有第一次读的时候,才真正的读了内存,后续都是读寄存器)
指令重排序:
instance = new SingletonLazy();
这个操作可以拆分为三个步骤:
1)申请内存空间。
2)调用构造方法,把这个内存空间初始化成一个合理的对象。
3)把内存空间的地址赋值给 instance 引用。
正常情况下,是按照 1 2 3 这个顺序执行代码,但是编译器存在指令重排序问题,编译器为了提高程序效率,会调整代码执行顺序, 1 2 3 可能就变成了 1 3 2 ,如果是单线程下,1 2 3 和 1 3 2 没有本质区别,但是多线程下就会出现问题了。
假设线程 t1 是 按照 1 3 2 的步骤执行的,t1 执行到 1 3 之后,执行 2 之前,被切除 cpu ,此时 t2执行,(当 t1 执行完 3 之后,t2 看起来此处的引用就非空了)此时此刻,t2 就相当于直接返回了 instance 引用,并且可能会尝试使用引用中的属性,但是由于 t1 中的 2 操作还没执行完呢,t2 拿到的是非法的对象,还没构造完成的不完整的对象。
因此,需要解决上述问题,使用 volatile !!!
修改代码如下:
懒汉模式完整代码如下:
class SingletonLazy {
private volatile static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
// 构造方法设置成私有的
private SingletonLazy(){}
}
public class Exercise {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}
先来解释一下什么是工厂模式。
工程模式用一句话概括:使用普通的方法,来代替构造方法创建对象。
为什么要代替呢? 这是因为构造方法有坑,坑就体现在,如果只构造一种对象,好办,如果要构造多种不同的情况,就不好办了。
举个例子:
假设现在有一个类,表示平面上的一个点
上述构造方法表示使用笛卡尔坐标系提供的坐标来构造点。
如果这里假设再使用极坐标来构造点,代码如下:
很明显,这个代码存在问题,正常来说,多个构造方法,是通过“重载”的方式来提供的,重载要求的是 方法名相同,参数的个数或者类型不同。
为了解决这个问题,就可以使用工厂模式:
普通方法的方法名没有限制,因此有多种方式构造,使用不同的方法名即可。
以上就是单例模式和工厂模式的介绍。