学习Java其实好久了,需要的时候也能写出个可以跑得程序,但是对其中很多机制没有仔细研究过。最近觉得该深入学习一下,于是抱着Java Core I 开始看了。
    前段时间看了Java集合,目前学的是多线程。发现多线程里确实有很多东西值得好好研究。首先同步就是一个不小的问题。今天主要对集合的同步问题进行了一下小小的测试,出现了几个我自己没弄明白的问题,希望版上高手给予解答。    多线程的同步主要是对多个线程共享一个资源时的访问控制,避免出现“乱套”现象。Java提供多种机制进行同步控制,比如锁和条件对象,synchronized关键字等。而集合经常作为这种资源被多个线程共享,而且集合框架中本身也对多线程同步进行过考虑,比如有Vector,HashTable等类。于是自己写了个简单的程序,进行下测试,到底哪种方法能有效控制集合在多线程并发中的同步。
    设置非常简单的一个场景,有一个Student集合,对该集合采用迭代器进行遍历之后,又对其添加了一个元素。由于添加元素是改变集合结构的操作,所以集合如果在迭代器构造之后发生改变,就会抛出ConcurrentModificationException异常。
    Student类代码,很简单:class Student{
public Student(String name, int age){
this.name = name;
this.age = age;
}
String name;
int age;

public String  toString(){
return "I am "+name+" , "+age+" years old.";
}
}    1. 创建一个修改Collection的线程,实现Runnable接口。不采取任何同步措施。class ModifyCollectionTask implements Runnable{
public ModifyCollectionTask(Collection<Student> slist){
this.slist = slist;
}
public void run(){
// 遍历学生列表,
for(Student s : slist){
System.out.println(Thread.currentThread().getName());
System.out.println(s);
}
// 向学生列表添加元素
slist.add(new Student("Katie", 30));
}
Collection<Student> slist;
}
在Main函数里启动100个线程,public class SyncCollection {
public static void main(String[] args) {

Collection<Student> slist = new ArrayList<Student>();
slist.add(new Student("AAA",10));
slist.add(new Student("BBB",12));
slist.add(new Student("CCC",14));
slist.add(new Student("DDD",16));
slist.add(new Student("EEE",18));

for(int i=0;i<100;i++){
new Thread(new ModifyCollectionTask(slist)).start();
}
}很明显,没有同步控制,那么很快就抛出了ConcurrentModificationException异常    2. 由于Vector类是线程安全的动态数组,所以,将集合实现改为Vector,在线程run方法中没做任何修改 // 使用Vector
List<Student> sVector = new Vector<Student>();
sVector.add(new Student("AAA",10));
sVector.add(new Student("BBB",12));
sVector.add(new Student("CCC",14));
sVector.add(new Student("DDD",16));
sVector.add(new Student("EEE",18));

for(int i=0;i<100;i++){
new Thread(new ModifyCollectionTask(sVector)).start();
}结果还是发生了ConcurrentModificationException异常。这让我有点怀疑Vector的同步机制。    3. 使用Collections工具类中的同步包装方法,将线程不安全ArrayList进行包装,而线程实现方法没有改动 // 使用Collections工具类中的同步包装器
List<Student> slist2 = Collections.synchronizedList(new ArrayList<Student>());
slist2.add(new Student("AAA",10));
slist2.add(new Student("BBB",12));
slist2.add(new Student("CCC",14));
slist2.add(new Student("DDD",16));
slist2.add(new Student("EEE",18));

for(int i=0;i<100;i++){
new Thread(new ModifyCollectionTask(slist2)).start();
}    结果还是发生了异常,不明白    4. 下面使用synchronized关键字进行同步控制,对线程实现代码进行了修改class syncModifyListTask implements Runnable{
public syncModifyListTask(Collection<Student> slist){
this.slist = slist;
}
public void run(){
synchronized(slist){
// 遍历学生列表
for(Student s : slist){
System.out.println(Thread.currentThread().getName());
System.out.println(s);
}
// 向学生列表添加元素
slist.add(new Student("Katie", 30));
}
}
Collection<Student> slist;
}由于有了synchronized关键字对代码片段进行了保护,所以没有出现异常    5. 使用java.util.concurrent包中的高效的同步集合, ConcurrentLinkedQueue,线程实现代码还是用ModifyCollectionTask Collection<Student> concurrentCollection = new ConcurrentLinkedQueue<Student>();
concurrentCollection.add(new Student("AAA",10));
concurrentCollection.add(new Student("BBB",12));
concurrentCollection.add(new Student("CCC",14));
concurrentCollection.add(new Student("DDD",16));
concurrentCollection.add(new Student("EEE",18)); for(int i=0;i<100;i++){
new Thread(new ModifyCollectionTask(concurrentCollection)).start();
}结果也没有出现异常,证明高效的同步集合还是很给力的!    将上述的各种集合同步方法进行一遍测试之后,发现Vector和Collections中的同步包装方法都不能保证同步,我就很纳闷儿,是我的使用方法出现了问题,还是这两种集合同步本身就做的不好?    希望版上的高手给予解答,谢谢!!!

解决方案 »

  1.   

    哦,抛异常就是检测出了不同步,然后没有保护机制?那用了synchronized关键字就没有抛异常啊
      

  2.   

    因为这段代码中含有 foreach 循环,对于 List 来说 foreach 循环编译之后会变成类似于这样的代码:for(Iterator<E> i = list.iterator(); i.hasNext(); );这里使用到了迭代器,对于 Vector 来说,iterator 方法是使用父类 AbstractList 中的,迭代器并没有同步,因此是线程不安全的,在迭代的时候如果集合数量有增加或者减少都会即可感知到,并抛出 ConcurrentModificationException 异常。对于 Collections 的 synchronizedCollection 或者 synchronizedList 方法包装过的集合来说,对于 iterator() 方法是需要用户手工进行同步的,详见 Collections.synchronizedCollection 方法的 API 文档。不单单是普通 List 集合有这个问题,普通的 Map 集合也会出这样的情况。而对于 java.util.concurrent 中的任何集合都是经过精心设计的,无论迭代、增加、删除都是线程而全的,而且在迭代时不会抛了 ConcurrentModificationException 的异常。PS:“HashTable”你写错了,J2SE 中只有 Hashtable,呵呵。
      

  3.   

    果子说得对,
    不是Vector的问题,而是,楼主用法上面的问题。
    JDK5的新特性,使用foreach语句,会转换成迭代器的样式进行循环迭代。
      

  4.   

    火龙果真是大虾啊,每个问题都解释的这么清晰,连我写错HashTable都看出来了,Java大牛~~~
    看来把问题贴出来就有机会得到大虾的帮忙,问题得到解决挺开心的。
    谢谢大虾~~
      

  5.   

    呵呵,楼主太客气了。关于 Hashtable 在《Java 核心技术·下卷》讲集合的那一章中的“遗留下来的集合”一节第一小节介绍 Hashtable 时有个注意特意提过这事,呵呵。
      

  6.   

    哦,火龙果大虾,您看的是以前版本的《Java核心技术》吧,现在讲集合的在第一卷中,不过确实有一节是讲“遗留下来的集合”,只是我没有仔细研究
    我回头好好看看~~
      

  7.   

    这个ConcurrentModificationException是由于在遍历集合时候,又同时对集合进行了修改导致的,跟同步没有什么关系的。简单的说,你的run方法这么写:
     for(Student s : slist){
             // 向学生列表添加元素
                slist.add(new Student("Katie", 30));
                System.out.println(Thread.currentThread().getName());
                System.out.println(s);
            }      不用多个线程,一个它也会出现ConcurrentModificationException异常的。加了synchronized关键字以后,保证了每次先遍历完毕然后再add,所以没有这种情况出现,就没有ConcurrentModificationException异常了。java.util.concurrent的确是一个值得学习的包PS:向火龙果大哥致敬,哈哈,java能力很深
      

  8.   

    我也有这个问题,下面代码
    package ycq.udplistener;
    import java.net.DatagramPacket;
    import java.util.Vector;
    public  class  Queue {  Vector<DatagramPacket> vector=new Vector<DatagramPacket> ();

    /**
     * 向队列里添加对象
     * */
    public  synchronized  void  addElement(DatagramPacket  x){  vector.addElement(x); 
    notify(); }   
      
    /**
     * 取出队列中的第一个对象
     * */
    public  synchronized  DatagramPacket  get()  { 

    if(vector.IsEmpty())//如果为空则等待
    {
    try {
    wait();
    } catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    }
    }
    return  vector.remove(0);  }   
      } 
    用两个线程,一个插入,一个读取,怎么读取出来的有很多重复的啊,这里高手多,希望能帮帮我
      

  9.   


    你这是想要实现队列吧,
    1. 在Java中你要使用队列的话,没有必要自己实现,LinkedList就是一个很好的队列实现。
    2. 你要是在多线程中使用队列的话,最好使用java.util.concurrent.ConcurrentLinkedQueue,这是高效的线程安全队列实现,在多线程访问中不会出现同步问题。
    3. 还可以使用阻塞队列ArrayBlockingQueue,满足FIFO规则,很适合生产者消费者共同存取一个队列的多线程访问。
    4. 我看了一下你的代码,你的代码太少了,都没有多线程实现的类,我姑且给你写了个Producer和Consumer,我也初学,不知道写的好不好,class Producer implements Runnable{
    private Queue q;
    private DatagramPacket x;
    public Producer(Queue q,DatagramPacket x){
    this.q = q;
    this.x = x;
    }
    public void run(){
    q.addElement(x);
    System.out.println(Thread.currentThread().toString()+"put it to queue "+x.getLength());
    }
    }
    class Consumer implements Runnable{
    private Queue q;
    public Consumer(Queue q){
    this.q = q;
    }
    public void run(){

    System.out.println(Thread.currentThread().toString()+"get it from queue "+q.get().getLength());
    }
    }然后在Queue里创建main函数运行public static void main(String[] args){
    Queue q = new Queue();
    byte[] b = "hello".getBytes();
    DatagramPacket d = new DatagramPacket(b,b.length);
    Thread pro = new Thread(new Producer(q,d));
    Thread con = new Thread(new Consumer(q));

    con.start();
    pro.start();
    }运行结果显示可以正确的取而没有如你所说读出来很多重复的。
    不过在你的代码中我觉得还有问题,就是return vector.remove(0); Vector是数组,不是链表,你只放一个只取一个数据的时候, 可以正确跑,但是你要是放多个取一个的时候,就不能正确运行了,Vector中的元素是有索引的,你取走了0号索引,下次取得时候,就取不到了。总之用Vector做队列,不太明智再声明,我也是初学,有什么问题欢迎版上高手指点!!