CopyOnWriteArrayList中的set方法随记

本文深入探讨了CopyOnWriteArrayList中set方法的工作原理,分析了其内部锁机制及volatile关键字的作用,解释了为何在元素未改变时仍调用setArray方法的原因,并讨论了这种实现方式对并发读写操作的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近一直在看并发编程网,这篇文章先记录下这个地方的理解。

上下文环境移步CopyOnWriteArrayList类set方法疑惑?

[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. /** The array, accessed only via getArray/setArray. */  
  2. private volatile transient Object[] array;  
  3.   
  4. /** 
  5.  * Replaces the element at the specified position in this list with the 
  6.  * specified element. 
  7.  * 
  8.  * @throws IndexOutOfBoundsException {@inheritDoc} 
  9.  */  
  10. public E set(int index, E element) {  
  11.     final ReentrantLock lock = this.lock;  
  12.     lock.lock();  
  13.     try {  
  14.         Object[] elements = getArray();  
  15.         E oldValue = get(elements, index);  
  16.   
  17.         if (oldValue != element) {  
  18.             int len = elements.length;  
  19.             Object[] newElements = Arrays.copyOf(elements, len);  
  20.             newElements[index] = element;  
  21.             setArray(newElements);  
  22.         } else {  
  23.             // Not quite a no-op; ensures volatile write semantics  
  24.             setArray(elements);  
  25.         }  
  26.         return oldValue;  
  27.     } finally {  
  28.         lock.unlock();  
  29.     }  
  30. }  
  31.   
  32. /** 
  33.  * Sets the array. 
  34.  */  
  35. final void setArray(Object[] a) {  
  36.     array = a;  
  37. }  
  38.   
  39. /** 
  40.  * Gets the array.  Non-private so as to also be accessible 
  41.  * from CopyOnWriteArraySet class. 
  42.  */  
  43. final Object[] getArray() {  
  44.     return array;  
  45. }  

可以看见set方法中else判断中看起来似乎有个空操作setArray,将读取的数组又塞回来赋值。

好像这个地方有没有不管对于单线程还是多线程对elements的结果或者读取的element都不造成影响,看了很久才理解一点那么个意思。

这里注意注释中说明,保证volatile写语义,要理解这句话得先理解java中的指令重排和happens before原则。

并发编程网也有很好的几篇文章解释,详细内容移步happens-before俗解有关“双重检查锁定失效”的说明

这里调用setArray的话主要保证了对于下列情况,set方法happens before于get方法。

线程1线程2
1a = 1;
2list.set(1,"t");
1list.get(0);
2int b = a;
假如set方法中没有那么一句话,而list[1]原来就是"t",那么这里可能由于指令重排,导致list.get(0)获得是t,而b得到的却不是1.


然后是这里为什么要将引用指向clone的一个新对象?

对于在set之前得到指向老对象引用的不进行干扰,包括getArray和iterator等,而且CopyOnWriteArrayList在大多为读操作时才使用,写操作性能较差。发布一个不可变对象,是不需要进一步的同步操作,这是CopyOnWrite的核心思路。


上面双重检查锁定失效的问题这里也提一下,主要问题是有可能对象赋值给引用的指令和对象初始化赋值的指令重排序,导致getInstance拿到对象时查看到未成功初始化完的对象属性。如果对单例引用加上volatile修饰符,会保证完成引用赋值时对象构造函数的赋值完成。

延迟初始化单例的方式还有一种更好的思路,采用占位类方式来实现,当使用类的时候才会初始化,而且不会出现并发问题。

public class ResourceFactory{
    private static class ResourceHolder{
        public static Resource resource = new Resource();
    }

    public static Resource getResource(){
      return ResourceHolder.resource;
    }
}


感觉还有些细节需要再深入了解下,如果有理解不对的地方麻烦指出,多谢。

### CopyOnWriteArrayList 的 `simulate` 方法实现与用法 #### 什么是 CopyOnWriteArrayListCopyOnWriteArrayList 是 Java 中的一个线程安全的集合类,属于 `java.util.concurrent` 包的一部分。它的核心特性在于写操作时会创建底层数组的新副本[^1]。这种机制使得读操作可以无锁地进行,从而提高了并发性能。 尽管标准库中并没有提供名为 `simulate` 的方法,可以通过自定义方式来模拟其行为或者展示如何使用该类的一些典型功能。以下是关于其实现和使用的详细介绍: --- #### 自定义 `simulate` 方法示例 假设我们需要通过一个 `simulate` 方法演示 CopyOnWriteArrayList 的基本特性和优势,则可以按照如下方式进行设计: ```java import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; public class CopyOnWriteSimulator { public static void simulate(List<String> list) { System.out.println("Initial List: " + list); // 添加新元素并观察底层数组的变化 addElements(list); System.out.println("After Adding Elements: " + list); // 删除某些元素并验证一致性 removeElements(list); System.out.println("After Removing Elements: " + list); // 并发访问测试 concurrentAccessTest(list); } private static void addElements(List<String> list) { for (int i = 0; i < 5; i++) { String element = "Element-" + i; list.add(element); // 每次修改都会复制整个数组 } } private static void removeElements(List<String> list) { int size = Math.min(3, list.size()); // 防止越界 for (int i = 0; i < size; i++) { list.remove(i); // 移除指定索引位置的元素 } } private static void concurrentAccessTest(List<String> list) { Thread t1 = new Thread(() -> { for (String s : list) { System.out.println(Thread.currentThread().getName() + ": Reading " + s); } }); Thread t2 = new Thread(() -> { try { Thread.sleep(100); // 让另一个线程先运行一段时间 } catch (InterruptedException e) { e.printStackTrace(); } list.add("Concurrent Element"); }); t1.start(); t2.start(); try { t1.join(); // 等待线程结束 t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Final List After Concurrent Access: " + list); } public static void main(String[] args) { List<String> cowList = new CopyOnWriteArrayList<>(); simulate(cowList); } } ``` 上述代码展示了以下几个方面: - **初始状态打印**:显示列表初始化后的内容。 - **添加元素**:通过循环向列表中追加多个字符串项。 - **删除元素**:移除部分已存在的条目。 - **并发访问测试**:启动两个独立线程分别负责读取和修改数据结构的内容。 --- #### 工作原理分析 CopyOnWriteArrayList 的主要特点是基于不可变性的设计理念。每当发生任何结构性变更(如增加或删除),它并不会直接修改现有的内部数组,而是克隆一份新的实例作为替代。因此,在多线程环境下能够有效避免竞争条件的发生。 具体来说: - 所有迭代器都只作用于原始版本的数据集上,即使其他地方正在进行更新也不会受到影响。 - 对于频繁发生的查询请求而言效率较高;但对于大规模批量插入/删除动作则开销较大,因为每次都需要重新分配内存空间以及拷贝全部现有记录。 此外需要注意的是,由于每一次改动都要生成完整的备份,所以在高频率写入场景下可能会带来显著的资源消耗问题[^4]。 --- #### 应用场景举例 考虑到以上优缺点权衡之后,通常推荐将此容器应用于那些读远超写的场合之中。比如日志记录系统中的事件缓冲区管理就是一个很好的案例——外部模块不断提交新的追踪信息进来,而内部处理单元周期性抽取出来做进一步加工整理即可[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值