<![CDATA[viLuo的世界]]> http://viluo.com/ zh-cn www.emlog.net Eclipse和DOS下运行程序的差异(主要涉及user.dir和classpath的问题) http://viluo.com/post/52 遇到过很多人,甚至工作了几年的人,经常搞不清classpath,写了一个小程序, Eclipse能运行的程序,在DOS下就经常运行不了了,一般都是报这样的异常:java.lang.ClassNotFoundException。群里一旦有人问起,一帮人看都不看,一般都由以下几种说法:

  • 1. 设置了环境变量没? 这种几率还真小, 安装了jdk基本上毫无疑问都会设置环境变量.
  • 2. java文件的问题. 这个完全就是在胡扯了
  • 3. 先把java文件编译一下. 这个都在eclilise能运行了. 已经都编译好了,无需再多此一举。
  • 4. classliath路径有问题. 这个还真要稍微注意一下. 如果是jdk5.0及以后的版本, 那么classliath是完全可以不用设置的.现在大部分人应该都不会再使用jdk1.4或者更老的版本了吧. 所以这个概率也很小.
  • 5. 把环境变量重新设置一下. 这个???
  • 6. 这个问题太多了,百度去吧. 我想这么说的人估计自己也搞不清楚。很多人都习惯了Eclilise,怎么在DOS下编译和运行还真不熟悉.

首先说明一下, 上面的程序是带有包名的, 很好解决, 只要进入到项目的bin目录下, 运行java 报名.类名 即可.

下面的程序稍微有点区别.也涉及到相对路径的问题. 项目结构如下:

点击查看原图

IDEAndDosTest.java的代码如下(不要去揪程序的规范):

public static void main(String[] args) throws IOException {
FileInputStream in = new FileInputStream("src/iotest/test.txt");
int data;
while((data = in.read()) != -1) {
System.out.print(data + " ");
}
in.close();
}

很简单, 就是读取一个文件, 打印字节.

在Eclipse下运行当然是没有任何问题的.

在DOS下运行,进入到的目录是bin. 这时候运行发现, 报异常了:

Exception in thread "main" java.io.FileNotFoundException: src\iotest\test.txt (系统找不到指定的路径。)

异常也很明显, 文件路径不对, 没错, 路径当然不对了. 既然现在在bin目录下, 那么肯定相对应bin目录了.

这时需要搞清楚的就是两个系统属性"user.dir"和"java.class.path".

GetClassPathAndUserDir类的代码很简单:

System.out.println(System.getProperty("user.dir"));
System.out.println(System.getProperty("java.class.path"));

在Eclipse下运行, 结果是:

user.dir = 磁盘路径\Test
java.class.path = 磁盘路径\Test\bin

在DOS下运行(目录是bin), 结果是:

user.dir = 磁盘路径\Test\bin
java.class.path = . //  其实就是磁盘路径\Test\bin

从以上的结果其实已经可以看出结果了。问题主要集中在"user.dir".

其实解决上面的问题也很简单. 换种方式, DOS下进入项目的目录, 也就是现在在Test目录下. 运行

java -cp bin iotest.IDEAndDosTest

OK. 没有任何问题.

其实这个时候user.dir已经发生了变化. 其实GetClassPathAndUserDir这个类在不同的目录下运行结果是有些差异的

比如,现在在Test目录下运行,结果是:

user.dir = 磁盘路径\Test
java.class.path = bin

来个更好玩的, 直接在E盘下 运行,结果是:

user.dir = E:\
java.classpath = E:\Java\workspace\framework_workspace\Test\bin

可以发现java.classpath是不变的, 而user.dir是不断变化的.

不过要是在E盘下运行IDEAndDosTest,一样还是会报java.io.FileNotFoundException

从上面的结果也可以看出了"user.dir", 可以简单的说就是当前用户的目录.

把IDEAndDosTest.java稍作修改:

FileInputStream in = new FileInputStream("iotest/test.txt");  

这个时候在DOS下(bin目录), 运行时没有任何的, 在Eclipse运行会报 java.io.FileNotFoundException

不过只要我们稍作修改就能让其运行正常:

点击查看原图

做如下的修改运行也就完全没问题了.

既然这两种方式都这么麻烦, 那有没有什么好的替换方式呢 ?当然有了:

InputStream in = IDETest.class.getResourceAsStream("test.txt"); // 当前类的同目录下 

或者

InputStream in = IDEAndDosTest.class.getClassLoader().getResourceAsStream("iotest/test.txt"); // classpath目录  

现在不管你怎么运行, 都没有问题了(当然了你要指定好classpath)

其实也可以看出了如果要用绝对路径, 千万别涉及到"user.dir"的问题, 最好相对于你的classpath而言

原文链接:Eclipse和DOS下运行程序的差异(主要涉及user.dir和classpath的问题)

]]>
Mon, 22 Oct 2012 07:37:01 +0000 viLuo http://viluo.com/post/52
java编程中'为了性能'一些尽量做到的地方 http://viluo.com/post/46 viLuo:这里面有精华,有糟粕,需要大家根据自己的实际开发经验取舍了。

最近的机器内存又爆满了,出了新增机器内存外,还应该好好review一下我们的代码,有很多代码编写过于随意化,这些不好的习惯或对程序语言的不了解是应该好好打压打压了。

下面是参考网络资源和总结一些在java编程中尽可能做到的一些地方

1.尽量在合适的场合使用单例

使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面

第一,控制资源的使用,通过线程同步来控制资源的并发访问

第二,控制实例的产生,以达到节约资源的目的

第三,控制数据共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信

2.尽量避免随意使用静态变量

要知道,当某个对象被定义为stataic变量所引用,那么gc通常是不会回收这个对象所占有的内存,如

public class A {
static B b = new B();
}

此时静态变量b的生命周期与A类同步,如果A类不会卸载,那么b对象会常驻内存,直到程序终止。

3.尽量避免过多过常的创建java对象

尽量避免在经常调用的方法,循环中new对象,由于系统不仅要花费时间来创建对象,而且还要花时间对这些对象进行垃圾回收和处理,在我们可以控制的范围内,最大限度的重用对象,最好能用基本的数据类型或数组来替代对象。

4.尽量使用final修饰符

带 有final修饰符的类是不可派生的。在Java核心API中,有许多应用final的例子,例如java.lang.String。为String类指 定final防止了使用者覆盖length()方法。另外,如果一个类是final的,则该类所有方法都是final的。java编译器会寻找机会内联 (inline)所有的final方法(这和具体的编译器实现有关)。此举能够使性能平均提高50%。

5.尽量使用局部变量

调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度较快。其他变量,如静态变量,实例变量等,都在堆(Heap)中创建,速度较慢。

6.尽量处理好包装类型和基本类型两者的使用场所

虽然包装类型和基本类型在使用过程中是可以相互转换,但它们两者所产生的内存区域是完全不同的,基本类型数据产生和处理都在栈中处理,包装类型是对象,是在堆中产生实例。

在集合类对象,有对象方面需要的处理适用包装类型,其他的处理提倡使用基本类型。

7.慎用synchronized,尽量减小synchronize的方法

都知道,实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。synchronize方法被调用时,直接会把当前对象锁了,在方法执行完之前其他线程无法调用当前对象的其他方法。所以synchronize的方法尽量小,并且应尽量使用方法同步代替代码块同步。

8.尽量使用StringBuilder和StringBuffer进行字符串连接

这个就不多讲了

9.尽量不要使用finalize方法

实际上,将资源清理放在finalize方法中完成是非常不好的选择,由于GC的工作量很大,尤其是回收Young代内存时,大都会引起应用程序暂停,所以再选择使用finalize方法进行资源清理,会导致GC负担更大,程序运行效率更差。(其实不推荐用finalize方法的根本原因在于,JVM的规范并不保证何时执行该方法,所以用这个方法来释放资源很不合适,有可能造成长时间资源得不到释放。)

10.尽量使用基本数据类型代替对象

String str = "hello";

上面这种方式会创建一个“hello”字符串,而且JVM的字符缓存池还会缓存这个字符串;

String str = new String("hello");

此时程序除创建字符串外,str所引用的String对象底层还包含一个char[]数组,这个char[]数组依次存放了h,e,l,l,o

11.单线程应尽量使用HashMap, ArrayList

HashTable,Vector等使用了同步机制,降低了性能。

12.尽量合理的创建HashMap

当你要创建一个比较大的hashMap时,充分利用另一个构造函数

public HashMap(int initialCapacity, float loadFactor)

避免HashMap多次进行了hash重构,扩容是一件很耗费性能的事,在默认中initialCapacity只有16,而loadFactor是 0.75,需要多大的容量,你最好能准确的估计你所需要的最佳大小,同样的Hashtable,Vectors也是一样的道理。

13.尽量减少对变量的重复计算

for (int i = 0; i < list.size(); i++) {
}

应该改为

for (int i = 0, len = list.size(); i < len; i++) {
}

并且在循环中应该避免使用复杂的表达式,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快。

14.尽量避免不必要的创建

A a = new A();
if (i == 1) {
list.add(a);
}

应该改为

if (i == 1) {
A a = new A();
list.add(a);
}

15.尽量在finally块中释放资源

程序中使用到的资源应当被释放,以避免资源泄漏。这最好在finally块中去做。不管程序执行的结果如何,finally块总是会执行的,以确保资源的正确关闭。

16.尽量确定StringBuffer的容量

StringBuffer 的构造器会创建一个默认大小(通常是16)的字符数组。在使用中,如果超出这个大小,就会重新分配内存,创建一个更大的数组,并将原先的数组复制过来,再丢弃旧的数组。在大多数情况下,你可以在创建 StringBuffer的时候指定大小,这样就避免了在容量不够的时候自动增长,以提高性能。如:

StringBuffer buffer = new StringBuffer(1000);

17.尽量早释放无用对象的引用

大部分时,方法局部引用变量所引用的对象 会随着方法结束而变成垃圾,因此,大部分时候程序无需将局部,引用变量显式设为null。

例如:

public void test() {
Object obj = new Object();
//……
obj = null;
}

上面这个就没必要了,随着方法test()的执行完成,程序中obj引用变量的作用域就结束了。但是如果是改成下面:

public void test() {
Object obj = new Object();
//……
obj = null;
//执行耗时,耗内存操作;或调用耗时,耗内存的方法
//……
}

这时候就有必要将obj赋值为null,可以尽早的释放对Object对象的引用。

18.尽量避免使用二维数组

二维数据占用的内存空间比一维数组多得多,大概10倍以上。

19.尽量避免使用split

除 非是必须的,否则应该避免使用split,split由于支持正则表达式,所以效率比较低,如果是频繁的几十,几百万的调用将会耗费大量资源,如果确实需 要频繁的调用split,可以考虑使用apache的StringUtils.split(string,char),频繁split的可以缓存结果。

20.ArrayList & LinkedList

一 个是线性表,一个是链表,一句话,随机查询尽量使用ArrayList,ArrayList优于LinkedList,LinkedList还要移动指 针,添加删除的操作LinkedList优于ArrayList,ArrayList还要移动数据,不过这是理论性分析,事实未必如此,重要的是理解好2 者得数据结构,对症下药。

21.尽量使用System.arraycopy ()代替通过来循环复制数组

System.arraycopy() 要比通过循环来复制数组快的多

22.尽量缓存经常使用的对象

尽可能将经常使用的对象进行缓存,可以使用数组,或HashMap的容器来进行缓存,但这种方式可能导致系统占用过多的缓存,性能下降,推荐可以使用一些第三方的开源工具,如EhCache,Oscache进行缓存,他们基本都实现了FIFO/FLU等缓存算法。

23.尽量避免非常大的内存分配

有时候问题不是由当时的堆状态造成的,而是因为分配失败造成的。分配的内存块都必须是连续的,而随着堆越来越满,找到较大的连续块越来越困难。

24.慎用异常

当创建一个异常时,需要收集一个栈跟踪(stack track),这个栈跟踪用于描述异常是在何处创建的。构建这些栈跟踪时需要为运行时栈做一份快照,正是这一部分开销很大。当需要创建一个 Exception 时,JVM 不得不说:先别动,我想就您现在的样子存一份快照,所以暂时停止入栈和出栈操作。栈跟踪不只包含运行时栈中的一两个元素,而是包含这个栈中的每一个元素。

如果您创建一个 Exception ,就得付出代价。好在捕获异常开销不大,因此可以使用 try catch 将核心内容包起来。从技术上讲,您甚至可以随意地抛出异常,而不用花费很大的代价。招致性能损失的并不是 throw 操作——尽管在没有预先创建异常的情况下就抛出异常是有点不寻常。真正要花代价的是创建异常。幸运的是,好的编程习惯已教会我们,不应该不管三七二十一就 抛出异常。异常是为异常的情况而设计的,使用时也应该牢记这一原则。

原文链接:java编程中'为了性能'一些尽量做到的地方

]]>
Mon, 10 Sep 2012 06:59:27 +0000 viLuo http://viluo.com/post/46
编码最佳实践--小心!这只是冰山一角 http://viluo.com/post/44 本期的案例依然是来自实际项目,很寻常的代码,却意外遭遇传说中的Java"内存溢出"。

先来看看发生了什么,代码逻辑很简单,在请求的处理过程中:

1. 创建了一个ArrayList,然后往这个list里面放了一些数据,得到了一个size很大的list

List cdrInfoList = new ArrayList();
for(...) {
cdrInfoList.add(cdrInfo);
} 

2. 从这个list里面,取出一个size很小的sublist(我们忽略这里的业务逻辑)

cdrSublist = cdrInfoList.subList(fromIndex, toIndex)

3. 这个cdrSublist被作为value保存到一个常驻内存的Map中(同样我们忽略这里的业务逻辑)

cache.put(key, cdrSublist);

4. 请求处理结果,原有的list和其他数据被抛弃

正常情况下保存到cdrSublist不是太多,其内存消耗应该很小,但是实际上sig的同事们在用JMAP工具检查SIG的内存时,却发现这 里的subList()方法生成的RandomAccessSubList占用的内存高达1.6G! 完全不合符常理。

我们来细看subList()和RandomAccessSubList在这里都干了些什么:详细的代码实现追踪过程请见附录1,我们来看关键代码,类SubList的实现代码,忽略不相关的内容

这里我们可以清楚的看到SubList的实现原理:

1. 保存一个原始list对象的引用

class SubList<E> extends AbstractList<E> {
private AbstractList<E> l;
private int offset;
private int size;
SubList(AbstractList<E> list, int fromIndex, int toIndex) {
......
l = list;
offset = fromIndex;
size = toIndex - fromIndex;
}
}

2. 用offset和size来表明当前sublist的在原始list中的范围

为了让大家有一个感性的认识,我们用debug模式跑了一下测试代码,截图如下:

点击查看原图

可以看到生成的sublist对象内有一个名为"l"的属性,这是一个ArrayList对象,注意它的id和原有的list对象相同(图中都是id=33)。

这种实现方式主要是考虑运行时性能,可以比较一下普通的sublist实现:

public List<E> subList(int fromIndex, int toIndex) {
List<E> result = ...; // new a empty list
for (int i = fromIndex; i <= toIndex; i++) {
result.add(this.get(i));
}
return result;
}

这种实现需要创建新的list对象,然后添加所需内容,相比之下无论是内存消耗还是运行效率都不如前面SubList直接引用原始 list+记录偏差量的方式。

但是SubList的这种方式,会有一个极大的隐患:这个SubList的实例中,保存有原有list对象的引用——而且是强引用,这意味着, 只要sublist没有被jvm回收,那么这个原有list对象就不能gc,这个list中保存的所有对象也不能gc,即使这个list和其包含的对象已经没有其他任何引用。

这个就是Java世界中“内存泄露"的一个经典实例:某些被期望能被JVM回收的对象,却因为某个没有被觉察到的角落中"偷偷的"保留 了一个引用而躲过GC......在SIG的这个例子中,我们本来只想在内存中保留很少很少的一点点数据,被意外的将整个list和它包含的所 有对象都留下来。注意在截图中,list的size为100000,而sublist只是1而已,这就是我们标题中所说的"冰山一角"。

这里有一段实例代码,大家可以运行一下,很快就可以看到Java世界中名声显赫的OOM:

public class SublistTest {
public static void main(String[] args) {
List<List<Integer>> cache = new ArrayList<List<Integer>>();
try {
while (true) {
List<Integer> list = new ArrayList<Integer>();
for (int j = 0; j < 100000; j++) {
list.add(j);
}
List<Integer> sublist = list.subList(0, 1);
cache.add(sublist);
}
} finally {
System.out.println("cache size = " + cache.size());
}
}
} 

在我的测试中,打印结果为"cache size = 121",也就是说我的测试中121个list,每个list里面只放了一个Integer对象,就可以吃 掉所有内存,造成out of memory.

仔细的同学会发现,其实在sublist()方法的javadoc里面,已经对此有明确的说明,“The returned list is backed by this list” ,因此提醒大家在使用某个不熟悉的方法之前最好读一读Javadoc:

Returns a view of the portion of this list between fromIndex, inclusive, and toIndex, exclusive. (If fromIndex and toIndex are equal, the returned list is empty.) The returned list is backed by this list, so changes in the returned list are reflected in this list, and vice-versa. The returned list supports all of the optional list operations supported by this list.

同样的,在java中还有一个非常类似的案例,来自最常见的String类,它的substring()方法和split()方法,大家可以翻开jdk 的源码看到具体代码。原理和sublist()方法非常类似,就不重复解释了。

简单给出一段代码,演示一下substring()方法在类似情景下是如何OOM的:

public class SubstringTest {
public static void main(String[] args) {
List<String> cache = new ArrayList<String>();
try {
int i = 1;
while (true) {
String original = buildABigString(i++);
String substring = original.substring(0, 1);
cache.add(substring);
}
} finally {
System.out.println("cache size = " + cache.size());
}
}
private static String buildABigString(int count) {
long thistime = System.currentTimeMillis() + count;
StringBuilder buf = new StringBuilder(1024 * 100);
for (int i = 0; i < 10000; i++) {
buf.append(thistime);
}
return buf.toString();
}
}

这一次,我的测试用只用了994个长度为1的字符串,就"成功"达到了OOM。

最后谈一下怎么解决上面的问题,当然前提是我们有需要将得到的小的list或者string长时间存放在内存中:

1. 对于sublist()方法得到的list,貌似没有太好的办法,只能用最直接的方式:自己创建新的list,然后将需要的内容添加进去

2. 对于substring()/split()方法得到的string,可以用String类的构造函数new String(String original)来创建一个新的String,这 样会重新创建底层的char[]并复制需要的内容,不会造成"浪费"。

String类的构造函数new String(String original)是一个非常特别的构造函数,通常没有必要使用,正如这个函数的javadoc所言 :Unless an explicit copy of original is needed, use of this constructor is unnecessary since Strings are immutable. 除非明确需要原始字符串的拷贝,否则没有必要使用这个构造函数,因为String是不可变的。

但是对于前面的这种特殊场景(从超大字符串中substring()得到后再放置到常驻内存的结构中),new String(String original)就 可以将我们从这种潜在的内存溢出(或者浪费)中拯救出来。因此,当遇到同时处理大字符串+长时间放置内容在内存中时,请小心。

最后鸣谢Ray Tao同学为本次分享提供素材!

附录:List.sublist() 代码实现追踪

1. ArrayList的代码,继承自AbstractList,实现了RandomAccess接口

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable 

2. AbstractList类的subList()函数的代码,对于ArrayList,返回RandomAccessSubList的实例

public List<E> subList(int fromIndex, int toIndex) {
return (this instanceof RandomAccess ? new RandomAccessSubList<E>(this, fromIndex, toIndex)
: new SubList<E>(this, fromIndex, toIndex));
}

3. RandomAccessSubList的代码,继承自SubList

class RandomAccessSubList<E> extends SubList<E> implements RandomAccess {
RandomAccessSubList(AbstractList<E> list, int fromIndex, int toIndex) {
super(list, fromIndex, toIndex);
}
public List<E> subList(int fromIndex, int toIndex) {
return new RandomAccessSubList<E>(this, fromIndex, toIndex);
}
}

原文链接:编码最佳实践(5)--小心!这只是冰山一角

]]>
Mon, 10 Sep 2012 02:24:19 +0000 viLuo http://viluo.com/post/44
编码最佳实践--小心LinkedHashMap的get()方法 http://viluo.com/post/43 这是一个来自实际项目的例子,在这个案例中,有同事基于jdk中的LinkedHashMap设计了一个LRUCache,为了提高性能,使用了 ReentrantReadWriteLock 读写锁:写锁对应put()方法,而读锁对应get()方法,期望通过读写锁来实现并发get()。

代码实现如下:

private ReentrantReadWriteLock  lock = new ReentrantReadWriteLock ();
lruMap = new LinkedHashMap<K, V>(initialCapacity, loadFactor, true)
public V get(K key) {
lock.readLock().lock();
try {
return lruMap.get(key);
} finally {
lock.readLock().unlock();
}
}
public int entries() {
lock.readLock().lock();
try {
return lruMap.size();
} finally {
lock.readLock().unlock();
}
}
public void put(K key, V value) {
...
lock.writeLock().lock();
try {
...
lruMap.put(key, value);
...
} finally {
lock.writeLock().unlock();
}
}

在测试中发现问题,跑了几个小时系统就会hung up,无法接收http请求。在将把线程栈打印出来检查后,发现很多http的线程都在等读锁。有一个 runnable的线程hold了写锁,但一直停在LinkedHashMap.transfer方法里。线程栈信息如下:

"http-0.0.0.0-8081-178" daemon prio=3 tid=0x0000000004673000 nid=0x135 waiting on condition [0xfffffd7f5759c000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for  <0xfffffd7f7cc86928> (a java.util.concurrent.locks.ReentrantReadWriteLock$NonfairSync)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:156)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:811)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireShared(AbstractQueuedSynchronizer.java:941)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireShared(AbstractQueuedSynchronizer.java:1261)
at java.util.concurrent.locks.ReentrantReadWriteLock$ReadLock.lock(ReentrantReadWriteLock.java:594)
......
"http-0.0.0.0-8081-210" daemon prio=3 tid=0x0000000001422800 nid=0x155 runnable [0xfffffd7f5557c000]
java.lang.Thread.State: RUNNABLE
at java.util.LinkedHashMap.transfer(LinkedHashMap.java:234)
at java.util.HashMap.resize(HashMap.java:463)
at java.util.LinkedHashMap.addEntry(LinkedHashMap.java:414)
at java.util.HashMap.put(HashMap.java:385)
...... 

大家都知道HashMap不是线程安全的,因此如果HashMap在多线程并发下,需要加互斥锁,如果put()不加锁,就很容易破坏内部链表,造成get()死循 环,一直hung住。这里有一个来自淘宝的例子,有对此现象的详细分析:https://gist.github.com/1081908

但是在MSDP的这个例子中,由于ReentrantReadWriteLock 读写锁的存在,put()和get()方法是互斥,不会有上述读写竞争的问题。

Google后发现这是个普遍存在的问题,其根结在于LinkedHashMap的get()方法会改变数据链表。我们来看一下LinkedHashMap的实现代码:

public V get(Object key) {
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
void transfer(HashMap.Entry[] newTable) {
int newCapacity = newTable.length;
for (Entry<K,V> e = header.after; e != header; e = e.after) {
int index = indexFor(e.hash, newCapacity);
e.next = newTable[index];
newTable[index] = e;
}
}

前面LRUCache的代码中,是这样初始化LinkedHashMap的:

lruMap = new LinkedHashMap<K, V>(initialCapacity, loadFactor, true)

LinkedHashMap构造函数中的参数true表明LinkedHashMap按照访问的次序来排序。这里所谓的按照访问的次序来排序的含义是:当调用LinkedHashMap 的get(key)或者put(key, value)时,如果key在map中被包含,那么LinkedHashMap会将key对象的entry放在线性结构的最后。正是因为LinkedHashMap提 供按照访问的次序来排序的功能,所以它才需要改写HashMap的get(key)方法(HashMap不需要排序)和HashMap.Entry的recordAccess(HashMap)方法。注 意addBefore(lm.header)是将该entry放在header线性表的最后。(参考LinkedHashMap.Entry extends HashMap.Entry 比起HashMap.Entry多了before, after两个域,是双向的)

在上面的LRUCache中,为了提供性能,通过使用ReentrantReadWriteLock读写锁实现了并发get(),结果导致了并发问题。解决问题的方式很简单, 去掉读写锁,让put()/get()都使用普通互斥锁就可以了。当然,这样get()方法就无法实现并发读了,对性能有所影响。

总结,在使用LinkedHashMap时,请小心LinkedHashMap的get()方法。

原文链接:编码最佳实践(4)--小心LinkedHashMap的get()方法

]]>
Thu, 06 Sep 2012 11:17:46 +0000 viLuo http://viluo.com/post/43
编码最佳实践--尽量重用昂贵的初始化对象 http://viluo.com/post/42 这里将要讲述的是一系列的类似案例,都是在各个产品进行performance tuning时被发现的,非常具有普适性。可以说在日常开发中,有非常大的概率遇到相同或者类似的情形,因此需要对其保持警惕以便避免陷入类似的性能问题。

我们从JAXBContext这个对象开始,JAXBContext 是JAXB API的入口,典型的代码实现如下:

private void unmarshal() {
JAXBContext context = JAXBContext.newInstance(DirectoryConstants.JAXB_CONTEXT_CLASS);
Unmarshaller u = context.createUnmarshaller();
Object obj = u.unmarshal(...);
}

这个是标准使用流程了,首先初始化JAXBContext对象,通过JAXBContext对象创建Unmarshaller对象,然后使用Unmarshaller对象来进行unmarshal操作。

这个写法在功能实现上没有任何问题,但是如果一旦进行压力测试,就会暴露性能问题。JAXBContext对象的初始化是个资源消耗非常大的操作,我们可以通过threaddump进行分析,会发现很多工作线程都在执行JAXBContext.newInstance()这个方法,而不是我们期待的u.unmarshal()。而事实上JAXBContext对象的Sun实现是线程安全的,即容许多线程同时调用一个JAXBContext对象的createUnmarshaller(),因此完全没有必要为每个xml的 marshaling 和 unmarshalling 操作去初始化一次JAXBContext对象。可以参考这里的说明:https://jaxb.dev.java.net/faq/index.html#threadSafety

简单点说,这个一个初始化代价昂贵,却又可重复使用的对象。

因此,我们只需要初始化JAXBContext对象一次并保存起来,然后重复使用这个JAXBContext对象即可。保存JAXBContext对象的方式可以有很多种,比如cache / threadlocal,或者使用一个单例来维持这个对象。比如下面的代码示例:

private void unmarshal() {
JAXBContext context = JAXBContextHolder.get();
...
}
public class JAXBContextHolder {
private static final JAXBContext instance = JAXBContext.newInstance(DirectoryConstants.JAXB_CONTEXT_CLASS);
public static JAXBContext get() {
return instance;
}
}

在实际项目中, 通过上面的简单改进之后,我们当时得到了一个非常巨大的回报:TPS (transactions per second 每秒事务处理量)直接*3 !!

这个案例从技术上讲非常的简单,道理很浅显,相信每个人都能轻松理解。或者说,这是一个“知道了就简单,不知道就容易犯错而不自知”的地方,因此依然有些东西需要注意:

(1) 有哪些对象有类似的特性

目前发现的类似对象有

1. 刚刚上面讲到的JAXB API中的 JAXBContext 对象

2. SOAP API中的javax.xml.soap.SOAPFactory

3. CFX client

通常情况下,在使用各种api或者工具类库时,如果发现调用代码中有类似的初始化语句,都应该稍加注意(除非明确当前代码对性能完全没有要求),可以去查一下这个类对象的javdoc或者直接看源码,如果发现满足上面所说的特性,则应该考虑进行上述的性能优化。

根据经验,类似的初始化对象通常的命名规则都是***Context/***Factory之类,或者***client,遇到类似名字时需要提高警惕。

(2)假设问题已经存在,如果才能在performance tuning中迅速发现问题的代码?

通常的办法就是用thread dump,一般连续dump个3-5次,然后通过分析thread dump信息 (推荐使用eclipse插件 lockness),看当前请求的线程(通常是一个线程池)都在干什么。一般初始化昂贵都昂贵在类似文件IO操作或者加锁之类的地方,很容易在thread dump中被发现。

原文链接:编码最佳实践(3)--尽量重用昂贵的初始化对象

]]>
Thu, 06 Sep 2012 10:56:46 +0000 viLuo http://viluo.com/post/42
编码最佳实践--推荐使用concurrent包中的Atomic类 http://viluo.com/post/41 这是一个真实案例,曾经惹出硕大风波,故事的起因却很简单,就是需要实现一个简单的计数器,每次取值然后加1,于是就有了下面这段代码:

private int counter = 0;
public int getCount(){
return counter++;
}

这个计数器被用于生成一个sessionId,这个sessionID用于和外部计费系统交互,这个sessionId理所当然的要求保证全局唯一而不重复。但是很遗憾,上面的代码最终被发现会产生相同的id,因此会造成一些请求莫名其妙的报错.....更痛苦的是,上面这段代码是一个来自其他部门开发的工具类,我们当时只是拿了它的jar包来调用,没有源码,更没有想这里面会有如此低级而可怕的错误。

由于重复的sessionId,造成有个别请求失败,虽然出现概率极低,经常跑一天测试都不见得能重现一次。因为是和计费相关,因此哪怕是再低的概率出错,也不得不要求解决。实际情况是,项目开发到最后阶段,都开始做发布前最后的稳定性测试了,在7*24小时的连续测试中,这个问题往往在测试开始几天后才重现,将当时负责trouble shooting的同事折腾的很惨......经过反复的查找,终于有人怀疑到这里,反编译了那个jar包,才看到上面这段出问题的代码。

这个低级的错误,源于一个java的基本知识:

++操作,无论是i++还是++i,都不是原子操作!

而一个非原子操作,在多线程并发下会有线程安全的问题:这里稍微解释一下,上面的"++"操作符,从原理上讲它其实包含以下:计算加1之后的新值,然后将这个新值赋值给原变量,返回原值。类似于下面的代码

private int counter = 0;
public int getCount() {
int result = counter;
int newValue = counter + 1; // 1. 计算新值
counter = newValue; // 2. 将新值赋值给原变量
return result;
}

多线程并发时,如果两个线程同时调用getCount()方法,则他们可能得到相同的counter值。为了保证安全,一个最简单的方法就是在getCount()方法上做同步:

private int counter = 0;
public synchronized int getCount() {
return counter++;
}

这样就可以避免因++操作符的非原子性而造成的并发危险。

我们在这个案例基础上稍微再扩展一下,如果这里的操作是原子操作,就可以不用同步而安全的并发访问吗?我们将这个代码稍作修改:

private int something = 0;
public int getSomething() {
return something;
}
public void setSomething(int something) {
this.something = something;
}

假设有多线程同时并发访问getSomething()和setSomething()方法,那么当一个线程通过调用setSomething()方法设置一个新的值时,其他调用getSomething()的方法是不是立即可以读到这个新值呢?这里的"this.something = something;" 是一个对int 类型的赋值,按照java 语言规范,对int的赋值是原子操作,这里不存在上面案例中的非原子操作的隐患。

但是这里还是有一个重要问题,称为"内存可见性"。这里涉及到java内存模型的一系列知识,限于篇幅,不详尽讲述,不清楚这些知识点的可以自己翻翻资料,最简单的办法就是google一下这两个关键词"java 内存模型", "java 内存可见性"。或者,可以参考这个帖子"java线程安全总结", http://www.iteye.com/topic/806990。

解决这里的"内存可见性"问题的方式有两个,一个是继续使用 synchronized 关键字,代码如下

private int something = 0;
public synchronized int getSomething() {
return something;
}
public synchronized void setSomething(int something) {
this.something = something;
}

另一个是使用volatile 关键字,

private volatile int something = 0;
public int getSomething() {
return something;
}
public void setSomething(int something) {
this.something = something;
}

使用volatile 关键字的方案,在性能上要好很多,因为volatile是一个轻量级的同步,只能保证多线程的内存可见性,不能保证多线程的执行有序性。因此开销远比synchronized要小。

让我们再回到开始的案例,因为我们采用直接在 getCount() 方法前加synchronized 的修改方式,因此不仅仅避免了非原子性操作带来的多线程的执行有序性问题,也"顺带"解决了内存可见性问题。

OK,现在可以继续了,前面讲到可以通过在 getCount() 方法前加synchronized 的方式来解决问题,但是其实还有更方便的方式,可以使用jdk 5.0之后引入的concurrent包中提供的原子类,java.util.concurrent.atomic.Atomic***,如AtomicInteger,AtomicLong等。

private AtomicInteger counter = new AtomicInteger(0);
public int getCount() {
return counter.incrementAndGet();
}

Atomic类不仅仅提供了对数据操作的线程安全保证,而且提供了一系列的语义清晰的方法如incrementAndGet(),getAndIncrement,addAndGet(),getAndAdd(),使用方便。更重要的是,Atomic类不是一个简单的同步封装,其内部实现不是简单的使用synchronized,而是一个更为高效的方式CAS (compare and swap) + volatile,从而避免了synchronized的高开销,执行效率大为提升。限于篇幅,关于“CAS”原理就不在这里讲诉。

因此,出于性能考虑,强烈建议尽量使用Atomic类,而不要去写基于synchronized关键字的代码实现。

最后总结一下,在这个帖子中我们讲诉了一下几个问题:

1. ++操作不是原子操作

2. 非原子操作有线程安全问题

3. 并发下的内存可见性

4. Atomic类通过CAS + volatile可以比synchronized做的更高效,推荐使用

原文链接:编码最佳实践(2)--推荐使用concurrent包中的Atomic类

]]>
Thu, 06 Sep 2012 10:24:02 +0000 viLuo http://viluo.com/post/41
编码最佳实践--小心"数据溢出" http://viluo.com/post/40 最近在公司内部做了一些收集和整理的工作,关于trouble shooting和performace tuning 中遇到并解决的典型问题,做了一些内部分享。我整理了一下,准备陆续放上来分享给大家。

这些问题,单个看每个问题都不算复杂或高深,但是都是在实际项目开发中出现并一度造成困扰的,而且带有一定的普适性,具体表现为不知道这些问题的同学很容易在日常开发中中招。因此我们开了一个专题,叫做编码最佳实践,似乎名字起的有点大......

先来看看第一个,如何做compare。

先看案例,问题的表现很简单,就是在排序后的结果中有时会很惊讶的发现排序错误。我们不纠结于具体的错误表现细节和排查的过程,直接来看最终被检查出问题所在的代码,这是一个很普通的Comparator接口实现:

private static class keyOrderComparator implements Comparator<Persistent> {
public int compare(Persistent p1, Persistent p2) {
return (int) (p1.getId().getKey() - p2.getId().getKey());
}
}

代码中的比较逻辑很简单,比较Persistent对象的id的key值就OK,实现中将两个key简单做一次减法运算,将结果作为compare()方法的返回值。如果p1的key大于 p2的key,则"p1.getId().getKey() - p2.getId().getKey()"的结果大于0,而compareTo()方法返回一个大于0的整数表示比较结果为"参数p1大于参数p2"。

但麻烦出现在key的数据类型上,这是一个long类型,因此减法运算的结果也是一个long,为了满足compare()方法要求返回int的要求,在return前做了一次强制类型转换。而问题就出现在这里:从long到int的强制类型转换是有风险的,如果long的数字超过了int所能表示的范围[Integer.Min_VALUE, Integer.Max_VALUE],则会发生"数据溢出"(data overflow)。

我们可以试着执行以下代码 System.out.println((int) (30000000000L - 1)); , 会发现它的结果是一个"-64771073",和意想中的29999999999完全不同,重要的是符号变了:从一个正数变成了负数!这直接导致了compare()方法得出了一个令人惊讶的比较结果:30000000000 比 1 小!

解决方式也很简单,不要做强制类型转换:

private static class keyOrderComparator implements Comparator<Persistent> {
public int compare(Persistent p1, Persistent p2) {
long key1 = p1.getId().getKey();
long key2 = p2.getId().getKey();
if (key1 == key2) {
return 0;
} else {
return key1 > key2 ? 1 : -1;
}
}
}

在这个简单案例当中,有一个比较明显的地方可以帮助我们发现问题,就是(int)这个强制类型转换,稍有经验的同学就会第一时间反应过来:long到int是有数据溢出风险的。那如果我们将这个案例稍微修改一下,假设p1.getId().getKey()返回的就是普通的int,结果会如何:

private static class keyOrderComparator implements Comparator<Persistent> {
public int compare(Persistent p1, Persistent p2) {
return p1.getId().getKey() - p2.getId().getKey();
}
}

这段代码貌似就没有问题啦?呵呵,让我们把这段代码的业务含义去掉,退化为一个普通的int比较:

private static class IntegerOrderComparator implements Comparator<Integer> {
public int compare(Integer p1, Integer p2) {
return p1 - p2;
}
}

这下应该能看出来了吧?如果p1=2147483647即Integer.MAX_VALUE,而p2=-1,则p1 - p2 = Integer.MAX_VALUE - (-1) = -2147483648 ! IntegerOrderComparator 会给出一个令人目瞪口呆的比较结果:2147483647 比 -1 小!类似的,在 p1= -2147483648 (Integer.MIN_VALUE), p2 = 1时,IntegerOrderComparator 同样会给出类似荒唐的比较结果:-2147483648 比 1 大!

导致错误发生的原因依然是"数据溢出"!和前面long到int的强制类型转换不同,这次数据溢出发生在int与int之间做数学运算。

我们来看问题发生在哪里:"int - int"这样的简单的运算,在我们的数学常识中,两个整型相减结果肯定还是整型,一个正数减一个负数结果肯定是正数,一个负数减一个正数结果肯定是负数......但是这里的数学常识中所谓的"整型",其取值范围可以是无穷小到无穷大,而java语言(其他语言也是类似)中的int,只能表示[Integer.Min_VALUE, Integer.Max_VALUE],即[-2147483648, 2147483647]这样一个范围。一旦运算的结果超过这个范围,就会发生数据溢出。

因此,在java中,类似"int + int", "int - int", "int * int" 这样的运算结果,用int来表示是不安全的,需要使用更大的数据类型比如long来。上面的代码可以修订为:

private static class IntegerOrderComparator implements Comparator<Integer> {
public int compare(Integer p1, Integer p2) {
long diff = p1 - p2;
return diff == 0 ? 0 : (diff > 0 : 1 : -1);
}
}

但是这种compare的写法,遇到数据范围更大的数据类型时依然有麻烦,因为总是要找到一个比它数据范围还要大的数据类型来承载这个diff的结果。因此还是推荐使用前面的比较方法:不做减法,直接做等于和大于/小于的比较。

最后总结一下这个案例:

1. compare方法实现时,尽量不要用"return p1 - p2"这种写法

2. 但凡进行数值运算时,都要小心考虑数据溢出的风险

3. 做trouble shooting时,要留意可能的数据溢出

PS: 有没有犯同样错误而不自知的同学?请自觉的留个爪子,呵呵

原文链接:编码最佳实践(1)--小心"数据溢出"

]]>
Thu, 06 Sep 2012 10:11:33 +0000 viLuo http://viluo.com/post/40
Java编程中写出好代码的建议 http://viluo.com/post/22 最近在做应用的性能优化,在review代码的过程中积累了一些规则和经验。做到这些规则的目的很简单,就是写出“优美”的代码来。

1、注释尽可能全面

对于方法的注释应该包含详细的入参和结果说明,有异常抛出的情况也要详细叙述;类的注释应该包含类的功能说明、作者和修改者。

2、多次使用的相同变量最好归纳成常量

多处使用的相同值的变量应该尽量归纳为一个常量,方便日后的维护。

3、尽量少的在循环中执行方法调用

尽量在循环中少做一些可避免的方法调用,这样可以节省方法栈的创建。例如:

for(int i=0;i<list.size();i++){
System.out.println(i);
}

可以修改为:

for(int i=0,size=list.size();i<size;i++){
System.out.println(i);
}

4、常量的定义可以放到接口中

在Java中,接口里只允许存在常量,因此把常量放到接口中声明就可以省去publicstatic final这几个关键词。

5、ArrayList和LinkedList的选择

这个问题比较常见。通常程序员最好能够对list的使用场景做出评估,然后根据特性作出选择。ArrayList底层是使用数组实现的,因此随机读取数据会比LinkedList快很多,而LinkedList是使用链表实现的,新增和删除数据的速度比ArrayList快不少。

6、String,StringBuffer和StringBuilder

这个问题也比较常见。在进行字符串拼接处理的时候,String通常会产生多个对象,而且将多个值缓存到常量池中。例如:

String a="a";
String b="b";
a=a+b;

这种情况下jvm会产生"a","b","ab"三个对象。而且字符串拼接的性能也很低。因此通常需要做字符串处理的时候尽量采用StringBuffer和StringBuilder来。

7、包装类和基本类型的选择

在代码中,如果可以使用基本数据类型来做局部变量类型的话尽量使用基本数据类型,因为基本类型的变量是存放在栈中的,包装类的变量是在堆中,栈的操作速度比堆快很多。

8、尽早的将不再使用的变量引用赋给null

这样做可以帮助jvm更快的进行内存回收。当然很多人其实对这种做法并不感冒。

9、在finally块中对资源进行释放

典型的场景是使用io流的时候,不论是否出现异常最后都应该在finally中对流进行关闭。

10、在HashMap中使用一个Object作为key时要注意如何区分Object是否相同

在jdk的HashMap实现中,判断两个Object类型的key是否相同的标准是hashcode是否相同和equals方法的返回值。如果业务上需要对两个数据相同的内存对象当作不同的key存储到hashmap中就要对hashcode和equals方法进行覆盖。

]]>
Thu, 30 Aug 2012 07:08:07 +0000 viLuo http://viluo.com/post/22