J

在 JDK1.5 之前没有推出同步集合的时候,可以通过 Conllections 集合工具类的 synchronized+集合名称

如:synchronizedSet(Set),现在不需要这种方式了。

我们可以使用并发包下集合类:

  • ConcurrentHashMap
  • ConcurrentSkipListMap
  • ConcurrentSkipListSet
  • ConcurrentLinkedQueue 
  • ......

 

1)线程不安全的集合出现的问题 - 死循环

Race Condition(也叫做资源竞争),是多线程编程中比较头疼的问题。

特别是 Java 多线程当中,经常会因为多线程同时访问相同的共享数据,而造成数据的不一致性。

为了解决这个问题通常来说需要加上同步标识 synchronized 来保证数据串行的访问

但是 synchronized 是个性能杀手,过多的使用会导致性能下降,特别是扩展性下降,使得系统不能使用多个CPU资源,这是性能测试当中,经常遇到的问题。

我在网上看到过这样一个案例:有一个公司的 ERP 系统出现了问题,然后就叫 Java 的高级工程师来解决,该工程师通发现当 500 个并发用访问的时候,居然把所有的 CPU 都压得满满的

通过过 DTrace for Java 工具发现很多 CPU 都在做同一件事,那就是不停的执行一条语句 (HashMap.get()方法)

也就是说 HashMap 不是线程安全的,它可能导致死循环,一直执行 hashMap.get 方法。

这是为什么呢?我们知道遍历 Map 集合的时候,是这样的情形(下面只是伪代码):

while(hasNext()){
    //每当循环一次cursor加1
    //假设该集合里面有4个元素(count=4),如果循环到最后一次了 cursor=4,就在此时另一个线程
    //跑来把集合里面的一个元素给删除了(remove()),这时候 count=3 了,这时候上一个线程接着执行
    //它会去判断 hasNext(),但是 count!=cursor 了,而本来是相等的.这样就是死循环了.

}


boolean hasNext(){
    //不需要再循环了
    if(cursor == count){
        return false;
    }
    return true;
}

 

总结:线程不安全的集合,如果多个线程同时对其进行添加和删除操作,有可能会出现致命的错误。

 

2) HashSet 和 HashMap关系

通过查看 HashSet 源码:

public HashSet() {
       map =new HashMap<E,Object>();
}

发现 HashSet 底层实际上就是通过 HashMap 来实现的,只不过用到了 HashMap 的 key 而已。

3) 线程不安全集合还有另一个隐患

例如下面一个例子:

public static void main(String[] args) {
        //Collection users = new CopyOnWriteArrayList();
        Collection users = new ArrayList();

        users.add(new User("张三",28));    
        users.add(new User("李四",25));            
        users.add(new User("王五",31));    
        Iterator itrUsers = users.iterator();
        while(itrUsers.hasNext()){
            System.out.println("aaaa");
            User user = (User)itrUsers.next();
            if("张三".equals(user.getName())){
                users.remove(user);
            } else {
                System.out.println(user);                
            }
        }
    }

运行程序发现出现异常:

JAVA List多线程 java多线程操作集合_HashMap死循环

 

这是为什么?  跟进 AbstractList 343 行,如下图所示:

JAVA List多线程 java多线程操作集合_HashMap死循环_02

原来是因为 modCount != expectedModCount 导致抛 ConcurrentModificationException 异常。

那么这两个变量到底表示什么呢?

查看 ArrayList 源码可得,只要对集合进行增删 modCount 都会发生变化

在初始的时候 modCount 和 expectedModCount 是相等的

当我们调用 ArrayList 的 remove 方法,modCount 就减一了,这样两个值就不相等了。

如果上面的程序将 if("张三".equals(user.getName())) 改成 if("李四".equals(user.getName()))

程序输出如下结果:{name:'张三', age:28}

为什么会如此,我们来分析如下,我们先来看一下这个:

while(itrUsers.hasNext()){
    User user = (User)itrUsers.next();
    if("张三".equals(user.getName())){
        users.remove(user);
    } else {
        System.out.println(user);
    }
}

查看 hasNext 源码:

public boolean hasNext() {
    return cursor != size();
}

如果把 if("张三".equals(user.getName())) 改成 if("王五".equals(user.getName()))

输出结果如下:

JAVA List多线程 java多线程操作集合_JAVA List多线程_03

出现了第一次出现的异常,分析如下:

  • 当第一循环执行到next()时候 expectedModCount=3 modCount=3 cursor=1,size()=3
  • 当第二循环执行到next()时候 expectedModCount=3 modCount=3 cursor=2,size()=3
  • 当第三循环执行到next()时候 expectedModCount=3 modCount=4 cursor=3,size()=2
  • 发现 cursor 任然不等于 size(),循环人仍在继续,最后发现 expectedModCount!=modCount 抛出异常.

所以控制台输出这样的结果。

要想迭代的时候同时操作集合可以使用 JDK 提供的线程安全的集合。只需要把集合改成如下即可: 

Collection users = new CopyOnWriteArrayList();