java map 优化_Java性能优化-高速Map存取

本博客来自我的新书Java性能优化(暂定名),第5章的Java代码优化技巧节选2,也欢迎阅读我的新书 《Spring Boot 2 精髓 》

5.2 高速Map存取

使用EnumMap来存取Key是Enum的,会有较快的速度,如下是一个网关返回对象Result的的状态属性,是一个枚举类

public static enum Status {

SUCCESS(1,"成功"),FAIL(2,"处理失败"),DEGRADE(98,"成功降级"),UNKOWN(99,"未知异常");

private int code;

String msg;

Status(int code,String msg){

this.code = code;

this.msg = msg;

}

public int getCode() {

return code;

}

public String getMsg() {

return msg;

}

}

考虑到定义微服务网关返回对象,应该尽量使用java自带类型,以避免各种序列化,反序列化问题,网关不返回此枚举值,而是返回msg字段,因此可以构造一个EnumMap,Key为Status枚举类型,Value为Status.msg 属性

Map enumMap =null;

private void initEnumMap(){

enumMap = new EnumMap(Status.class);

for(Status status:Status.values()) {

enumMap.put(status,status.msg);

}

}

构造EnumMap的时候,内部实际上通过一个数组保存了所有的枚举值,索引是枚举的ordinal得到 当要根据Enum来操作EnumMap,只需要先调用ordinal,得到其索引,然后直接操作数组即可。操作一维数组有这最快的速度

有些场景下Key是int类型,这时候可以参考第二章的IntMap

有很多Key-Value使用场景,都可以转化为根据索引对数组的存取,我们学过的C语言,操作的是变量名,但实际上还是根据指针获取到内存中的值,Beetl模板语言,对变量的访问,也不像其他脚本语言那样,通过变量名访问Map获取其值,而是在编译期间就为这个变量分配好了索引值,所有变量都保存在一个一维数组里,这样的存取,相比于Map存取,有十倍以上性能提高。

如下一段脚本语言

var a = 1;

var b = 2+a;

有些语言引擎会翻译成类似如下java代码

context.put("a",1);

context.put("b",context.get("a")+2);

这里context是一个Map。从Map里通过Key存取尽管很快,但是Beetl还是在解析脚本语言的时候给变量设置了数组所在索引,因此如上脚本在Beetl中翻译如下

Object[] vars = context.vars;

vars[0] =1 ;

vars[1] = vars[0]+2

这里为变量a,b 分别设置了在变量表中的索引是0和1;

曾优化过一个电商的基础组件,电商系统每天调用这个组件的次数高达10+万亿次,这个组件是用来统计方法调用的时长,收集一段时间后,定期发送到 分析系统,用于查找和分析方法的性能,其中有一部分代码是记录以调用时长分类,记录条用次数,下面的代码

Watch watch = Watch.instance("orderByWx"); //初始化,从微信来的订单

//调用其他业务逻辑.....

Profile.add(watch.endWatch());//记录一次

Watch类定义如下

public class Watch {

String key;

long start;

long millis =-1;

private Watch(String key){

this.key = key;

this.start = System.nanoTime();

}

public static Watch instance(String key){

return new Watch(key);

}

public Watch endWatch(){

millis = millisConsume();

return this;

}

/**

* 返回方法调用消耗的毫秒

* [@return](https://my.oschina.net/u/556800)

*/

private long millisConsume(){

return TimeUnit.NANOSECONDS.toMillis(System.nanoTime()-start);

}

}

Watch的key属性记录调用类型,如订单调用,商品查询信息等,可以为任意值,start属性记录了调用时候的时间点,millis会在调用endWatch后记录调用消耗的毫秒数。

Profile类用来记录监控信息,并通过其他后台线程发送到性能分析中心,例子做了一定简化,只呈现保存部分

public class Profile {

//调用时长和调用次数

static Map countMap = new ConcurrentHashMap<>();

/**

* 对调用时间计数

* [@param](https://my.oschina.net/u/2303379) watch

*/

public static void addWatch(Watch watch){

int consumeTime = (int)watch.millis;

AtomicInteger count = countMap.get(consumeTime);

if(count==null){

count = new AtomicInteger();

AtomicInteger old = countMap.putIfAbsent(consumeTime,count);

if(old!=null){

count = old;

}

}

count.incrementAndGet();

}

}

Profile会初始化一个ConcurrentHashMap用于计数,Key为Integer类型,表示调用时长,Value为AtomicInteger,用来计数,每次调用,都会自增一个

Profile性能有一点优化空间,如果从业务角度考虑,大部分需要监控的方法或者代码块,执行时间并不长,假设不超过32毫秒(这是一个假设值,根据系统运维统计分析后得出),因此,可以考虑用一个32长度的数组来存放32毫秒以内的所有计数,超过32毫秒的再沿用以前的方法

static Map countMap = new ConcurrentHashMap<>();

static final int MAX = 32;

//保存消耗时间为32毫秒的调用次数

static AtomicInteger[] counts = new AtomicInteger[MAX];

static{

for(int i=0;i

counts[i] = new AtomicInteger();

}

}

/**

* 对调用时间计数

* [@param](https://my.oschina.net/u/2303379) watch

*/

public static void addWatch(Watch watch){

int consumeTime = (int)watch.millis;

if(consumeTime

counts[consumeTime].incrementAndGet();

return ;

}

AtomicInteger count = countMap.get(consumeTime);

//原有的Profile.addWatch逻辑,在此忽略

}

新完善的代码使用counts数组记录32毫秒以内调用计数,因此当addWatch被调用的时候,先判断millis是否小于32毫秒,如果是,直接用数组获取计数器,然后自增。否则,沿用以前的逻辑

优化后,通过JMH测试(com.ibeetl.code.ch05.WatchTest),性能略有提升,如下输出:

Benchmark Mode Samples Score Score error Units

c.i.c.c.WatchTest.better thrpt 20 16447.171 2195.344 ops/ms

c.i.c.c.WatchTest.general thrpt 20 11566.545 601.579 ops/ms

性能优化提高了40%,尽管看着不如本书其他例子性能提升那么明显,但考虑这是一个基础工具,性能提升会对所有系统都有帮助,实际上,作者优化为跟此性能监控工具后,对业务系统的有提升5%的性能提升,在拥有数十万台服务器的大型电商系统,这个系统提升还是有意义的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值