首先:ArrayList的底层通过数组实现
ArrayList<E>:说明ArrayList支持泛型。
extends AbstractList<E> :继承了AbstractList。AbstractList提供List接口的骨干实现,以最大限度地减少“随机访问”数据存储(如ArrayList)实现Llist所需的工作。
implements List<E>:实现了List。实现了所有可选列表操作。
implements RandomAccess:表明ArrayList支持快速(通常是固定时间)随机访问。此接口的主要目的是允许一般的算法更改其行为,从而在将其应用到随机或连续访问列表时能提供良好的性能。
implements Cloneable:表明其可以调用clone()方法来返回实例的field-for-field拷贝。
implements java.io.Serializable:表明该类具有序列化功能。有序列化功能
下面来看一些比较重要的参数:
初始化的默认容量
指定该ArrayList数组容量为零时返回该对象数组
默认调用空构造方法时返回该对象数组
ArrayList底层数组中存储真正元素的数组
ArrayList的实际大小(数组包含真正的元素个数)
下面是ArrayList的三种构造方法:
第一种:
执行完构造方法时还是一个空数组,等到add方法执行的时候则会初始化容量为10;
第二种:
自己穿入想要的容量参数,对容量进行判断如果容量小于零,则会抛出异常,否则会创建一个容量为initialCapacity的空ArrayList
第三种:
构造一个包含指定 collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。
下面来看ArrayList的扩容机制()
先来看其中的add()方法:
add方法首先会调用ensureCapacityInternal(size+1)方法,
ensureCapacityInternal(size+1)方法是用来进行容量检查,决定扩容的想要的最小容量,举个例子,第一次扩容的时候,size默认为0则minCapacity(即想要的最小容量)为1,执行完Math.max语句minCapacity就变为10,这个时候也就是进行空构造第一次add的情况,当ensureCapacityInternal(size+1)传入的参数小于默认参数的时候,就把默认参数当做想要的最小容量,如果大于默认参数就把你想要的参数当做想要的最小容量
这个方法是用来判断是否扩容,如果你想要的最小容量大于数组长度则会调用grow方法进行扩容
通过grow方法可以看到新容量为当前数组容量的1.5倍,然后进行判断,如果理论扩容后新的容量小于小于你想要的最小容量(但我觉得这种情况不太可能会出现,因为为了节省空间,假如当前容量为10,只会传入11来和扩容后的进行比较因此我自己认为不会出现if为真的情况)真正的实现扩容其实是Arrays.copy方法,就是复制数组实现扩容,
但是如果出现了if为真的情况,从这个方法中可以看到数组的理论最大值为Integer的最大值减去8,但是如果你想要的最小容量已经大于数组的理论最大值,则会进行大容量分配为Integer的最大值至此扩容结束,
下面粗略的谈一下快速失败机制(因为对线程还没有一个好的认识)
public class FastFailTest {
private static List<String> list = new ArrayList<String>();
//private static List<String> list = new CopyOnWriteArrayList<String>();
public static void main(String[] args) {
// 同时启动两个线程对list进行操作!
new ThreadOne().start();
new ThreadTwo().start();
}
private static void printAll() {
System.out.println("");
String value = null;
Iterator iter = list.iterator();
while(iter.hasNext()) {
value = (String)iter.next();
System.out.print(value+", ");
}
}
/**
* 向list中依次添加0,1,2,3,4,5,每添加一个数之后,就通过printAll()遍历整个list
*/
private static class ThreadOne extends Thread {
public void run() {
int i = 0;
while (i<6) {
list.add(String.valueOf(i));
printAll();
i++;
}
}
}
/**
* 向list中依次添加10,11,12,13,14,15,每添加一个数之后,就通过printAll()遍历整个list
*/
private static class ThreadTwo extends Thread {
public void run() {
int i = 10;
while (i<16) {
list.add(String.valueOf(i));
printAll();
i++;
}
}
}
}
fail-fast是怎么产生的。步骤如下:
新建了一个ArrayList,名称为arrayList。
向arrayList中添加内容
新建一个“线程a”,并在“线程a”中通过Iterator反复的读取arrayList的值。
新建一个“线程b”,在“线程b”中删除arrayList中的一个“节点A”。
这时,就会产生有趣的事件了。
在某一时刻,“线程a”创建了arrayList的Iterator。此时“节点A”仍然存在于arrayList中,创建arrayList时,expectedModCount = modCount(假设它们此时的值为N)。
在“线程a”在遍历arrayList过程中的某一时刻,“线程b”执行了,并且“线程b”删除了arrayList中的“节点A”。“线程b”执行remove()进行删除操作时,在remove()中执行了“modCount++”,此时modCount变成了N+1!
“线程a”接着遍历,当它执行到next()函数时,调用checkForComodification()比较“expectedModCount”和“modCount”的大小;而“expectedModCount=N”,“modCount=N+1”,这样,便抛出ConcurrentModificationException异常,产生fail-fast事件。
至此,我们就完全了解了fail-fast是如何产生的!
即,当多个线程对同一个集合进行操作的时候,某线程访问集合的过程中,该集合的内容被其他线程所改变(即其它线程通过add、remove、clear等方法,改变了modCount的值);这时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
方法1
在单线程的遍历过程中,如果要进行remove操作,可以调用迭代器的remove方法而不是集合类的remove方法。看看ArrayList中迭代器的remove方法的源码:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
可以看到,该remove方法并不会修改modCount的值,并且不会对后面的遍历造成影响,因为该方法remove不能指定元素,只能remove当前遍历过的那个元素,所以调用该方法并不会发生fail-fast现象。该方法有局限性。
例子:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0 ; i < 10 ; i++ ) {
list.add(i + "");
}
Iterator<String> iterator = list.iterator();
int i = 0 ;
while(iterator.hasNext()) {
if (i == 3) {
iterator.remove(); //迭代器的remove()方法
}
System.out.println(iterator.next());
i ++;
}
}
方法2
使用java并发包(java.util.concurrent)中的类来代替ArrayList 和hashMap。
比如使用 CopyOnWriterArrayList代替ArrayList,CopyOnWriterArrayList在是使用上跟ArrayList几乎一样,CopyOnWriter是写时复制的容器(COW),在读写时是线程安全的。该容器在对add和remove等操作时,并不是在原数组上进行修改,而是将原数组拷贝一份,在新数组上进行修改,待完成后,才将指向旧数组的引用指向新数组,所以对于CopyOnWriterArrayList在迭代过程并不会发生fail-fast现象。但 CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
对于HashMap,可以使用ConcurrentHashMap,ConcurrentHashMap采用了锁机制,是线程安全的。在迭代方面,ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。即迭代不会发生fail-fast,但不保证获取的是最新的数据。