目录
- 1.什么是单例模式?
- 2.单例模式的实现
- 2.1 饿汉式单例模式
- 2.2 懒汉式单例模式
- 3.线程安全的单例模式
- 3.1 版本 1
- 3.2 版本 2 (双重检测)
- 3.3 版本 3 (禁止指令重排)
1.什么是单例模式?
单例模式是 Java
中最简单也是最常用的设计模式之一,这种模式属于创建者模式。单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有一个对象被创建。这个类提供了一种访问其唯一对象的方式,可以直接获取该对象。
2.单例模式的实现
单例模式的实现,我们主要分为两种,一种叫饿汉式另外一种叫懒汉式。
2.1 饿汉式单例模式
饿汉式,顾名思义让人感觉非常急不可耐,所以它会直接在类加载时就将我们的唯一对象实例化好了。用不用该对象它不管,反正它提前帮你实例化好了。具体的细节见代码注释
//饿汉模式
class Singleton {
//一个 Java 程序中,一个类对象只存在一份
//1.使用static创建一个实例,并且立即进行初始化
private static Singleton instance = new Singleton();
//2.为了防止在其他地方new这个对象,将构造方法写为private
private Singleton() {
}
//3.提供一个方法,让外面能够拿到唯一实例
public static Singleton getInstance() {
return instance;
}
}
为了检验我们得到的对象是否是同一个对象,我们可以调用两次getInstance
方法来检验一下获取的两个对象是否是同一个。
public class Main{
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1==instance2); //打印结果为 true
}
}
打印结果为true
,说明饿汉式的单例模式我们就实现完毕了。
2.2 懒汉式单例模式
懒汉式,顾名思义就是非常懒,它只有在需要调用该对象的时候,如果该对象还未初始化,才会去将其初始化。这就解决了饿汉式的缺点,如果创建了对象却不使用,那不是白白占着内存吗?有句好话说的好,这不是占着茅坑不那啥吗 ????。因此我们可以更改我们的代码实现
//单例模式——懒汉模式
class Singleton2 {
//1.不会立即进行初始化
private static Singleton2 instance = null;
//2.构造方法设为 private
private Singleton2() {
}
//3.提供一个方法获取单例实例
public static Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
这样,我们就完成了懒汉式单例模式的实现,它只有在第一次调用getInstance
方法时才触发对象实例化,我们同样可以检测一下对象是否一致。
public class Main{
public static void main(String[] args) {
Singleton2 s1=Singleton2.getInstance();
Singleton2 s2=Singleton2.getInstance();
System.out.println(s1==s2); //返回结果为true
}
}
3.线程安全的单例模式
很显然,上述代码的是一个线程不安全的单例模式实现。如果两个线程 A,B同时通过 instance==null
的判断,那么两个线程都会执行实例化操作,那这样对象就被实例了两次了。
3.1 版本 1
那如何解决线程安全问题呢?显然根据我们学过的线程知识那就是进行加锁。 既然如此,我们每次只允许一个线程来判断即可,于是写出下列代码:
class Singleton {
//1.不会立即进行初始化
private static Singleton instance = null;
//2.构造方法设为 private
private Singleton() {
}
//3.提供一个方法获取单例实例
public static Singleton getInstance() {
// 使读写为原子操作,读写合一
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
不过如果你在面试时写出上面的代码,面试官八成是要让你回去等通知了???? 。因为仔细观察这个代码,每次在调用getInstance
方法时,每个线程都会进行锁竞争导致排队等待,但实际上锁内的实例化代码只会执行一次,这其实就是毫无意义的锁竞争,白白浪费系统资源。
3.2 版本 2 (双重检测)
想要解决上述问题也非常简单,我们只需要在外层再加上一层判断非空判断即可。这样两次判空的机制叫做 双重检测机制。
class Singleton {
//1.不会立即进行初始化
private static Singleton instance = null;
//2.构造方法设为 private
private Singleton() {
}
//3.提供一个方法获取单例实例
public static Singleton getInstance() {
// 为了防止大量不必要的锁竞争
if (instance == null) { 判断1
// 使读写为原子操作,读写合一
synchronized (Singleton.class) {
if (instance == null) { 判断2
instance = new Singleton();
}
}
}
return instance;
}
}
我们来分析一下为什么这样可以解决版本 1
代码的问题,当多个线程开始并发执行时,肯定会有多个线程同时调用getInstance
方法,此时它们他们都能通过判断1
。但当进来后,只有持有锁的第一个线程可以通过判断2
并完成instance
的实例化,然后和它同一批通过判断1
的线程轮流持有锁后,都无法通过判断2
,也就保证了实例化只进行一次。
最重要的一点来了,当后续有线程再次调用getInstance
时,此时instance
已经被实例化了,那这些后续来的线程连判断1
都无法通过,这也就保证了不会产生无意义的锁竞争。
3.3 版本 3 (禁止指令重排)
虽然版本2
的代码已经看上去非常完美了,但仍然存在一个容易让人忽略的问题,那就是 指令重排问题。
那么我们首先要知道——什么是指令重排序?
假设我们要去商场买水果,我们接收到命令需要买的水果有——苹果、西瓜、香蕉、桃子。于是我们按照下图的顺序分别购买,最后离开商场。
但很容易发现,上面这个人好像笨笨的,他先去买最远的又倒回来买最近的,但它完全可以按照 苹果-香蕉-西瓜-桃子 这样的顺序购买,消耗的时间更短。这样重新更改执行顺序我们就可以叫做指令重排序。
对于我们给出的指令,jvm
也会进行指令重排序,以此选择一些它认为更加优秀的执行方式,但正是如此会带来一些问题。对于一条实例化代码instance = new Singleton();
,它实际上的指令会被分为三条:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
正是因为指令重排序的情况,jvm
有可能会更改三条指令的执行顺序,比如出现下面这种情况
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象
这会产生什么情况呢?
假设存在线程AB
分别执行到下面两个位置,此时线程 A
执行了指令 1,3
,还没有执行初始化对象这一步。但线程B
在判断时已经发现instacne
指向不为null
了,于是 返回了一个没有完成初始化的instacne对象。
那么我们该如何避免上面这种情况呢?也非常简单,只需要在instacne
对象前面加上关键字 volatile。 它其中一个作用即是禁止指令重排序,那么jvm
在操作它时,都会严格按照正常流程执行,这也就解决了上述问题,那么请看最终版代码:
//完善版线程安全单例模式
class Singleton {
//1.不会立即进行初始化
private static volatile Singleton instance = null;
//2.构造方法设为 private
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}