区区一个单例模式居然成了面试的重灾区……

本文详细介绍了Java中的单例模式,包括饿汉式和懒汉式的实现,并分析了它们的线程安全性和内存效率。重点讨论了懒汉式的双重校验锁写法及volatile修饰符的应用,以及静态内部类实现单例的线程安全性与性能优势。通过这些,读者可以深入理解单例模式在多线程环境下的设计考虑。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

单例模式(Singleton Pattern)是 Java 中最常用的设计模式之一,同时也是面试的重灾区。有些人可能觉的单例模式很简单,没有什么难的。其实不然,因为牵扯到线程安全的问题,所以单例模式绝对能体现出你的功底。不信接着往下看。

单例模式详解

单例模式大体分为二种写法:饿汉式和懒汉式。

1.饿汉式

这种方式最简单,所以我们先把这种方式介绍一下,代码如下:

public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return instance;  
    }  
}

这种方式优点就是效率高、写法简单并且是线程安全的。但是缺点就是在类加载的时候就要初始化,浪费内存。

饿汉式单例天生就是线程安全的。饿汉式在类加载过程中就会初始化,因为类在加载过程中会加锁,所以线程安全。

2.懒汉式

懒汉式的特点是在第一次调用的时候才初始化,避免浪费内存。懒汉式是面试重灾区,为了让大家了解每一种写法存在的问题,我们从简单到复杂一步步写。

2.1 懒汉式初级写法(线程不安全):

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
    if (instance == null) {  // 问题出在这里,导致线程不安全。
        instance = new Singleton();  
    }  
    return instance;  
    }  
}

这个写法之所以不安全,是因为当有多个线程同时进入 if (singleton2 == null) {…} 语句块的时候,该单例类有可能会创建出多个实例,违背单例模式的初衷,因此,传统的懒汉式单例是非线程安全的。

2.2 懒汉中级写法(线程安全)

既然上面的写法线程不安全,那么我们在getInstance()方法上加一把锁,代码如下:

// 线程安全的懒汉式单例
public class Singleton {
    private static Singleton singleton;
    private Singleton(){}
    // 使用 synchronized 修饰,临界资源的同步互斥访问
    public static synchronized Singleton getSingleton(){
        if (singleton2 == null) {
            singleton2 = new Singleton2();
        }
        return singleton2;
    }

该实现与上面2.1传统懒汉式单例的实现唯一的差别就在于:是否使用 synchronized 修饰 getSingleton()方法。若使用,就保证了对临界资源的同步互斥访问,也就保证了单例。

这种写法乍一看没问题,但是这种实现方式的运行效率会很低,因为我们把整个getSingleton()加锁,同步块的作用域有点大,而且锁的粒度有点粗,所以我们继续升级写法。

2.3 双重校验锁写法(线程不安全)

public class Singleton {  
    private  static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  //位置1:问题出在这里
        }  
        }  
    }  
    return singleton;  
    }  
}

这种写法也是有问题的。为什么呢?问题出在singleton = new Singleton();这条语句上。

singleton = new Singleton();这条语句在创建对象的过程中会分成3个步骤,如下:

memory = allocate();  // 步骤1.分配对象的内存空间
ctorInstance(memory);  // 步骤2.初始化对象
sInstance = memory;  // 步骤3.设置sInstance指向刚分配的内存地址

JVM在执行的过程中步骤2和步骤3可能会发生指令重排序,重排后执行顺序如下:

memory = allocate();  // 步骤1.分配对象的内存空间
sInstance = memory;  // 步骤2.设置sInstance指向刚分配的内存地址,此时对象还没有被初始化
ctorInstance(memory);  // 步骤3.初始化对象

这种指令重排序在单线程下不会有问题,但是在并发情况下就会出现问题,如下图:

我觉的这张图我画的很明白了,大家应该是可以看懂的。因为并发的原因线程2访问到的是一个还未初始化的对象。这种不安全的因素是极难复现的,但是理论上还是存在线程不安全的因素。所以我们还要继续改进。

2.4 volatile修饰写法(线程安全)

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}

这个方式和上面双重锁检查写法唯一的区别就是加volatile来修饰singleton。

volatile要仔细解释起来篇幅就大了,本章主要介绍单例,所以由于篇幅的问题,这里只是简单介绍一下volatile关键字。volatile有三大作用:

1.可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,
								其他线程能够立即看得到修改的值
2.原子性:一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,
								要么就都不执行、
3.有序性:被volatile修饰的变量不会发生指令重排序。

2.5 静态内部类写法(线程安全)

public class Singleton {  
    private static class SingletonHolder {  
    		private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
}

我们上面提到过,类的加载机制是线程安全的。这种方式能达到双检锁方式一样的功效,但实现更简单,对静态域使用延迟初始化。

最后

好了,本章就讲到这里吧。怎么样,单例模式是不是没你想象的那么简单。除开今天分享的,我这里专门和几个大佬准备了一份技术进阶+项目经验+面试突击的资料,包含了十多个互联网大厂的面试题、面经汇总和20个技术栈资料合集,吃透这份资料,技术面和面试关完全不是问题!同时还有大厂项目实战的视频解析,那些你缺乏的项目经验都可以从中积累。

 点击下方名片可以直接领取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值