目录
ArrayList和CopyOnWriteArrayList区别
前言
目前也是金三银四跳槽找工作的最好时机,可能很多小伙伴在面试中被面试官问到集合方面的问题,比如有使用过ArrayList吗?ArrayList线程安全吗?线程安全的List集合有哪些?了解过CopyOnWriteArrayList集合吗?了解过是吧,那么你说说他如何保证线程安全的呢?等等一系列的问题。所以特意写一篇关于CopyOnWriteArrayList的帖子希望能帮助到你们。
正文
ArrayList的回顾
- 初始大小为10,懒加载机制。
- 扩容时机是当数组满了后扩容
- 扩容大小为原有的1.5倍
- 没有任何锁机制,多线程情况下线程不安全
- 底层是基于数组
- 查找效率高,但是新增和删除效率低
ArrayList和CopyOnWriteArrayList区别
CopyOnWriteArrayList线程安全,jdk1.8使用到ReentrantLock来保证线程安全问题,默认构造方法是创建一个0长度的数组。每次新增操作都是获取到CopyOnWriteArrayList内部维护的数组,也就是原有数组再原有长度加一的情况下,对新的数组进行数组最后一位的赋值,然后将新数组赋值到CopyOnWriteArrayList内部维护的数组中。而CopyOnWriteArrayList中读取操作并没有上锁。所以读读并发,读写并发,写写互斥,但是是弱一致。
CopyOnWriteArrayList源码解读
add()新增方法
public boolean add(E e) {
// 上锁操作,保证写操作的原子性
final ReentrantLock lock = this.lock;
lock.lock();
try {
// getArray()方法是获取到内部维护的数组
Object[] elements = getArray();
// 获取到长度
int len = elements.length;
// 通过Arrays工具类的copyOf()方法复制出一个新的数组,并且长度+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 新数组的最后一位的赋值
newElements[len] = e;
// 将新数组赋值到内部维护的数组中
setArray(newElements);
return true;
} finally {
// ReentrantLock手动释放锁
lock.unlock();
}
}
可以看到add()方法通过ReentrantLock保证新增的原子性,并且是在原有数组的基础上长度+1复制出的一个新的数组,通过操作新的数组,然后将新的数组赋值到内部维护的数组中完成的一个新增操作。
add()新增重载方法
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();
}
}
指定索引位置添加元素。
就不逐行注释讲解,大概就是先判断规定的索引位置是否超过数组长度,超过就抛异常,如果没超过就再判断是否是数组长度最后一位的索引,这代表是本次添加是需要扩容一位,因为数组长度是从1开始,而数组的索引是从0开始,如果不是最后一位就将原有数组的索引位置前所有的内容复制到新数组,再将原有数组索引位置后所有的内容添加到新数组的索引位置+1位置的后面,然后再将需要新增的元素添加到新数组的索引位置,然后将新数组赋值给内部维护的数组。
考虑到博主的语言描述能力可能欠佳,所以特意画图,如下图所示。
remove()移除方法
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)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
因为写写互斥,所以remove()方法肯定是上锁的。
这里其实没啥好讲的,跟add()方法的重载方法挺相似的,如果删除的元素是原有数组的非最后一位也是通过一个新数组将删除元素的索引前所有元素复制到新数组,将删除元素索引+1后的所有元素复制到新数组,然后新数组赋值到内部维护的数组中。
再看到重载的remove()方法
public boolean remove(Object o) {
// 获取到内部的数组
Object[] snapshot = getArray();
// 获取到元素的索引
int index = indexOf(o, snapshot, 0, snapshot.length);
// 如果没获取到是为-1,获取到了就走remove()重载方法,也就是下面的方法
return (index < 0) ? false : remove(o, snapshot, index);
}
private boolean remove(Object o, Object[] snapshot, int index) {
// 上锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 这里要注意到上面的remove()方法并没有上锁,可能获取到的数组并不是最新的,所以这里重新获取一次
Object[] current = getArray();
// 最新数组的长度
int len = current.length;
// 能进到if里面就代表2个数组不一致。
if (snapshot != current) findIndex: {
// 这里我们要思考,索引值和最新数组长度为什么要取最小的?
// 因为在我们上面remove()重载方法获取到的索引可能是之前的,可能经过了新增或者是删除或者是修改,索引位置已经发生了变化。
// 因为能进来这里肯定是数组已经发生了变化,这里真的写的很妙,
// 其实细心的小伙伴可能已经发现完全可以直接使用indexOf()方法直接来再次查找位置。
// 而这里是先获取到最小的来for循环来尝试获取到最新索引位置,再通过几个if来获取,
// 而直接用indexOf()的效率太低了,indexOf()是直接从头到位for循环遍历。
int prefix = Math.min(index, len);
// 这里是遍历最小的,因为把查询的步骤拆分了,如果这个循环没有查到就走下面的逻辑继续查找,如果查到了效率就比使用indexOf()的效率高出很多
for (int i = 0; i < prefix; i++) {
// 因为能进来就代表数组已经变化,
// 这里的意义就是顺序打乱了,打乱可能是删除、新增、修改
// 如果旧的和新的数组下表的元素相等就没必要获取,如果都没打乱就可以通过下面的if来获取
// 如果打乱了,并且打乱的下标的元素和当前删除的元素相等就直接返回打乱下标。
if (current[i] != snapshot[i] && eq(o, current[i])) {
index = i;
break findIndex;
}
}
// 索引值已经大于新的数组长度,就代表remove()掉了索引下标之前的元素。
if (index >= len)
return false;
// 这里就代表索引值下标的元素并没有移动也没有被修改。
if (current[index] == o)
break findIndex;
// 这里是通过前面的方式没获取到,可能是在remove()的时候,并发添加了新元素在索引值前面,
// 造成了我目前要获取的索引值可能已经大于前面for循环的范围了,所以直接从老索引值开始遍历到新数组长度就行。
index = indexOf(o, current, index, len);
// 如果为-1就是没遍历到,可能被修改了,也又可能已经被remove了。
if (index < 0)
return false;
}
// 下面的操作就没必要讲解了,常规操作。
Object[] newElements = new Object[len - 1];
System.arraycopy(current, 0, newElements, 0, index);
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
讲解看代码块中的注释。并且一定要自己思考。
真的写的太细了,为什么重载的remove()方法中没有直接indexOf()呢?
肯定是效率啊!完全可以把indexOf()的操作拆分,如果运气好可以用很少次判断就获取到最新的索引位置,这也是一种写代码的思想,拆分复杂的代码提高效率。
set()修改方法
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();
}
}
这里比较简单,就不逐行注释了,大致讲解一下。
获取到内部维护的数组以后,再通过index索引获取到元素,再判断元素是否一致,一致的话就直接返回,如果不一致的话就复制一个数组,并且将索引位置元素替换成传进来的元素。
但是这里还是有一点值得学习的,换成我们来设计可能就是直接替换索引位置的元素,而这里做了一个判断。如果在修改的时候,修改的元素跟下标原元素一致的话效率会高出很多。
get()读取方法
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}
获取方法没啥好讲的了,并没有上锁,所以是读写并发,读读并发,写写互斥。
CopyOnWriteArrayList为什么弱一致性
增删改查都追完了,我们现再思考一个问题。
读写并发,读得代码并没有上锁,会不会get()方法中获取到的数组是原来的数组,可能获取下标元素的过程中,其他线程进行了写、修改、删操作呢? 如下图所示
因为get()方法是通过获取到当前内部维护的数组再通过get()方法重载从数组中通过索引下标获取到值,所以会出现get获取到的数据并不是最新的数据。
有得有舍,想要考虑并发效率肯定是要丢失一定的一致性,看业务场景来决定技术选型。
总结
比起Spring那些框架来说,这些源码还是比较简单,但是得花时间来思考,多去思考为什么要这样设计,学习其中的思想,因为万变不离其宗,变的是代码,不变的是思想!
最后如果帖子对您有帮助,麻烦点赞+关注不迷路,一直在更新源码帖!