Java程序性能优化 读书笔记(八)优化组件:缓冲和缓存

本文介绍了缓存和缓冲两种常见的性能优化手段。缓存通过暂存数据处理结果以供重复利用,减少不必要的计算开销;缓冲则通过调节不同层级间的性能差异,提高I/O效率。文章还展示了使用Java实现这两种技术的具体案例。
摘要由CSDN通过智能技术生成

一、缓冲

缓冲区是一块特定的内存区域。开辟缓冲区的目的是通过缓解应用程序上下层之间的性能差异,提高系统的性能。缓冲可以协调上层组件和下层组件的性能差。当上层组件性能优于下层组件时,可以有效减少上层组件对下层组件的等待时间。基于这样的结构,上层应用组件不需要等待下层组件真实地接受全部数据,即可返回操作,加快了上层组件的处理速度,从而提升系统整体性能。

缓冲最常用的场景就是提高I/O的速度。为此,JDK内不少I/O组件都提供了缓冲功能。

比如,当使用FileWriter时,进行文件写操作的代码如下:

Writer writer = new FileWriter(new File("file.txt"));  
long begin=System.currentTimeMillis();  
for (int i = 0; i < CIRCLE; i++) {  
    writer.write(i);                                    //写入文件  
}  
writer.close();  
System.out.println("testFileWriter spend:"+(System.currentTimeMillis()-begin));  
为进行I/O优化,可以为FileWriter加上缓冲:
Writer writer = new BufferedWriter(new FileWriter(new File("file.txt")));  //增加了缓冲  
long begin=System.currentTimeMillis();  
for (int i = 0; i < CIRCLE; i++) {  
    writer.write(i);  
}  
writer.close();  
System.out.println("testFileWriterBuffer spend:"+(System.currentTimeMillis()-begin));  
以上代码使用BufferedWriter为FileWriter对象增加缓冲功能。BufferedWriter对象拥有两个构造函数:

public BufferedWriter(Writer out)  
public BufferedWriter(Writer out, int sz)  

其中,第2个构造函数允许在应用层指定缓冲区的大小,第1个构造函数将构造大小为8K的缓冲区。一般来说,缓冲区不宜过小,过小的缓冲区无法起到真正的缓冲作用,缓冲区也不宜过大,过大的缓存区会浪费系统内存,增加GC负担。在本例中,设置循环次数CIRCLE为10万,若不使用缓冲区操作,则相对耗时63ms;而使用缓冲区的FileWriter仅相对耗时32ms,性能提升一倍。

另一个有用的缓冲组件是BufferedOutputStream。在前文"装饰者模式"一节中,已经提到,使用BufferedOutputStream可以包装所有的OutputStream,为其提供缓冲功能,提高输出流的效率。和BufferedWriter类似,它也提供了两个构造函数:

public BufferedOutputStream(OutputStream out)  
public BufferedOutputStream(OutputStream out, int size) 
第2个构造函数可以指定缓冲区大小,默认情况下,和BufferedWriter一样,缓冲区大小为 8K

二、缓存

缓存(Cache)也是一块为提升系统性能而开辟的内存空间。缓存的主要作用是暂存数据处理结果,并提供下次访问使用。在很多场合,数据的处理或者数据获取可能会非常费时,当对这个数据的请求量很大时,频繁的数据处理会耗尽CPU资源。缓存的作用就是将这些来之不易的数据处理结果暂存起来,当有其他线程或者客户端需要查询相同的数据资源时,可以省略对这些数据的处理流程,而直接从缓存中获取处理结果,并立即返回给请求组件,以此提高系统的响应时间。

缓存的使用非常普遍,比如,目前流行的几种浏览器都会在本地缓存远程的页面,从而减少远程HTTP访问次数,加快网页的加载速度。又比如,在服务端的系统开发中,设计人员可以为一些核心API加上缓存,从而提高系统的整体性能。

最为简单的缓存可以直接使用HashMap实现。当然,这样做会遇到很多问题,比如,何时应该清理无效的数据;如何防止缓存数据过多而导致内存溢出等。一个稍好的替代方案是直接使用WeakHashMap,它使用弱引用维护一张哈希表,从而避免了潜在的内存溢出问题,但是,作为专业的缓存,它的功能也略有不足。

注意:缓存可以保存一些来之不易的数据或者计算结果。当需要再次使用这些数据时,可以从缓存中低成本地获取,而不需要再占用宝贵的系统资源。

幸运的是,目前有很多基于Java的缓存框架,比如EHCache、OSCache和JBossCache等。EHCache缓存出自Hibernate,是Hibernte框架默认的数据缓存解决方案;OSCache缓存是由OpenSymphony设计的,它可以用于缓存任何对象,甚至是缓存部分JSP页面或者HTTP请求;JBossCache是由JBoss开发、可用于JBoss集群间数据共享的缓存框架。

下面,以EHCache缓存为例,简单介绍一下缓存的基本使用方法。

在使用EHCache前,需要对EHCache进行必要的配置。一个典型的配置可能如下:

<ehcache> 
    <diskStore path="data/ehcache" /> 
 
    <defaultCache maxElementsInMemory="10000" eternal="false" 
        overflowToDisk="true" timeToIdleSeconds="120" timeToLiveSeconds="120" 
        diskPersistent="false" diskExpiryThreadIntervalSeconds="120" /> 
 
    <cache name="cache1" maxElementsInMemory="100" eternal="false" 
        timeToIdleSeconds="6" timeToLiveSeconds="60" overflowToDisk="true" 
        diskPersistent="false" /> 
 
    <cache name="cache2" maxElementsInMemory="100000" eternal="false" 
        timeToIdleSeconds="300" timeToLiveSeconds="600" overflowToDisk=             "false" 
        diskPersistent="false" /> 
</ehcache> 

以上配置文件首先设置了一个默认的cache模版。在程序中使用EHCache接口动态生成缓存时,会使用这些参数定义新的缓存。随后,定义了两个缓存,名字分别是cache1和cache2。

配置文件中一些主要参数的含义如下:

maxElementsInMemory:该缓存中允许存放的最大条目数量。

eternal:缓存内容是否永久储存。

overflowToDisk:如果内存中的数据超过maxElementsInMemory,是否使用磁盘存储。

timeToIdleSeconds:如果不是永久储存的缓存,那么在timeToIdleSeconds指定时间内没有访问一个条目,则移除它。

timeToLiveSeconds:如果不是永久储存的缓存,一个条目可以存在的最长时间。

diskPersistent:磁盘储存的条目是否永久保存。

diskExpiryThreadIntervalSeconds:磁盘清理线程的运行时间间隔。

EHCache使用简单,可以像使用HashMap一样使用它。但为了能够更方便地使用EHCache,笔者还是对EHCache进行了简单的封装,提供了EHCacheUtil工具类,专门针对EHCache做各种操作。

首先是EHCache的初始化操作

static{  
    try {  
        //载入EHCache的配置文件,创建CacheManager  
        manager = CacheManager.create  
(EHCacheUtil.class.getClassLoader().getResourceAsStream(configfile));  
    } catch (CacheException e) {  
        e.printStackTrace();  
    }  
}  

以上代码将载入EHCache的配置文件,并生成CacheManager的实例。之后,就可以通过CacheManager对Cache进行管理。

将数据存入Cache的实现如下:

public static void put(String cachename,Serializable key,Serializable value){  
    manager.getCache(cachename).put(new Element(key, value));  
}
在put()操作中,首先指定要使用的Cache名称,接着就是类似于HashMap的名值对。get()操作也是类似:
public static Serializable get(String cachename,Serializable key){  
    try {  
        Element e=manager.getCache(cachename).get(key);  
        if(e==null)return null;  
        return e.getValue();                //取得缓存中的数据  
    } catch (IllegalStateException e) {  
        e.printStackTrace();  
    } catch (CacheException e) {  
        e.printStackTrace();  
    }  
    return null;  
}  

有了以上的工具类,便可以更方便地在实际工作中使用EHCache。从软件设计的角度来说,笔者建议在频繁使用且重负载的函数实现中,加入缓存,以提高它在频繁调用时的性能。

在为方法加入缓存时,可以使用最原始的硬编码方式,根据传入的参数构造key,然后去缓存中查找结果,如果找到则立即返回;如果找不到,则再进行相关的业务逻辑处理,得到最终结果,并将结果保存到缓存中,并返回这个结果。这种方式的实现好处是代码比较直白,缺点是缓存组件和业务层代码紧密耦合,依赖性强。

本小节介绍基于动态代理的缓存解决方案,对动态代理尚不了解的读者,可以回顾前文中"代理模式"一节。基于动态代理的缓存方案的最大好处是,在业务层,无需关注对缓存的操作,缓存操作代码被完全独立并隔离,并且对一个新的函数方法加入缓存不会影响原有的方法实现,是一种非常灵活的软件结构。

注意:使用动态代理无需修改一个逻辑方法的代码,便可以为它加上缓存功能,提高其性能。

现在,假设有一个可能被频繁调用的方法,它用于对一个整数做因式分解。实现如下:(由于本文不关注因式分解算法,故只列出该类的结构):

public class HeavyMethodDemo {  
    public String heavyMethod(int num) {  
        StringBuffer sb = new StringBuffer();  
        //对 num 进行因式分解,将结果保存在sb中  
        return sb.toString();  
    }  
}  
使用CGLIB生成动态代理类的方法拦截器的逻辑如下:
public class CglibHeavyMethodInterceptor implements MethodInterceptor {  
    HeavyMethodDemo real=new HeavyMethodDemo();  
    @Override  
    public Object intercept(Object arg0, Method arg1, Object[] arg2,  
            MethodProxy arg3) throws Throwable {  
        String v=(String)EHCacheUtil.get("cache1", (Serializable)arg2[0]);//查询缓存  
        if(v==null){  
           v=real.heavyMethod((Integer)arg2[0]);        //缓存中未找到结果  
           EHCacheUtil.put("cache1", (Integer)arg2[0], v);  //保存计算结果  
        }  
        return v;  
    }  
//省略其他代码 
在这个方法拦截器中,实现了对缓存的操作,它首先查询系统 是否已经计算并缓存了所请求的数字,如果没有,则进行计算,并将结果保存在缓存中;如果有,则直接从缓存中取得结果。在使用动态代理时,可以通过下面的代码生成动态代理对象,包含上述缓存逻辑:
public static HeavyMethodDemo newCacheHeavyMethod(){//生成带有缓存功能的类  
    Enhancer enhancer = new Enhancer();    
    enhancer.setSuperclass(HeavyMethodDemo.class);  
    enhancer.setCallback(new CglibHeavyMethodInterceptor());//设置缓存逻辑  
    HeavyMethodDemo cglibProxy = (HeavyMethodDemo) enhancer.create();    
    return cglibProxy;    
}  

以上代码首先生成一个HeavyMethodDemo类的子类,并使用CglibHeavyMethodInterceptor作为它的方法拦截器,最后生成动态类的对象。这个对象是HeavyMethodDemo的动态子类的实例。

以下代码只是简单地生成了HeavyMethodDemo类。下文将对newHeavyMethod()和newCacheHeavyMethod()生成的对象进行简单的性能测试。

public static HeavyMethodDemo newHeavyMethod(){     //不带有缓存功能  
    return new HeavyMethodDemo();  
}  
一段测试代码如下,它分别使用代理类对象和HeavyMethodDemo对象,对一个大整数进行因式分解运算。在笔者的计算机上,使用动态代理的缓存对象相对耗时188ms,而HeavyMethodDemo相对耗时609ms。
public static void main(String args[]){  
    HeavyMethodDemo m=newCacheHeavyMethod();    //使用缓存  
    long begin = System.currentTimeMillis();  
    for(int i=0;i<100000;i++)                   //使用缓存时,只需要计算一次  
        m.heavyMethod(2147483646);  
    System.out.println("cache method spend:"+(System.currentTimeMillis()-begin));  
      
    m=newHeavyMethod();                         //不使用缓存  
    begin = System.currentTimeMillis();  
    for(int i=0;i<100000;i++)                   //不使用缓存时,每次都要计算  
        m.heavyMethod(2147483646);  
    System.out.println("no cache method spend:"+(System.currentTimeMillis()-begin));  
}  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值