CopyOnWriteArrayList 写时复制

1、从String类的不可变性实现说起

  • Immuability模式:对象创建之后,状态就不再变化
  • String是不可变类,本质上是个字符数组

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/*省略其他方法和属性*/
public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; 
            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                // 返回的实际是一个新的 String对象(新的字符数组)
                return new String(buf, true);
            }
        }
        return this;
    }
}

2、COW的应用领域

  • OS:
    • 类Unix OS下, 传统的 fork()会创建父进程的一个完整副本,比如父进程地址空间用到1G内存,fork()时要复制父进程整个地址空间(1G)给子进程。耗时耗内存。
    • Linux下,fork()时,不复制整个进程空间,而让父子进程共享同一个地址空间;只用在父进程或子进程要写入的时候才复制地址空间,使得父子进程有各自地址空间。
    • 父子地址空间、数据总归要隔离,用COW体现的是延时策略,即“真正需要复制的时候才复制,而不是提前复制好”。
  • Java中COW
    • CopyOnWriteArrayList
    • CopyOnWriteArraySet
    • CopyOnWriteArrayList 支持无锁并发读,极大提升了读性能,但以内存复制为代价。
  • FunctionProgramming
    • 函数式编程基础 --> 不可变性,函数式编程中所有修改都基于 COW
  • Docker容器image
  • Git 等

3、CopyOnWriteArrayList 与 CopyOnWriteArraySet的场景

  • COW容器在修改时会复制整个数组,因此,若容器经常被修改或数组本身很大,不适用
  • 适用于【读多写少、元素少、对读性能要求高、能容忍短暂脏读的场景】

4、一个案例

描述:一个RPC框架,Provder分实例部署,Consumer调用有一个LB的过程(见图)
[外链图片转存失败(img-MlsGUVvl-1562753498483)(http://note.youdao.com/yws/res/39437/WEBRESOURCEae57d998d2f7b3184b9dfd2a6033d4a5)]

RPC框架核心任务之一:维护服务路由关系,当Provider上线或者下线后,需要更新Consumer的路由表,简化后如图:
[外链图片转存失败(img-h1F564l7-1562753498483)(http://note.youdao.com/yws/res/39445/WEBRESOURCE80386051d3c7801a9c03885b97ca3c4c)]

实现时的思考:

  • 每次RPC调用都要调用LB算Provider 的IP+Port,LB需要通过路由表拿到所有接口路由。路由表的读性能很关键。
  • 路由表对数据一致性要求不高。Provider从上线到反馈到路由表中,即使要 5s 也能接受。
  • 路由表是典型【读多写少】
  • 关键词: 【对读性能要求高,读多写少、弱一致性】、CopyOnWriteArrayListCopyOnWriteArraySet 天生合适。。

再来看 Router 的设计:

  • 思路1:更新Router的状态位来标识,则访问状态位的地方要同步访问,影响性能
  • 思路2:采用Immuability 模式,每次上线、下线,创建、删除新的Router对象。
  • 上下线频率低,思路2 较合适。
// 路由信息
public final class Router{
  private final String  ip;
  private final Integer port;
  private final String  iface;
  // 构造函数
  public Router(String ip, 
      Integer port, String iface){
    this.ip = ip;
    this.port = port;
    this.iface = iface;
  }
  // 重写 equals 方法
  public boolean equals(Object obj){
    if (obj instanceof Router) {
      Router r = (Router)obj;
      return iface.equals(r.iface) &&
             ip.equals(r.ip) &&
             port.equals(r.port);
    }
    return false;
  }
  public int hashCode() {
    // 省略 hashCode 相关代码
  }
}
// 路由表信息
public class RouterTable {
  //Key: 接口名
  //Value: 路由集合
  ConcurrentHashMap<String, CopyOnWriteArraySet<Router>> 
    rt = new ConcurrentHashMap<>();
  // 根据接口名获取路由表
  public Set<Router> get(String iface){
    return rt.get(iface);
  }
  // 删除路由
  public void remove(Router router) {
    Set<Router> set=rt.get(router.iface);
    if (set != null) {
      set.remove(router);
    }
  }
  // 增加路由
  public void add(Router router) {
    Set<Router> set = rt.computeIfAbsent(
      route.iface, r -> 
        new CopyOnWriteArraySet<>());
    set.add(router);
  }
}

5、部分关键源码

并发容器中,实现List接口的只有 CopyOnWriteArrayList
原理简单说:

CopyOnWriteArrayList内维护一个数组,成员变量 array --> 内部数组,所以读基于该 array 。迭代器 遍历的就是该array。
[外链图片转存失败(img-a2wLhqUB-1562753498483)(http://note.youdao.com/yws/res/39468/WEBRESOURCE165e7577013621c1c8875b7cadd132e8)]

假设遍历 array 时,有一个写操作(加元素),CopyOnWriteArrayList会:

  • 1) 将 array复制一份
  • 2)在新复制数组上增加元素
  • 3)增加完元素后,将 array 指向该新数组

本质上,读写是并行的,遍历基于原来的array,写 基于新array。
[外链图片转存失败(img-DxHNMKfZ-1562753498484)(http://note.youdao.com/yws/res/39475/WEBRESOURCEf0c9811fba6e3712b2e4f129a3a3bd2d)]

要注意:

  • 1)CopyOnWriteArrayList适用于写很少的场景,而且容忍读写短暂不一致。新加的元素可能不被很快遍历到
  • 2)CopyOnWriteArrayList 的迭代器只读(本质是个快照),不支持增删改。

CopyOnWriteArrayList源码:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

    // ************删除无关代码****************
    // *** get()源码
    // Positional Access Operations
    @SuppressWarnings("unchecked")
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

    //**** add() 源码
    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    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();
        }
    }

  }	
  
/**
 * 该API返回该list的快照
 */
public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

/**
 内部实现了 COWIterator 。
**/
static final class COWIterator<E> implements ListIterator<E> {
        /** 全局变量 array 的快照 */
        private final Object[] snapshot;
        /** 游标 */
        private int cursor;

        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }

        public boolean hasNext() {
            return cursor < snapshot.length;
        }

        public boolean hasPrevious() {
            return cursor > 0;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }

        @SuppressWarnings("unchecked")
        public E previous() {
            if (! hasPrevious())
                throw new NoSuchElementException();
            return (E) snapshot[--cursor];
        }

        public int nextIndex() {
            return cursor;
        }

        public int previousIndex() {
            return cursor-1;
        }

        /**
         remove 时会抛出异常
         */
        public void remove() {
            throw new UnsupportedOperationException();
        }

        /**
         * Set 时会抛出异常.
         */
        public void set(E e) {
            throw new UnsupportedOperationException();
        }

        /**
         add()时会抛出异常
         */
        public void add(E e) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void forEachRemaining(Consumer<? super E> action) {
            Objects.requireNonNull(action);
            Object[] elements = snapshot;
            final int size = elements.length;
            for (int i = cursor; i < size; i++) {
                @SuppressWarnings("unchecked") E e = (E) elements[i];
                action.accept(e);
            }
            cursor = size;
        }
    }

思考下:为啥没CopyOnWriteLinkedList?

<think>首先,用户的问题是:“我想了解CopyOnWriteArrayList的读分离实现方式请问CopyOnWriteArrayList分离实现原理Java并发”。我需要基于提供的引用内容来回答。关键引用:-引用[1]:提到CopyOnWriteArrayList是基于CopyOnWrite机制实现的,读操作快,操作慢。-引用[2]:说明CopyOnWriteArrayList是线程安全的,读操作无锁,操作通过创建底层数组的新副本来实现。-引用[3]:强调CopyOnWriteArrayList是读分离的并发策略。-引用[4]和[5]:讨论了弱一致性,并提供了一个示例代码,展示了迭代器在操作后可能读取旧数据。用户的问题焦点是“读分离实现原理”。我需要解释CopyOnWriteArrayList如何实现读分离。###回答结构-**介绍**:简要介绍CopyOnWriteArrayList和其目的。-**读分离原理**:详细解释读操作和操作的处理方式。-读操作:无锁,直接访问数组。-操作:创建新副本,修改副本,然后原子性地替换原数组。-**弱一致性**:解释为什么迭代器是弱一致性的,并引用示例。-**适用场景**:基于引用,提到读多少的场景。-**代码示例**:提供简单的Java代码来演示原理。-**相关问题**:根据系统指令,生成相关问题。###确保格式-行内数学表达式用$...$(但在这个主题中可能不常用,除非涉及公式)。-独立公式用$$...$$(同样,可能不适用)。-使用中文回答。-添加引用标识:例如,[^1]。-生成相关问题。###内容大纲1.**介绍**:CopyOnWriteArrayListJava并发包中的一个线程安全列表,基于读分离机制。2.**读分离实现**:-读操作:直接访问底层数组,不需要锁,因此高效。-操作:当修改列表(如add,remove),会创建一个新数组的副本,在新副本上进行修改,然后原子性地将引用指向新数组。这确保操作不影响读操作。-使用volatile数组引用来保证可见性。3.**弱一致性**:迭代器在创建会捕获当前数组的快照,因此即使在迭代过程中列表被修改,迭代器也不会看到新变化,这可能导致弱一致性(引用[4]和[5])。4.**优点和缺点**:-优点:读操作高效,无锁;操作不阻塞读。-缺点:操作开销大,因为需要复制数组;内存占用高。5.**适用场景**:读多少的场景,如缓存或配置管理(引用[1])。6.**代码示例**:提供简化版代码或伪代码来演示原理。7.**相关问题**:生成2-3个相关问题。###生成相关问题基于系统指令:1.CopyOnWriteArrayList的弱一致性如何影响并发编程?2.CopyOnWriteArrayList和Vector在性能上有什么区别?3.CopyOnWriteArrayList在哪些实际应用中常用?###引用处理在回答中,自然地添加引用标识:-当提到读操作快:[^1]-当解释操作:[^2],[^3]-当讨论弱一致性:[^4],[^5]###完整回答-用中文。-结构清晰:分段落。-确保真实可靠:基于提供的引用。</think>### CopyOnWriteArrayList 的读分离实现原理 CopyOnWriteArrayListJava 并发包(`java.util.concurrent`)中的一个线程安全列表类,它通过读分离机制实现高效并发访问。核心思想是将读操作和操作解耦:**读操作无锁且直接访问数据,操作则通过创建底层数组的新副本来执行修改**,确保线程安全。下面基于 Java 并发机制逐步解析其实现原理。 #### 1. **读分离的核心机制** - **读操作(无锁访问)**: - CopyOnWriteArrayList 内部维护一个 volatile 数组(如 `volatile Object[] array`),所有读操作(如 `get()`、`size()`)直接访问该数组。由于数组引用被声明为 `volatile`,保证了多线程下的可见性,读操作无需加锁即可获取最新数据[^2][^3]。 - 示例代码:读操作简单高效。 ```java public E get(int index) { return (E) array[index]; // 直接访问数组,无锁 } ``` - **操作(副本复制)**: - 当执行操作(如 `add()`、`remove()`、`set()`),会创建一个底层数组的新副本(拷贝),在新副本上执行修改。修改完成后,通过原子操作(如 `ReentrantLock`)将 volatile 数组引用指向新副本。旧数组会被垃圾回收[^2][^3]。 - 关键步骤: 1. 加锁(确保操作互斥)。 2. 复制当前数组到新数组。 3. 在新数组上执行修改。 4. 原子性地更新数组引用。 5. 释放锁。 - 示例代码:操作的核心逻辑。 ```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(); // 释放锁 } } ``` - 这种机制确保操作不影响正在进行的读操作(读操作始终访问旧数组),实现了读分离[^3]。 #### 2. **弱一致性的表现** - 由于读操作访问的是操作发生前的数组快照,迭代器(`Iterator`)在创建会捕获当前数组状态。即使后续操作修改了列表,迭代器也不会感知新变化,这称为**弱一致性**(weakly consistent)[^4][^5]。 - 示例场景(基于引用[4]): - 线程 A 创建迭代器并开始遍历列表。 - 线程 B 同删除一个元素(创建新副本并更新引用)。 - 线程 A 的迭代器仍基于旧数组遍历,可能输出已被删除的元素。 - 代码演示: ```java CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); list.add("A"); list.add("B"); Iterator<String> it = list.iterator(); // 创建迭代器(捕获快照) list.remove(0); // 操作修改列表 while (it.hasNext()) { System.out.println(it.next()); // 输出 "A" 和 "B",尽管 "A" 已被删除 } ``` - 弱一致性适用于读多少场景,避免了 `ConcurrentModificationException`,但可能返回过数据[^5]。 #### 3. **优点与适用场景** - **优点**: - 读操作高效无锁,适合高频读取。 - 操作线程安全,不阻塞读操作。 - **缺点**: - 操作开销大(数组复制),不适合密集型场景。 - 内存占用高(旧副本暂存)。 - **适用场景**:读多少的应用,如缓存系统、配置管理或事件监听器列表(引用[1]中提到“读操作可以尽可能的快,而即使慢一些也没关系”)[^1][^3]。 #### 4. **与其他容器的对比** - **vs Vector**:Vector 使用全表锁(`synchronized`),读均互斥;CopyOnWriteArrayList分离,读操作无锁,性能更高。 - **vs Collections.synchronizedList**:类似 Vector,锁粒度粗,而 CopyOnWriteArrayList 优化了读并发。 通过读分离,CopyOnWriteArrayList 在保证线程安全的同,显著提升了并发读性能,但需权衡操作的开销和弱一致性[^2][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值