您的位置:首页 > 编程语言 > Java开发

jdk源码分析之CopyOnWriteArrayList

2016-05-25 14:53 495 查看

CopyOnWriteArrayList的原理

CopyOnWriteArrayList的核心思想是利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制一份新的数组,在新的数组上面修改,然后将新数组赋值给旧数组的引用,并通过volatile 保证其可见性,通过Lock保证并发写。

底层数据结构

private volatile transient Object[] array;
final Object[] getArray() {
return array;
final void setArray(Object[] a) {
array = a;
}


底层采用Object数组存储数据

数组使用volatile修饰保证可见性,不读缓存直接读写内存

数组使用private修饰限制访问与,只能通过getter和setter访问

数组使用transient修饰,表示序列化时忽略此字段(自己定制序列化操作)

定制序列化操作

因为Object数组被transient修饰,因此需要CopyOnWriteArrayList类自己制定序列化方案

方法writeObject和readObject处理对象的序列化。如果声明该方法,它将会被ObjectOutputStream调用而不是采用默认的序列化方案。ObjectOutputStream使用了反射来寻找是否声明了这两个方法。因为ObjectOutputStream使用getPrivateMethod,所以这些方法不得不被声明为priate以至于供ObjectOutputStream来使用。

在两个方法的开始处,调用了defaultWriteObject()和defaultReadObject()。它们做的是默认的序列化进程,就像写/读所有的non-transient和 non-static字段(但他们不会去做serialVersionUID的检查).通常说来,所有我们想要自己处理的字段都应该声明为transient。这样的话,defaultWriteObject/defaultReadObject便可以专注于其余字段,而我们则可为被transient修饰的字段定制序列化。

/**
* Saves the state of the list to a stream (that is, serializes it).
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
s.defaultWriteObject();

Object[] elements = getArray();
// Write out array length
s.writeInt(elements.length);

// Write out all elements in the proper order.
for (Object element : elements)
s.writeObject(element);
}


先调用s.defaultWriteObject()对非transient修饰的字段进行序列化操作

然后序列化写入数组的长度,再循环写入数组的元素

/**
* Reconstitutes the list from a stream (that is, deserializes it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// bind to new lock
resetLock();

// Read in array length and allocate array
int len = s.readInt();
Object[] elements = new Object[len];

// Read in all elements in the proper order.
for (int i = 0; i < len; i++)
elements[i] = s.readObject();
setArray(elements);
}


反序列化的时候先调用s.defaultReadObject()恢复没有被transient修饰的字段

然后为反序列化得到的CopyOnWriteArrayList对象创建一把新锁

接着恢复数组的长度,根据数组的长度创建一个Object的数组

然后恢复数组的每一个元素

读操作不加锁

public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}


读操作是直接通过getArray方法获取Object数组,然后通过下标index直接访问数据。读操作并没有加锁,也没有并发的带来的问题,因为写操作是加锁写数组的副本,写操作成功将副本替换为原数据,这也是写时复制名字的由来。

加锁写副本

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();
}
}


set方法先通过lock加锁,然后获取index位置的旧数据,供最后方法返回使用

E oldValue = get(elements, index);

接着创建数组的副本,在副本上进行数据的替换

Object[] newElements = Arrays.copyOf(elements, len);


Arrays.copyOf(elements, len)方法将会从elements数组复制len个数据创建一个新的数组返回

然后在新数组上进行数据替换,然后将新数组设置为CopyOnWriteArrayList的底层数组

newElements[index] = element;

setArray(newElements);

最后在finally块里边释放锁

特定位置添加数据

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();
}
}


添加数据和替换数据类似,先加锁,然后数组下标检查,接着创建数组副本,在副本里边添加数据,将副本设置为CopyOnWriteArrayList的底层数组
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  jdk 源码 并发