CopyOnWriteArrayList源码解读——CopyOnWrite思想的利与弊

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

}

}

2、add(int index, E element)在指定位置添加元素

如何在指定位置添加元素呢?通过index将原数组分成两半复制:

  • 先从0开始复制旧数组index个元素到新数组的0至index-1位置。

  • 然后从index复制旧数组numMoved(len - index)个元素到新数组的index+1至最后位置。

  • 新数组空出的index位置,放置新元素。

  • 最后将新数组赋值给旧数组。

index位置添加新元素,二次复制,相当于旧数组从index位开始元素纷纷向后移动一位。

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)

// numMoved=0 即是在数组的最后加入element

newElements = Arrays.copyOf(elements, len + 1);

else {

newElements = new Object[len + 1];

// elements 从0开始拷贝元素至 newElements(从0开始),length=index

System.arraycopy(elements, 0, newElements, 0, index);

// elements 从index开始拷贝元素至 newElements(从index+1开始),length=numMoved

System.arraycopy(elements, index, newElements, index + 1,

numMoved);

}

// newElements的index位置留给新元素element

newElements[index] = element;

// newElements 赋值给 旧数组array

setArray(newElements);

} finally {

lock.unlock();

}

}

3、set(int index, E element)替换指定位置元素

  • 找到旧元素指定位置的元素与新元素比较,不相等则复制替换。

  • 新旧元素相等,则不替换,但是也要进行setArray赋值操作,为的是确保volatile写语意。

public E set(int index, E element) {

final ReentrantLock lock = this.lock;

lock.lock();

try {

Object[] elements = getArray();

E oldValue = get(elements, index);

// oldValue和element不相等,进行替换

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

// 二者相等,不是完全没有操作;确保volatile写语义,volatile write happen before read

setArray(elements);

}

return oldValue;

} finally {

lock.unlock();

}

}

4、addIfAbsent(E e)添加元素时判断元素是否存在

addIfAbsent(E e)操作是两个操作,先判断数组中是否有新元素,有则返回false不添加,无则继续添加逻辑。添加逻辑是有锁线程安全的,而判断元素是否存在是不安全的,如何保证这两个操作加起来线程安全呢?答案是double check

在单例模式-懒汉式中double check思想非常典型,先查该单例是否存在,存在直接返回,不存在,加锁,二次判断单例是否存在,存在则返回,不存在则新建赋值。

addIfAbsent(E e)运用了同样的思想:

  • 首先获取原数组快照,判断新增元素是否存在,存在则返回false。(first check)

  • 新增元素不存在,则继续添加逻辑addIfAbsent(e, snapshot)

  • addIfAbsent(e, snapshot)方法是加锁的,获取锁后首先判断当前数组和快照是否相等,相等则说明数组没有改动,可以直接进行新增元素的操作。如果有修改,则需要判断当前数组中是否有新增元素,有则返回false,无则新增。(second check)

public boolean addIfAbsent(E e) {

// 获取数组快照

Object[] snapshot = getArray();

return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :

addIfAbsent(e, snapshot);

}

private boolean addIfAbsent(E e, Object[] snapshot) {

final ReentrantLock lock = this.lock;

lock.lock();

try {

Object[] current = getArray();

int len = current.length;

// 快照与当前数组比较,double-check,

// 这里1.7和1.8不同,1.8对1.6进行了优化

// 看源码时需要对比不同版本的差异

if (snapshot != current) {

// 数组可能已经被修改,

// Optimize for lost race to another addXXX operation

// remove common=len

// add common=snapshot.length 先检查current中前snapshot.length个,然后检查新增的

int common = Math.min(snapshot.length, len);

for (int i = 0; i < common; i++)

if (current[i] != snapshot[i] && eq(e, current[i]))

return false;

if (indexOf(e, current, common, len) >= 0)

return false;

}

Object[] newElements = Arrays.copyOf(current, len + 1);

newElements[len] = e;

setArray(newElements);

return true;

} finally {

lock.unlock();

}

}

注意:java1.8之前的addIfAbsent(E e)和java8之后的代码有些许不同,以下摘自java7源码,java7中addIfAbsent(E e)并没有使用快照和double check的思想,而是直接将两个操作加锁,虽然保证了线程安全,但是因为方法一上来就加锁,性能比较。

所以java8中对其进行了优化,加了double check思想,第一次判断元素要是在数组中就不进行添加操作,也就不会加锁;而第二次判断,是先判断快照地址和当前数组地址,地址判断当然比遍历数组性能要高了。

public boolean addIfAbsent(E e) {

final java.util.concurrent.locks.ReentrantLock lock = this.lock;

lock.lock();

try {

// Copy while checking if already present.

// This wins in the most common case where it is not present

Object[] elements = getArray();

int len = elements.length;

Object[] newElements = new Object[len + 1];

for (int i = 0; i < len; ++i) {

if (eq(e, elements[i]))

return false; // exit, throwing away copy

else

newElements[i] = elements[i];

}

newElements[len] = e;

setArray(newElements);

return true;

} finally {

lock.unlock();

}

}

5、remove(int index)删除指定位置元素

删除中间位置的元素,需要将index后面的元素前移填补空缺,同样使用分两半复制的方式达到删除元素并前移的效果。

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)

// numMoved == 0 即是删除数组的最后一个元素,则不需要移动其他元素。

setArray(Arrays.copyOf(elements, len - 1));

else {

// 新数组 length= len-1

Object[] newElements = new Object[len - 1];

System.arraycopy(elements, 0, newElements, 0, index);

// 丢掉旧数组index位置的元素,达到删除的效果

System.arraycopy(elements, index + 1, newElements, index,

numMoved);

setArray(newElements);

}

return oldValue;

} finally {

lock.unlock();

}

}

6、remove(Object o)通过元素值删除元素

通过元素值删除元素,需要先查询数组中是否有该元素,无则不做操作,有则继续走删除逻辑,同样使用了double check的思想。

public boolean remove(Object o) {

// 获取旧数组快照

Object[] snapshot = getArray();

// 判断需要删除的元素是否在数组中,不在则直接返回false,是则继续删除操作

int index = indexOf(o, snapshot, 0, snapshot.length);

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 {

Object[] current = getArray();

int len = current.length;

// double check

// 快照与当前数组进行比较,不相等说明数组已经被其他线程修改

if (snapshot != current) findIndex: {

int prefix = Math.min(index, len);

// 遍历查找当前数组中是否有需要删除的元素

for (int i = 0; i < prefix; i++) {

if (current[i] != snapshot[i] && eq(o, current[i])) {

index = i;

// 有则结束判断

break findIndex;

}

}

// 上面找了一遍没有找到

// index >= len 说明上面查找的是当前整个数组,需要删除的元素已经被修改

if (index >= len)

return false;

// 当前数组变长了,current index位置的元素依然是需要删除的元素,停止判断

if (current[index] == o)

break findIndex;

// 走到这了数组变长了,index位置的元素也已经被删除,但是不代表其他线程新增的元素没有需要删除的元素,继续判断

index = indexOf(o, current, index, len);

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(资料价值较高,非无偿)

img

面试结束复盘查漏补缺

每次面试都是检验自己知识与技术实力的一次机会,面试结束后建议大家及时总结复盘,查漏补缺,然后有针对性地进行学习,既能提高下一场面试的成功概率,还能增加自己的技术知识栈储备,可谓是一举两得。

以下最新总结的阿里P6资深Java必考题范围和答案,包含最全MySQL、Redis、Java并发编程等等面试题和答案,用于参考~

重要的事说三遍,关注+关注+关注!

历经30天,说说我的支付宝4面+美团4面+拼多多四面,侥幸全获Offer

image.png

更多笔记分享

历经30天,说说我的支付宝4面+美团4面+拼多多四面,侥幸全获Offer

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
sdnimg.cn/images/e5c14a7895254671a72faed303032d36.jpg" alt=“img” style=“zoom: 33%;” />

面试结束复盘查漏补缺

每次面试都是检验自己知识与技术实力的一次机会,面试结束后建议大家及时总结复盘,查漏补缺,然后有针对性地进行学习,既能提高下一场面试的成功概率,还能增加自己的技术知识栈储备,可谓是一举两得。

以下最新总结的阿里P6资深Java必考题范围和答案,包含最全MySQL、Redis、Java并发编程等等面试题和答案,用于参考~

重要的事说三遍,关注+关注+关注!

[外链图片转存中…(img-cge44XL5-1711589549830)]

[外链图片转存中…(img-p20MMBS5-1711589549831)]

更多笔记分享

[外链图片转存中…(img-VhHNi9xZ-1711589549831)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值