Java高并发学习(7)
程序中的幽灵:隐蔽的错误
作为一名软件开发人员,修复BUG应该是基本的日常工作之一。作为java程序员,也许你经常会被抛出的一大堆异常堆栈所困扰。因为这可能预示着你又有工作可做了。但是我在这里想说的是,如果程序出错,你看到了异常堆栈,那你应该感到格外的高兴。最可怕的情况是:系统没有任何异常表现,没有日志,也没有堆栈。但是却给出了个错误的执行结果,这种情况才让你抓狂。不幸的是,错误的使用并行,会非常容易产生这类问题。接下来列举几个在多线程程序中容易忽略的“幽灵”。
1. 并发下的ArrayList
我们都知道,ArrayList是一个线程不安全的容器。如果在多线程中使用ArrayList,可能会导致程序出错。那究可能引起哪些问题呢?试看下面的代码:
import java.util.ArrayList;
public class fist{
static ArrayList<Integer> al = new ArrayList<Integer>(10);
public static class MyThread extends Thread{
@Override
public void run(){
for(int i = 0;i<100000;i++){
al.add(i);
}
}
}
public static void main(String args[]) throws InterruptedException {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(al.size());
}
}
输出结果:
上述代码中,t1和t2两个线程同时向一个ArrayList容器添加元素。他们各自添加100000个元素,因此我们期望最后可以有200000个元素在ArrayList里。但如果你执行这段代码,你可能会得到3种结果。
(1) .程序正常结束,ArrayList的最终大小确实是200000。这说明并行程序即使有问题,但是一次的运行并不能将其显示出来。
(2) .程序抛出异常,并且ArrayList的大小小于200000。上面的输出结果就是这个情况。
程序抛出异常是因为,ArrayList在扩容的过程中,内部一致性被破坏,但由于没有锁的保护,另外的一个线程访问到了不一致多的内部状态,导致出现越界问题。ArrayList的大小小于200000,是因为,多线程的访问冲突,使得保存容器大小的变量被多线程不正常的访问,同时两个线程也同时对ArrayList中的同一个位置进行赋值导致的。
那么如何解决这个问题呢?其实很简单,使用线程安全的vector代替ArrayList就可以了。(小结:ArrayList是线程不安全容器,vector是线程安全容器)
2. 并发下诡异的HashMap。
Hashmap同样不是线程安全的。当你使用多线程访问Hashmap时,也可能会遇到意想不到的错误。不过和ArrayList不同,Hashmap的问题似乎更加诡异。
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class fist{
static Map<String, String> map = new HashMap<String,String>();
public static class MyThread extends Thread{
int start = 0;
public MyThread(int start){
this.start = start;
}
@Override
public void run(){
for(int i = start;i<10000;i+=2){
map.put(Integer.toString(i), Integer.toBinaryString(i));
}
}
}
public static void main(String args[]) throws InterruptedException {
MyThread t1 = new MyThread(0);
MyThread t2 = new MyThread(1);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(map.size());
}
}
上述代码使用t1和t2两个线程同时对HashMap进行put()操作。如果一切正常,我们期待的map.size()就是100000。但实际上,你可能会得到以下三种情况。
(1) .程序正常结束,结果也是符合预期的。HashMap的大小为100000.
(2) .程序正常结束,但结果不符合预期,而是一个小于100000的数字。
(3) .程序永远无法结束。
对于前两种可能,和ArrayList的情况非常相似,因此,不必解释。而对于第三种情况,如果是第一次看到,我想大家一定会觉得惊讶,因为看似非常正常的程序,这么可能结束不了呢?
这属于HashMap内部构造问题,会造成访问它的两个线程死锁,这个死循环一旦发生,着实可以让你郁闷一把。但这个问题在JDK8中已经不存在了。由于JDK8对HashMap的内部实现做了大规模的调整,因此规避了这个问题。但,即使这样,贸然在多线程环境下使用HashMap依然会导致内部数据不一致。最简单的解决方案是使用ConcurrenHashMap代替HashMap。
3. 总结
在多线程环境中,若要使用java提供的容器,先查看是否为线程安全容器,这很重要。