java也能写出点点算法-像C++一样去优化核心并发的代码例子1

java其实更多用来写业务代码,代码写得好不好,关键看抽象能力如何,不过如果你要用java写很核心的插件和高并发的片段,那么可能还是需要注意一些写法,那种写法可能会更好,才能使得并发量提高,而且更少的使用CPU和内存;我最近在一段采集系统访问的java代码,通过过滤器切入到应用中,遇到的一些小细节的调整,感觉还有点意思,以下为收集信息中碰到的两个需要判定的地方(对java优化没有任何要求的,本文纯属扯淡,呵呵):


原始代码片段1(用于判定是否为静态资源):

if(servletPath == null
                || servletPath.endsWith(".gif")
                || servletPath.endsWith(".png")
                || servletPath.endsWith(".jpg")
                || servletPath.endsWith(".bmp")
                || servletPath.endsWith(".js")
                || servletPath.endsWith(".css")
                || servletPath.endsWith(".ico")) return true;


原始代码片段2(用于判定浏览器类型):

        

        if(StringUtils.isEmpty(userAgent)) return null;
        if(userAgent.contains("MetaSr")) return "metasr";
        if(userAgent.contains("Chrome")) return "chrome";
        if(userAgent.contains("Firefox")) return "firefox";
        if(userAgent.contains("Maxthon")) return "maxthon";
        if(userAgent.contains("360SE")) return "360se";
        if(userAgent.contains("Safari")) return "safari";
        if(userAgent.contains("opera")) return "opera";
        if(userAgent.contains("MSIE")) return "ie";



貌似一眨眼的样子,这个代码没啥优化的余地,代码片段1无非是将出现概率高的放在前面,就尽快判定成功,片段2也是这样,但是片段1里头其实有很多请求都不是静态资源,不是静态资源的话就会将7个endwith全部执行一遍下来了,也就是有很多很多的请求都将遍历所有的内容。


endWith做什么?看看源码,就是将字符串最后那部分截取下来(截取和对比串一样长的,然后和对比串对比),那么7个就会发生7次substring,每个substring操作将会生成一个新的java对象(这个大家应该清楚),每个字符串进行对比是按照字符对比的,所以按照4个四个字符进行计算,也就是会发生20多次对比操作(最坏情况),当然说最好的情况就是进行一次substring,4次对比操作(因为对比成功是每个字符对比成功才算成功)。


那么怎么优化呢,这个貌似除了调试顺序,没有太多优化的空间,根据数据你会发现一个规律,对比的结束字符有5种可能性,那么通过结束字符,就可以定位到一到两个字符串上面,所以我们第一个考虑就是取出要对比的那个字符串的结束符,看下结束符是那个字符串的结束符,然后就在一两个字符串上使用endWith,那么概率就很低了,于是乎,我们对代码片段1,做第一步优化就是:

        

if(servletPath == null) return false;
int length = servletPath.length();
char last = servletPath.charAt(length - 1);
        if((last == 'f' && servletPath.endsWith(".gif"))
                || (last == 'g' && (servletPath.endsWith(".png") || servletPath.endsWith(".jpg")))
                || (last == 'p' && servletPath.endsWith(".bmp"))
                || (last == 's' && (servletPath.endsWith(".js")|| servletPath.endsWith(".css")))
                || (last == 'o' && servletPath.endsWith(".ico"))) return true;


此时判定完首字母后,就使用最多2个字符串进行endWith操作,经过测试,在上面最坏的情况下,效率要高出5-8倍,最初的写法在最好的情况下,和现在速度基本保持一致,但是我们上面说了,很多请求都不是静态资源,所以有很多请求都是走的最坏情况。


好了,如果你的代码不需要极高级别的优化,走到这一步已经够用了,或者说这个已经非常非常够用了,虽然优化的思路非常简单,再写就写成C++了;呵呵,不过我喜欢钻牛角尖,我也有点点洁癖,就是要玩什么,就喜欢玩到我认为的至高境界,所以我又再进行分析,画个图看看:



想办法是否可以去掉subtring的操作,也就是不做字符串截取的操作,我看到就那么几个字母,那么就匹配几个字母而已嘛!

于是乎代码有点像C写的了,做了进一步的改动


if(servletPath == null) return false;
int length = servletPath.length();
if(length < 3) return false;
char last = servletPath.charAt(length - 1);
char second = servletPath.charAt(length - 2);
char third = servletPath.charAt(length - 3);
char forth;
if(length > 3 && third != '.')
    forth = servletPath.charAt(length - 4);
else
    forth = 0;
        if((last == 'f' && second == 'i' && third == 'g' && forth == '.')
                || (last == 'g' && ((second == 'n' && third == 'p' && forth == '.') || (second == 'p' && third == 'j' && forth == '.')))
                || (last == 'p' && (second == 'm' && third == 'b' && forth == '.'))
                || (last == 's' && ((second == 'j' && third == '.')|| (second == 's' && third == 'c' && forth == '.')))
                || (last == 'o' && (second == 'c' && third == 'i'))) return true;


改动后的代码,更加像低级语言在写代码,呵呵,不过效率的确提高了一些,这个时候提高就不是太明显了,只是减少substring创创建中间对象生成时间,能到这一步,我想很少有人再去想了,但是但是代码洁癖超级高的话,还会去想,五个字符,最坏情况要匹配5次才能找到自己想要的入口,有没有办法一次性找到,还有,找到后通过路径直接匹配到内容,回到那个图上,看到很像图形结构


首先考虑入口应该如何处理?看到入口全是字母,这些字母的ascii都是数据0-127的,所以我们想直接用字母的ascii作为下标,我们姑且浪费点空间,创建一个长度为128的数组,使用结束字母ascii作为入口位置;


再考虑,进入后该如何算?我们就想通过图上的路由,最终可以找到最后一个字符的就是成立的,此时设计到子节点的查找,理论上这样是最快的,但是,实现起来每一层需要循环到自己要的那个路由,循环体本身对CPU的开销也是有的,而且要构造对象和指针来实现,访问数据需要通过间接访问,理论上的最优化,并不是我们想要的最优,但是我们的优化到此结束了吗?不是,算法行不通,我更喜欢钻牛角尖了,呵呵,


怎么解决第二步无法完成的情况呢?我考虑到虽然不能按照图的结构完成,那么我发现开始字符都是 "."将它抛开算,如果入参不是这样直接错误,另外,剩余的字符,就只有1-2个字符,而且这个字符的ascii都是数据0-127的,我惊喜了,0-127就是在byte的范围内,我可以组织成任何我想要的内容,我此时将两个字符,按照byte拼接,就可以拼接为2个byte位的数字,也就是short int 类型,因为java在JVM内核实现中其实在局部变量中都是一样大的,所以我们就直接用int了,如果int和int匹配就是一个compare,而不是按照每个字符去compare了;

再发现,上面的图结构中,按照路由向下走,每个入口下面最多2个可匹配的内容,所以我们就定死一个结构就是一个入口下两个int,默认为-1,如下(我这里定义的是内部类,写成static是为了静态方法调用):

static class TempNode {
    int c1 = -1;
    int c2 = -1;
}


OK,开始尝试,首先我们初始化我们需要进行对比的信息,就像编译时完成一些东西一样:

我们首先初始化一个128长度的数组:

private static TempNode[] tempNode1 = new TempNode[128];

      private static void addNode(char []chars) {
          int b = (int)chars[0];
          TempNode node = tempNode1[b];
          if(node == null) {
             node = new TempNode();
             tempNode1[b] = node;
          }
          int size = chars.length;
          int tmp = 0;
          if(size == 2) {
             tmp = (int)(chars[1]);
          }else if(size == 3) {
             tmp = (int)(chars[1] << 8 | chars[2]);
          }
          if(node.c1 == -1) node.c1 = tmp;
          else node.c2 = tmp;
      }

      static {
          addNode(new char[] {'f' , 'i' , 'g'});
          addNode(new char[] {'g' , 'n' , 'p'});
          addNode(new char[] {'g' , 'p' , 'j'});
          addNode(new char[] {'p' , 'm' , 'b'});
          addNode(new char[] {'s' , 'j'});
          addNode(new char[] {'s' , 's' , 'c'});
          addNode(new char[] {'o' , 'c' , 'i'});
      }

这部分访问这个类的时候,就会被初始化掉,初始化的目的就是为了可以被反复使用,而没有将这部分运算时间抛开掉了;此时怎么运算呢?代码描述如下:

1、取出访问字符最后一个字母,根据ascii到tempNode1取出对象,如果对象为空,则就没有任何匹配的,直接返回错误。

2、倒转访问字符,如果长度超过3个,且第三个字符不是“.”,则看第四个字符是不是“.”。

3、上述成立后,根据字符长度拼接处对应的int数据,与取出的TempNode对象中的两个int值进行匹配,得到boolean类型的值返回。


实际代码如下:

       

private static boolean testServletPath(String servletPath) {
     int length = servletPath.length();
     char last = servletPath.charAt(length - 1);
     TempNode node = tempNode1[(int)last];
     if(node == null) return false;
     char second = servletPath.charAt(length - 2);
     char third = servletPath.charAt(length - 3);
     char forth;
     int tmp = -1;
     if(length > 3 && third != '.') {
         forth = servletPath.charAt(length - 4);
         if(forth != '.') return false;
         tmp = (int)(second << 8 | third);
     }else if(third != '.') {
         return false;
     }else {
         forth = 0;
         tmp = second;
     }
     return tmp == node.c1 || tmp == node.c2;
}


我想这已经快到极点了,可以看到上面有进行 ‘.’ 的compare,也就是多进行操作了,那么是否可以直接将这个字符也放入到int中呢,int本身还有2个byte的空位,是的,可以这样做,但是会增加一次位偏移操作,所以和多一次判定,所以总体的效率会看不出多大的区别,但是也是可以那样做的,因为你会发现compare操作会做2次。


也就是只要能挖,在这种代码中能挖出很多宝贝,在高并发的应用系统中,如果在极高并发的代码段,尤其是无论任何程序都会经过的代码段,而且经过多次的那种,那么,这种优化就会产生总体上的提升。


最后,我们看看代码片段2,其实和片段1类似,只不过第一步是考虑endWith,第二个是contains,contarins其实有可能会更加慢,因为会在内部在到相应的字符做一次匹配;就像对比MSIE这个字符串,如果文本中出现多次M开头就又要开始匹配,这里将同一个文本和8种情况对比是很痛苦的事情,其实我们完全可以将上面8个字符串的第一个字母取出来,后面字符串作为byte位,最后组成一个long数据,代表一个数字,放在一个小数组中(此时就是用起始字符作为数组下标了);然后,在使用传入字符串时,每位进行位偏移,将数字和对应下标下的数组看看是否一致,一致就直接返回那个下标记录下的常量,若不是就继续向后了,也就是一遍扫描下来8个字符串可以对比到,而且每次内部对比的时候,就是一个简单的数字对比而已,这就是一种优化。


不论不论怎么说,优化还是归结到能不做的就不做,能少做的就少做,能只做一次就只做一次!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值