ArrayList是单线程的数据结构,在多线程环境中容易发生不可预知的错误。因此Java类库为我们提供了CopyOnWriteArrayList在多线程中使用。
先来看看ArrayList,它有以下属性
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access
private int size;
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
- elementData:类型是transient Object[],说明ArrayList底层存储数据的方式还是数组,只不过ArrayList可以动态改变数组的大小(当然数组本身的大小在声明后就不能改变,ArrayList这里是用了创建新数组的方式改变它的容量)。
- EMPTY_ELEMENTDATA:当使用new ArryaList(0)创建一个空ArrayList时elementData = EMPTY_ELEMENTDATA,如果程序中有多个空ArrayList,那么它们都会指向同一个EMPTY_ELEMENTDATA,这样程序就得到了优化。
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA:当使用new ArrayList()创建一个空ArrayList时elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA,然后向数组中添加第一个元素时elementData大小会扩展为DEFAULT_CAPACITY
上述两者的区别是在java1.8才有的。通过ArrayList的构造函数可以很清楚的知道它们两者使用的区别
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
- DEFAULT_CAPACITY:ArrayList默认容量
- size:ArrayList大小(含有元素的个数),由于没有声明大小,默认为0。注意这里的size和elementData.length不一样,size指的是数组中的元素个数,而elementDate.length是数组长度。数组末尾可能会空出来因此elementData.length>size。
- MAX_ARRAY_SIZE:ArrayList的最大容量,为Integer.MAX_VALUE - 8
那add()方法是如何让一个空数组大小变为DEFAULT_CAPACITY的呢?先来看看add()方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
其中调用了ensureCapacityInternal()方法,这个方法又使用了一套组合拳,如下:
//组合拳入口,用于确保elementData.length > size,否则调用grow方法扩容
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
/**
* modCount是AbstractList中的属性,用于记录ArrayList被修改的次数,用于确保同一时间只能有一个线程对
* 它进行操作。
*
*/
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
//一次增加0.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);
}
其他的方法没什么比较特别的就不多讲,接下来再看看CopyOnWriteArrayList,看看它使用了什么机制能够在多线程环境中使用。
CopyOnWriteArrayList有以下属性:
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
private static final sun.misc.Unsafe UNSAFE;
private static final long lockOffset;
lock:用于同步
array:底层存放数据的地方
UNSAFE和lockOffset用于对lock在反序列化是进行重新设置。因为lock是transient类型的,是不会进行序列化的。
为什么CopyOnWriteArrayList没有size属性?我们可以看看它的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();
}
}
1.获取锁,进行加锁
2.获取原数组及其长度,并拷贝到新数组中,最后在将新数组最后一个元素设置为要添加的元素。在 将新数组替换原数组
3.释放锁
可以看出上述操作在每次add()后数组大小只增加一,并没有像ArrayList一次增加多个。因此数组的size就等于array.length。
再来看看修改操作:
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();
}
}
1.获取锁,加锁
2.获取原数组和索引对应的元素
3.将要修改的元素和传入的元素相比较,如果相等,就将原数组重新设置为当前数组
4.如果不相等,将原数组复制到新数组,再在新数组索引处修改元素的值,再将新数组替换原数组
5.释放锁
有同学可能会问,为什么获取array和设置array要通过getArray()和setArray()呢?不能直接对array进行操作吗?先看下这两个方法的源码:
/**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array;
}
/**
* Sets the array.
*/
final void setArray(Object[] a) {
array = a;
}
原因其实很简单,因为array是私有属性,为了能够实现array在concurrent包里能够访问,只能通过getArray()和setArray(),又为了编码风格一致,所以在该类里也使用这两个函数。