CopyOnWriteArrayList并发容器源码解析

CopyOnWriteArrayList并发LIST容器源码解析

OnWriteArrayList源码分析

备注:下面的源码拷贝自JDK11

类结构

OnWriteArrayList


实现的接口

  • Serializable: 支持对象的序列化

  • Cloneable: 支持对象的复制

  • RandomAccess: 支持通过索引的随机访问

  • List: 支持List的所有操作

    核心数据结构

由下面源码实现上来看,内部还是使用和ArrayList相同的普通对象数组。而且可以看到,相较于ArrayList增加了一个对象的实例锁lock,使用synchronized关键字给增删改上锁

public class OnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
  /** The array, accessed only via getArray/setArray. */
  private transient volatile Object[] array;


  /**
  * The lock protecting all mutators.  (We have a mild preference
  * for builtin monitors over ReentrantLock when either will do.)
  */
  final transient Object lock = new Object();


  public OnWriteArrayList() {
    setArray(new Object[0]);
  }
}

内部类

COWIterator

OnWriteArrayList的内部迭代器类

static final class COWIterator<E> implements ListIterator<E> {
    /** Snapshot of the array */
    private final Object[] snapshot;

    /** Index of element to be returned by subsequent call to next.  */
    private int cursor;

    COWIterator(Object[] es, int initialCursor) {
       cursor = initialCursor;
       snapshot = es;
    }
  //.........
}

// OnWriteArrayList的iterator方法返回一个迭代器实例就是COWIterator类的实例
public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

COWSubList

OnWriteArrayList内部子列表视图

用于返回内部某一个时刻的数组的一个字视图,子列表视图的部分的,增删改查都用到了OnWriteArrayList的实例锁

private class COWSubList implements List<E>, RandomAccess {
    private final int offset;
    private int size;
    private Object[] expectedArray;

    COWSubList(Object[] es, int offset, int size) {
        // assert Thread.holdsLock(lock);
        expectedArray = es;
        this.offset = offset;
        this.size = size;
    }
    //.........

    // 可以看到子列表的改操作用到实例锁
    public E set(int index, E element) {
      synchronized (lock) {
        rangeCheck(index);
        checkForComodification();
        E x = OnWriteArrayList.this.set(offset + index, element);
        expectedArray = getArray();
        return x;
      }
    }

    // 读操作用到实例锁
    public E get(int index) {
      synchronized (lock) {
        rangeCheck(index);
        checkForComodification();
        return OnWriteArrayList.this.get(offset + index);
      }
    }

    // 读操作用到实例锁
    public int size() {
      synchronized (lock) {
        checkForComodification();
        return size;
      }
    }

    // 加操作用到实例锁
    public boolean add(E element) {
      synchronized (lock) {
        checkForComodification();
        OnWriteArrayList.this.add(offset + size, element);
        expectedArray = getArray();
        size++;
      }
      return true;
    }

    // 删操作用到了实例锁
    public E remove(int index) {
      synchronized (lock) {
        rangeCheck(index);
        checkForComodification();
        E result = OnWriteArrayList.this.remove(offset + index);
        expectedArray = getArray();
        size--;
        return result;
      }
    }
    //.........
}

COWSubListIterator

子列表视图迭代器,可以看到实现了ListIterator接口,作用和COWIterator类似,用于迭代访问数组某一个时刻的切片

private static class COWSubListIterator<E> implements ListIterator<E> {
  private final ListIterator<E> it;
  private final int offset;
  private final int size;

  COWSubListIterator(List<E> l, int index, int offset, int size) {
    this.offset = offset;
    this.size = size;
    it = l.listIterator(index + offset);
  }
}

读写线程安全的实现原理

总结来说 OnWriteArrayList线程安全的实现原理简单来说就是:

当进行增删改操作时候,通过synchronized修饰操作方法中的对象锁lock。然后对原数组进行复制,复制出一份和原数组一模一样的拷贝,然后在拷贝上进行增删改操作。操作完成后用新的拷贝代替原数组。然后解锁,就实现了多线程间安全的操作同一个List实例。

对于读操作,不上锁,随便读。这就是OnWriteArrayList适合读多写少的场景的原因

读操作源码可以看到,对于读操作是最简单的实现。没有上任何的锁机制

public E get(int index) {
   return elementAt(getArray(), index);
}

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

// 没错就是你看到的这么简单,直接通过索引读取对应位置的元素
@SuppressWarnings("unchecked")
static <E> E elementAt(Object[] a, int index) {
  return (E) a[index];
}

增加操作源码

public boolean add(E e) {
  // 使用synchronized对lock对象锁上锁,所以多线程的修改操作都是互斥的。同时也避免了多线程写时复制出多个副本出来
  synchronized (lock) {
    // 1.取原数组
    Object[] es = getArray();
    int len = es.length;

    // 2.拷贝一份新的数组实例,长度+1
    es = Arrays.copyOf(es, len + 1);

    // 3.将新的元素放在数组的最后面
    es[len] = e;

    // 4.用新的数组代替老数组
    setArray(es);
    return true;
  }
}

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

替换操作源码

替换操作的逻辑和增加操作是一样的,只是做了一定的优化。先判断被替换的元素和新元素是不是一样。如果一样就没必要替换了

public E set(int index, E element) {
  synchronized (lock) {
    Object[] es = getArray();
    E oldValue = elementAt(es, index);

    // 如果老元素和新元素是同一个,就没必要替换了
    if (oldValue != element) {
      // 拷贝一份新的数组
      es = es.clone();

      // 替换对应位置的元素
      es[index] = element;
      setArray(es);
    }
    return oldValue;
  }
}

删除操作源码

替换操作相对复杂一点,涉及到元素的两次拷贝

public E remove(int index) {
  synchronized (lock) {
    // 取原数组
    Object[] es = getArray();
    // 原数组长度
    int len = es.length;

    // 要移除位置的元素
    E oldValue = elementAt(es, index);

    // 删除位置元素之后的剩余数组右侧需要复制的长度
    int numMoved = len - index - 1;

    // 新的空数组
    Object[] newElements;

    // 移除的是原始数组的最后一个元素
    if (numMoved == 0)
      // 直接复制到倒数第二个元素
      newElements = Arrays.copyOf(es, len - 1);
    else {
      // 长度减1的新空数组
      newElements = new Object[len - 1];

      // 将原始数组从0开始复制index个元素到新数组从0位置开始的数组中
      System.arraycopy(es, 0, newElements, 0, index);

      // 将原始数组跳过要移除的元素复制numMoved个元素到新数组从index开始的数组中
      System.arraycopy(es, index + 1, newElements, index, numMoved);
    }
    setArray(newElements);
    return oldValue;
  }
}

ONWRITEARRAYLIST的特点

  • 同时读读不加锁:读高性能
  • 同时写写互斥:写同步
  • 同时读写不阻塞:读写分离,互相不干预
  • 没有像ArrayList一样的扩容机制:因为所有的修改操作都是进行对象的复制,所以没有必要引入扩容机制

ONWRITEARRAYLIST 使用场景以及注意点

从OnWriteArrayList的源码实现中可以看出,为了提高列表的读写性能,对读操作是不加锁的,换句话说,其实多线程读同一个元素,也没必要加锁。对增删改操作使用实例对象锁,进行操作的同步,只需要写入和写入之间需要同步,提高整体的读写性能。

从实现机理上也能看出,每次的写操作都会进行数组的复制操作。如果数组长度很大的化,效率会降低。

适合的应用场景

  • 读多写少情况
  • 列表规模不大情况

参考

更多推荐

更多
  • AWS自动化机器学习-十一、MLSDLC 的持续集成、部署和训练 技术要求,编纂持续集成阶段,管理持续部署阶段,管理持续训练,延伸,构建集成工件,构建测试工件,构建生产工件,自动化持续集成流程,回顾构建阶段,回顾测试阶段,审查部署和维护阶段,回顾应用用户体验,创建新的鲍鱼调查数据,回顾持续训练流程,清
    Apache CN

  • AWS自动化机器学习-六、使用 AWS 步骤函数自动化机器学习过程 技术要求,介绍 AWS 步骤功能,使用 Step 函数 Data Science SDK for CI/CD,建立 CI/CD 渠道资源,创建状态机,解决状态机的复杂性,更新开发环境,创建管道工件库,构建管道应用构件,部署 CI/CD
    Apache CN

  • AWS自动化机器学习-第三部分:优化以源代码为中心的自动化机器学习方法 本节将向您介绍整体 CI/CD 流程的局限性,以及如何将 ML 从业者的角色进一步整合到管道构建流程中。本节还将介绍这种角色集成如何简化自动化过程,并通过向您介绍 AWS Step 函数向您展示一种优化的方法。本节包括以下章节:
    Apache CN

  • AWS自动化机器学习-一、AWS 上的自动化机器学习入门 技术要求,洗钱流程概述,洗钱过程的复杂性,端到端 ML 流程示例,AWS 如何使 ML 开发和部署过程更容易自动化,介绍 ACME 渔业物流,ML 的情况,从数据中获得洞察力,建立正确的模型,训练模型,评估训练好的模型,探索可能的后续步
    Apache CN

  • AWS自动化机器学习-二、使用 SageMaker 自动驾驶器自动化机器学习模型开发 技术要求,介绍 AWS AI 和 ML 前景,SageMaker 自动驾驶器概述,利用 SageMaker 自动驾驶器克服自动化挑战,使用 SageMaker SDK 自动化 ML 实验,SageMaker Studio 入门,准备实验
    Apache CN

  • AWS自动化机器学习-四、机器学习的持续集成和持续交(CI/CD) 四、机器学习的持续集成和持续交CI/CD技术要求,介绍 CI/CD 方法,通过 CI/CD 实现 ML 自动化,在 AWS 上创建 CI/CD 管道,介绍 CI/CD 的 CI 部分,介绍 CI/CD 的 CD 部分,结束循环,采取以部
    Apache CN

  • AWS自动化机器学习-九、使用 Amazon Managed Workflows 为 Apache AirFlow 构建 ML 工作流 技术要求,开发以数据为中心的工作流程,创建合成鲍鱼调查数据,执行以数据为中心的工作流程,构建和单元测试数据 ETL 工件,构建气流 DAG,清理, 在前面的年龄计算器示例中,我们了解了如何通过 ML 从业者和开发人员团队之间的跨职能
    Apache CN

  • AWS自动化机器学习-七、使用 AWS 步骤函数构建 ML 工作流 技术要求,构建状态机工作流,执行集成测试,监控管道进度,设置服务权限,创建 ML 工作流程, 在本章中,我们将从第六章中的 [处继续,使用 AWS 步骤函数自动化机器学习过程。您将从那一章中回忆起,我们正在努力实现的主要目标是简化
    Apache CN

  • AWS自动化机器学习-八、使用 Apache Airflow 实现机器学习过程的自动化 技术要求,介绍阿帕奇气流,介绍亚马逊 MWAA,利用气流处理鲍鱼数据集,配置 MWAA 系统的先决条件,配置 MWAA 环境, 当建立一个 ML 模型时,有一个所有 ML 从业者都知道的基本原则;也就是说,最大似然模型只有在数据被训练时
    Apache CN

  • AWS自动化机器学习-五、自动化 ML 模型的持续部署 技术要求,部署 CI/CD 管道,构建 ML 模型工件,执行自动化 ML 模型部署,整理管道结构,创建 CDK 应用,部署管道应用,查看建模文件,审查申请文件,查看模型服务文件,查看容器构建文件,提交 ML 工件,清理, 在 [第 4
    Apache CN

  • 近期文章

    更多
    文章目录

      推荐作者

      更多