标签 Vector 下的文章

作者:傅榕锋,OceanBase 高级技术专家

AI 开发者需要什么样的数据库

在开始正式话题前,我们不妨先思考一个问题: AI 时代下开发者需要什么样的数据库?

自本世纪初以来数据库需求的演变历程。Web 2.0及业务在线化的时代,强调的是一个可靠、精确的记录系统,能够精准地记录每一笔交易数据,满足典型的事务处理(TP)需求。进入移动互联网和数据智能化时代后,随着数据量的爆发式增长,海量数据分析的需求成为主流。这时,分析型(AP)数据库开始占据重要位置。AI 时代的真正到来,驱动数据库不仅要支持查询和分析功能,更需具备理解和推理的能力。

快来关注我,获取 OceanBase 第一手的产品信息和技术资源,与行业大咖 “唠” 出真知!

AI 时代开发者的痛点

作为数据库从业者,我们需要深入分析 AI 时代下开发者对数据库的具体需求。

数据类型的多维化:在传统数据库中,图片、视频、音频仅能被存储而难以有效利用。借助 AI 模型的帮助,这些非结构化数据可以转化为可检索的形式,如通过嵌入模型转换为向量,或使用大语言模型提取文本描述和标签,从而将非结构化数据转变为结构化或半结构化数据以实现高效检索。

性能与规模的极致化:鉴于向量数据对内存和磁盘资源的高占用特性,在成本与性能之间寻求最佳平衡显得尤为关键。为此,亟需采用高效的算法,以优化召回率与资源成本之间的权衡关系。

智能处理的内生化:例如,在 RAG 场景中,文档需先进行切片并生成向量,这通常涉及向量数据库、文档型数据库以及事务型数据库的联合使用。为了简化这一流程,理想的解决方案是让数据库自身承担更多的标准化数据处理任务,减少开发者的负担。

开发流程的敏捷化:目标是让开发者更加专注于业务逻辑本身,而非陷入复杂的数据处理流程之中。

AI 时代的理想数据库

基于上述痛点,AI 时代的理想数据库应具备以下四个特征。

  • 多模态支持:提供统一的平台,支持多种数据类型,包括但不限于向量、全文、标量、 JSON 格式。
  • 高性能引擎:针对 AI 工作负载进行优化,确保在控制成本的前提下实现最优性能表现。
  • 智能化集成:内置 AI 运行时环境,使数据库可以直接执行复杂的智能处理任务,减少对外部系统的依赖。
  • 简易操作:设计直观易用的界面和工具,降低非专业开发者的使用门槛,促进更多领域专家参与到数据处理工作中。

综上所述, AI 时代我们期待的数据库应该是强大、智能、一体化的,是数据与 AI 融合的平台。

AI原生的一体化数据库是否存在

正所谓“需求决定市场”,契合AI时代理想数据库特质的产品必然会出现。而就目前来看,OceanBase 新发布的 seekdb 已率先落地,不仅具备了相关核心能力,更在快速迭代中持续进化。

混搜架构的轻量级、多模态的AI原生数据库

OceanBase seekdb 是一款面向 AI 场景的轻量级、多模态的原生数据库,专为支持混合搜索、上下文理解与智能数据处理而设计。其整体架构分为五个核心层级,实现从数据存储到查询执行的全链路优化。

1. 统一应用接口层。

seekdb提供基于 SQL 的统一查询语言,兼容标准 SQL 语法,支持多模态数据的联合查询。同时,提供面向开发者的 Python SDK,具备简洁易用的 API 接口,支持 skip-by-list 等高效检索模式,显著降低开发者使用门槛。

2. 支持混合负载的多模计算层。

继承自 OceanBase 的成熟优化器体系,seekdb具备强大的查询规划与执行能力,在混合检索场景中,会自动进行自适应执行和查询优化,能够根据查询条件自动选择最优执行路径。同时,支持混合负载自适应执行、AI 函数调用、ACID 事务保障及灵活 UDF 扩展,满足复杂业务需求。

3. 多模数据层。

支持多种数据类型统一存储,实现“存即能检”,打破传统系统中不同数据类型需分库管理的局限。包括:

  • 关系表(传统结构化数据)
  • 向量(Embedding 向量)
  • 文本(原始文本内容)
  • JSON(半结构化数据)
  • GIS(地理空间数据)
  • 数组、位图等扩展类型

4. 多模索引层。

构建业界领先的多模索引体系,支持的索引类型如下。

  • 向量索引:高效支持近邻搜索(ANN),兼顾精度与性能。
  • 全文索引:支持中文分词与语义匹配。
  • 混合索引:结合向量与标量条件进行联合检索。
  • JSON 索引:加速嵌套字段查询。
  • 二级索引、GIS 索引:满足多样化查询需求。

支持多索引协同查询,在一次请求中完成跨模态数据的融合检索。

5. 部署模式层。

  • 服务器模式:传统集群部署,适用于高并发、大规模生产环境。
  • 嵌入式模式:以库的形式内嵌于应用程序中,生命周期与应用一致,适合边缘计算、AI 应用快速构建等轻量化场景。

OceanBase seekdb 通过“统一接口 + 多模存储 + 智能索引 + 灵活部署”的一体化设计,实现了对 AI 工作负载的端到端支持,真正做到了“一个数据库,搞定所有数据”。

快速构建:更灵活、更轻量、不止于 SQL

OceanBase seekdb 不仅具备强大的功能,更在易用性和部署灵活性上进行了深度优化,助力开发者快速构建 AI 应用。

1. 更灵活:双运行模式,适配多样场景。

  • 服务器模式:适用于企业级、高可用、分布式部署。
  • 嵌入式模式:直接集成到 #Python 应用中,无需独立部署数据库服务,极大简化开发流程,特别适合 RAG、Agent、智能问答等轻量级 AI 应用。

2. 更轻量:极简资源占用,轻松跑起基准测试。

单实例仅需 1C2G 内存即可运行 VectorDBBench 基准测试,相比传统数据库,资源消耗更低,启动更快,非常适合本地调试、原型验证和边缘部署。

3. 不止于 SQL:引入 Schemaless SDK。

引入 Schemaless SDK,开发者无需定义表结构即可直接插入和查询数据,提升开发灵活性。

使用seekdb快速创建RAG应用

下面我们演示一下如何使用 seekdb 快使创建一个 RAG 应用。

第一步:三行代码快速创建一个知识库(SETUP)

  1. 导入 pyseekdb 模块,启用 seekdb 的 Python SDK。
  2. 初始化客户端实例,参数为空表示采用嵌入式模式,数据库生命周期与应用绑定,无需独立部署服务。
  3. 创建知识库并定义为 Collection。

第二步:批量插入文档片段(INSERT)

功能说明:

  • 调用 upsert() 函数批量插入文档内容(documents)。
  • 同时关联元数据(metadatas),包括分类、内存、存储、价格等结构化信息。
  • 显式指定文档 ID(ids),便于后续检索与更新。

关键特性:

  • 用户仅需提供原始文本和元数据,无需手动调用嵌入模型生成向量。
  • 数据库内部自动调用内置的嵌入模型,将文本转换为向量并存储。

AI 能力下沉至数据库,开发者无需关注向量化过程,seekdb 自动完成文本 → 向量的转换,实现“透明化”处理。

第三步:混合检索,精准召回(QUERY)

查询维度分析:

  • query_texts:输入自然语言文本,触发向量检索,用于语义匹配。
  • where:设置关系型过滤条件,如 category == laptop 和 ram >= 16,实现精确筛选。
  • where_document:基于全文索引进行关键词匹配,要求文档内容包含 “RAM”。
  • n_results:限制返回结果数量为 2 条。

实现机制:

  • 查询时,seekdb 内部自动将 query_texts 输入传递给嵌入模型,生成查询向量。
  • 结合向量索引、全文索引、二级索引等多种索引执行混合检索。
  • 最终返回满足所有条件的最相关结果。

第四步:效果展示

输入检索条件为:需要一个 12 GB 内存以上的高性能笔记本。运行后输出结果如下图所示,

召回结果分析如下。

  • 第一条:16GB 内存、512GB 固态的专业笔记本,完全满足“高性能 + 16GB 以上内存”的要求。
  • 第二条:32GB 内存、1TB 固态的游戏本,虽非专业用途但性能卓越,符合语义意图。

该案例模拟了典型的 RAG 场景,用户只需输入自然语言问题,系统即可自动完成文本向量化、多条件联合检索、高精度召回。全流程由数据库内核统一处理,极大简化了开发复杂度,真正实现“让开发者专注于业务,而非数据处理”。

欢迎亲自上手试用:https://github.com/oceanbase/seekdb。当前版本支持 Linux 平台下的嵌入式模式运行,Windows 和 macOS 版本将在近期和大家见面。可访问 oceanbase.ai 获取样例代码,支持本地测试与快速验证。

SQL 直接调用 AI 的原生体验

OceanBase seekdb 不仅是一个支持多模态数据存储与混合检索的数据库,更致力于将 AI 能力深度集成于数据库内核,实现“SQL 直接调用 AI”的原生体验。

seekdb AI Inside 的内置处理除了 AI_EMBED 方法外,还引入 AI_RERANK 和 AI_COMPLETE,可以实现数据分析自动化、特征提取、智能内容生成、语义搜索增强、结果优化等效果。在 seekdb 中使用可以构建从粗排到精排的高效分层混合检索处理流程。该流程分为四个阶段。

阶段1:标量过滤(Scalar Filtering)。在全量数据集上首先执行关系型条件过滤(如 category = 'laptop', ram >= 16),缩小候选集过滤范围。

阶段2:向量搜索(Vector Search)。对过滤后的候选集执行向量相似度检索,基于语义匹配找出最相关的文档,使用近邻搜索算法(ANN)高效完成高维向量比对。

阶段3:全文搜索(Full-text Search)。在候选集中进一步执行关键词匹配,确保结果包含用户关心的关键信息(如 "RAM"),支持中文分词与模糊匹配,提升召回精度。其中标量、向量、全文的过滤顺序取决于优化器。

阶段4:粗排 → 精排 → 大模型重排。经过以上过滤后得到粗排的结果,此时再去调用 AI_RERANK,数据库会直接调用 RERANK 模型进行精排,精排结束后,通过调用 AI_COMPLETE 即可调用大模型,大模型会直接进行回答。以上所有的 AI 标准操作流程都在数据库中进行,开发者只需在查询中添加相应函数,即可让数据库自动调用大模型对数据进行处理,显著提升用户体验。

OceanBase seekdb 适用场景

OceanBase seekdb 作为一款轻量级、多模态、AI 原生的数据库,凭借其统一存储、混合检索、内嵌 AI 能力 和嵌入式部署支持,在多个新兴与传统智能化场景中展现出显著优势。以下是其典型的适用场景。

1.替代“三库并行”,降本增效

在 RAG 架构中,传统方案通常需要同时维护三类数据库。

  • 向量数据库存储文本嵌入向量。
  • 文档数据库保存原始文本内容。
  • 关系型数据库管理元数据(如分类、时间、权限等)。

这种“三库并行”模式不仅带来高昂的运维复杂度,还导致资源重复占用(三份独立实例),难以在资源受限的本地或边缘环境中落地。seekdb 通过单一数据库统一承载向量、文本与结构化元数据,实现一次写入,多路索引(向量索引 + 全文索引 + 二级索引)、统一查询接口,支持混合条件过滤、极低资源开销(1C2G 即可运行),适合个人本地知识库、中小企业内部知识管理系统、边缘侧智能问答应用等。

2.语义搜索引擎,打破模态壁垒

seekdb 的多模态能力使其天然适配跨模态语义搜索场景。无论是文本、图片、音频还是视频,均可通过嵌入模型转化为统一的向量表示,并结合元数据进行联合检索,通过统一向量 + 元数据 + 全文的混合检索框架,打破模态壁垒。典型应用包括:以图搜图、音频内容检、视频片段语义匹配、多媒体资产管理系统。

3.Agentic AI 应用,保证数据一致性

在 Agentic AI(智能体)场景中,Agent 需要频繁执行上下文感知的混合检索,比如结合用户历史行为(标量过滤)、匹配任务目标语义(向量搜索)、检索相关文档片段(全文匹配)。seekdb 的原生混合检索引擎与内嵌 AI 函数能够高效支撑此类复杂查询,避免外部服务调用带来的延迟与一致性问题。适用于任务型对话系统、自主决策机器人、智能工作流引擎等应用场景。

4.AI 辅助编程,提升质量,降低成本

AI 编程助手存在云端 + 客户端双端检索需求,传统方案面临两大挑战。

  • 架构割裂:云端使用多源召回(向量+全文+语法树),客户端依赖轻量插件(如 SQLite + 向量扩展),两套系统逻辑不一致。
  • 性能瓶颈:通用数据库缺乏专业向量索引与优化器,召回效果与效率受限。

seekdb 提供统一的 SDK 与查询接口,可使云端与客户端使用同一套 API,且客户端在嵌入式模式下仍具备专业级向量检索能力。seekdb还支持代码语义搜索、API 推荐、错误修复建议等高级功能。通过这些能力统一技术栈,提升召回质量,降低双端开发与维护成本。

5.企业应用智能化丝滑升级

对于大量仍在使用 MySQL 的传统企业应用,seekdb 提供了一条平滑演进路径:

  • 高度兼容 MySQL 协议,现有应用可无缝迁移。
  • 迁移后即可获得 向量检索、全文搜索、JSON 支持等 AI 原生能力。
  • 为未来引入 RAG、智能报表、自动化分析等 AI 功能奠定数据基础。

因此,MySQL 到 OceanBase 的迁移是“最丝滑”的路径之一。seekdb 作为其轻量化延伸,进一步降低了企业智能化转型的技术门槛。

6.端侧应用智能化的理想选择

随着终端设备算力提升,越来越多智能应用向端侧迁移。seekdb 的嵌入式部署能力使其成为端侧智能数据库的理想选择:

  • 资源占用极低(1C2G 可运行)。
  • 支持离线向量检索与语义理解。
  • 生命周期与应用绑定,无需独立服务进程。
  • 让端侧应用具备“本地大脑”,减少对云服务的依赖。

典型场景包括:

  • 智能家居设备中的本地知识问答。
  • 工业机器人中的实时故障诊断。
  • 移动端个人助理的上下文记忆管理。
  • 车载系统的本地语义导航。

从轻到重、从简到繁: AI 应用快速迭代的理想基础设施

在 AI 应用快速迭代的背景下,开发者面临从原型验证、开发测试到生产部署的多阶段需求。OceanBase 与 seekdb 的深度融合,构建了一套覆盖全生命周期、支持平滑演进的弹性数据库架构,能够满足不同阶段、不同规模场景下的灵活部署需求。

原型验证与开发测试阶段:嵌入式模式(seekdb)

在项目初期,开发者通常需要快速验证 AI 模型效果或构建最小可行产品(MVP)。此时可采用 seekdb 嵌入式模式:

  • 将 libseekdb.so 动态库直接集成至应用中,作为本地数据库运行。
  • 数据库生命周期与应用绑定,启动即用,关闭即销毁。
  • 无需独立部署服务,极大简化环境搭建流程。
  • 支持向量、文本、JSON 等多模态数据存储与混合检索。

嵌入式模式适用于个人开发者快速原型开发、端侧智能应用(如移动端、机器人)、本地调试与算法验证等场景。

测试与小规模生产环境:单机部署模式

当应用进入测试或小规模上线阶段,可迁移到单机部署模式:

  • 启动独立的 seekdb 进程,提供服务端接口。
  • 支持多客户端连接,适合团队协作开发。
  • 可通过配置文件管理数据路径、内存参数等。
  • 仍保持与嵌入式模式的 API 兼容性,代码无需变更。

单机部署模式适用于小型工作负载、测试环境与生产环境、多租户需求等场景。

生产环境:多租户与高可用架构

随着业务稳定运行,需考虑资源隔离、高可用性和容灾能力,此时可选择以下两种生产级部署方式。

  • 单机多租户模式(OceanBase 单机部署) :

    • 使用 OceanBase 单机实例,通过多租户机制实现多个业务之间的资源隔离。
    • 适用于多个业务共享同一数据库实例但需独立管理资源的场景。
    • 支持独立的配额控制、备份策略和监控告警。
  • 主备模式 / 三副本模式(OceanBase 高可用架构):

    • 采用主备架构或三副本(2F1A)架构,保障数据高可用。
    • 支持自动故障切换与读写分离。
    • 适用于对稳定性要求较高的中小规模业务系统。

多租户与高可用架构适用于中小规模工作负载、对容灾和高可用有明确要求的业务、多租户共用数据库的 SaaS 平台等场景。

大规模与高性能场景:分布式集群架构

当业务持续增长,数据量和并发请求激增时,可进一步扩展为分布式集群架构。

  • 无共享分布式集群 :

    • 由多个 OBServer 节点组成,支持水平扩展。
    • 支持大规模工作负载、关键业务高并发访问。
    • 具备强一致性、线性可扩展性与动态扩容能力。
  • 基于对象存储的存算分离集群:

    • 存储层使用对象存储(如 OSS),计算层由 OBServer 提供。
    • 实现“冷热数据分离”,降低存储成本。
    • 适用于海量非敏感数据分析场景(如日志分析、历史归档)。
    • 提供更高的性价比与更强的扩展能力。

分布式集群架构适用于大规模工作负载、关键业务系统、高性能高并发、更高性价比的大数据处理任务等场景。

OceanBase 与 seekdb 的组合形成了一个 “从轻到重、从简到繁” 的完整弹性架构体系,核心优势有如下三点。

  • API 完全兼容:无论选择哪种部署模式,业务代码无需修改;
  • 配置驱动升级:只需更改连接地址与配置参数,即可完成架构迁移;
  • 平滑演进路径:支持从个人开发到企业级生产的无缝过渡。

这使得 OceanBase + seekdb 成为 AI 应用快速迭代的理想基础设施,真正实现了“一次开发,全栈适配”,助力企业在 AI 时代加速创新落地。

当然,在AI时代,AI数据库不足以支撑应用所需的完整基础设施能力,因此,OceanBase构建了上下文工程体系中的关键能力。让我们敬请期待下一篇文章。

前言

Vector无论是add方法还是get方法都加上了synchronized修饰,当多线程读写List必须排队执行,很显然这样效率比较是低下的,CopyOnWriteArrayList是读写分离的,好处是提高线程访问效率。

CopyOnWrite容器即写时复制的容器。通俗的理解是当往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器里的值Copy到新的容器,然后再往新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读 要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

底层原理

  1. CopyOnWriteArrayList的动态数组机制 -- 它内部有个volatile数组(array)来保持数据。在“添加/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给volatile数组。这就是它叫做CopyOnWriteArrayList的原因!
  2. 每一个CopyOnWriteArrayList都和一个监视器锁lock绑定,通过lock,实现了对CopyOnWriteArrayList的互斥添加/删除。

类的继承关系

CopyOnWriteArrayList实现了List接口,List接口定义了对列表的基本操作;同时实现了RandomAccess接口,表示可以随机访问(数组具有随机访问的特性);同时实现了Cloneable接口,表示可克隆;同时也实现了Serializable接口,表示可被序列化。

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

类的内部类

  • COWIterator类

COWIterator表示迭代器,其也有一个Object类型的数组作为CopyOnWriteArrayList数组的快照,这种快照风格的迭代器方法在创建迭代器时使用了对当时数组状态的引用。此数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。在迭代器上进行的元素更改操作(remove、set 和 add)不受支持。这些方法将抛出 UnsupportedOperationException。

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;
    // 构造函数
    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }
    // 是否还有下一项
    public boolean hasNext() {
        return cursor < snapshot.length;
    }
    // 是否有上一项
    public boolean hasPrevious() {
        return cursor > 0;
    }
    // next项
    @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;
    }

    /**
        * Not supported. Always throws UnsupportedOperationException.
        * @throws UnsupportedOperationException always; {@code remove}
        *         is not supported by this iterator.
        */
    // 不支持remove操作
    public void remove() {
        throw new UnsupportedOperationException();
    }

    /**
        * Not supported. Always throws UnsupportedOperationException.
        * @throws UnsupportedOperationException always; {@code set}
        *         is not supported by this iterator.
        */
    // 不支持set操作
    public void set(E e) {
        throw new UnsupportedOperationException();
    }

    /**
        * Not supported. Always throws UnsupportedOperationException.
        * @throws UnsupportedOperationException always; {@code add}
        *         is not supported by this iterator.
        */
    // 不支持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;
    }
}

类的属性

属性中有一个可重入锁,用来保证线程安全访问,还有一个Object类型的数组,用来存放具体的元素。当然,也使用到了反射机制和CAS来保证原子性的修改lock域。

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // 版本序列号
    private static final long serialVersionUID = 8673264195747942595L;
    // 可重入锁
    final transient ReentrantLock lock = new ReentrantLock();
    // 对象数组,用于存放元素
    private transient volatile Object[] array;
    // 反射机制
    private static final sun.misc.Unsafe UNSAFE;
    // lock域的内存偏移量
    private static final long lockOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = CopyOnWriteArrayList.class;
            lockOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("lock"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

类的构造函数

  • 默认构造函数
public CopyOnWriteArrayList() {
    // 设置数组
    setArray(new Object[0]);
}
  • CopyOnWriteArrayList(Collection<? extends E>)
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    if (c.getClass() == CopyOnWriteArrayList.class) // 类型相同
        // 获取c集合的数组
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else { // 类型不相同
        // 将c集合转化为数组并赋值给elements
        elements = c.toArray();
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elements.getClass() != Object[].class) // elements类型不为Object[]类型
            // 将elements数组转化为Object[]类型的数组
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    // 设置数组
    setArray(elements);
}

该构造函数的处理流程如下

  1. 判断传入的集合c的类型是否为CopyOnWriteArrayList类型,若是,则获取该集合类型的底层数组(Object[]),并且设置当前CopyOnWriteArrayList的数组(Object[]数组),进入步骤③;否则,进入步骤②
  2. 将传入的集合转化为数组elements,判断elements的类型是否为Object[]类型(toArray方法可能不会返回Object类型的数组),若不是,则将elements转化为Object类型的数组。进入步骤③
  3. 设置当前CopyOnWriteArrayList的Object[]为elements。
  • CopyOnWriteArrayList(E[]):该构造函数用于创建一个保存给定数组的副本的列表。
public CopyOnWriteArrayList(E[] toCopyIn) {
    // 将toCopyIn转化为Object[]类型数组,然后设置当前数组
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

核心函数分析

对于CopyOnWriteArrayList的函数分析,主要明白Arrays.copyOf方法即可理解CopyOnWriteArrayList其他函数的意义。

copyOf函数

该函数用于复制指定的数组,截取或用 null 填充(如有必要),以使副本具有指定的长度。

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    // 确定copy的类型(将newType转化为Object类型,将Object[].class转化为Object类型;
    // 判断两者是否相等,若相等,则生成指定长度的Object数组
    // 否则,生成指定长度的新类型的数组)
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    // 将original数组从下标0开始,复制长度为(original.length和newLength的较小者),复制到copy数组中(也从下标0开始)
    System.arraycopy(original, 0, copy, 0,
                        Math.min(original.length, newLength));
    return copy;
}

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);
        // 存放元素e
        newElements[len] = e;
        // 设置数组
        setArray(newElements);
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

此函数用于将指定元素添加到此列表的尾部,处理流程如下

  1. 获取锁(保证多线程的安全访问),获取当前的Object数组,获取Object数组的长度为length,进入步骤②。
  2. 根据Object数组复制一个长度为length+1的Object数组为newElements(此时,newElements[length]为null),进入下一步骤。
  3. 将下标为length的数组元素newElements[length]设置为元素e,再设置当前Object[]为newElements,释放锁,返回。这样就完成了元素的添加。

addIfAbsent方法

该函数用于添加元素(如果数组中不存在,则添加;否则,不添加,直接返回),可以保证多线程环境下不会重复添加元素。

private boolean addIfAbsent(E e, Object[] snapshot) {
    // 重入锁
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        // 获取数组
        Object[] current = getArray();
        // 数组长度
        int len = current.length;
        if (snapshot != current) { // 快照不等于当前数组,对数组进行了修改
            // Optimize for lost race to another addXXX operation
            // 取较小者
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++) // 遍历
                if (current[i] != snapshot[i] && eq(e, current[i])) // 当前数组的元素与快照的元素不相等并且e与当前元素相等
                    // 表示在snapshot与current之间修改了数组,并且设置了数组某一元素为e,已经存在
                    // 返回
                    return false;
            if (indexOf(e, current, common, len) >= 0) // 在当前数组中找到e元素
                // 返回
                return false;
        }
        // 复制数组
        Object[] newElements = Arrays.copyOf(current, len + 1);
        // 对数组len索引的元素赋值为e
        newElements[len] = e;
        // 设置数组
        setArray(newElements);
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

该函数的流程如下:

  1. 获取锁,获取当前数组为current,current长度为len,判断数组之前的快照snapshot是否等于当前数组current,若不相等,则进入步骤2;否则,进入步骤4
  2. 不相等,表示在snapshot与current之间,对数组进行了修改(如进行了add、set、remove等操作),获取长度(snapshot与current之间的较小者),对current进行遍历操作,若遍历过程发现snapshot与current的元素不相等并且current的元素与指定元素相等(可能进行了set操作),进入步骤5,否则,进入步骤3
  3. 在当前数组中索引指定元素,若能够找到,进入步骤5,否则,进入步骤4
  4. 复制当前数组current为newElements,长度为len+1,此时newElements[len]为null。再设置newElements[len]为指定元素e,再设置数组,进入步骤5
  5. 释放锁,返回。

set函数

此函数用于用指定的元素替代此列表指定位置上的元素,也是基于数组的复制来实现的。

public E set(int index, E element) {
    // 可重入锁
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        // 获取数组
        Object[] elements = getArray();
        // 获取index索引的元素
        E oldValue = get(elements, index);

        if (oldValue != element) { // 旧值等于element
            // 数组长度
            int len = elements.length;
            // 复制数组
            Object[] newElements = Arrays.copyOf(elements, len);
            // 重新赋值index索引的值
            newElements[index] = element;
            // 设置数组
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics
            // 设置数组
            setArray(elements);
        }
        // 返回旧值
        return oldValue;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

remove函数

此函数用于移除此列表指定位置上的元素。

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) // 移动个数为0
            // 复制后设置数组
            setArray(Arrays.copyOf(elements, len - 1));
        else { // 移动个数不为0
            // 新生数组
            Object[] newElements = new Object[len - 1];
            // 复制index索引之前的元素
            System.arraycopy(elements, 0, newElements, 0, index);
            // 复制index索引之后的元素
            System.arraycopy(elements, index + 1, newElements, index,
                                numMoved);
            // 设置索引
            setArray(newElements);
        }
        // 返回旧值
        return oldValue;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

处理流程如下

  1. 获取锁,获取数组elements,数组长度为length,获取索引的值elements[index],计算需要移动的元素个数(length - index - 1),若个数为0,则表示移除的是数组的最后一个元素,复制elements数组,复制长度为length-1,然后设置数组,进入步骤③;否则,进入步骤②
  2. 先复制index索引前的元素,再复制index索引后的元素,然后设置数组。
  3. 释放锁,返回旧值

CopyOnWriteArrayList是Fail Safe的

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。

缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

Vector无论是add方法还是get方法都加上了synchronized修饰,当多线程读写List必须排队执行,很显然这样效率比较是低下的,CopyOnWriteArrayList是读写分离的,好处是提高线程访问效率。

缺陷和使用场景

  • CopyOnWriteArrayList的写效率比Vector慢。当CopyOnWriteArrayList写元素时是通过备份数组的方式实现的,当多线程同步激烈,数据量较大时会不停的复制数组,内存浪费严重。如果原数组的内容比较多的情况下,可能导致young gc或者full gc
  • 弱一致性:不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;

小结: CopyOnWriteArrayList合适读多写少的场景,例如黑名单白名单等

为什么不推荐使用Stack

Java已不推荐使用Stack,而是推荐使用更高效的ArrayDeque

为什么不推荐使用

  • 性能低:是因为 Stack 继承自 Vector, 而 Vector 在每个方法中都加了锁。由于需要兼容老的项目,很难在原有的基础上进行优化,因此 Vector 就被淘汰掉了,使用 ArrayListCopyOnWriteArrayList 来代替,如果在非线程安全的情况下可以使用 ArrayList,线程安全的情况下可以使用 CopyOnWriteArrayList
  • 破坏了原有的数据结构:栈的定义是在一端进行 push 和 pop 操作,除此之外不应该包含其他 入栈和出栈 的方法,但是 Stack 继承自 Vector,使得 Stack 可以使用父类 Vector 公有的方法。

为什么现在还在用

但是为什么还有很多人在使用 Stack。总结了一下主要有两个原因。

  • JDK 官方是不推荐使用 Stack,之所以还有很多人在使用,是因为 JDK 并没有加 deprecation 注解,只是在文档和注释中声明不建议使用,但是很少有人会去关注其实现细节
  • 在笔试面试需要做算法题的时候,更多关注点是在解决问题的算法逻辑思路上,并不会关注在不同语言下 Stack 实现细节,但是对于使用 Java 语言的业务开发者,不仅需要关注算法逻辑本身,也需要关注它的实现细节

为什么推荐使用 Deque 接口替换栈

如果 JDK 不推荐使用 Stack,那应该使用什么集合类来替换栈,一起看看官方的文档。

正如图中标注部分所示,栈的相关操作应该由 Deque 接口来提供,推荐使用 Deque 这种数据结构, 以及它的子类,例如 ArrayDeque。

val stack: Deque<Int> = ArrayDeque()

使用 Deque 接口来实现栈的功能有什么好处:

  • 速度比 Stack 快

这个类作为栈使用时可能比 Stack 快,作为队列使用时可能比 LinkedList 快。因为原来的 Java 的 Stack 继承自 Vector,而 Vector 在每个方法中都加了锁,而 Deque 的子类 ArrayDeque 并没有锁的开销。

  • 屏蔽掉无关的方法

原来的 Java 的 Stack,包含了在任何位置添加或者删除元素的方法,这些不是栈应该有的方法,所以需要屏蔽掉这些无关的方法。声明为 Deque 接口可以解决这个问题,在接口中声明栈需要用到的方法,无需管子类是如何是实现的,对于上层使用者来说,只可以调用和栈相关的方法。

Stack 和 ArrayDeque的 区别

集合类型数据结构是否线程安全
Stack数组
ArrayDeque数组

Stack 常用的方法如下所示:

操作方法
入栈push(E item)
出栈pop()
查看栈顶peek() 为空时返回 null

ArrayDeque 常用的方法如下所示:

操作方法
入栈push(E item)
出栈poll() 栈为空时返回 nullpop() 栈为空时会抛出异常
查看栈顶peek() 为空时返回 null

Queue介绍

Java里有一个叫做Stack的类,却没有叫做Queue的类(它是个接口名字)。当需要使用栈时,Java已不推荐使用Stack,而是推荐使用更高效的ArrayDeque;既然Queue只是一个接口,当需要使用队列时也就首选ArrayDeque了(次选是LinkedList)。

Queue

Queue接口继承自Collection接口,除了最基本的Collection的方法之外,它还支持额外的insertion, extraction和inspection操作。这里有两组格式,共6个方法,一组是抛出异常的实现;另外一组是返回值的实现(没有则返回null)。

Deque

Deque 是"double ended queue", 表示双向的队列,英文读作"deck". Deque 继承自 Queue接口,除了支持Queue的方法之外,还支持 insert , remove 和 examine操作,由于Deque是双向的,所以可以对队列的头和尾都进行操作,它同时也支持两组格式,一组是抛出异常的实现;另外一组是返回值的实现(没有则返回null)。共12个方法如下:

当把 Deque 当做FIFO的 queue 来使用时,元素是从 deque 的尾部添加,从头部进行删除的; 所以 deque 的部分方法是和 queue 是等同的。具体如下:

Deque的含义是“double ended queue”,即双端队列,它既可以当作栈使用,也可以当作队列使用。下表列出了Deque与Queue相对应的接口:

下表列出了Deque与Stack对应的接口:

上面两个表共定义了Deque的12个接口。添加,删除,取值都有两套接口,它们功能相同,区别是对失败情况的处理不同。一套接口遇到失败就会抛出异常,另一套遇到失败会返回特殊值( false 或 null )。除非某种实现对容量有限制,大多数情况下,添加操作是不会失败的。虽然Deque的接口有12个之多,但无非就是对容器的两端进行操作,或添加,或删除,或查看。

ArrayDeque和LinkedList是Deque的两个通用实现,由于官方更推荐使用AarryDeque用作栈和队列,加之上一篇已经讲解过LinkedList,本文将着重讲解ArrayDeque的具体实现

从名字可以看出ArrayDeque底层通过数组实现,为了满足可以同时在数组两端插入或删除元素的需求,该数组还必须是循环的,即循环数组(circular array),也就是说数组的任何一点都可能被看作起点或者终点。ArrayDeque是非线程安全的(not thread-safe),当多个线程同时使用的时候,需要程序员手动同步;另外,该容器不允许放入 null 元素。

上图中我们看到, head 指向首端第一个有效元素, tail 指向尾端第一个可以插入元素的空位。因为是循环数组,所以 head 不一定总等于0, tail 也不一定总是比 head 大。

方法剖析

addFirst()

addFirst(E e)的作用是在Deque的首端插入元素,也就是在head的前面插入元素,在空间足够且下标没有越界的情况下,只需要将elements[--head] = e即可。

实际需要考虑:

  1. 空间是否够用
  2. 下标是否越界的问题

上图中,如果head为0之后接着调用addFirst(),虽然空余空间还够用,但head为-1,下标越界了。

//addFirst(E e)
public void addFirst(E e) {
    if (e == null)//不允许放入null
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;//2.下标是否越界
    if (head == tail)//1.空间是否够用
        doubleCapacity();//扩容
}

上述代码可以看到, 空间问题是在插入之后解决的;首先,因为tail总是指向下一个可插入的空位,也就意味着elements数组至少有一个空位,所以插入元素的时候不用考虑空间问题。

下标越界的处理解决起来非常简单,head = (head - 1) & (elements.length - 1)就可以了,这段代码相当于取余,同时解决了head为负值的情况。因为elements.length必需是2的指数倍,elements - 1就是二进制低位全1,跟head - 1相与之后就起到了取模的作用,如果head - 1为负数(其实只可能是-1),则相当于对其取相对于elements.length的补码。

计算机里数值都是用补码表示的,如果是8位的,-1就是1111 1111,而 (elements.length - 1) 也是 1111 1111,因此两者相与也就是(elements.length - 1);

head = (head - 1) & (elements.length - 1) 最后再让算出的位置赋值给head,因此其实这段代码就是让head再从后往前赋值

扩容函数doubleCapacity(),其逻辑是申请一个更大的数组(原数组的两倍),然后将原数组复制过去。过程如下图所示:

图中可以看到,复制分两次进行,第一次复制head右边的元素,第二次复制head左边的元素。

//doubleCapacity()
private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; // head右边元素的个数
    int newCapacity = n << 1;//原空间的2倍
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    Object[] a = new Object[newCapacity];
    System.arraycopy(elements, p, a, 0, r);//复制右半部分,对应上图中绿色部分
    System.arraycopy(elements, 0, a, r, p);//复制左半部分,对应上图中灰色部分
    elements = (E[])a;
    head = 0;
    tail = n;
}

addLast()

addLast(E e)的作用是在Deque的尾端插入元素,也就是在tail的位置插入元素,由于tail总是指向下一个可以插入的空位,因此只需要elements[tail] = e;即可。插入完成后再检查空间,如果空间已经用光,则调用doubleCapacity()进行扩容。

public void addLast(E e) {
    if (e == null)//不允许放入null
        throw new NullPointerException();
    elements[tail] = e;//赋值
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)//下标越界处理
        doubleCapacity();//扩容
}

pollFirst()

pollFirst()的作用是删除并返回Deque首端元素,也即是head位置处的元素。如果容器不空,只需要直接返回elements[head]即可,当然还需要处理下标的问题。由于ArrayDeque中不允许放入null,当elements[head] == null时,意味着容器为空。

public E pollFirst() {
    int h = head;
    E result = elements[head];
    if (result == null)//null值意味着deque为空
        return null;
    elements[h] = null;//let GC work
    head = (head + 1) & (elements.length - 1);//下标越界处理
    return result;
}

pollLast()

pollLast()的作用是删除并返回Deque尾端元素,也即是tail位置前面的那个元素。

public E pollLast() {
    int t = (tail - 1) & (elements.length - 1);//tail的上一个位置是最后一个元素
    E result = elements[t];
    if (result == null)//null值意味着deque为空
        return null;
    elements[t] = null;//let GC work
    tail = t;
    return result;
}

peekFirst()

peekFirst()的作用是返回但不删除Deque首端元素,也即是head位置处的元素,直接返回elements[head]即可。

public E peekFirst() {
    return elements[head]; // elements[head] is null if deque empty
}

peekLast()

peekLast()的作用是返回但不删除Deque尾端元素,也即是tail位置前面的那个元素。

public E peekLast() {
    return elements[(tail - 1) & (elements.length - 1)];
}