由CopyOnWriteArrayList类的set方法引发对volatile深入理解

本文深入探讨了CopyOnWriteArrayList中的setArray方法为何保留,即使未对元素进行修改。文章详细解析了volatile变量的内存语义,包括可见性和原子性,并通过具体示例解释了在不同场景下volatile读写操作的内存屏障插入策略。

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

 

转载自:http://ifeve.com/copyonwritearraylist-set/
              http://ifeve.com/java-memory-model-4/
              http://tech.meituan.com/java-memory-reordering.html

       在CopyOnWriteArrayList类的set方法中有一段setArray(elements)代码(else块),实际上这段代码并未对elements做任何改动,注意这里的,实现的volatile语意并不对CopyOnWriteArrayList实例产生任何影响,为什么还是要保留这行语句?

 

private transient volatile Object[] array;

final Object[] getArray() {
    return array;
}

final void setArray(Object[] a) {
    array = a;
}

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

       在这里首先理解下volatile,我们知道volatile变量自身具有下列特性:
       可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
       原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

       从JSR-133开始,volatile变量的写-读可以实现线程之间的通信。从内存语义的角度来说,volatile与锁有相同的效果。
       下面使用volatile变量的示例代码:

class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;                   //1
        flag = true;               //2
    }

    public void reader() {
        if (flag) {                //3
            int i =  a;           //4
            ……
        }
    }
}

       假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens before规则,这个过程建立的happens before关系可以分为两类:
       1. 根据程序次序规则,1 happens before 2; 3 happens before 4。
       2. 根据volatile规则,2 happens before 3。
       3. 根据happens before 的传递性规则,1 happens before 4。

volatile写-读的内存语义:
       volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。上面示例程序VolatileExample为例,假设线程A首先执行writer()方法,随后线程B执行reader()方法,初始时两个线程的本地内存中的flag和a都是初始状态。下图是线程A执行volatile写后,共享变量的状态示意图:
     
       如上图所示,线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值被刷新到主内存中。此时,本地内存A和主内存中的共享变量的值是一致的。

volatile读的内存语义
       当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
       下面是线程B读同一个volatile变量后,共享变量的状态示意图:
            
       如上图所示,在读flag变量后,本地内存B已经被置为无效。此时,线程B必须从主内存中读取共享变量。线程B的读取操作将导致本地内存B与主内存中的共享变量的值也变成一致的了。
       如果把volatile写和volatile读这两个步骤综合起来看的话,在读线程B读一个volatile变量后,写线程A在写这个volatile变量之前所有可见的共享变量的值都将立即变得对读线程B可见。

下面对volatile写和volatile读的内存语义做个总结:
       线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息。
       线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
       线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

那么JMM如何实现volatile写/读的内存语义?
      前文提到过重排序分为编译器重排序和处理器重排序,为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型。下面是JMM针对编译器制定的volatile重排序规则表:
      
       举例来说,在程序顺序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。
       从上表可以看出:
        • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
        • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
        • 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
       为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

       内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。
       内存屏障可以被分为以下几种类型:
          LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
          StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
          LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
          StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

       JSR-133的规定,Java编译器会这样使用内存屏障。下面是JMM内存屏障插入策略:
        • 在每个volatile写操作的前面插入一个StoreStore屏障。
        • 在每个volatile写操作的后面插入一个StoreLoad屏障。
        • 在每个volatile读操作的后面插入一个LoadLoad屏障。
        • 在每个volatile读操作的后面插入一个LoadStore屏障。
        • 为了保证final字段的特殊语义,也会在下面的语句加入内存屏障。x.finalField = v; StoreStore; sharedRef = x;
       上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。关于JMM主内存与工作内存交互,可以参考:http://blog.csdn.net/zero__007/article/details/53025425
       下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图:
       
       上图中的StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。volatile写后面的StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面,是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)。
       为了保证能正确实现volatile的内存语义,JMM在这里采取了保守策略:在每个volatile写的后面或在每个volatile读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,JMM选择了在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile 变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。
       下面是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图:
       
       上图中的LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
       上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。下面通过具体的示例代码来说明:

class VolatileBarrierExample {
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite() {
        int i = v1;           //第一个volatile读
        int j = v2;           // 第二个volatile读
        a = i + j;            //普通写
        v1 = i + 1;          // 第一个volatile写
        v2 = j * 2;          //第二个 volatile写
    }

    …                    //其他方法
}

       针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化:
       
       注意,最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写,为了安全起见,编译器常常会在这里插入一个StoreLoad屏障。

       在JSR-133之前的旧Java内存模型中,虽然不允许volatile变量之间重排序,但旧的Java内存模型允许volatile变量与普通变量之间重排序。在旧的内存模型中,VolatileExample示例程序可能被重排序成下列时序来执行:
       
       在旧的内存模型中,当1和2之间没有数据依赖关系时,1和2之间就可能被重排序(3和4类似)。其结果就是:读线程B执行4时,不一定能看到写线程A在执行1时对共享变量的修改。
       因此在旧的内存模型中 ,volatile的写-读没有锁的释放-获所具有的内存语义。为了提供一种比锁更轻量级的线程之间通信的机制,JSR-133增强 volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取一样,具有相同的内存语义。从编译器重排序规则和处理器内存屏障插入策略来看,只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语意,这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止。


       那么回到之前的问题,未对elements做任何改动为什还要在else块中调用setArray(elements);是否是多余呢?
       如下例子:a为非volatile的某基本类型变量,coal为CopyOnWriteArrayList对象,

thread1:
x:a = calValue;
y:coal.set….
———————
thread2:
m:coal.get…
n:int tmp = a;

       假设存在以上场景,如果能保证只会存在这样的轨迹:x,y,m,n.根据约定有happen-before(y,m),根据线程内的操作相关规定有happen-before(x,y), happen-before(m,n),根据happen-before的传递性读写a变量就有happen-before(x,n),所以thread1对a的写操作对thread2中a的读操作可见。如果CopyOnWriteArrayList的set的else里没有setArray(elements) 的话,before(y,m)就不再有了,上述的可见性也就无法保证。
       所以实际上else中的setArray(elements)不是为了保证CopyOnWriteArrayList本身的可见性,而是保证外部的非volatile变量的happen-before。可以参考http://stackoverflow.com/questions/28772539/why-setarray-method-call-required-in-copyonwritearraylist。

相关阅读:http://blog.csdn.net/zero__007/article/details/44080975

<think>嗯,用户现在想了解CopyOnWriteArrayList的底层原理和常用方法。之前用户问过List的常用实现,我详细介绍了ArrayList、LinkedList这些,还提到了Vector和Stack这些线程安全的替代方案,其中就包括CopyOnWriteArrayList。看来用户对线程安全集合感兴趣,可能是在处理并发问题。 用户提供的引用材料挺有用。引用[3]提到Vector被淘汰,推荐用CopyOnWriteArrayList替代;引用[2]提到Set的实现基于Map,但CopyOnWriteArraySet例外(它底层是CopyOnWriteArrayList),这侧面印证了CopyOnWrite系列的重要性。用户没有直接问并发问题,但突然聚焦这个,大概率是遇到了实际开发中的并发修改异常。 需要重点讲清楚它的写时复制机制——写操作加锁复制新数组这个核心点。用户可能混淆它和普通ArrayList的区别,得强调“读写分离”的特点。引用[5]提到的CAS原子操作虽然相关,但CopyOnWriteArrayList用的是独占锁(ReentrantLock),这点要区分开。 内存消耗问题也得提。引用[4]的LinkedList和ArrayList内存对比示例正好用上,可以比说明CopyOnWriteArrayList频繁复制带来的内存开销。用户没问性能,但实际使用中O(n)的写入开销是硬伤,需要预警。 方法介绍部分要突出线程安全场景的专用API,比如带锁的addIfAbsent。用户可能从ArrayList转过来,要提醒他们不能用普通迭代器做删除操作——引用[1]里详细解释过ArrayList遍历删除的ConcurrentModificationException问题,而CopyOnWriteArrayList用快照迭代器解决了这个痛点。 最后考虑设计相关问题。用户显然在探索Java并发集合,可以引导到CopyOnWriteArraySet或对比ConcurrentHashMap——毕竟引用[2]提到Set通常基于Map,但CopyOnWriteArraySet却是基于CopyOnWriteArrayList的特殊案例。</think>### CopyOnWriteArrayList 底层原理与常用方法详解 #### 一、底层实现原理 **核心机制:写时复制(Copy-On-Write)** 1. **数据结构** - 内部通过 `volatile Object[] array` 存储数据,保证多线程可见性[^3]。 - 所有修改操作(增删改)会创建新数组副本,避免直接修改原始数组[^1]。 2. **写操作流程** ```java 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(); } } ``` - **加锁保护**:通过 `ReentrantLock` 保证写操作的原子性[^3]。 - **复制扩容**:每次修改创建新数组(长度+1),旧数组保持不变。 - **引用切换**:`setArray()` 用 `volatile` 保证新数组对其他线程立即可见。 3. **读操作特性** - 无锁直接访问数组:`get(int index)` 直接返回当前数组元素。 - 读操作可能访问到旧数组数据,但保证最终一致性。 #### 二、适用场景与优缺点 | **场景** | **优点** | **缺点** | |-------------------------|-----------------------------------|-----------------------------------| | 读多写少(如监听器列表)| 读操作完全无锁,性能极高 | 写操作需复制数组,性能差($O(n)$)| | 弱一致性需求 | 避免读写冲突,线程安全 | 内存占用高(频繁复制数组)[^4] | | 遍历操作频繁 | 迭代器使用快照,避免`ConcurrentModificationException` | 数据实时性弱(可能读到旧数据) | #### 三、常用方法详解 1. **写操作方法**(均加锁) - `add(E e)`:追加元素,触发数组复制。 - `add(int index, E element)`:指定位置插入,复制两侧元素。 - `set(int index, E element)`:修改指定位置元素。 - `remove(int index)`:删除元素,创建长度-1的新数组。 2. **读操作方法**(无锁) - `E get(int index)`:直接返回数组元素。 - `int size()`:返回当前数组长度。 - `boolean contains(Object o)`:遍历检查存在性。 3. **线程安全增强方法** - `boolean addIfAbsent(E e)`:元素不存在时才添加(避免重复)。 - `int addAllAbsent(Collection<? extends E> c)`:添加集合中所有不存在的元素。 4. **迭代器特性** ```java Iterator<String> it = list.iterator(); while (it.hasNext()) { System.out.println(it.next()); // 基于创建时的数组快照 // 支持遍历中删除元素? ❌ 迭代器不支持remove() } ``` - 迭代器基于调用时的数组快照,遍历期间不受写操作影响。 - **不支持** `remove()` 操作(抛出 `UnsupportedOperationException`)。 #### 四、与 Vector 的对比 | **特性** | `CopyOnWriteArrayList` | `Vector` | |------------------|----------------------------------|------------------------| | 锁机制 | 写操作使用 `ReentrantLock` | 全方法 `synchronized` | | 并发读 | 完全无锁,性能极高 | 读操作也需同步 | | 内存占用 | 写操作复制整个数组,内存开销大[^4] | 动态扩容,内存更友好 | | 数据一致性 | 弱一致性(读可能滞后) | 强一致性 | > **示例场景**:适合存储事件监听器列表(极少修改,频繁遍历)。若用于高频写入场景(如实时计数器),推荐改用 `ConcurrentLinkedQueue` 或原子[^5]。 --- ### 总结 `CopyOnWriteArrayList` 通过**写时复制+读写分离**实现线程安全,在**读多写少**场景下性能卓越,但需警惕内存开销和弱一致性问题。其迭代器快照机制彻底规避了遍历时的并发修改异常,是替代传统同步集合(如 `Vector`)的现代解决方案[^3][^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值