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);
}
}
}
运行程序发现出现异常:
这是为什么? 跟进 AbstractList 343 行,如下图所示:
原来是因为 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()))
输出结果如下:
出现了第一次出现的异常,分析如下:
- 当第一循环执行到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();