Java高并发学习(七)

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());
	}
}

输出结果:


   上述代码中,t1t2两个线程同时向一个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());
	}
}

   上述代码使用t1t2两个线程同时对HashMap进行put()操作。如果一切正常,我们期待的map.size()就是100000。但实际上,你可能会得到以下三种情况。

        (1) .程序正常结束,结果也是符合预期的。HashMap的大小为100000.

        (2) .程序正常结束,但结果不符合预期,而是一个小于100000的数字。

        (3) .程序永远无法结束

   对于前两种可能,和ArrayList的情况非常相似,因此,不必解释。而对于第三种情况,如果是第一次看到,我想大家一定会觉得惊讶,因为看似非常正常的程序,这么可能结束不了呢?

   这属于HashMap内部构造问题,会造成访问它的两个线程死锁,这个死循环一旦发生,着实可以让你郁闷一把。但这个问题在JDK8中已经不存在了。由于JDK8HashMap的内部实现做了大规模的调整,因此规避了这个问题。但,即使这样,贸然在多线程环境下使用HashMap依然会导致内部数据不一致。最简单的解决方案是使用ConcurrenHashMap代替HashMap

3.  总结

   在多线程环境中,若要使用java提供的容器,先查看是否为线程安全容器,这很重要



猜你喜欢

转载自blog.csdn.net/huxiny/article/details/79792723