目录
引言
在Java中,ArrayList集合是最常用的集合之一。但是它是非线程安全的。所谓非线程安全,就是当有多条线程并发访问时,就往往会出现各种问题。它的“兄弟”CopyOnWriteArrayList是线程安全的;它的思想主要是加锁,然后获取原数组,然后复制将原数组复制至一个新数组newElements,然后用newElements替换原数组;元素它是怎样实现线程安全的呢?我们从下面几个常用的方法来看看。
1、Set()方法
首先获得锁对象,对接下来的操作加锁。主要的操作就是:先获取集合中保存数据的数组elements,然后的获取数组elements数组中index位置的元素oldValue。判断oldValue是否和element相同,如果不同:将elements数组重新赋值一份newElements,然后将newElements的index位置元素替换为element,通过setArray()方法让CopyOnWriteArrayList中保存数据的数组的引用指向修改后的数组。如果相同:则无需修改,直接调用setArray()方法;然后将oldValue返回出去,释放锁;
public E set(int index, E element) {
//获取锁对象
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
//获取存储元素的数组
Object[] elements = getArray();
//获取在原数组中index位置存储的元素
E oldValue = get(elements, index);
//判断要替换的元素是否是原来的元素
if (oldValue != element) {
//获取原数组的长度
int len = elements.length;
//将原数组内容复制至一个新数组
Object[] newElements = Arrays.copyOf(elements, len);
//替换index位置的元素
newElements[index] = element;
//将数组中原来的数组的引用指向修改后的数组
setArray(newElements);
} else {
//如果相同,则无需修改,相当于直接将次数组放回
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
2、add(E e)方法
add()方法还是获取锁对象,然后对添加操作加锁,然后拿到原来保存数据的数组elements,获得其长度,然后将次数组复制并多余留一个位置,就是最后那个位置。然后将此位置存储我们要添加的元素。然后通过setArray()方法让CopyOnWriteArrayList中保存数据的数组的引用指向修改后的数组。返回true,释放锁。
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();
}
}
3、add(int index,E element)
这个方法的主要操作是创建一个比原来数组长度大一的新数组newElements,如果要添加的位置index是数组的最后,直接将index前面的内容复制至新数组,如果是中间的某个地方,则先将index以前的内容复制至新数组,然后将index之后的内容复制至新数组,将index空出。然后给index位置存值。将newElements放回,释放锁。
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];
//复制index以前的元素
System.arraycopy(elements, 0, newElements, 0, index);
//复制index以后的元素
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
在要插入的位置存值
newElements[index] = element;
//存入原数组
setArray(newElements);
} finally {
lock.unlock();
}
}
4、remove()方法
remove()方法的主要操作是计算要删除的元素是计算要删除的位置是否是最后一个位置,如果是最后一个位置,就直接把原数组中除最后一个元素之外的元素复制至新数组(相当于直接删除了最后一个元素),如果是中间的某个位置,则先复制index之前的元素,然后再把index之后的元素复制至新数组newElements,相当于是跳过了index位置的那个值,然后通过setArray()方法让CopyOnWriteArrayList中保存数据的数组的引用指向修改后的数组。返回true,释放锁。
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)
//如果是最后一个,则从原数组中复制len-1个,也就值不复制最后那个元素
setArray(Arrays.copyOf(elements, len - 1));
else {
//如果是中间的某个元素
Object[] newElements = new Object[len - 1];
//复制index之前的元素
System.arraycopy(elements, 0, newElements, 0, index);
//复制index之后的元素
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
总结
1、ArrayList但访问速度快,线程不安全。绝对不能在有多线程并发条件下使用。CopyOnWriteArrayList用了ReentrantLock锁机制,线程安全。但正是锁机制每次操作会加锁、释放锁、所以速度慢;
2、ArrayList有自己的扩容机制,而CopyOnWriteArrayList每次操作都是基于数组的复制(相当于数组容量已算好),所以它没有不需要扩容机制,但它会浪费大量的内存空间。所以它适用于读多写少的场景;
3、CopyOnWriteArrayList在写操作时加锁,也就是指允许一条线程写入,允许多条线程读。它只能保证数据的最终一致性而不能保证数据的实时一致性;