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