程序中的幽灵错误_基础
# 程序中的幽灵错误_基础
修复程序BUG应该说是基本的日常工作之一。作为Java程序员,也许你经常会被抛出的一大堆的异常堆栈所困扰,因为这可能预示着你又有工作可做了。但我这里想说的是,如果程序出错,你看到了异常堆栈,那你应该感到格外的高兴,因为这也意味着你极有可能可以在两分钟内修复这个问题(当然,并不是所有的异常都是错误)。最可怕的情况是:系统没有任何异常表现,没有日志,也没有堆栈,但是却给出了一个错误的执行结果,这种情况下,才真会让你抓狂。
# 无提示的错误
int v1=1073741827;
int v2=1431655768;
System.out.println("v1="+v1);
System.out.println("v2="+v2);
int ave=(v1+v2)/2;
System.out.println("ave="+ave);
2
3
4
5
6
结果竟然是ave=-894784850
。这种不合理的结果,你稍微一思考其实就能明白原因,v1+v2的结果过大了导致了int的溢出。
这种错误就是明显的系统没有报错,但结果是错的。或许你觉得这种错误还好,但是如果这个问题发生在一个复杂的系统内部呢?在复杂的业务逻辑的掩盖下,再加上程序没有任何日志或异常,你是否还能这么轻易的发现问题呢?藏木于林是很厉害的。
这种幽灵错误——没有任何错误信息或异常,在并发的情况下,出现的频率更高。
# 并发下的ArrayList
public class ArrayListMultiThread {
static ArrayList<Integer> al = new ArrayList<Integer>(10);
public static class AddThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
al.add(i);
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(new AddThread());
Thread t2=new Thread(new AddThread());
t1.start();
t2.start();
t1.join();t2.join();
System.out.println(al.size());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
因为ArrayList是线程不安全的容器,所以上述代码可能会出现以下几种结果:
- 结果正常,ArrayList最终大小为 2000000
- 最终结果小于,2000000,因为两个线程同时对一个位置进行赋值,并且这种情况没有任何错误提示,也是一个幽灵错误,同时不一定能复现。
- 程序抛出异常
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 22
at java.util.ArrayList.add(ArrayList.java:441)
at geym.conc.ch2.notsafe.ArrayListMultiThread$AddThread.run
(ArrayListMultiThread.java:12)
at java.lang.Thread.run(Thread.java:724)
1000015
2
3
4
5
6
这种原因是因为ArrayList在扩容的过程中,内部一致性被破坏,另一个线程访问到了不一致的内部状态,导致出现越界问题。
这种情况在线程不安全的集合中常常出现,HashMap就是其中之一。当然也有解决方案,其中最简单的就是使用ConcurrentHashMap代替HashMap。
# 错误的加锁
使用synchronized
固然能帮助我们解决这种幽灵问题,但如果如果我们疏忽了,这种情况依然会发生,下面一种情况,是包装类型导致的,也是容易发生的:
public class Demo8 implements Runnable{
public static Integer i = 0;
static Demo8 badDemo = new Demo8();
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
synchronized(i){
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(badDemo);
Thread t2 = new Thread(badDemo);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
实际结果比20000要小,使用了锁,但似乎没有效果。哪里出了问题呢?
要解释这个问题,得从Integer说起。在Java中,Integer属于不变对象。也就是对象一旦被创建,就不可能被修改。也就是说,如果你有一个Integer代表1,那么它就永远表示1,你不可能修改Integer的值,使它为2。那如果你需要2怎么办呢?也很简单,新建一个Integer,并让它表示2即可。
在上述代码中,
i++;
// 实际变成了如下形式
i= Integer.valueOf(i.intValue()+1);
2
3
i++的本质是,创建一个新的Integer对象,并将它的引用赋值给i.
由于在多个线程间,并不一定能够看到同一个i对象(因为i对象一直在变),因此,两个线程每次加锁可能都加在了不同的对象实例上,从而导致对临界区代码控制出现问题。
既然问题找到了,那么也好解决,将锁的对象修改即可
synchronized(badDemo){
}
2
3