2021-06-06

java系统 性能优化实战

java代码优化

架构师在优化java系统性能的过程中,可以做出很多重要决策以全面提升系统的性能,例如使用高版本的jdk,引入Redis或redis+JVM缓存,甚至考虑JVM缓存分成多层,比如热点缓存+普通数据缓存等。
在数据上可以考虑数据库分库分表或者一主多从,考虑引入中间件提供表的路由,引入分布式管理器或者状态机保证事务一致性。对于大数据查询,可以考虑Elasticsearch或Hive大数据系统建立统一的数据查询接口,架构师需要考虑如何把数据库的数据同步带大数据系统,以及Redis缓存中。
系统交互上使用消息中间件实现异步通信,也可以使用RPC进行远程调用,架构师还可以把单体系统服务改成微服务系统,这种架构的改变“牵一发而动全身”。
优化系统的方式主要是优化自己或者他人写的代码。代码是系统的基石,没有良好的代码,系统架构不牢固。

可优化的代码

来看一段优化前的代码:

public Map buildArea(List<Area> areas){
	if(areas.isEmpty()){
		return new HashMap();
	}
}
Map<String,Area> map =new HashMap();
for(Area area :areas){
	String key=area.getProvinceId()+"#" +area.getCityId();
	map.put(key,area);
}
return map;

当判断这段代码每次请求都被调用说明这段代码需要进行优化、方法返回值是个Map,非常不容易阅读,因为当其他代码阅读者看到方法签名是并不清楚Key和Value的类型,必须阅读代码才知道,因此将方法签名改成如下内容:
public Map<String,Area> buildArea(List areas)
Map中的key是字符串类型,使用者必须了解方法的实现才知道如何使用Key,这里有两种方法改善方法。
一是在方法的Javadoc注释说明Key的格式:

/**
*构建一个地址Map
*@param areas 初始化地址列表
*@return Key的格式是省编号+“#”+市编号
*/
 public Map<String,Area> bulidArea(List<Area> areas)

二是在使用buildArea方法是通过一个有意义的名称来描述,例如:

 Map<String,Area> provinceCityMap=buildArea(areas);

这两种方法都能帮助阅读者明白bulidarea返回值的含义,但仍然有一定的阅读负担,使用者不得不再次阅读注释来了解Map的含义,注释很少跟随代码的改动很少,而重构往往遗漏了注释,如果Key值得构成发生了变化,比如包含城镇编号:

 String key=area.getProvinceId()+"#"+areaa.getCityId()+"#"+area.getTownId();

以String来描述对象,这个改动对于项目来说是一个巨大的灾难,所有使用buildArea的地方都必须改动,如服务果忘记了,则会造成损失,导致服务不可用。
这段代码从可读性和性能上来说,最大的问题时Key值得类型是String, 从可读性上来说,字符串让人难以理解其构成,即使方法签名从Map变成Map<String,Area>,仍然难以理解,如果想用字符串来表达一个对象,那么嗨不如直接使用一个对象,我们可以创建一个新的对象来描述“需求”:

 @Getter
@Setter
public class CityKey {
    private Integer provinceId;
    private Integer cityId;
}

这样,代码可以改成如下内容:

public Map<CityKey,Area> buildArea(List<Area> areas)
{
    if (areas.isEmpty()){
        return new HashMap();
    }
    Map<CityKey,Area> map=new HashMap<>();
    for (Area area: areas) {
        CityKey key=new CityKey(area.getprpovinceId(),area.getCityId);
        map.put(key,area);
    }
}

修改后的代码更容易阅读,还可以改进的地方是CityKey的构造通过Area来生成,可以为Area添加一个buildKey方法:

public class Area{
    ...
    public CityKey buildKey(){
        return  new CityKey(provinceId,cityId);
    }
}

这样CityKey由Area来维护,任何CityKey含义的变更、重构,都不会影响使用它的代码。
在讨论完代码易用性后,读者也许会疑惑,构造一个新的CityKey对象会不会是性能降低呢?其实恰恰相反,之前使用String来构造一个Key的代价相当昂贵,这也是接下来要讨论的内容。
在编译期遇到字符串相加时,都会使用StringBuilder来完成字符串拼接功能,对于如下代码:

area.getProviceId()+"#"+area.getCityId();

在编译后,实际的代码如下:

StringBuilder sb=new StringBuilder();
sb.append(area.getprovinceId());
sb.append("#");
ab.append(area.getCityId());

可以使用jdk的反编译工具javac -p aea.class
下面的这段代码性能问题主要在于整型值转化为字符串时,StringBuilder最终会调用Integer.toString()方法:

public static String toString(int i) {
        if (i == Integer.MIN_VALUE)
            return "-2147483648";
        int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
        char[] buf = new char[size];
        getChars(i, size, buf);
        return new String(buf, true);
    }

为了将数字转化为字符串,首先需要确定字符串的长度,可以通过
Integer.stringSize()方法获取字符串的长度。

final static int [] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999,
                                      99999999, 999999999, Integer.MAX_VALUE };

    // Requires positive x
    static int stringSize(int x) {
        for (int i=0; ; i++)
            if (x <= sizeTable[i])
                return i+1;
    }

比如数字139需要长度为3的字符串数组,通过循环3次,在i=0和i=1的时候(x<=sizeTable[i])条件不满足,继续循环,当i=2的时候,条件满足,返回i+1,从而确定数字139需要创建一个长度为3的字符串数组。
在获取字符串长度后,需要从内存中分配一个buf 数组,getchars()用于真正转化int类型到字符串,并赋值给buf数组,getChar需要30行代码来完成。

    static void getChars(int i, int index, char[] buf) {
        int q, r;
        int charPos = index;
        char sign = 0;

        if (i < 0) {
            sign = '-';
            i = -i;
        }

        // Generate two digits per iteration
        while (i >= 65536) {
            q = i / 100;
        // really: r = i - (q * 100);
            r = i - ((q << 6) + (q << 5) + (q << 2));
            i = q;
            buf [--charPos] = DigitOnes[r];
            buf [--charPos] = DigitTens[r];
        }

        // Fall thru to fast mode for smaller numbers
        // assert(i <= 65536, i);
        for (;;) {
            q = (i * 52429) >>> (16+3);
            r = i - ((q << 3) + (q << 1));  // r = i-(q*10) ...
            buf [--charPos] = digits [r];
            i = q;
            if (i == 0) break;
        }
        if (sign != 0) {
            buf [--charPos] = sign;
        }
    }
StringBuilder类型获取int对应的字符串后,是否后面就没有什么代码能影响性能了?实际上还有一系列操作才能真正完成字符串的拼接,实现字符串来表达Key的功能。
public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

ensureCapacityInternal方法用于保证StringBuilder的buf足够长,以容纳str字符串,如果buf不够长,则需要扩容,扩容意味着进一步加大内存块,然后将原有的内容复制到这份内存块中,这也是一个消耗内存的地方。
str.getChars方法也会做一次内存复制,将str的内容复制到新的buf中,至此,一个字符串的拼接真正完成。
当我们需要将一个String对象作为一个Key的时候,会调用StringBuilder.toString方法

@Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

正如JDK文档的注释Create a copy, don’t sharethe array所描述的,实际上,这也是一个消耗资源的操作, 内部会开批一段空间,并把StringBuilder的char数组内容复制到String中:

public String(char value[], int offset, int count) {
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

通过StringBuilder.append()方法可以发现执行代码还是非常长的 ,我们可以通过PerformanceAreaTest验证一下对象表示Key和用字符串表示Key两种方式的性能。前者吞吐量是后者吞吐量的5倍,如果只计较Key的生成,则对象Key生成的吞吐量是字符串Key生成的吞吐量的50倍。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值