ArrayList
文章目录
ArrayList调整数组容量
默认扩容
每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//grow是扩容方法,传入的参数为所需最小容量,通过判断,即可实现默认扩容为原来1.5倍,也可扩容到自己需要的容量
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
手动扩容
默认扩容长度为原来的1.5倍,当需要添加数量比较大时,频繁扩容会影响效率,如果清楚知道业务数据量,可以使用ensureCapacity手动添加容量
手动扩容首先会判断数组是否有元素,需要容量minCapacity是否比默认容量DEFAULT_CAPACITY大,如果大才会触发扩容,否则就没有必要扩容。
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
ensureCapacityTest
long startTime = System.currentTimeMillis();
ArrayList<Object> list1 = new ArrayList<Object>();
for (int i = 1; i < 10000000; i++) {
list1.add(new Object());
}
System.out.println(System.currentTimeMillis() - startTime);
//2098
long startTime2 = System.currentTimeMillis();
ArrayList<Object> list2 = new ArrayList<Object>();
list2.ensureCapacity(10000000);
for (int i = 1; i < 10000000; i++) {
list2.add(new Object());
}
System.out.println(System.currentTimeMillis() - startTime2);
//237
trimToSize方法
使用trimToSize方法可以将底层数组的容量调整为当前列表保存的实际元素的大小
public void trimToSize() {
//操作次数加一
modCount++;
//调整可能会出现两种情况,元素为空或是元素个数小于容量
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
fail-fast机制
fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
单线程环境下的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) {
list.remove(3);
}
System.out.println(iterator.next());
i ++;
}
}
多线程环境下的fail-fast
public class FailFastTest {
public static List<String> list = new ArrayList<>();
//线程1读取
private static class MyThread1 extends Thread {
@Override
public void run() {
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {
String s = iterator.next();
System.out.println(this.getName() + ":" + s);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
super.run();
}
}
//线程2修改
private static class MyThread2 extends Thread {
int i = 0;
@Override
public void run() {
while (i < 10) {
System.out.println("thread2:" + i);
if (i == 2) {
list.remove(i);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
i ++;
}
}
}
public static void main(String[] args) {
for(int i = 0 ; i < 10;i++){
list.add(i+"");
}
MyThread1 thread1 = new MyThread1();
MyThread2 thread2 = new MyThread2();
thread1.setName("thread1");
thread2.setName("thread2");
thread1.start();
thread2.start();
}
}
iterator分析
当调用list.iterator()时,其源码是
public Iterator<E> iterator() {
return new Itr();
}
即它会返回一个新的Itr类,而Itr类是ArrayList的内部类,实现了Iterator接口
>private class Itr implements Iterator<E>
三个属性:
//集合遍历过程中的即将遍历的元素的索引
int cursor; // index of next element to return
//记录刚刚遍历过的元素的索引,不存在上一个时为-1
int lastRet = -1; // index of last element returned; -1 if no such
//初始值就为ArrayList中的modCount
int expectedModCount = modCount;
modCount是抽象类AbstractList中的变量,默认为0,而ArrayList 继承了AbstractList ,所以也有这个变量,modCount用于记录集合操作过程中作的修改次数,与size还是有区别的,并不一定等于size
public boolean hasNext() {
//用cursor游标和size(集合中的元素数目)进行对比
//相等时,表示遍历完成,返回false
return cursor != size;
}
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];
}
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();
}
}
final void checkForComodification() {
//modCounut可能会改变
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
expectedModCount在整个迭代过程除了一开始赋予初始值modCount外,并没有再发生改变,所以可能发生改变的就只有modCount,可以知道在ArrayList进行add,remove,clear等涉及到修改集合中的元素个数的操作时,modCount就会发生改变(modCount ++),所以当另一个线程(并发修改)或者同一个线程遍历过程中,调用相关方法使集合的个数发生改变,就会使modCount发生变化,这样在checkForComodification方法中就会抛出ConcurrentModificationException异常。
避免fail-fast
方法1
在单线程的迭代器遍历过程中,如果要进行remove操作,可以调用迭代器的remove方法而不是集合类的remove方法。
方法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,但不保证获取的是最新的数据。
来源:CSDN
作者:baidu_35181420
链接:https://blog.csdn.net/baidu_35181420/article/details/103546965