常用的集合ArrayList,LinkedList,HashSet,TreeSet,HashMap,TreeMap等均为线程不安全集合。当运行以下程序时,会报java.util.ConcurrentModificationException异常
public class ContinerNotSafeDemo {
public static void main(String[] args) {
List<Integer> list=new ArrayList<>();
for(int i=0;i<50;i++){
new Thread(()->{
list.add(new Random().nextInt());
System.out.println(list);
},Thread.currentThread().getName()).start();
}
}
}
要想让线程安全,有三种选择:
一、可以用Vector类,
二、使用Collections.synchronizedXXX来获得线程安全的集合对象;
三、CopyOnWriteArrayList。
CopyOnWriteArrayList是JDK1.5加入的,在集合添加元素时对方法加锁,同时采用读写分离的技术,具体看代码:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
这是CopyOnWriteArrayList中的add方法,第7行进行复制和扩容,扩容长度为1;第8步将扩容后的空间赋为新添加的元素;第9步将指针指向新的空间,完成元素添加。
上一步分析了添加的代码,现在来看看获取的代码:
private E get(Object[] a, int index) {
return (E) a[index];
}
final void setArray(Object[] a) {
array = a;
}
private transient volatile Object[] array;
上一步中已经调用了setArray方法,这个方法的作用是将旧的数组的指针指向新扩容的数组,也就是新数组上位,旧数组抛弃;这里的get方法是从array数组中按index拿到数据。这其就说明了CopyOnWriteArrayList的读写分离的方式,通过加锁保证线程安全性,读写分离方式保证多个线程在读和写的过程中不会发生异常。
总结:在多线程环境中,如果没有共享变量则线程之间一定是安全的;再细一步,如果共享变量没有写操作,只有读,那这样也是线程安全的,因为不会出现不一致的问题。CopyOnWriteArrayList采用了一种读写分离的思想,这种思想在写的时候加锁,并将集合内部元素复制并扩容一个单位,将新加入的元素写入这个新扩容的单位上;如果此时有读操作,那么读的还是以前未扩容前的数组。扩容完成后,将数组指针指向新的数组,抛弃旧数组。采用读写分离使得读操作变得更加高效。类似还有CopyOnWriteArraySet。