2024年鸿蒙最新单例模式(万字长文精讲)_我是如何用单例模式征服面试官的 (4),字节跳动算法岗面经

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  1. java.lang.Runtime

public class Runtime {

private static Runtime currentRuntime = new Runtime();

public static Runtime getRuntime() {
return currentRuntime;
}

private Runtime() {}

// …
}

  1. java.awt.GraphicsEnvironment

public abstract class GraphicsEnvironment {
protected GraphicsEnvironment() {
}

public static synchronized GraphicsEnvironment getLocalGraphicsEnvironment() {
if (localEnv == null) {
localEnv = createGE();
}

return localEnv;
}

// …
}

  1. java.awt.Desktop

public class Desktop {
private Desktop() {
peer = Toolkit.getDefaultToolkit().createDesktopPeer(this);
}

public static synchronized Desktop getDesktop(){
if (GraphicsEnvironment.isHeadless()) throw new HeadlessException();
if (!Desktop.isDesktopSupported()) {
throw new UnsupportedOperationException("Desktop API is not " +
“supported on the current platform”);
}

sun.awt.AppContext context = sun.awt.AppContext.getAppContext();
Desktop desktop = (Desktop)context.get(Desktop.class);

if (desktop == null) {
desktop = new Desktop();
context.put(Desktop.class, desktop);
}

return desktop;
}
}

除了在jdk源码中对于单例模式有不少的使用之外,我们最常见的Spring框架中,每个Bean默认就是单例的,这样做的原因在于Spring容器可以管理Bean的生命周期。修改Spring中Bean的单例属性,只需要设置为Prototype类型即可,这样Bean的生命周期将不再有Spring容器跟踪。

4、饿汉式单例

饿汉式单例主要体现在一个饿字,也就是说在使用这个对象之前,在类加载的时候就立即初始化,创建其实例对象。这样做的好处是线程安全、使用时速度更快。饿汉式单例的写法如下:

写法一:

package com.lizba.pattern.singleton.hungry;

/**
*


* 饿汉式单例写法一
*


*
* @Author: Liziba
* @Date: 2021/6/27 15:46
*/
public class HungrySingletonDemo1 {

private static final HungrySingletonDemo1 HUNGRY_SINGLETON_DEMO_1 = new HungrySingletonDemo1();

private HungrySingletonDemo1() {}

public static HungrySingletonDemo1 getInstance() {
return HUNGRY_SINGLETON_DEMO_1;
}
}

写法二:

package com.lizba.pattern.singleton.hungry;

/**
*


* 饿汉式单例写法二(静态代码块)
*


*
* @Author: Liziba
* @Date: 2021/6/27 15:49
*/
public class HungrySingletonDemo2 {

private static final HungrySingletonDemo2 HUNGRY_SINGLETON_DEMO_2;

static {
HUNGRY_SINGLETON_DEMO_2 = new HungrySingletonDemo2();
}

public static HungrySingletonDemo2 getInstance() {
return HUNGRY_SINGLETON_DEMO_2;
}

}

饿汉式单例可以保证线程安全,执行效率高,同时编码简单容易理解。但是饿汉式单例只适用于单例对象减少的情况,如果大量编写饿汉式单例不仅会给系统启动带来负担,也可能会导致内存的浪费,比如创建的单例对象在程序运行过程中并未使用。因此这对内存浪费这个问题,我们衍生出了懒汉式单例。

5、懒汉式单例

懒汉式单例主要体现在一个懒字,也就是说类加载的时候我懒得实例化,等你需要用了再说吧!接下来的懒汉式单例中我通过一步步的优化和推翻来演进如何编写一个优秀的单例。

5.1 普通懒汉式单例

package com.lizba.pattern.singleton.lazy;

/**
*


* 简单懒汉式单例示例代码1
*


*
* @Author: Liziba
* @Date: 2021/6/27 16:01
*/
public class LazySingletonDemo1 {

private static LazySingletonDemo1 LAZY_SINGLETON = null;

private LazySingletonDemo1() {
}

public static LazySingletonDemo1 getInstance() {
if (LAZY_SINGLETON == null) {
LAZY_SINGLETON = new LazySingletonDemo1();
}
return LAZY_SINGLETON;
}
}

这是一个非常普通的懒汉式单例模式的写法,相信很多初学者会编码成这样,但其实这是一种错误的线程不安全的写法。我们通过一个断点测试来证明其不安全,我采用的是IDEA编写代码,通过设置断点为Thread模式来使得线程1和线程2同时满足LAZY_SINGLETON == null。

测试代码

package com.lizba.pattern.singleton.lazy;

/**
*


* 测试懒汉式单例1的线程不安全
*


*
* @Author: Liziba
* @Date: 2021/6/27 16:05
*/
public class LazySingletonTest {

public static void main(String[] args) {
new Thread(() -> run(), “Thread-1”).start();
new Thread(() -> run(), “Thread-2”).start();

System.out.println(“End of test…”);
}

public static void run() {
LazySingletonDemo1 lad = LazySingletonDemo1.getInstance();
System.out.println(Thread.currentThread().getName() + " : " + lad);
}

}

断点设置如下图,邮件打在LAZY_SINGLETON == null的断点上,选择Suspend为Thread,然后点击Done即可。
在这里插入图片描述

调试过程,我们执行测试代码,使其两个线程均执行到LAZY_SINGLETON == null,然后F8使得线程1进入if语句;切换至线程2F8进入if语句,此时我们的目的便达到了(在实际的并发使用场景中,这种情况是非常可能出现的)
在这里插入图片描述

此时线程1和线程2中的对象并不是同一个对象,所以这种单例编码方式是不正确的。
在这里插入图片描述

5.2 同步懒汉式单例

关于上述的单例模式编码,部分读者会想到通过同步原语synchronized加在getInstance()方法上来解决,此时代码如下所示:

package com.lizba.pattern.singleton.lazy;

/**
*


* synchronized修饰方法getInstance()的懒汉式单例
*


*
* @Author: Liziba
* @Date: 2021/6/27 16:37
*/
public class LazySingletonDemo2 {

private static LazySingletonDemo2 LAZY_SINGLETON = null;

private LazySingletonDemo2() {
}

// synchronized修饰getInstance()方法
public static synchronized LazySingletonDemo2 getInstance() {
if (LAZY_SINGLETON == null) {
LAZY_SINGLETON = new LazySingletonDemo2();
}
return LAZY_SINGLETON;
}
}

如上通过给getInstance()加上synchronized关键字,使得同步方法中的代码块线程安全了,但是随之而来的是在大量并发调用该方法的性能问题,大量的线程会阻塞在这个方法上,所以有些我们通过缩小锁锁住的范围来尽可能的提升其并发性能。代码衍生为如下所示:

package com.lizba.pattern.singleton.lazy;

/**
*


* synchronized修饰方法代码块
*


*
* @Author: Liziba
* @Date: 2021/6/27 16:53
*/
public class LazySingletonDemo3 {

private static LazySingletonDemo3 LAZY_SINGLETON = null;

private LazySingletonDemo3() {
}

public static LazySingletonDemo3 getInstance() {
synchronized (LazySingletonDemo3.class) {
if (LAZY_SINGLETON == null) {
LAZY_SINGLETON = new LazySingletonDemo3();
}
return LAZY_SINGLETON;
}
}

}

如上这种锁住代码块的方式,虽然缩小了锁同步的范围,但是并未本质上的改变每个线程均需要阻塞这个问题,因此我们可以前置if(LAZY_SINGLETON == null)这个判断,使得部分线程判断对象被实例化后无需加锁,直接返回即可,其代码如下所示:

package com.lizba.pattern.singleton.lazy;

/**
*


* if(LAZY_SINGLETON == null)前置于同步代码块,单例模式示例4
*


*
* @Author: Liziba
* @Date: 2021/6/27 16:56
*/
public class LazySingletonDemo4 {

private static LazySingletonDemo4 LAZY_SINGLETON = null;

private LazySingletonDemo4() {
}

public static LazySingletonDemo4 getInstance() {
// 前置if判断
if (LAZY_SINGLETON == null) {
synchronized (LazySingletonDemo4.class) {
LAZY_SINGLETON = new LazySingletonDemo4();
}
}
return LAZY_SINGLETON;
}

}

如上代码看似兼顾了性能和同步对象的实例化,但是这里的同步语言并不能保证对象只实例化一次,它只能保证每次只有一个线程在实例化这个对象。试想一下,如果线程1和线程2均执行到代码21行,此时线程1获得锁继续执行,实例化对象LazySingletonDemo4,当线程1退出锁后,线程2获取到锁,线程2也会对LazySingletonDemo4进行实例化,这种情况也可以用上面的断点法测试出来。所以这种情况是错误的,但是只有小小的改动一下即可,改动后的方式如下所示。

5.3 双重检查锁懒汉式单例

双重检查锁看名字高大上,其实就是在上面的LazySingletonDemo4多个if判断而已,理解为双重检查+锁可能更明了,其代码如下所示:

package com.lizba.pattern.singleton.lazy;

/**
*


* 双重检查锁
*


*
* @Author: Liziba
* @Date: 2021/6/27 17:04
*/
public class LazySingletonDemo5 {

private static LazySingletonDemo5 LAZY_SINGLETON = null;

private LazySingletonDemo5() {
}

public static LazySingletonDemo5 getInstance() {
// 检查1
if (LAZY_SINGLETON == null) {
synchronized (LazySingletonDemo5.class) {
// 检查2(这就是双重检查)
if (LAZY_SINGLETON == null) {
LAZY_SINGLETON = new LazySingletonDemo5();
}
}
}
return LAZY_SINGLETON;
}

}

双重检查锁看似完全正确,其实还存在一个指令重排序问题,至于什么是指令重排序,在我的并发编程专题中有详细的讲述,这里不再细讲;简单来说,就是编译器和处理器会针对编译后的指令进行重新排序,这种重排序对应编译器和处理器来说是会带来一定的性能提升的,但是对于编写代码的程序员来说,如果不正确的使用同步语义,将会导致非预期结果出现。在上述示例中第24行代码LAZY_SINGLETON = new LazySingletonDemo5(),在编译器编译后生成的字节码会有三条指令如下所示:

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

这三条指令中,指令2和指令3可能会出现重排序,也就是说对象的初始化会被后置到将分配的地址指向对象的引用这个指令的后面;这种重排序,假设是在线程1执行中发生了,此时线程2执行到第20行 if (LAZY_SINGLETON == null),此时LAZY_SINGLETON 并不为null,程序会直接返回LAZY_SINGLETON对象,但是此时的对象是一个实例化不完全的对象,这种情况是不允许存在的。
其解决办法是通过volatile关键字借助其内存语义来禁止指令重排序,这样2和3指令之间的重排序将会被禁止,这涉及到JMM规范,需要的请查看我的并发专题系列文章。其改造代码如下所示:

package com.lizba.pattern.singleton.lazy;

/**
*


* volatile修饰 LAZY_SINGLETON禁止指令重排序
*


*
* @Author: Liziba
* @Date: 2021/6/27 17:12
*/
public class LazySingletonDemo6 {

// volatile修饰LAZY_SINGLETON反正指令重排序
private static volatile LazySingletonDemo6 LAZY_SINGLETON = null;

private LazySingletonDemo6() {
}

public static LazySingletonDemo6 getInstance() {
// 检查1
if (LAZY_SINGLETON == null) {
synchronized (LazySingletonDemo6.class) {
// 检查2(这就是双重检查)
if (LAZY_SINGLETON == null) {
LAZY_SINGLETON = new LazySingletonDemo6();
}
}
}
return LAZY_SINGLETON;
}

}

上述演进过程,其实已经编写一个比较完整的懒汉式单例的示例代码,但是介于语言的一些特性,也就是反射这个无事不为的操作,能破坏这种场景。我们先来看看怎么破坏的,其破坏示例代码如下:

package com.lizba.pattern.singleton.lazy;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
*


* 破坏双重检查锁单例
*


*
* @Author: Liziba
* @Date: 2021/6/27 17:34
*/
public class DoubleCheckTest {

public static void main(String[] args) {
Class singletonClass = LazySingletonDemo6.class;
try {
Constructor c = singletonClass.getDeclaredConstructor(null);
// 破坏private访问权限
c.setAccessible(true);
LazySingletonDemo6 lsd1 = c.newInstance();
LazySingletonDemo6 lsd2 = c.newInstance();

System.out.println(lsd1 == lsd2);
} catch (Exception e) {
e.printStackTrace();
}

}

}

输出结果:false
在这里插入图片描述

处理方式十分简单粗暴,就是在构造方法中抛出异常,抛出异常这种解决方式,在很多不合法的场景中非常常见,修改后的代码如下:

package com.lizba.pattern.singleton.lazy;

/**
*


* 防止反射破坏单例
*


*
* @Author: Liziba
* @Date: 2021/6/27 17:56
*/
public class LazySingletonDemo7 {

private static volatile LazySingletonDemo7 LAZY_SINGLETON = null;

// 在构造函数中判断如果LAZY_SINGLETON不为空,则抛出异常即可
private LazySingletonDemo7() {
if (LAZY_SINGLETON != null) {
throw new RuntimeException(“This operation is forbidden.”);
}
}

public static LazySingletonDemo7 getInstance() {
// 检查1
if (LAZY_SINGLETON == null) {
synchronized (LazySingletonDemo7.class) {
// 检查2(这就是双重检查)
if (LAZY_SINGLETON == null) {
LAZY_SINGLETON = new LazySingletonDemo7();
}
}
}
return LAZY_SINGLETON;
}

}

5.4 静态内部类懒汉式单例

在双重检查锁的演进中,我们通过不断的缩小锁的范围,以及对对象是否未实例化做了两次判断,最后对反射获取对象这种操作做了处理;但是归根到底,双重检查锁中有个锁字,就难规避性能讨论的问题,其实这种问题在大部分场景中是可以接受;如果硬要寻求一种既是懒汉式,又不需要锁的单例模式,那么通过静态内部类的加载特性,巧妙的实现懒汉式单例模式会是一种不错的选择。
Java语言中的内部类是延时加载的,只有在第一次使用的时候才会被加载,不使用则不加载。
我们通过该特性,编码的懒汉式单例如下所示:

package com.lizba.pattern.singleton.lazy;

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

就难规避性能讨论的问题,其实这种问题在大部分场景中是可以接受;如果硬要寻求一种既是懒汉式,又不需要锁的单例模式,那么通过静态内部类的加载特性,巧妙的实现懒汉式单例模式会是一种不错的选择。
Java语言中的内部类是延时加载的,只有在第一次使用的时候才会被加载,不使用则不加载。
我们通过该特性,编码的懒汉式单例如下所示:

package com.lizba.pattern.singleton.lazy;

[外链图片转存中…(img-A0PEK3IT-1715277182529)]
[外链图片转存中…(img-DZIy0ng0-1715277182529)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值