Java性能优化

Wesley13
• 阅读 703

本博客来自我的新书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<Status,String> enumMap =null;
    private void initEnumMap(){
      enumMap = new EnumMap<Status,String>(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<Integer, AtomicInteger> 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<Integer, AtomicInteger> countMap = new ConcurrentHashMap<>();
    static final int MAX  = 32;
    //保存消耗时间为32毫秒的调用次数
    static AtomicInteger[] counts = new AtomicInteger[MAX];
    static{
        for(int i=0;i<MAX;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<MAX){
            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%的性能提升,在拥有数十万台服务器的大型电商系统,这个系统提升还是有意义的。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
java系统优化中的位操作优化
本博客来自我的新书《Java系统性能优化》(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fitem.jd.com%2F12742086.html"《Java系统性能优化》")的第四章15节,也欢迎阅读我的的经典书《SpringBoot2精髓》(https://www.o
Wesley13 Wesley13
3年前
Java日期时间API系列31
  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前
Wesley13 Wesley13
3年前
4.22 错误优化策略
本博客来自我的新书Java系统性能优化(暂定名),也欢迎阅读我的新书《SpringBoot2精髓》(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.kancloud.cn%2Fxiandafu%2Fspringboot2inpractice%2F"《Spring
Wesley13 Wesley13
3年前
P2P技术揭秘.P2P网络技术原理与典型系统开发
Modular.Java(2009.06)\.Craig.Walls.文字版.pdf:http://www.t00y.com/file/59501950(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fwww.t00y.com%2Ffile%2F59501950)\More.E
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这