背景
枚举在系统中的地位不言而喻,状态、类型、场景、标识等等,少则十几个多则上百个,相信以下这段代码很常见,而且类似的代码到处都是,目标:消除这类冗余代码。
1 | /** |
枚举缓存
- 减少代码冗余,代码简洁
- 去掉for循环,性能稳定高效
模块设计图
缓存结构
源码展示
1 |
|
关键解读
开闭原则
什么是开闭原则?
对修改是封闭的,对新增扩展是开放的。为了满足开闭原则,这里设计成有枚举主动注册到缓存,而不是有缓存主动加载枚举,这样设计的好处就是:当增加一个枚举时只需要在当前枚举的静态块中自主注册即可,不需要修改其他的代码
比如我们现在要新增一个状态类枚举:
1 | public enum StatusEnum { |
注册时机
将注册放在静态块中,那么静态块什么时候执行呢?
1、当第一次创建某个类的新实例时
2、当第一次调用某个类的任意静态方法时
3、当第一次使用某个类或接口的任意非final静态字段时
4、当第一次Class.forName时
如果我们入StatusEnum创建枚举,那么在应用系统启动的过程中StatusEnum的静态块可能从未执行过,则枚举缓存注册失败,所有我们需要考虑延迟注册,代码如下:
1 | private static <E extends Enum> void executeEnumStatic(Class<E> clazz) { |
Class.forName(clazz.getName())被执行的两个必备条件:
1、缓存中没有枚举class的键,也就是说没有执行过枚举向缓存注册的调用,见EnumCache.find方法对executeEnumStatic方法的调用;
2、executeEnumStatic中的LOADED.put(clazz, true);还没有被执行过,也就是Class.forName(clazz.getName());没有被执行过;
我们看到executeEnumStatic中用到了双重检查锁,所以分析一下正常情况下代码执行情况和性能:
1、当静态块还未执行时,大量的并发执行find查询。
- 此时executeEnumStatic中synchronized会阻塞其他线程;
- 第一个拿到锁的线程会执行Class.forName(clazz.getName());同时触发枚举静态块的同步执行;
- 之后其他线程会逐一拿到锁,第二次检查会不成立跳出executeEnumStatic;
2、当静态块已经执行,且静态块里面正常执行了缓存注册,大量的并发执行find查询。
- executeEnumStatic方法不会调用,没有synchronized引发的排队问题;
3、当静态块已经执行,但是静态块里面没有调用缓存注册,大量的并发执行find查询。
- find方法会调用executeEnumStatic方法,但是executeEnumStatic的第一次检查通不过;
- find方法会提示异常需要在静态块中添加注册缓存的代码;
总结:第一种场景下会有短暂的串行,但是这种内存计算短暂串行相比应用系统的业务逻辑执行是微不足道的,也就是说这种短暂的串行不会成为系统的性能瓶颈。
样例展示
- 构造枚举
1 | public enum StatusEnum { |
- 测试类
1 | public class Test{ |
- 执行结果
1 | SUCCESS |
性能对比
- 对比代码,如果OrderType中的实例数越多性能差异会越大
1 | public class Test { |
- 执行结果
1 | 枚举->NotExist : 312 |
总结
1、代码简洁;
2、枚举中实例数越多,缓存模式的性能优势越多;
评价
- 绝大多数情况不必如此设计,一个枚举里面才有几个值,考虑cacheLine的话,for 并不一定会比map性能低。10000000次几乎是为了论证而论证。好的设计是用抽象去屏蔽复杂,如本来就挺简单可以保持。