你知道吗?枚举单例模式是世界上最好的单例模式!
经过日积月累, 以下是小编归纳整理的深入了解Java虚拟机文档,希望可以帮助大家过关斩将顺利通过面试。由于整个文档比较全面,内容比较多,篇幅不允许,下面以截图方式展示。由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!来,每个节点里面
{
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.Enum
的valueOf
方法根据名字查找对象,而不是新建一个新的对象,所以防止了反序列化对单例的破坏。
可以查看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开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
最后
经过日积月累, 以下是小编归纳整理的深入了解Java虚拟机文档,希望可以帮助大家过关斩将顺利通过面试。
由于整个文档比较全面,内容比较多,篇幅不允许,下面以截图方式展示 。
由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门即可获取!
来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!**
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
最后
经过日积月累, 以下是小编归纳整理的深入了解Java虚拟机文档,希望可以帮助大家过关斩将顺利通过面试。
由于整个文档比较全面,内容比较多,篇幅不允许,下面以截图方式展示 。
[外链图片转存中…(img-8VLnFxHA-1712319824880)]
[外链图片转存中…(img-ZWK236p3-1712319824881)]
[外链图片转存中…(img-Jyv7b2KY-1712319824881)]
[外链图片转存中…(img-X1linkaV-1712319824882)]
[外链图片转存中…(img-Di8bpsKS-1712319824882)]
[外链图片转存中…(img-ZwmHzIIU-1712319824882)]
[外链图片转存中…(img-uTpQwIea-1712319824882)]
由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门即可获取!
更多推荐
所有评论(0)