这怕是最全的【单例模式】,2024大厂Java面试真题集锦

}

/**

  • 调用的时候创建对象并返回

*/

public static LazySingle getInstance(){

if(lazySingle == null){

lazySingle = new LazySingle();

}

return lazySingle;

}

}

小李:面试官,您看我这样的解释可还行。

面试官:单线程下是挺好的,如果在多线程环境下呢?

小李:这个我知道,加锁啊!

面试官:出门左转电梯直达!

其实加锁也没答错,关键问题在于如何加锁!

直接将获取实例的方法内容写入同步代码块中,解决了多线程安全的问题,但是并发效率的问题又暴露了出来。你想啊,现在锁住了这方法,而无论单例的对象是否创建,都会经过获取锁、释放锁的过程。这样的性能显然是不能接受的。

小李:我想想啊~~~! Emmmmm…! 有了,我们可以在同步代码块外层加一个判断,如果对象已经创建则直接返回。

面试官:这样解决了一部分的并发效率问题,但是如果在创建的时候同时有很多的线程访问,是不是也会有并发的效率问题呢?再优化优化。

小李一想,确实是这样,如果对象还没有创建出来的时候,就有很多的线程来访问,也会出现问题,假设有两个线程同时访问,当A线程优先争抢到锁,A进入同步代码块执行,此时B没有争抢到锁,将处于等待状态,而当A线程执行完成后释放锁,B进入同步代码块执行,此时B线程同样会创建出一个对象,破坏了单例。

小李:面试官,我明白了,可以在同步代码块中再加一层if判断,如果对象已经创建,就直接返回即可。

Double Check

上面最后的结果就是我们常说的Double Check,即双重锁检查。双重锁检查在很多地方都被运用到,代码如下。

package demo.single;

/**

  • 懒汉模式

*/

public class LazySingle {

/**

  • 懒汉模式,不会先创建对象,而是在调用的时候才会创建对象

*/

private static LazySingle lazySingle = null;

private LazySingle() {

}

/**

  • 调用的时候创建对象并返回

*/

public static LazySingle getInstance(){

//first check

if(lazySingle == null){

synchronized (LazySingle.class){

//double check

if(lazySingle == null){

lazySingle = new LazySingle();

}

}

}

return lazySingle;

}

}

面试官:小李,你多线程运行一下代码看看呢。

小李:好勒! 好像挺正常啊。等等, 好像不对, 这里还是出现了多个对象!!!啊~~,这是为什么啊,我都懵了,这完全超出了我的能力范围。

面试官:哈哈,小子,这下知道谁是大佬了吧?我来给你好好解释一下,其实,这和我们的代码没有关系,正常来讲,应该不会出现这样的问题,但是我们都知道,代码在运行过程中,会被编译成一条一条的指令运行,而JVM在运行时,在保证单线程最终结果不会受影响的情况下,对指令进行优化,就有可能对指令进行重排序,同样会破坏单例。

lazySingle = new LazySingle();

//这样一段代码在运行时会生成3条指令,即: 1. 分配内存空间 2. 创建对象 3. 指向引用

//正常情况下是会按照1 2 3顺序执行,但JVM优化器进行指令重排后,则可能变为:1. 分配内存空间 3. 指向引用 2. 创建对象

//在单线程下,这样的优化没有问题,但是多线程下,线程是在争抢CPU时间碎片的。假设A刚刚执行完 1 3 //条指令,此时B争抢到时间碎片,发现对象不为空了,就直接返回,但此时对象还没有真正被创建。B调用

//此对象就会抛出异常

//而volatile关键字修饰的变量可以禁止指令重排序,则可以保证指令会是1 2 3顺序执行。

//加上volatile修饰

private volatile static LazySingle lazySingle = null;

小李: 终于解决了,好难啊,一个简单的单例模式居然有这么多的细节。

面试官:你以为这就完了?

内部类的单例

使用内部类的方式可以非常完美的完成单例模式,而实现代码也非常简单。

package demo.single;

/**

  • 内部类的方式实现单例

*/

public class InnerSingle {

/**

  • 私有化构造器

*/

private InnerSingle(){

}

/**

  • 私有内部类

*/

private static class Inner{

//Jingtai内部类持有外部类的对象

public static final InnerSingle SINGLE = new InnerSingle();

}

/**

  • 返回静态内部类持有的对象

*/

public static InnerSingle getInstance(){

return Inner.SINGLE;

}

}

可以看到,代码中并没有出现同步方法或者同步代码块,那么静态内部类的方式是如何做到安全的单例模式呢?

  1. 外部类加载的时候,不会立即加载内部类,而是在调用的时候会加载内部类。

  2. 不管多少线程访问,JVM一定会保证类被正确的初始化,即静态内部类的方式是在JVM层面保证了线程安全

当然,这样也有一些缺点,那就是在创建单例对象的时候,如果需要传参,那么静态内部类的方式会非常麻烦。

破坏单例

那么,上面的单例已经完美了吗?并没有,看我如何将单例给破坏掉。

反射破坏

反射可以绕过私有构造器的限制,创建对象。当然正常的调用是不会发生单例被破坏的情况,但是如果偏偏有人不走寻常路呢,比如下面的调用。

package demo.single;

import java.lang.reflect.Constructor;

/**

  • 反射破坏单例

*/

public class RefBreakSingleTest {

public static void main(String[] args) throws Exception {

//获取类对象

Class lazySingleClass = LazySingle.class;

//获取构造器

Constructor constructor = lazySingleClass.getDeclaredConstructor(null);

constructor.setAccessible(true);

//创建对象

LazySingle lazySingle = constructor.newInstance(null);

System.out.println(lazySingle);

System.out.println(LazySingle.getInstance());

System.out.println(lazySingle == LazySingle.getInstance());

}

}

image

image

测试结果

很明显看到出现了两个不同的兑现,显然,单例被破坏了! 对于这样的情况该如何禁止呢?在网上查阅了很多资料,大部分是使用变量控制法,即在类中添加一个变量用于判断单例类的构造器是否有被调用,代码如下。

//添加变量控制,防止反射破坏

private static boolean isInstance = false;

private volatile static LazySingle lazySingle = null;

private LazySingle() throws Exception {

if(isInstance){

throw new Exception(“the Constructor has be used”);

}

isInstance = true;

}

再次调用测试代码,发现不能再创建多个单例对象,程序抛出了异常。

image

image

但是别忘了,属性也是可以通过反射修改的(count、instance的判断反射都能绕过)。

public class RefBreakSingleTest {

public static void main(String[] args) throws Exception {

//获取类对象

Class lazySingleClass = LazySingle.class;

//获取构造器

Constructor constructor = lazySingleClass.getDeclaredConstructor(null);

constructor.setAccessible(true);

//创建对象

LazySingle lazySingle = constructor.newInstance(null);

System.out.println(lazySingle);

Field isInstance = lazySingleClass.getDeclaredField(“isInstance”);

isInstance.setAccessible(true);

isInstance.set(null,false);

System.out.println(LazySingle.getInstance());

System.out.println(lazySingle == LazySingle.getInstance());

}

}

image

image

单例再次被破坏,感觉是不是已经快崩溃了,一个单例咋这么多事呢!!既然私有属性、私有方法在外部都能通过反射获取,那有没有反射不能获取的呢?我在网上也找到了另外一种写法,即私有内部类的来持有实例控制变量,而我也通过测试,发现反射同样能够绕过从而破坏单例。

package demo.pattren.single;

import java.lang.reflect.Constructor;

import java.lang.reflect.Method;

public class BreakInnerTest {

public static void main(String[] args) throws Exception {

Class lazySingleClass = LazySingle.class;

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
img

最后

各位读者,由于本篇幅度过长,为了避免影响阅读体验,下面我就大概概括了整理了

存中…(img-2ysc2YXZ-1710434840310)]
[外链图片转存中…(img-TBhvg2Nw-1710434840311)]
[外链图片转存中…(img-OlPdJZ7p-1710434840312)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
[外链图片转存中…(img-66ziF0tq-1710434840312)]

最后

各位读者,由于本篇幅度过长,为了避免影响阅读体验,下面我就大概概括了整理了

[外链图片转存中…(img-4fWtS34J-1710434840313)]

[外链图片转存中…(img-RP0Z8cOx-1710434840313)]

[外链图片转存中…(img-nfOmwdrf-1710434840314)]

[外链图片转存中…(img-LeZTU5tb-1710434840314)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值