举一个例子,我们做项目需要安排计划,每一个模块可以由多人同时并行做多项任务,也可以一个人或者多个人串行工作,但始终会有一条关键路径,这条路径就是项目的工期。系统一次调用的响应时间跟项目计划一样,也有一条关键路径,这个关键路径是就是系统影响时间。关键路径由 CPU 运算、IO、外部系统响应等等组成。
对于一个系统的用户来说,从用户点击一个按钮、链接或发出一条指令开始,到系统把结果以用户希望的形式展现出来为终止,整个过程所消耗的时间是用户对这个软件性能的直观印象,也就是我们所说的响应时间。当响应时间较短时,用户体验是很好的,当然用户体验的响应时间包括个人主观因素和客观响应时间。在设计软件时,我们就需要考虑到如何更好地结合这两部分达到用户最佳的体验。如:用户在大数据量查询时,我们可以将先提取出来的数据展示给用户,在用户看的过程中继续进行数据检索,这时用户并不知道我们后台在做什么,用户关注的是用户操作的响应时间。
我们经常说的一个系统吞吐量,通常由 QPS(TPS)、并发数两个因素决定,每套系统这两个值都有一个相对极限值,在应用场景访问压力下,只要某一项达到系统最高值,系统的吞吐量就上不去了,如果压力继续增大,系统的吞吐量反而会下降,原因是系统超负荷工作,上下文切换、内存等等其它消耗导致系统性能下降,决定系统响应时间要素。
缓冲区是一块特定的内存区域,开辟缓冲区的目的是通过缓解应用程序上下层之间的性能差异,提高系统的性能。在日常生活中,缓冲的一个典型应用是漏斗。缓冲可以协调上层组件和下层组件的性能差,当上层组件性能优于下层组件时,可以有效减少上层组件对下层组件的等待时间。基于这样的结构,上层应用组件不需要等待下层组件真实地接受全部数据,即可返回操作,加快了上层组件的处理速度,从而提升系统整体性能。
BufferedWriter 就是一个缓冲区用法,一般来说,缓冲区不宜过小,过小的缓冲区无法起到真正的缓冲作用,缓冲区也不宜过大,过大的缓冲区会浪费系统内存,增加 GC 负担。尽量在 I/O 组件内加入缓冲区,可以提高性能。一个缓冲区例子代码如清单 1 所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
import
java.awt.Color;
import
java.awt.Graphics;
import
java.awt.Graphics2D;
import
java.awt.Image;
import
javax.swing.JApplet;
public
class
NoBufferMovingCircle
extends
JApplet
implements
Runnable{
Image screenImage =
null
;
Thread thread;
int
x =
5
;
int
move =
1
;
public
void
init(){
screenImage = createImage(
230
,
160
);
}
public
void
start(){
if
(thread ==
null
){
thread =
new
Thread(
this
);
thread.start();
}
}
@Override
public
void
run() {
// TODO Auto-generated method stub
try
{
System.out.println(x);
while
(
true
){
x+=move;
System.out.println(x);
if
((x>
105
)||(x<
5
)){
move*=-
1
;
}
repaint();
Thread.sleep(
10
);
}
}
catch
(Exception e){
}
}
public
void
drawCircle(Graphics gc){
Graphics2D g = (Graphics2D) gc;
g.setColor(Color.GREEN);
g.fillRect(
0
,
0
,
200
,
100
);
g.setColor(Color.red);
g.fillOval(x,
5
,
90
,
90
);
}
public
void
paint(Graphics g){
g.setColor(Color.white);
g.fillRect(
0
,
0
,
200
,
100
);
drawCircle(g);
}
}
|
程序可以完成红球的左右平移,但是效果较差,因为每次的界面刷新都涉及图片的重新绘制,这较为费时,因此,画面的抖动和白光效果明显。为了得到更优质的显示效果,可以为它加上缓冲区。代码如清单 2 所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import
java.awt.Color;
import
java.awt.Graphics;
public
class
BufferMovingCircle
extends
NoBufferMovingCircle{
Graphics doubleBuffer =
null
;
//缓冲区
public
void
init(){
super
.init();
doubleBuffer = screenImage.getGraphics();
}
public
void
paint(Graphics g){
//使用缓冲区,优化原有的 paint 方法
doubleBuffer.setColor(Color.white);
//先在内存中画图
doubleBuffer.fillRect(
0
,
0
,
200
,
100
);
drawCircle(doubleBuffer);
g.drawImage(screenImage,
0
,
0
,
this
);
}
}
|
除 NIO 外,使用 Java 进行 I/O 操作有两种基本方式:
无论使用哪种方式进行文件 I/O,如果能合理地使用缓冲,就能有效地提高 I/O 的性能。
下面显示了可与 InputStream、OutputStream、Writer 和 Reader 配套使用的缓冲组件。
OutputStream-FileOutputStream-BufferedOutputStream
InputStream-FileInputStream-BufferedInputStream
Writer-FileWriter-BufferedWriter
Reader-FileReader-BufferedReader
使用缓冲组件对文件 I/O 进行包装,可以有效提高文件 I/O 的性能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
import
java.io.BufferedInputStream;
import
java.io.BufferedOutputStream;
import
java.io.DataInputStream;
import
java.io.DataOutputStream;
import
java.io.FileInputStream;
import
java.io.FileNotFoundException;
import
java.io.FileOutputStream;
import
java.io.IOException;
public
class
StreamVSBuffer {
public
static
void
streamMethod()
throws
IOException{
try
{
long
start = System.currentTimeMillis();
//请替换成自己的文件
DataOutputStream dos =
new
DataOutputStream(
new
FileOutputStream(
"C:\\StreamVSBuffertest.txt"
));
for
(
int
i=
0
;i<
10000
;i++){
dos.writeBytes(String.valueOf(i)+
"\r\n"
);
//循环 1 万次写入数据
}
dos.close();
DataInputStream dis =
new
DataInputStream(
new
FileInputStream(
"C:\\StreamVSBuffertest.txt"
));
while
(dis.readLine() !=
null
){
}
dis.close();
System.out.println(System.currentTimeMillis() - start);
}
catch
(FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public
static
void
bufferMethod()
throws
IOException{
try
{
long
start = System.currentTimeMillis();
//请替换成自己的文件
DataOutputStream dos =
new
DataOutputStream(
new
BufferedOutputStream(
new
FileOutputStream(
"C:\\StreamVSBuffertest.txt"
)));
for
(
int
i=
0
;i<
10000
;i++){
dos.writeBytes(String.valueOf(i)+
"\r\n"
);
//循环 1 万次写入数据
}
dos.close();
DataInputStream dis =
new
DataInputStream(
new
BufferedInputStream(
new
FileInputStream(
"C:\\StreamVSBuffertest.txt"
)));
while
(dis.readLine() !=
null
){
}
dis.close();
System.out.println(System.currentTimeMillis() - start);
}
catch
(FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public
static
void
main(String[] args){
try
{
StreamVSBuffer.streamMethod();
StreamVSBuffer.bufferMethod();
}
catch
(IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
|
运行结果如清单 4 所示。
1
2
|
889
31
|
很明显使用缓冲的代码性能比没有使用缓冲的快了很多倍。清单 5 所示代码对 FileWriter 和 FileReader 进行了相似的测试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
import
java.io.BufferedInputStream;
import
java.io.BufferedOutputStream;
import
java.io.BufferedReader;
import
java.io.BufferedWriter;
import
java.io.FileNotFoundException;
import
java.io.FileReader;
import
java.io.FileWriter;
import
java.io.IOException;
public
class
WriterVSBuffer {
public
static
void
streamMethod()
throws
IOException{
try
{
long
start = System.currentTimeMillis();
FileWriter fw =
new
FileWriter(
"C:\\StreamVSBuffertest.txt"
);
//请替换成自己的文件
for
(
int
i=
0
;i<
10000
;i++){
fw.write(String.valueOf(i)+
"\r\n"
);
//循环 1 万次写入数据
}
fw.close();
FileReader fr =
new
FileReader(
"C:\\StreamVSBuffertest.txt"
);
while
(fr.ready() !=
false
){
}
fr.close();
System.out.println(System.currentTimeMillis() - start);
}
catch
(FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public
static
void
bufferMethod()
throws
IOException{
try
{
long
start = System.currentTimeMillis();
BufferedWriter fw =
new
BufferedWriter(
new
FileWriter(
"C:\\StreamVSBuffertest.txt"
));
//请替换成自己的文件
for
(
int
i=
0
;i<
10000
;i++){
fw.write(String.valueOf(i)+
"\r\n"
);
//循环 1 万次写入数据
}
fw.close();
BufferedReader fr =
new
BufferedReader(
new
FileReader(
"C:\\StreamVSBuffertest.txt"
));
while
(fr.ready() !=
false
){
}
fr.close();
System.out.println(System.currentTimeMillis() - start);
}
catch
(FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public
static
void
main(String[] args){
try
{
StreamVSBuffer.streamMethod();
StreamVSBuffer.bufferMethod();
}
catch
(IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
|
运行输出如清单 6 所示。
1
2
|
1295
31
|
从上面例子可以看出,无论对于读取还是写入文件,适当地使用缓冲,可以有效地提升系统的文件读写性能,为用户减少响应时间。
缓存也是一块为提升系统性能而开辟的内存空间。缓存的主要作用是暂存数据处理结果,并提供下次访问使用。在很多场合,数据的处理或者数据获取可能会非常费时,当对这个数据的请求量很大时,频繁的数据处理会耗尽 CPU 资源。缓存的作用就是将这些来之不易的数据处理结果暂存起来,当有其他线程或者客户端需要查询相同的数据资源时,可以省略对这些数据的处理流程,而直接从缓存中获取处理结果,并立即返回给请求组件,以此提高系统的响应时间。
目前有很多基于 Java 的缓存框架,比如 EHCache、OSCache 和 JBossCache 等。EHCache 缓存出自 Hibernate,是其默认的数据缓存解决方案;OSCache 缓存是有 OpenSymphony 设计的,它可以用于缓存任何对象,甚至是缓存部分 JSP 页面或者 HTTP 请求;JBossCache 是由 JBoss 开发、可用于 JBoss 集群间数据共享的缓存框架。
以 EHCache 为例,EhCache 的主要特性有:
由于 EhCache 是进程中的缓存系统,一旦将应用部署在集群环境中,每一个节点维护各自的缓存数据,当某个节点对缓存数据进行更新,这些更新的数据无法在其它节点中共享,这不仅会降低节点运行的效率,而且会导致数据不同步的情况发生。例如某个网站采用 A、B 两个节点作为集群部署,当 A 节点的缓存更新后,而 B 节点缓存尚未更新就可能出现用户在浏览页面的时候,一会是更新后的数据,一会是尚未更新的数据,尽管我们也可以通过 Session Sticky 技术来将用户锁定在某个节点上,但对于一些交互性比较强或者是非 Web 方式的系统来说,Session Sticky 显然不太适合。所以就需要用到 EhCache 的集群解决方案。清单 7 所示是 EHCache 示例代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
import
net.sf.ehcache.Cache;
import
net.sf.ehcache.CacheManager;
import
net.sf.ehcache.Element;
/**
* 第一步:生成 CacheManager 对象
* 第二步:生成 Cache 对象
* 第三步:向 Cache 对象里添加由 key,value 组成的键值对的 Element 元素
* @author mahaibo
*
*/
public
class
EHCacheDemo{
public
static
void
main(String[] args) {
//指定 ehcache.xml 的位置
String fileName=
"E:\\1008\\workspace\\ehcachetest\\ehcache.xml"
;
CacheManager manager =
new
CacheManager(fileName);
//取出所有的 cacheName
String names[] = manager.getCacheNames();
for
(
int
i=
0
;i<names.length;i++){
System.out.println(names[i]);
}
//根据 cacheName 生成一个 Cache 对象
//第一种方式:
Cache cache=manager.getCache(names[
0
]);
//第二种方式,ehcache 里必须有 defaultCache 存在,"test"可以换成任何值
// Cache cache = new Cache("test", 1, true, false, 5, 2);
// manager.addCache(cache);
//向 Cache 对象里添加 Element 元素,Element 元素有 key,value 键值对组成
cache.put(
new
Element(
"key1"
,
"values1"
));
Element element = cache.get(
"key1"
);
System.out.println(element.getValue());
Object obj = element.getObjectValue();
System.out.println((String)obj);
manager.shutdown();
}
}
|
对象复用池是目前很常用的一种系统优化技术。它的核心思想是,如果一个类被频繁请求使用,那么不必每次都生成一个实例,可以将这个类的一些实例保存在一个“池”中,待需要使用的时候直接从池中获取。这个“池”就称为对象池。在实现细节上,它可能是一个数组,一个链表或者任何集合类。对象池的使用非常广泛,例如线程池和数据库连接池。线程池中保存着可以被重用的线程对象,当有任务被提交到线程时,系统并不需要新建线程,而是从池中获得一个可用的线程,执行这个任务。在任务结束后,不需要关闭线程,而将它返回到池中,以便下次继续使用。由于线程的创建和销毁是较为费时的工作,因此,在线程频繁调度的系统中,线程池可以很好地改善性能。数据库连接池也是一种特殊的对象池,它用于维护数据库连接的集合。当系统需要访问数据库时,不需要重新建立数据库连接,而可以直接从池中获取;在数据库操作完成后,也不关闭数据库连接,而是将连接返回到连接池中。由于数据库连接的创建和销毁是重量级的操作,因此,避免频繁进行这两个操作对改善系统的性能也有积极意义。目前应用较为广泛的数据库连接池组件有 C3P0 和 Proxool。
以 C3P0 为例,它是一个开源的 JDBC 连接池,它实现了数据源和 JNDI 绑定,支持 JDBC3 规范和 JDBC2 的标准扩展。目前使用它的开源项目有 Hibernate,Spring 等。如果采用 JNDI 方式配置,如清单 8 所示。
1
2
3
4
5
6
7
8
|
<Resource name=
"jdbc/dbsource"
type=
"com.mchange.v2.c3p0.ComboPooledDataSource"
maxPoolSize=
"50"
minPoolSize=
"5"
acquireIncrement=
"2"
initialPoolSize=
"10"
maxIdleTime=
"60"
factory=
"org.apache.naming.factory.BeanFactory"
user=
"xxxx"
password=
"xxxx"
driverClass=
"oracle.jdbc.driver.OracleDriver"
jdbcUrl=
"jdbc:oracle:thin:@192.168.x.x:1521:orcl"
idleConnectionTestPeriod=
"10"
/>
|
参数说明:
如果使用 spring,同时项目中不使用 JNDI,又不想配置 Hibernate,可以直接将 C3P0 配置到 dataSource 中即可,如清单 9 所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<bean id=
"dataSource"
destroy-method=
"close"
>
<property name=
"driverClass"
><value>oracle.jdbc.driver.OracleDriver</value></property>
<property name=
"jdbcUrl"
><value>jdbc:oracle:thin:@localhost:
1521
:Test</value></property>
<property name=
"user"
><value>Kay</value></property>
<property name=
"password"
><value>root</value></property>
<!--连接池中保留的最小连接数。-->
<property name=
"minPoolSize"
value=
"10"
/>
<!--连接池中保留的最大连接数。Default:
15
-->
<property name=
"maxPoolSize"
value=
"100"
/>
<!--最大空闲时间,
1800
秒内未使用则连接被丢弃。若为
0
则永不丢弃。Default:
0
-->
<property name=
"maxIdleTime"
value=
"1800"
/>
<!--当连接池中的连接耗尽的时候 c3p0 一次同时获取的连接数。Default:
3
-->
<property name=
"acquireIncrement"
value=
"3"
/>
<property name=
"maxStatements"
value=
"1000"
/>
<property name=
"initialPoolSize"
value=
"10"
/>
<!--每
60
秒检查所有连接池中的空闲连接。Default:
0
-->
<property name=
"idleConnectionTestPeriod"
value=
"60"
/>
<!--定义在从数据库获取新连接失败后重复尝试的次数。Default:
30
-->
<property name=
"acquireRetryAttempts"
value=
"30"
/>
<property name=
"breakAfterAcquireFailure"
value=
"true"
/>
<property name=
"testConnectionOnCheckout"
value=
"false"
/>
</bean>
|
类似的做法存在很多种,用户可以自行上网搜索。
计算方式转换比较出名的是时间换空间方式,它通常用于嵌入式设备,或者内存、硬盘空间不足的情况。通过使用牺牲 CPU 的方式,获得原本需要更多内存或者硬盘空间才能完成的工作。
一个非常简单的时间换空间的算法,实现了 a、b 两个变量的值交换。交换两个变量最常用的方法是使用一个中间变量,而引入额外的变量意味着要使用更多的空间。采用下面的方法可以免去中间变量,而达到变量交换的目的,其代价是引入了更多的 CPU 运算。
1
2
3
|
a=a+b;
b=a-b;
a=a-b;
|
另一个较为有用的例子是对无符号整数的支持。在 Java 语言中,不支持无符号整数,这意味着当需要无符号的 Byte 时,需要使用 Short 代替,这也意味着空间的浪费。下面代码演示了使用位运算模拟无符号 Byte。虽然在取值和设值过程中需要更多的 CPU 运算,但是可以大大降低对内存空间的需求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public
class
UnsignedByte {
public
short
getValue(
byte
i){
//将 byte 转为无符号的数字
short
li = (
short
)(i &
0xff
);
return
li;
}
public
byte
toUnsignedByte(
short
i){
return
(
byte
)(i &
0xff
);
//将 short 转为无符号 byte
}
public
static
void
main(String[] args){
UnsignedByte ins =
new
UnsignedByte();
short
[] shorts =
new
short
[
256
];
//声明一个 short 数组
for
(
int
i=
0
;i<shorts.length;i++){
//数组不能超过无符号 byte 的上限
shorts[i]=(
short
)i;
}
byte
[] bytes =
new
byte
[
256
];
//使用 byte 数组替代 short 数组
for
(
int
i=
0
;i<bytes.length;i++){
bytes[i]=ins.toUnsignedByte(shorts[i]);
//short 数组的数据存到 byte 数组中
}
for
(
int
i=
0
;i<bytes.length;i++){
System.out.println(ins.getValue(bytes[i])+
" "
);
//从 byte 数组中取出无符号的 byte
}
}
}
|
运行输出如清单 12 所示,篇幅所限,只显示到 10 为止。
1
2
3
4
5
6
7
8
9
10
11
|
0
1
2
3
4
5
6
7
8
9
10
|
如果 CPU 的能力较弱,可以采用牺牲空间的方式提高计算能力,实例代码如清单 13 所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
import
java.util.Arrays;
import
java.util.HashMap;
import
java.util.Map;
public
class
SpaceSort {
public
static
int
arrayLen =
1000000
;
public
static
void
main(String[] args){
int
[] a =
new
int
[arrayLen];
int
[] old =
new
int
[arrayLen];
Map<Integer,Object> map =
new
HashMap<Integer,Object>();
int
count =
0
;
while
(count < a.length){
//初始化数组
int
value = (
int
)(Math.random()*arrayLen*
10
)+
1
;
if
(map.get(value)==
null
){
map.put(value, value);
a[count] = value;
count++;
}
}
System.arraycopy(a,
0
, old,
0
, a.length);
//从 a 数组拷贝所有数据到 old 数组
long
start = System.currentTimeMillis();
Arrays.sort(a);
System.out.println(
"Arrays.sort spend:"
+(System.currentTimeMillis() - start)+
"ms"
);
System.arraycopy(old,
0
, a,
0
, old.length);
//恢复 原有数据
start = System.currentTimeMillis();
spaceTotime(a);
System.out.println(
"spaceTotime spend:"
+(System.currentTimeMillis() - start)+
"ms"
);
}
public
static
void
spaceTotime(
int
[] array){
int
i =
0
;
int
max = array[
0
];
int
l = array.length;
for
(i=
1
;i<l;i++){
if
(array[i]>max){
max = array[i];
}
}
int
[] temp =
new
int
[max+
1
];
for
(i=
0
;i<l;i++){
temp[array[i]] = array[i];
}
int
j =
0
;
int
max1 = max +
1
;
for
(i=
0
;i<max1;i++){
if
(temp[i] >
0
){
array[j++] = temp[i];
}
}
}
}
|
函数 spaceToTime() 实现了数组的排序,它不计空间成本,以数组的索引下标来表示数据大小,因此避免了数字间的相互比较,这是一种典型的以空间换时间的思路。
应对、处理高吞吐量系统有很多方面可以入手,作者将以系列的方式逐步介绍覆盖所有领域。本文主要介绍了缓冲区、缓存操作、对象复用池、计算方式转换等优化及建议,从实际代码演示入手,对优化建议及方案进行了验证。作者始终坚信,没有什么优化方案是百分百有效的,需要读者根据实际情况进行选择、实践。