目录
前言
在聊单例模式之前,我们先来了解一下什么是设计模式?
设计模式是在软件工程中用来解决常见问题的一种标准方法或模板,它们代表了软件开发者在面对特定类型的问题时,经过验证的、可复用的解决方案。设计模式并不是完成具体任务的代码,而是一种描述问题和解决方案的方式,帮助开发者理解如何设计灵活、可维护和可扩展的系统。
设计模式通常分为三大类:
- 创建型模式(Creational Patterns):关注于对象的创建机制,确保系统在合适的地方创建合适的对象。
- 结构型模式(Structural Patterns):关注的是如何组合类或对象构成更大的结构。
- 行为型模式(Behavioral Patterns):关注的是对象之间的职责分配和通信机制。
单例模式是在面试中常见的设计模式之一,属于创建型模式。
1.单例模式
单例模式能够保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例,并通过提供一个全局访问点来访问这个实例。单例模式可以避免重复创建,减轻系统性能压力。
常见的单例模式有:1、饿汉模式;2、懒汉模式。
2.如何实现单例模式
2.1饿汉模式
饿汉模式是最简单的单例模式实现,实例在类加载时就被创建。
为什么构造方法要设置为private权限?
为了防止其他人不小new了这个对象。每次调用都需要通过getInstance方法
java
/**
* 单例模式的实现类。
* 保证一个类只有一个实例,并提供一个全局访问点。
*/
class Singleton{
// 静态实例变量,确保在类加载时就初始化,从而实现单例。
private static Singleton singleton=new Singleton();
/**
* 公共的静态方法,用于获取Singleton类的唯一实例。
*
* @return Singleton类的唯一实例。
*/
public static Singleton getInstance(){
return singleton;
}
// 将构造函数私有化,防止外部通过new方式创建实例,确保单例的唯一性。
//为了让外面的不可以创建实例,单例模式,构造方法要私有化
private Singleton(){}
}
public class Singles {
public static void main(String[] args) {
// 通过getInstance方法获取Singleton的实例
Singleton s1=Singleton.getInstance();
// 再次通过getInstance方法获取Singleton的实例
Singleton s2=Singleton.getInstance();
// 检查两个实例是否相同,预期输出为true
System.out.println(s1==s2);//true
}
}
运行之后,我们期望的结果是两个对象的实例是一样的。
上述的饿汉模式在创建实例时,是在类加载时就创建好了,但如果想要让其晚一点创建呢?
那么我们就得使用懒汉模式。
2.2懒汉模式
类加载的时候不创建实例,第一次使用的时候才创建实例。饿汉模式是在通过在类里创建了一个静态的实例,静态的成员变量在类加载的时候就会被创建,这样就会导致不管是否有用到这个实例,这个实例都会被创建,这样就会造成资源浪费。而懒汉模式则是通过在类中定义一个静态成员变量,并初始化为null。当调用getInstance时,如果静态成员变量为空,那么就会创建一个实例;反之,则直接返回这个实例。
java
class SingleLazy{
private static SingleLazy singleLazy=null;
public static SingleLazy getInstance(){
if(singleLazy==null){
singleLazy=new SingleLazy();
}
return singleLazy;
}
private SingleLazy(){}
}
但如果是在多线程中,懒汉模式按照上面写真的没事吗,其实不然,若真的按照上面去实例对象,会出现线程安全问题。
3.单例模式中的线程安全问题
在上一篇中,我们已经知道了线程在操作系统中是抢占式执行、随机调度的。
对于饿汉模式,在创建对象时不会出现线程安全问题,但对于懒汉模式,会有线程安全问题。
懒汉模式的getInstance方法中,想要创建实例,是需要先通过判断再进行修改的。这种是典型的线程不安全代码,在判断和修改之间,线程可能会进行切换。
3.1加锁
对于上述这种线程安全问题,那么我们就可以进行加锁。那这把锁加在哪里?我们知道线程安全问题是出现在if和new操作上,这两操作应该打包为一个整体。
java
class SingleLazy{
private static SingleLazy singleLazy=null;
public static SingleLazy getInstance(){
synchronized (SingleLazy.class) {
if (singleLazy == null) {
singleLazy = new SingleLazy();
}
}
return singleLazy;
}
private SingleLazy(){}
}
虽然通过加锁,我们解决了在判断和实例之间线程安全问题,但同时,我们还需要考虑一下,如果我们已经创建了实例,那么后续调用getInstance方法还需要进行加锁操作吗?不需要,我们在此加锁,是为了将if和new合为一个整体,防止在线程在if和new之间来回切换。
当我们实例完之后,if语句是进不去的。后续操作都是读操作。若每个线程调用getInstance方法都需要加锁,就会产生阻塞,影响性能。
3.2解决懒汉模式中频繁加锁问题
针对上述问题,我们需要在加锁之前,判断一下当前的singlelazy对象是否为null,若为null,则进行加锁操作,并进行实例;反之,则直接返回singlelazy。
java
/**
* 单例模式的懒汉式实现,确保在多线程环境下安全地创建单例对象。
* 这种实现方式称为"双重检查锁定",既延迟了单例的初始化,又保证了线程安全性。
*/
class SingleLazy {
// 静态实例变量,初始为null,用于存储单例对象
private static SingleLazy singleLazy = null;
/**
* 静态方法,用于获取单例对象。
* 如果实例尚未创建,则通过双重检查锁定来确保线程安全地创建单例。
*
* @return 单例对象的实例
*/
public static SingleLazy getInstance() {
// 检查实例是否已经存在,如果不存在则进行实例化
if (singleLazy == null) {
// 使用synchronized关键字确保线程安全
synchronized (SingleLazy.class) {
// 再次检查实例是否存在,避免多线程环境下重复实例化
if (singleLazy == null) {
// 实例化单例对象
singleLazy = new SingleLazy();
}
}
}
// 返回单例对象
return singleLazy;
}
// 将构造函数设为私有,防止外部直接实例化对象
private SingleLazy() {}
}
可能有人会疑惑,这里为什么要判断两次singlelazy是否为空?
第一次if:判断是否需要加锁
第二次if:判断是否要创建实例。
懒汉模式的代码到这,问题都解决了吗?
还没有,在new对象时,可能会造成重排序问题。重排序问题也是造成线程安全问题的因素之一。
在new一个对象时,其实可以分为三个步骤:
- 向内存申请空间
- 执行构造方法并初始化
- 将内存空间的地址赋值给引用变量
如果发生指令重排序,本来线程的执行顺序为1、2、3,但却被重排序为1、3、2,那么在线程并发执行时,可能就会出现问题。
从图中可以看到,线程t1判断完singlelazy为空之后,进入if分支创建实例时,此时若执行完两条指令,但此时线程被切换到t2,线程t2中singlelazy在判断完不为空,直接返回singlelazy对象,但由于此时的singlelazy并没有被初始化,若被使用,则会出现问题。
3.3解决new中指令重排序的线程安全问题
对于上述问题,这种情况就是因为编译器为了提高性能,对指令的顺序进行了优化。
示例:
假如我们现在有个清单:
- 买西瓜
- 买酱油
- 买哈密瓜
- 买葡萄
若我们按着清单的顺序来买,即是:
但是一般水果都是在店里都有,所以可以进行优化:
但是我们在某些情况下我们不想要进行优化,那么我们就可以使用volatile关键字来防止指令重排序,保证内存的可见性。
java
/**
* 单例模式的懒汉式实现,确保在多线程环境下安全地创建单例对象。
* 这种实现方式称为"双重检查锁定",既延迟了单例的初始化,又保证了线程安全性。
*/
class SingleLazy {
// 使用volatile修饰符确保多线程环境下的可见性,避免出现指令重排序的问题
// 静态实例变量,初始为null,用于存储单例对象
private static volatile SingleLazy singleLazy = null;
/**
* 静态方法,用于获取单例对象。
* 如果实例尚未创建,则通过双重检查锁定来确保线程安全地创建单例。
*
* @return 单例对象的实例
*/
public static SingleLazy getInstance() {
// 双重检查锁定的第一重检查,如果实例已经存在,则直接返回,避免不必要的同步锁定
// 检查实例是否已经存在,如果不存在则进行实例化
if (singleLazy == null) {
// 使用synchronized关键字确保线程安全,避免多个线程同时进入创建实例的代码块
// 使用synchronized关键字确保线程安全
synchronized (SingleLazy.class) {
// 双重检查锁定的第二重检查,再次确认实例是否已经被其他线程创建,避免重复创建实例
// 再次检查实例是否存在,避免多线程环境下重复实例化
if (singleLazy == null) {
// 实例化单例对象
singleLazy = new SingleLazy();
}
}
}
// 返回单例对象
return singleLazy;
}
// 将构造函数设为私有,防止外部直接通过new关键字创建实例,确保单例的唯一性
// 将构造函数设为私有,防止外部直接实例化对象
private SingleLazy() {}
}
所以我们在使用懒汉模式时,需要考虑三个因素:
- 由if和new引起的线程安全问题
- 频繁加锁产生的阻塞
- 指令重排序引起的线程安全问题
单例模式的讲解就先到这了~
若有不足,欢迎指正~