你知道吗?枚举单例模式是世界上最好的单例模式!

{

return id.incrementAndGet();

}

public static final IdGeneratorEnum INSTANCE;

private AtomicLong id;

private static final IdGeneratorEnum $VALUES[];

static

{

INSTANCE = new IdGeneratorEnum(“INSTANCE”, 0);

$VALUES = (new IdGeneratorEnum[] {

INSTANCE

});

}

}

1、JVM级别的线程安全

反编译的代码中可以发现枚举中的各个枚举项都是通过static代码块来定义和初始化的,他们会在类被加载时完成初始化,而Java的类加载由JVM保证线程安全。

2、防止反序列化的破坏

Java的序列化专门对枚举的序列化做了规定,在序列化时,只是将枚举对象的name属性输出到结果中,在反序列化时通过java.lang.EnumvalueOf方法根据名字查找对象,而不是新建一个新的对象,所以防止了反序列化对单例的破坏。

可以查看java.io.ObjectInputStream#readObject验证。readObject判断到枚举类时,调用的了这个方法java.io.ObjectInputStream#readEnum

private Enum<?> readEnum(boolean unshared) throws IOException {

if (this.bin.readByte() != 126) {

throw new InternalError();

} else {

ObjectStreamClass desc = this.readClassDesc(false);

if (!desc.isEnum()) {

throw new InvalidClassException("non-enum class: " + desc);

} else {

int enumHandle = this.handles.assign(unshared ? unsharedMarker : null);

ClassNotFoundException resolveEx = desc.getResolveException();

if (resolveEx != null) {

this.handles.markException(enumHandle, resolveEx);

}

String name = this.readString(false);

Enum<?> result = null;

Class<?> cl = desc.forClass();

if (cl != null) {

try {

Enum<?> en = Enum.valueOf(cl, name);

result = en;

} catch (IllegalArgumentException var9) {

throw (IOException)(new InvalidObjectException("enum constant " + name + " does not exist in " + cl)).initCause(var9);

}

if (!unshared) {

this.handles.setObject(enumHandle, result);

}

}

this.handles.finish(enumHandle);

this.passHandle = enumHandle;

return result;

}

}

}

3、防止反射的破坏

对于反射,枚举类同样有防御措施,反射在通过newInstance创建对象时会检查这个类是否是枚举类,如果是枚举类就会throw new IllegalArgumentException("Cannot reflectively create enum objects");,如下是源码java.lang.reflect.Constructor#newInstance

public T newInstance(Object… initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {

if (!this.override) {

Class<?> caller = Reflection.getCallerClass();

this.checkAccess(caller, this.clazz, this.clazz, this.modifiers);

}

if ((this.clazz.getModifiers() & 16384) != 0) {

throw new IllegalArgumentException(“Cannot reflectively create enum objects”);

} else {

ConstructorAccessor ca = this.constructorAccessor;

if (ca == null) {

ca = this.acquireConstructorAccessor();

}

T inst = ca.newInstance(initargs);

return inst;

}

}

五、单例模式存在哪些问题?

单例模式有节省资源、保证结果正确、方便管理等优点,同时也存在一些问题。

1、单例对 OOP 特性的支持不友好

OOP 的四大特性是封装、抽象、继承、多态。单例模式对于其中的抽象、继承、多态都支持得不好。

就拿唯一递增id生成器来说,上面实现的单例IdGenerator 的使用方式违背了基于接口而非实现的设计原则,也就违背了广义上理解的 OOP 的抽象特性。如果未来某一天,希望针对不同的业务采用不同的 ID 生成算法。比如,订单 ID 和用户 ID 采用不同的 ID 生成器来生成。为了应对这个需求变化,我们需要修改所有用到 IdGenerator 类的地方,这样代码的改动就会比较大。

除此之外,单例对继承、多态特性的支持也不友好。从理论上来讲,单例类也可以被继承、也可以实现多态,只是实现起来会非常奇怪,导致代码的可读性变差。所以,一旦选择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性。

2、单例会隐藏类之间的依赖关系

通过构造函数、参数传递等方式声明的类之间的依赖关系,查看函数的定义,就能很容易识别出来。但是,单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。

3、单例对代码的扩展性不友好

单例类只能有一个对象实例。如果未来某一天,需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。

拿数据库连接池来举例说明,在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便我们控制对数据库连接资源的消耗。所以,初始把数据库连接池类设计成了单例类。后期我们想把慢sql和普通sql进行隔离,这样就需要建立两个数据库连接池实例。但是初始数据库连接池设计成了单例类,显然就无法适应这样的需求变更,也就是说,单例类在某些情况下会影响代码的扩展性和灵活性。

所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。实际上,一些开源的数据库连接池、线程池也确实没有设计成单例类。

4、单例不支持有参数的构造函数

单例不支持有参数的构造函数,比如创建一个连接池的单例对象,没法通过参数来指定连接池的大小。解决方式是,将参数配置化。在单例实例化时,从外部读取参数。

六、单例模式的替代方案

为了保证全局唯一,除了使用单例,还可以用静态方法来实现。这其实就是平时常用的工具类了,但是它比单例更加不灵活,比如,它无法支持延迟加载,扩展性也很差。

public class IdGeneratorUtil {

private static AtomicLong id = new AtomicLong(0);

public static long getId() {

return id.incrementAndGet();

}

public static void main(String[] args) {

System.out.println(IdGeneratorUtil.getId());

}

}

实际上,类对象的全局唯一性可以通过多种不同的方式来保证,单例模式、工厂模式、IOC 容器(比如 Spring IOC 容器),还可以通过自我约束,在编写代码时自我保证不要创建两个对象。

七、重新理解单例模式的唯一性

1、单例模式的唯一性

经典的单例模式,创建的全局唯一对象,属于进程唯一性,如果一个系统部署了多个实例,就有多个进程,每个进程都存在唯一一个对象,且进程间的实例对象不是同一个对象。

2、实现线程唯一的单例

实现线程唯一的单例,就是一个对象在一个线程只能存在一个,不同的线程有不同的对象。

/**

  • 线程唯一单例

*/

public class ThreadIdGenerator {

private AtomicLong id = new AtomicLong(0);

private static final ConcurrentHashMap<Long, ThreadIdGenerator> instances = new ConcurrentHashMap<Long, ThreadIdGenerator>();

private ThreadIdGenerator() {}

public ThreadIdGenerator getInstance() {

long threadId = Thread.currentThread().getId();

instances.putIfAbsent(threadId, new ThreadIdGenerator());

return instances.get(threadId);

}

public long getId() {

return id.incrementAndGet();

}

}

在代码中,通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样就可以做到不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。

/**

  • 线程唯一单例 ThreadLocal

*/

public class ThreadLocalIdGenerator {

private AtomicLong id = new AtomicLong(0);

private static ThreadLocal instances = new ThreadLocal();

private ThreadLocalIdGenerator(){}

public static ThreadLocalIdGenerator getIntance() {

ThreadLocalIdGenerator instance = instances.get();

if (instance != null) {

return instance;

}

instance = new ThreadLocalIdGenerator();

instances.set(instance);

return instance;

}

public long getId() {

return id.incrementAndGet();

}

}

// 代码测试:

public class Test {

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

Thread thread = new Thread() {

@Override

public void run() {

System.out.println("sub1: " + ThreadLocalIdGenerator.getIntance().toString() + “–>” + ThreadLocalIdGenerator.getIntance().getId());

System.out.println("sub2: " + ThreadLocalIdGenerator.getIntance().toString() + “–>” + ThreadLocalIdGenerator.getIntance().getId());

}

};

thread.start();

thread.join();

System.out.println("main1: " + ThreadLocalIdGenerator.getIntance().toString() + “–>” + ThreadLocalIdGenerator.getIntance().getId());

System.out.println("main2: " + ThreadLocalIdGenerator.getIntance().toString() + “–>” + ThreadLocalIdGenerator.getIntance().getId());

}

}

// 控制台打印

sub1: com.stefan.designPattern.singleton.ThreadLocalIdGenerator@480a0d08–>1

sub2: com.stefan.designPattern.singleton.ThreadLocalIdGenerator@480a0d08–>2

main1: com.stefan.designPattern.singleton.ThreadLocalIdGenerator@4f3f5b24–>1

main2: com.stefan.designPattern.singleton.ThreadLocalIdGenerator@4f3f5b24–>2

显然主线程和子线程生成的单例不是同一个,且一个线程调用多次生成的是同一个单例对象。

3、实现集群唯一的单例

集群代表着有多个进程,实现集群唯一单例,就是进程间共享一个对象。

集群唯一的单例实现起来相对复杂。

(1)需要考虑共享对象存放在哪里,

(2)进程创建单例对象时需要加分布式锁,防止多个进程创建多个不同的对象。

4、实现一个多例模式

“单例”指的是,一个类只能创建一个对象,对应地,“多例”指的就是,一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。

(1)以map存储多例的方式

/**

  • 多例模式的唯一自增id生成器

  • 用户表一个IdGenerator

  • 商品表一个IdGenerator

  • 订单表一个IdGenerator

*/

public class MultIdGenerator {

private AtomicLong id = new AtomicLong(0);

private MultIdGenerator() {}

public long getId() {

return id.incrementAndGet();

}

private static Map<String, MultIdGenerator> instances = new HashMap<String, MultIdGenerator>();

static {

instances.put(Type.USER, new MultIdGenerator());

instances.put(Type.PRODUCT, new MultIdGenerator());

instances.put(Type.ORDER, new MultIdGenerator());

}

public static MultIdGenerator getInstance(String type) {

return instances.get(type);

}

public static final class Type {

public static final String USER = “user”;

public static final String PRODUCT = “product”;

public static final String ORDER = “order”;

}

}

// 测试

public class Test3 {

public static void main(String[] args) {

System.out.println("USER: " + MultIdGenerator.getInstance(MultIdGenerator.Type.USER).toString() + “–>” + MultIdGenerator.getInstance(MultIdGenerator.Type.USER).getId());

System.out.println("USER: " + MultIdGenerator.getInstance(MultIdGenerator.Type.USER).toString() + “–>” + MultIdGenerator.getInstance(MultIdGenerator.Type.USER).getId());

System.out.println("PRODUCT: " + MultIdGenerator.getInstance(MultIdGenerator.Type.PRODUCT).toString() + “–>” + MultIdGenerator.getInstance(MultIdGenerator.Type.PRODUCT).getId());

System.out.println("PRODUCT: " + MultIdGenerator.getInstance(MultIdGenerator.Type.PRODUCT).toString() + “–>” + MultIdGenerator.getInstance(MultIdGenerator.Type.PRODUCT).getId());

System.out.println("ORDER: " + MultIdGenerator.getInstance(MultIdGenerator.Type.ORDER).toString() + “–>” + MultIdGenerator.getInstance(MultIdGenerator.Type.ORDER).getId());

System.out.println("ORDER: " + MultIdGenerator.getInstance(MultIdGenerator.Type.ORDER).toString() + “–>” + MultIdGenerator.getInstance(MultIdGenerator.Type.ORDER).getId());

}

}

// 控制台

USER: com.stefan.designPattern.singleton.MultIdGenerator@4f3f5b24–>1

USER: com.stefan.designPattern.singleton.MultIdGenerator@4f3f5b24–>2

PRODUCT: com.stefan.designPattern.singleton.MultIdGenerator@15aeb7ab–>1

PRODUCT: com.stefan.designPattern.singleton.MultIdGenerator@15aeb7ab–>2

ORDER: com.stefan.designPattern.singleton.MultIdGenerator@7b23ec81–>1

ORDER: com.stefan.designPattern.singleton.MultIdGenerator@7b23ec81–>2

(2)使用枚举实现多例模式

/**

  • 多例模式的唯一自增id生成器

  • 用户表一个IdGenerator

  • 商品表一个IdGenerator

  • 订单表一个IdGenerator

  • 枚举无法实现多例模式

*/

public enum MultIdGeneratorEnum {

USER,

PRODUCT,

ORDER;

private AtomicLong id = new AtomicLong(0);

public long getId() {

return id.incrementAndGet();

}

}

测试

public class Test2 {

public static void main(String[] args) {

System.out.println("USER: " + MultIdGeneratorEnum.USER.toString() + “–>” + MultIdGeneratorEnum.USER.getId());

System.out.println("USER: " + MultIdGeneratorEnum.USER.toString() + “–>” + MultIdGeneratorEnum.USER.getId());

System.out.println("PRODUCT: " + MultIdGeneratorEnum.PRODUCT.toString() + “–>” + MultIdGeneratorEnum.PRODUCT.getId());

System.out.println("PRODUCT: " + MultIdGeneratorEnum.PRODUCT.toString() + “–>” + MultIdGeneratorEnum.PRODUCT.getId());

System.out.println("ORDER: " + MultIdGeneratorEnum.ORDER.toString() + “–>” + MultIdGeneratorEnum.ORDER.getId());

System.out.println("ORDER: " + MultIdGeneratorEnum.ORDER.toString() + “–>” + MultIdGeneratorEnum.ORDER.getId());

//USER: USER–>1

//USER: USER–>2

//PRODUCT: PRODUCT–>1

//PRODUCT: PRODUCT–>2

//ORDER: ORDER–>1

//ORDER: ORDER–>2

}

}

枚举的方式明显要简洁很多,强烈推荐使用枚举方式实现单例和多例模式。 通过反编译查看源码,枚举方式实现的多例其实和前面以map存储多例的方式差不多的原理,都是用HashMap把实例化对象存储起来。

使用jad反编译MultIdGeneratorEnum.class :

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.

// Jad home page: http://www.kpdus.com/jad.html

// Decompiler options: packimports(3)

// Source File Name: MultIdGeneratorEnum.java

package com.stefan.designPattern.singleton;

import java.util.concurrent.atomic.AtomicLong;

public final class MultIdGeneratorEnum extends Enum

{

public static MultIdGeneratorEnum[] values()

{

return (MultIdGeneratorEnum[])$VALUES.clone();

}

public static MultIdGeneratorEnum valueOf(String name)

{

return (MultIdGeneratorEnum)Enum.valueOf(com/stefan/designPattern/singleton/MultIdGeneratorEnum, name);

}

private MultIdGeneratorEnum(String s, int i)

{

super(s, i);

id = new AtomicLong(0L);

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

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

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

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

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

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

言尽于此,完结

无论是一个初级的 coder,高级的程序员,还是顶级的系统架构师,应该都有深刻的领会到设计模式的重要性。

  • 第一,设计模式能让专业人之间交流方便,如下:

程序员A:这里我用了XXX设计模式

程序员B:那我大致了解你程序的设计思路了

  • 第二,易维护

项目经理:今天客户有这样一个需求…

程序员:明白了,这里我使用了XXX设计模式,所以改起来很快

  • 第三,设计模式是编程经验的总结

程序员A:B,你怎么想到要这样去构建你的代码

程序员B:在我学习了XXX设计模式之后,好像自然而然就感觉这样写能避免一些问题

  • 第四,学习设计模式并不是必须的

程序员A:B,你这段代码使用的是XXX设计模式对吗?

程序员B:不好意思,我没有学习过设计模式,但是我的经验告诉我是这样写的

image

从设计思想解读开源框架,一步一步到Spring、Spring5、SpringMVC、MyBatis等源码解读,我都已收集整理全套,篇幅有限,这块只是详细的解说了23种设计模式,整理的文件如下图一览无余!

image

搜集费时费力,能看到此处的都是真爱!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
5c14a7895254671a72faed303032d36.jpg" alt=“img” style=“zoom: 33%;” />

言尽于此,完结

无论是一个初级的 coder,高级的程序员,还是顶级的系统架构师,应该都有深刻的领会到设计模式的重要性。

  • 第一,设计模式能让专业人之间交流方便,如下:

程序员A:这里我用了XXX设计模式

程序员B:那我大致了解你程序的设计思路了

  • 第二,易维护

项目经理:今天客户有这样一个需求…

程序员:明白了,这里我使用了XXX设计模式,所以改起来很快

  • 第三,设计模式是编程经验的总结

程序员A:B,你怎么想到要这样去构建你的代码

程序员B:在我学习了XXX设计模式之后,好像自然而然就感觉这样写能避免一些问题

  • 第四,学习设计模式并不是必须的

程序员A:B,你这段代码使用的是XXX设计模式对吗?

程序员B:不好意思,我没有学习过设计模式,但是我的经验告诉我是这样写的

[外链图片转存中…(img-cICyX3wZ-1712759478159)]

从设计思想解读开源框架,一步一步到Spring、Spring5、SpringMVC、MyBatis等源码解读,我都已收集整理全套,篇幅有限,这块只是详细的解说了23种设计模式,整理的文件如下图一览无余!

[外链图片转存中…(img-X4wVhA54-1712759478159)]

搜集费时费力,能看到此处的都是真爱!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 16
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值