目录
1 不安全集合ArrayList
1.1 不安全现象1:元素丢失
1.2 不安全现象2:ConcurrentModificationException
2 不安全集合HashMap
2.1 不安全现象1:元素覆盖
2.2 不安全现象2:ConcurrentModificationException
2.3 不安全现象3:死锁
3 集合线程安全化
测试代码
for (int i = 0; i < 1000; i ++) {
List list = new ArrayList<>();
executor.execute(() -> list.add(1));
executor.execute(() -> list.add(2));
Thread.sleep(100);
System.out.println(list);
}
测试结果:
[1]
[null, 2]
[2]
ArrayList.add源码如下
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
这里分析一个比较简单的并发问题,size ++
线程1和线程2同时执行add方法,其中线程1先执行,给数组中size位置放置好元素后,还没有自增1,cpu调度到线程2执行,线程2同样给size处放元素,就导致后执行的线程覆盖掉前一个线程放置的元素。
ArrayList迭代器:
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
原因:每次add,modCount++,迭代的时候会初始化expectModCount(迭代器变量)=modCount(ArrayList变量)。
如果线程1先获取迭代器,执行next()的时候停住了线程2添加一个element,那么modCount发生变化,线程1然后
线程1又开始执行,这个时候checkForComodification就会检查expectModCount != modCount抛异常。
为了证明这个问题以及确保线程执行顺序,我们对ArrayList源码进行修改
// 修改add方法
public boolean add(E e) {
try {
// 这里睡100ms,确保先获取到迭代器
Thread.sleep(100);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size ++] = e;
System.out.println(Thread.currentThread().getName() + "添加元素:" + e);
return true;
}
// 修改iterator方法
public Iterator iterator() {
Itr itr = new Itr();
System.out.println(Thread.currentThread().getName() + "获取迭代器");
try {
// 这里睡200ms,确保获取到迭代器后有添加元素的操作
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return itr;
}
// Itr.next方法添加日志
public E next() {
System.out.println(Thread.currentThread().getName() + "开始遍历,checkForComodification");
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
测试代码
@Test
public void test() throws InterruptedException {
List list = new ArrayList<>(4);
executor.execute(() -> list.add(1));
System.out.println(list);
TimeUnit.MINUTES.sleep(1);
}
测试结果如下,100%复现异常
main获取迭代器
pool-1-thread-1添加元素:1
main开始遍历,checkForComodification
java.util.ConcurrentModificationException
at com.demo.util.ArrayList$Itr.checkForComodification(ArrayList.java:918)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
// 两个线程同时走到这里会发生后一个线程把前一个线程添加的元素给覆盖掉
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
HashMap的keySet遍历机制和ArrayList类似,这里不再赘述
该现象在jdk1.7中出现,jdk1.8 hashmap采用尾插法避免了此问题
那么如何获取线程安全的集合呢?可以通过如下几个方式
1 用Vector,HashTable代替ArrayList和HashMap
2 通过Collections工具类构建线程安全的集合:Collections.synchronizedList(), Collections.synchronizedMap();
上面两种方式优点就是实现比较简单,直接用synchronized锁住方法或者锁住方法里的整段代码。那么有没有集合类,既是线程安全的,性能也比传统的加锁方式好呢?答案是有的,JUC并发包下有丰富的并发集合,在确保线程安全的同时又可以改善传统加锁方式的性能,可以参考笔者JUC并发工具系列博客