文章目录
- 一、介绍什么是单例模式
- 二、饿汉模式
- 三、懒汉模式
- 四、讨论两种模式的线程安全问题
一、介绍什么是单例模式
在介绍单例模式之前,我们得先明确一个名词设计模式。
所谓设计模式其实不难理解,就是在计算机这个圈子中,呢些大佬们为了防止我们这些资质平平的程序猿不把代码写的太差,所设计出的针对一些场景的问题解决方案,解决框架。
单例模式就是多个设计模式中的一个。
单例模式,逐字来理解就是:单个示例对象的意思。
在某些场景中,有些特定的类只能创建出一个实例,不应该创建多个实例。这样的要求虽然可以通过程序员本人进行控制,但是仍然存在不确定性。
使用单例模式后,此时只能创建 1 个 实例,单例模式就是针对上述的特殊需求的一个强制的保证。
在 Java 中实现单例模式的方法有很多,这里介绍一下最常见的两类。
(1) 饿汉模式
(2) 懒汉模式
二、饿汉模式
我们已经知道,单例模式就是要让某个类创建出唯一一个对象,所谓饿汉,字面理解就是一个饿了很久的人,这样的人在看到吃的就会异常急切。
这里的饿汉模式,就是让代码在一开始执行的时候,即就是类加载阶段,就去创建这个类的实例,这种效果就给人一种 “非常急切” 的感觉。
下面我来展示一下相关的代码示例:
//饿汉模式
//保证 Singleton 这个类只能创建一个实例
class Singleton{
//在此处先将实例创建出来
private static Singleton instance = new Singleton();
//如果要使用这个唯一实例,同意通过 Singleton.getInstance() 方式获取
public static Singleton getInstance(){
return instance;
}
//为了避免 Singleton 类被复制多份
//将构造方法设置为 private。再类外面无法通过 new 来实现创建 Singleton 实例
private Singleton(){}
}
public class ThreadDemo {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
上面的结果就表明这是同一个实例。
static 在这里的作用(了解 static 关键字作用的可以跳过)
static 在这里保证了使用的对象是唯一的,下面通过代码示例来解释一下:
class Test{
public int A;
public static int B;
}
public class StaticTest {
public static void main(String[] args) {
Test t1 = new Test();
Test t2 = new Test();
//设置非 static 修饰的变量
t1.A = 10;
t2.A = 20;
System.out.println("t1.A的值"+ t1.A);
System.out.println("t2.A的值"+ t2.A);
//设置由 static 修饰的变量
t1.B = 10;
t2.B = 20;
System.out.println("t1.B的值"+ t1.B);
System.out.println("t2.B的值"+ t1.B);
}
}
总的来说,被 static 修饰的关键字在代码中表现的内容与最后一次设定是一样的,这样也就确保了使用对象的唯一性。
三、懒汉模式
对于懒汉模式 “懒汉” 字面意义上不难理解,就是表示一个人很懒,直到事情迫在眉睫才会去做。
在这里,懒汉模式也是同样,在类加载之初是不会进行对象的创建,一直到第一次真正使用的时候才会去创建。
代码示例:
//实现懒汉模式
class SingletonLazy{
//这里先将对象不进行创建,先设定为 null
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
//判断是否有对象被创建,没有就进行创建
if (instance == null){
instance = new SingletonLazy();
}
return instance;
}
//将构造方法设定为 static 不能进行 new 来创建
private SingletonLazy(){}
}
public class ThreadDemo21 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}
运行结果
同样的,这一样表示了上面运用的是同一个对象。
四、讨论两种模式的线程安全问题
上面的 饿汉模式 和 懒汉模式,在多线程的调用下是很有可能存在线程安全问题的。下面我来简单分析一下。
前面我们已经了解了关于线程安全问题的部分知识,我们知道,计算机中对数据的处理分为,1. 从内存 “读”。2.在cpu寄存器上 “改”。3. “写”入内存。这几个操作。在这里我们就根据上面几点来进行解释。
如上图所示,我们发现饿汉模式只存在读这个操作,操作很单一,在这里就没有线程安全问题。懒汉模式存在着读和写两个操作,对此如果不进行约束,是有可能会出现线程安全问题。如下图所示:
呢么,如何才能让懒汉模式 线程安全?答案是:加锁。
class SingletonLazy{
//这里先将对象不进行创建,先设定为 null
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
//通过加锁消除线程安全问题,在此处加锁才能保证读操作和修改操作是一个整体
synchronized (SingletonLazy.class){
//判断是否有对象被创建,没有就进行创建
if (instance == null){
instance = new SingletonLazy();
}
}
return instance;
}
//将构造方法设定为 static 不能进行 new 来创建
private SingletonLazy(){}
}
这里加锁之后,t1 线程已经创建了一个对象,之后的 t2 线程 load 到的结果是前面线程修改的结果。因此 t2 线程就不会在创建新的对象,直接返回现有的对象。
需要注意的是,在这里的加锁操作,在线程每进行 getInstance 操作时每次都会进行加锁,但是每次的加锁都要有开销,真的需要每次加锁吗?我们知道在创建对象前,需要进行加锁,但是,在第一次创建对象后,后续的操作会直接触发 return,对此可以对代码进行如下修改:
//实现懒汉模式
class SingletonLazy{
//这里先将对象不进行创建,先设定为 null
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
//负责判断是否要加锁
if(instance == null){
//通过加锁消除线程安全问题,在此处加锁才能保证读操作和修改操作是一个整体
synchronized (SingletonLazy.class){
//判断是否有对象被创建,没有就进行创建
if (instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
//将构造方法设定为 static 不能进行 new 来创建
private SingletonLazy(){}
}
注:上面的两个 if 条件语句虽然条件相同,但是两者之间控制的问题完全不同。
有关懒汉模式上面的加锁操作以及对加锁操作的修改仍然没有完全解决其中存在的问题。这里还存在内存可见性问题,指令重排序问题。
内存可见性问题: 在这里,假设有很多线程都去进行 getInstance,此时就很有可能有被优化的风险,只有第一次读取了真正的内存,后续读取的都是寄存器中的数据。
指令重排序问题: 在这里,我们将 instance = new SingIeton(); 拆分为下面三部分。
- 1.申请内存空间
- 2.调用构造方法,将内存空间初始化为一个合理的对象
- 3.将内存地址赋值给 instance 引用。
在正常情况下,按照1,2,3这样的顺序进行执行的。但是,在多线程的环境下,就会出现问题。
假设线程 t1 是在排序后按照 1,3,2这个顺序进行执行的。当执行完 1,3 这两个步骤后,被切出 cpu ,此时 t2 进入cpu,这里就出现问题了,这里的 t2 拿到的是非法的对象,是没有构造完成的不完整对象。
要解决上面的问题 volatile关键字可以解决问题。
volatile 关键字有下面两个功能:
(1) 解决内存可见性
(2) 禁止指令重排序
代码如下:
//实现懒汉模式
class SingletonLazy{
//这里先将对象不进行创建,先设定为 null
//添加 volatile 关键字,防止指令重排序,解决内存可见性问题
private volatile static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
//负责判断是否要加锁
if(instance == null){
//通过加锁消除线程安全问题,在此处加锁才能保证读操作和修改操作是一个整体
synchronized (SingletonLazy.class){
//判断是否有对象被创建,没有就进行创建
if (instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
//将构造方法设定为 static 不能进行 new 来创建
private SingletonLazy(){}
}
到此,关于懒汉模式的相关问题基本解决。