文章目录
- 1. 集合的快速失败 (fail-fast)
- 1. 使用增强for遍历集合并使用ArrayList的 remove() 方法删除集合元素
- 2. 使用 forEach 遍历集合并使用ArrayList的 remove() 方法删除集合元素
- 3. 使用迭代器遍历集合并使用ArrayList的 remove() 方法删除集合元素
- 4. 使用迭代器遍历集合并使用迭代器的 remove() 方法删除元素
- 5. 源码分析
- 2. 同步容器
- 3. 集合的安全失败机制 (fail-safe)
- 4. CopyOnWriteArrayList
- 1. CopyOnWriteArrayList的使用
- 2. CopyOnWriteArrayList的原理
- 3. CopyOnWriteArrayList源码分析
- 4. CopyOnWriteArrayList的优点
- 5. CopyOnWriteArrayList和ReentrantReadWriteLock的比较
请思考以下几个问题:
1、遍历一个 List 有哪些不同的方式?实现原理是什么?Java 中 List遍历的最佳实践是什么?
2、如何边遍历边删除集合元素?
3、什么是集合的快速失败机制?
4、多线程场景下如何使用 ArrayList?
1. 集合的快速失败 (fail-fast)
采用快速失败机制的集合容器,使用迭代器进行遍历集合时,除了通过迭代器自身的 remove() 方法之外,对集合进行任何其他方式的结构性修改,则会抛出 ConcurrentModificationException 异常。
1. 使用增强for遍历集合并使用ArrayList的 remove() 方法删除集合元素
public class Main {
public static void main(String[] args) {
List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");
List<String> list = new ArrayList(initList);
for (String element : list) {
if (element.startsWith("李")) {
// 利用ArrayList的remove()方法对集合进行修改
list.remove(element);
}
}
System.out.println(list);
}
}
抛出并发修改异常!其实,for(xx in xx) 就是增强的 for循环,即迭代器 Iterator 的加强实现,其内部是调用的 Iterator 的方法。
2. 使用 forEach 遍历集合并使用ArrayList的 remove() 方法删除集合元素
public class Main {
public static void main(String[] args) {
List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");
List<String> list = new ArrayList(initList);
list.forEach((e) -> {
if (e.contains("李")) {
list.remove(e);
}
});
System.out.println(list);
}
}
forEach 方法的背后其实就是增强的 for 循环,底层即迭代器,所以使用 list.remove 同样抛出 ConcurrentModificationException
异常。
3. 使用迭代器遍历集合并使用ArrayList的 remove() 方法删除集合元素
public class Main {
public static void main(String[] args) {
List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");
List<String> list = new ArrayList(initList);
for (Iterator<String> ite = list.iterator(); ite.hasNext(); ) {
String str = ite.next();
if (str.contains("李")) {
// 利用ArrayList的remove()方法对集合进行修改
list.remove(str);
}
}
System.out.println(list);
}
}
又是那个并发修改异常,这个示例虽然使用了 Iterator 循环,但删除的时候却使用了 list.remove 方法,同样是有问题的。
4. 使用迭代器遍历集合并使用迭代器的 remove() 方法删除元素
public class Main {
public static void main(String[] args) {
List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");
List<String> list = new ArrayList(initList);
for (Iterator<String> iterator = list.iterator(); iterator.hasNext(); ) {
String str = iterator.next();
if (str.contains("李")) {
iterator.remove();
}
}
System.out.println(list);
}
}
结果输出正常,这是因为迭代器中的 remove 方法将期待修改的数量(expectedModCount)值进行了同步。
5. 源码分析
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
// 1、添加元素方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
// 快速失败机制:迭代器内部维护了一个 modCount 变量,添加元素时,modCount++
modCount++;
if (elementData.length < minCapacity)
grow(minCapacity);
}
// 2、删除元素方法
public E remove(int index) {
rangeCheck(index);
// 快速失败机制:迭代器内部维护了一个 modCount 变量,添加元素时,modCount++
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
// 3、迭代器方法
public Iterator<E> iterator() {
return new Itr();
}
// 4、ArrayList中的内部类
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
// 每次调动迭代器的next()方法都会检查集合元素是否修改,如果修改了modCount的值会改变
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];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
// 每次调动迭代器的remove()方法都会检查集合元素是否修改,如果修改了modCount的值会改变
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
// 使用迭代器的remove()方法,内部在调用ArrayList的remove()方法后,会同步修改expectedModCount,所以不会发生并发修改异常!
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
// 当检测到 modCount != expectedmodCount 时,抛出异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
}
迭代器在遍历时直接访问集合的内容时,集合中的内容在遍历的过程中无法被修改。为了保证不被修改,迭代器内部维护了一个 modCount 变量 ,当集合结构改变(添加、删除),就会改变 modCount 的值。每当迭代器使用 next() 方法遍历下一个元素时,都会检查 modCount 的值是否等于 expectedmodCount 的值,当检测到 modCount != expectedmodCount 时,抛出 ConcurrentModificationException 异常,反之继续遍历。
2. 同步容器
根据上面的分析可见,ArrayList集合单线程下效率相对较高,多线程环境下,线程不安全。 java.util
包下的集合类都是快速失败的,不能在多线程下发生并发修改。
如果想要保证线程安全,可以使用线程安全的同步容器,即使用sychronized关键字来实现线程安全的容器,比如 Vector,Collections工具类中的同步方法。
3. 集合的安全失败机制 (fail-safe)
采用安全失败机制的集合容器,使用迭代器进行遍历时不是直接在集合内容上访问的,而是将原有集合内容进行拷贝,在拷贝的集合上进行遍历。
迭代器在遍历时访问的是拷贝的集合,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 ConcurrentModificationException 异常。
优缺点:
(1) 由于对集合进行了拷贝,避免了 ConcurrentModificationException 异常,但拷贝时产生大量的无效对象,开销大。
(2) 无法保证读取到的数据是原集合中最新的数据,即迭代器进行遍历的是拷贝的集合,在遍历期间原集合发生的修改,迭代器是检测不到的。
(3) java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
4. CopyOnWriteArrayList
1. CopyOnWriteArrayList的使用
Collections可以将基础容器包装为线程安全的同步容器,但是这些同步容器包装类在进行元素迭代时并不能进行元素添加操作。
public class CopyOnWriteArrayListTest {
//并发操作的执行目标
public static class CocurrentTarget implements Runnable {
//并发操作的目标队列
List<String> list = null;
public CocurrentTarget(List<String> targetList) {
this.list = targetList;
}
@Override
public void run() {
Iterator<String> iterator = list.iterator();
//迭代操作
while (iterator.hasNext()) {
// 在迭代操作时,进行列表的修改
String threadName = Thread.currentThread().getName();
System.out.println("开始往同步队列加入线程名称:" + threadName);
list.add(threadName);
}
}
}
//测试同步队列:在迭代操作时,进行列表的修改
public static void main(String[] args) throws InterruptedException {
List<String> notSafeList = Arrays.asList("a", "b", "c");
List<String> synList = Collections.synchronizedList(notSafeList);
//创建一个执行目标
CocurrentTarget synchronizedListListDemo = new CocurrentTarget(synList);
//10个线程并发
for (int i = 0; i < 10; i++) {
new Thread(synchronizedListListDemo, "线程" + i).start();
}
//主线程等待
Thread.sleep(1000);
}
}
那么,该如何解决此问题呢?可使用CopyOnWriteArrayList替代Collections.synchronizedList同步包装实例:
public class CopyOnWriteArrayListTest {
//并发操作的执行目标
public static class CocurrentTarget implements Runnable {
//并发操作的目标队列
List<String> list = null;
public CocurrentTarget(List<String> targetList) {
this.list = targetList;
}
@Override
public void run() {
Iterator<String> iterator = list.iterator();
//迭代操作
while (iterator.hasNext()) {
// 在迭代操作时,进行列表的修改
String threadName = Thread.currentThread().getName();
System.out.println("开始往同步队列加入线程名称:" + threadName);
list.add(threadName);
}
}
}
//测试同步队列:在迭代操作时,进行列表的修改
public static void main(String[] args) throws InterruptedException {
List<String> notSafeList = Arrays.asList("a", "b", "c");
List<String> copyOnWriteArrayList = new CopyOnWriteArrayList(notSafeList);
//创建一个执行目标
CocurrentTarget cocurrentTarget = new CocurrentTarget(copyOnWriteArrayList);
//10个线程并发
for (int i = 0; i < 10; i++) {
new Thread(cocurrentTarget, "线程" + i).start();
}
//主线程等待
Thread.sleep(1000);
}
}
使用CopyOnWriteArrayList容器可以在进行元素迭代的同时进行元素添加操作。那么CopyOnWriteArrayList是如何做到的呢?
2. CopyOnWriteArrayList的原理
CopyOnWrite(写时复制)就是在修改器对一块内存进行修改时,不直接在原有内存块上进行写操作,而是将内存复制一份,在新的内存中进行写操作,写完之后,再将原来的指针(或者引用)指向新的内存,原来的内存被回收。
CopyOnWriteArrayList是写时复制思想的一种典型实现,其含有一个指向操作内存的内部指针array,而可变操作(add、set等)是在array数组的副本上进行的。当元素需要被修改或者增加时,并不直接在array指向的原有数组上操作,而是首先对array进行一次复制,将修改的内容写入复制的副本中。写完之后,再将内部指针array指向新的副本,这样就可以确保修改操作不会影响访问器的读取操作。
CopyOnWriteArrayList是一个满足CopyOnWrite思想并使用Array数组存储数据的线程安全List。
3. CopyOnWriteArrayList源码分析
(1) CopyOnWriteArrayList的属性:
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/** 对所有的修改器方法进行保护,访问器方法并不需要保护 */
final transient ReentrantLock lock = new ReentrantLock();
/** 内部对象数组,通过 getArray/setArray方法访问 */
private transient volatile Object[] array;
//获取内部对象数组
final Object[] getArray() {
return array;
}
// 设置内部对象数组
final void setArray(Object[] a) {
array = a;
}
// 省略其他代码
}
(2) CopyOnWriteArrayList的读取操作:
访问器的读取操作没有任何同步控制和锁操作,理由是内部数组array不会发生修改,只会被另一个array替换,因此可以保证数据安全。
/** 操作内存的引用*/
private transient volatile Object[] array;
public E get(int index) {
return get(getArray(), index);
}
//获取元素
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
//返回操作内存
final Object[] getArray() {
return array;
}
(3) CopyOnWriteArrayList的写入操作:
CopyOnWriteArrayList的写入操作add()方法在执行时加了独占锁以确保只能有一个线程进行写入操作,避免多线程写的时候会复制出多个副本。
// 在集合的末尾添加元素
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(); // 释放锁
}
}
// 在集合的指定位置添加指定元素
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
int numMoved = len - index;
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
else {
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}
从add()操作可以看出,在每次进行添加操作时,CopyOnWriteArrayList底层都是重新复制一份数组,再往新的数组中添加新元素,待添加完了,再将新的array引用指向新的数组。当add()操作完成后,array的引用就已经指向另一个存储空间了。
既然每次添加元素的时候都会重新复制一份新的数组,那就带来了一个问题,就是增加了内存的开销,如果容器的写操作比较频繁,那么其开销就比较大。所以,在实际应用的时候,CopyOnWriteArrayList并不适合进行添加操作。但是在并发场景下,迭代操作比较频繁,CopyOnWriteArrayList就是一个不错的选择。
(4) CopyOnWriteArrayList的迭代器实现:
CopyOnWriteArray有自己的迭代器,该迭代器不会检查修改状态,也无须检查状态。为什么呢?因为被迭代的array数组可以说是只读的,不会有其他线程能够修改它。
//获取迭代器
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
//返回操作内存
final Object[] getArray() {
return array;
}
static final class COWIterator<E> implements ListIterator<E> {
/**对象数组的快照(snapshot)*/
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
//下一个元素
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
}
4. CopyOnWriteArrayList的优点
CopyOnWriteArrayList有一个显著的优点,那就是读取、遍历操作不需要同步,速度会非常快。所以,CopyOnWriteArrayList适用于读操作多、写操作相对较少的场景(读多写少),比如可以在进行“黑名单”拦截时使用CopyOnWriteArrayList。
5. CopyOnWriteArrayList和ReentrantReadWriteLock的比较
CopyOnWriteArrayList和ReentrantReadWriteLock读写锁的思想非常类似,即读读共享、写写互斥、读写互斥、写读互斥。但是前者相比后者的更进一步:为了将读取的性能发挥到极致,CopyOnWriteArrayList读取是完全不用加锁的,而且写入也不会阻塞读取操作,只有写入和写入之间需要进行同步等待,读操作的性能得到大幅度提升。