一. 线程不安全的ArrayList
1、为什么说ArrayList是线程不安全的:
add()操作抛出数组越界异常;
add()操作会丢失元素;
set()操作去修改元素,get()操作去获取元素时,可以读到新值也可能读到旧值,无法保证一致性。
源码分析:
public boolean add(E e) {
//确定添加元素之后,集合的大小是否足够,若不够则会进行扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//插入元素
elementData[size++] = e;
return true;
}
场景1:多个线程都没进行扩容,但是执行了elementData[size++] = e;时,便会出现“数组越界异常”;
场景2:因为size++本身就是非原子性的,多个线程之间访问冲突,这时候两个线程可能对同一个位置赋值,就会出现“size小于期望值的结果”;
场景3:迭代器遍历修改报错
ArrayList是线程不安全的集合类,在单线程环境下,使用iterator进行遍历修改的时候会出现 java.util.ConcurrentModificationException并发修改异常,如下
import java.util.*;
public class Test {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("1w");
list.add("2w");
list.add("3w");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
String cur = iterator.next();
if(cur=="1w"){
list.remove(cur);
}
System.out.println(cur);
}
}
}
该程序会报出ConcurrentModificationException异常:
这就是fail-fast机制在单线程环境下的体现。但是原理是是什么呢?
这里涉及到我们在ArrayList的源码中有一个变量modCount,该变量表明当前ArrayList结构修改的次数。而ArrayList中重新定义了一个Itr类实现了Iterator接口,在该接口中有一个变量expectedModCount,迭代以前expectedModCount等于modCount。
是我们在迭代的过程中在list中增加或者删除数据导致了list结构有所变化,modCount也变化了,不再等于expectedModCount。导致利用迭代器的next方法的时候,判断两者不相等,从而出现并发修改异常:
调用迭代器的next方法,会调用checkForComodification()方法进行一个检验:
既然了解了fail-fast的原理了,那么下面的例子出现问题我们应该也能知道是怎么回事了:就是因为在获取迭代器之后,对list的结构做了修改,导致expectedModCount与modCount不相等,之后调用next()从而检验两者是否相等,从而报出异常。
二 、CopyOnWriteArrayList
一:适用场景
1.读操作尽可能地快,而写即使慢一点也没有太大的关系
2.读多写少:热点数据;监听器:迭代操作远多余修改操作
3.CopyOnWriteArrayList可以在迭代的过程中修改内容,但是ArrayList不行。
4.CopyOnWrite的含义:创建新副本,读写分离。
二:CopyOnWriteArrayList缺点
1.数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的数据马上能读到,请不要使用CopyOnWrite容器
2.内存占用问题:因为CopyOnWrite的写是复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存。
三:读写规则
1.读写锁:读读共享,其他都互斥(写写互斥,读写互斥,写读互斥)
2.读写锁规则的升级:读是完全不用加锁的,并且更厉害的是,写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待
源码分析:
CopyOnWriteArrayList构造函数:
public CopyOnWriteArrayList()
//Collection做初始化参数
public CopyOnWriteArrayList(Collection<? extends E> c)
//Array做初始化参数
public CopyOnWriteArrayList(E[] toCopyIn)
读操作
privateE get(Object[] a, intindex) {
return(E) a[index];
}
publicE get(intindex) {
returnget(getArray(), index);
}
直接读取,不需要加锁,因为即使读取过程中有其他线程改动List,也是开辟新的数组并在新数组上改动,旧数组对象始终是可用的。
2.写操作
在CopyOnWriteArrayList中写操作过程大致是这样的。在原有数组的基础上拷贝一份新的数组(容器副本),将改动操作在新数组上完成(即把新增元素加入新数组中),然后再把新数组对象的引用赋给CopyOnWriteArrayList的array。显然,在多线程环境中,为了保证线程安全,整个过程需要加锁。所以CopyOnWriteArrayList的写操作性能损耗是很大的,一方面需要竞争获取锁,另一方面需要进行复制操作。
下面以add(int index, E element)方法为例说明CopyOnWriteArrayList的修改操作:
//指定位置增加元素
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
//修改array前获取锁
lock.lock();
try {
//获取原有array
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)
//index=length,即数组尾部新增一个元素,同add(E element)
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;
//将新数组引用赋值给array
setArray(newElements);
} finally {
//释放锁
lock.unlock();
}
}
除了add方法,还有remove、removeRange、addIfAbsent等其他修改操作原理都是一样的,都是新new一个数组对象,在新array上进行修改操作,完事后再将新数组引用赋值给实例变量array,当然修改操作都是需要加锁的。
修改:
/**
* Replaces the element at the specified position in this list with the
* specified element.
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = get(elements, index);
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
删除
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices). Returns the element that was removed from the list.
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
final ReentrantLock lock = this.lock;
//获取独占锁
lock.lock();
try {
//获取数组
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
//删除最后一个元素
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
线程安全性
我们可以看到,CopyOnWriteArrayList内部的array数组对象从被创建,到这个对象生命结束,是不可变的。变的是array变量的引用值,每做一次修改操作,array变量就指向新生成的数组对象,原对象被gc,如此反复。这种方式核心思想是减少锁竞争,从而提高高并发时的读取性能,但一定程度上牺牲了写的性能。
由此可得:“写入时复制(Copy-On-Write)”容器的线程安全性在于:只有正确地发布一个事实不可变的对象,那么在访问该对象时就不需要做同步操作。这也就解释了为什么通过迭代器Iterator是不允许进行修改操作的了。