28.jdk源码阅读之CopyOnWriteArraySet

news2025/4/27 15:23:18

1. 写在前面

类继承和实现关系图
CopyOnWriteArraySet 是 Java 中一个线程安全的 Set 实现,它的底层是基于 CopyOnWriteArrayList 实现的。这种数据结构在并发编程中非常有用,因为它在写操作时会创建一个新的数组副本,从而避免了并发修改问题。不知道大家对它的底层实现有没有研究过,比如下面几个问题:

  1. CopyOnWriteArraySet 适用于哪些场景?
  2. CopyOnWriteArraySet 的优缺点是什么?
  3. CopyOnWriteArraySet 如何保证线程安全?
  4. CopyOnWriteArraySet 的迭代器是线程安全的吗?
  5. CopyOnWriteArraySet 如何实现添加元素的?
  6. CopyOnWriteArraySet 如何实现删除元素的?
  7. CopyOnWriteArraySet 与 HashSet 的区别是什么?
  8. CopyOnWriteArraySet 如何应对高频写操作

2. 从使用说起

2.1 基础使用

import java.util.concurrent.CopyOnWriteArraySet;

public class BasicUsage {
    public static void main(String[] args) {
        CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
        
        // 添加元素
        set.add("A");
        set.add("B");
        set.add("C");
        
        // 迭代元素
        for (String s : set) {
            System.out.println(s);
        }
        
        // 删除元素
        set.remove("B");
        
        // 再次迭代元素
        for (String s : set) {
            System.out.println(s);
        }
    }
}

2.2 多线程读操作

在多线程环境下,CopyOnWriteArraySet 可以高效地进行读操作,因为读操作不会被写操作阻塞。

import java.util.concurrent.CopyOnWriteArraySet;

public class MultiThreadedRead {
    public static void main(String[] args) {
        CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
        set.add("A");
        set.add("B");
        set.add("C");

        Runnable readTask = () -> {
            for (String s : set) {
                System.out.println(Thread.currentThread().getName() + " - " + s);
            }
        };

        Thread t1 = new Thread(readTask);
        Thread t2 = new Thread(readTask);

        t1.start();
        t2.start();
    }
}

2.3 读多写少的场景

CopyOnWriteArraySet 非常适合读多写少的场景,例如缓存、配置数据等。

import java.util.concurrent.CopyOnWriteArraySet;

public class ReadMostly {
    private static CopyOnWriteArraySet<String> cache = new CopyOnWriteArraySet<>();

    public static void main(String[] args) {
        cache.add("Config1");
        cache.add("Config2");

        // 读操作
        System.out.println(getConfig("Config1"));

        // 写操作
        updateConfig("Config3");

        // 再次读操作
        System.out.println(getConfig("Config3"));
    }

    public static String getConfig(String config) {
        for (String s : cache) {
            if (s.equals(config)) {
                return s;
            }
        }
        return null;
    }

    public static void updateConfig(String config) {
        cache.add(config);
    }
}

2.4 并发迭代

由于 CopyOnWriteArraySet 的迭代器是基于快照的,因此在迭代过程中,可以进行安全的读写操作,迭代器不会抛出 ConcurrentModificationException。

import java.util.concurrent.CopyOnWriteArraySet;

public class ConcurrentIteration {
    public static void main(String[] args) {
        CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
        set.add("A");
        set.add("B");
        set.add("C");

        Runnable readTask = () -> {
            for (String s : set) {
                System.out.println(Thread.currentThread().getName() + " - " + s);
            }
        };

        Runnable writeTask = () -> {
            set.add("D");
            set.remove("A");
        };

        Thread t1 = new Thread(readTask);
        Thread t2 = new Thread(writeTask);

        t1.start();
        t2.start();
    }
}

2.5 高频写操作的替代方案

虽然 CopyOnWriteArraySet 不适合高频写操作,但在这种场景下,可以考虑使用其他线程安全的集合类,如 ConcurrentHashMap

import java.util.concurrent.ConcurrentHashMap;

public class HighFrequencyWrite {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Boolean> map = new ConcurrentHashMap<>();
        
        // 添加元素
        map.put("A", true);
        map.put("B", true);
        map.put("C", true);
        
        // 迭代元素
        for (String key : map.keySet()) {
            System.out.println(key);
        }
        
        // 删除元素
        map.remove("B");
        
        // 再次迭代元素
        for (String key : map.keySet()) {
            System.out.println(key);
        }
    }
}

3. add(E e)的底层实现

这段代码展示了 CopyOnWriteArraySet 的核心方法之一:add 方法及其辅助方法 addIfAbsent。CopyOnWriteArraySet 是基于 CopyOnWriteArrayList 实现的,al 就是 CopyOnWriteArrayList 的实例。下面我们逐行分析这段代码,解释其工作原理。

 public boolean add(E e) {
        return al.addIfAbsent(e);
    }
  public boolean addIfAbsent(E e) {
        Object[] snapshot = getArray();
        return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
            addIfAbsent(e, snapshot);
    }

3.1 addIfAbsent 方法

public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
        addIfAbsent(e, snapshot);
}

addIfAbsent 方法用于在集合中添加元素 e,如果元素已经存在,则不添加。它的实现分为两个步骤:

  1. 获取当前数组的快照。
Object[] snapshot = getArray();

getArray 方法返回当前 CopyOnWriteArrayList 的底层数组。这是一个快照,保证了在迭代过程中数组不会被修改。
2. 检查元素是否已经存在,如果不存在则添加。

return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
    addIfAbsent(e, snapshot);
  • indexOf(e, snapshot, 0, snapshot.length) >= 0:检查元素 e 是否已经存在于数组中。如果存在,返回 false 表示添加失败。
  • addIfAbsent(e, snapshot):如果元素 e 不存在,则调用 addIfAbsent(e, snapshot) 方法将其添加到数组中。

3.2 辅助方法 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]))
                    return false;
            }
            if (indexOf(e, current, common, len) >= 0)
                return false;
        }
        Object[] newArray = Arrays.copyOf(current, len + 1);
        newArray[len] = e;
        setArray(newArray);
        return true;
    } finally {
        lock.unlock();
    }
}

addIfAbsent 方法在持有锁的情况下执行,以确保线程安全。

3.2.1 获取锁

final ReentrantLock lock = this.lock;
lock.lock();

获取锁以确保线程安全。

3.2.2 获取当前数组

Object[] current = getArray();
int len = current.length;

获取当前数组并记录其长度。

3.2.3 检查快照和当前数组是否一致

if (snapshot != current) {
    int common = Math.min(snapshot.length, len);
    for (int i = 0; i < common; i++) {
        if (current[i] != snapshot[i] && eq(e, current[i]))
            return false;
    }
    if (indexOf(e, current, common, len) >= 0)
        return false;
}

如果快照和当前数组不一致,说明在获取快照后,数组可能已经被其他线程修改。需要再次检查元素是否已经存在。

3.2.4 创建新数组并添加元素

Object[] newArray = Arrays.copyOf(current, len + 1);
newArray[len] = e;
setArray(newArray);

创建一个新数组,将当前数组的元素复制到新数组中,并在新数组的末尾添加新元素 e。

3.2.5 释放锁并返回结果

return true;

返回 true 表示添加成功,并在 finally 块中释放锁。

4. set(int index, E element) 的底层实现

4.1 方法签名

public E set(int index, E element) {

set 方法用于替换指定索引 index 处的元素为 element,并返回旧值。

4.2 获取锁

final ReentrantLock lock = this.lock;
lock.lock();

获取 ReentrantLock 锁以确保线程安全。所有的修改操作都在持有锁的情况下进行。

4.3 主体逻辑

try {
    Object[] elements = getArray();
    E oldValue = get(elements, index);

4.3.1 获取当前数组

Object[] elements = getArray();

调用 getArray 方法获取当前 CopyOnWriteArrayList 的底层数组。

4.3.2 获取旧值

E oldValue = get(elements, index);

调用 get 方法获取指定索引 index 处的旧值。

4.4 检查并替换元素

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

4.4.1 检查旧值和新值是否相同

if (oldValue != element) {

如果旧值和新值不同,则进行替换操作。

4.4.2 创建新数组并替换元素

int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
  • Arrays.copyOf(elements, len):创建一个新数组 newElements,将当前数组 elements 的所有元素复制到新数组中。
  • newElements[index] = element:将新值 element 替换到指定索引 index 处。
  • setArray(newElements):将新数组设置为 CopyOnWriteArrayList 的底层数组。

4.4.3 处理相同值的情况

} else {
    // Not quite a no-op; ensures volatile write semantics
    setArray(elements);
}

如果旧值和新值相同,虽然不需要实际替换元素,但仍然调用 setArray 方法来确保 volatile 写语义(即确保内存可见性)。

4.5 返回旧值并释放锁

return oldValue;
} finally {
    lock.unlock();
}
  • return oldValue;:返回旧值。
  • finally 块:在 finally 块中释放锁,以确保锁在任何情况下都能被释放,避免死锁。

5. remove(int index)的底层实现

这段代码展示了 CopyOnWriteArrayList 类中的 remove 方法,它用于移除指定索引处的元素并返回被移除的值。这是一个线程安全的方法,通过使用 ReentrantLock 来确保操作的原子性。下面我们逐行分析这段代码,解释其工作原理。

 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)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

5.1 方法签名

public E remove(int index) {

remove 方法用于移除指定索引 index 处的元素,并返回被移除的元素。

5.2 获取锁

final ReentrantLock lock = this.lock;
lock.lock();

获取 ReentrantLock 锁以确保线程安全。所有的修改操作都在持有锁的情况下进行。

5.3 主体逻辑

try {
    Object[] elements = getArray();
    int len = elements.length;
    E oldValue = get(elements, index);

5.3.1 获取当前数组

Object[] elements = getArray();

调用 getArray 方法获取当前 CopyOnWriteArrayList 的底层数组。

5.3.2 获取数组长度

int len = elements.length;

获取当前数组的长度。

5.3.3 获取旧值

E oldValue = get(elements, index);

调用 get 方法获取指定索引 index 处的旧值。

5.4 移除元素

int numMoved = len - index - 1;
if (numMoved == 0)
    setArray(Arrays.copyOf(elements, len - 1));
else {
    Object[] newElements = new Object[len - 1];
    System.arraycopy(elements, 0, newElements, 0, index);
    System.arraycopy(elements, index + 1, newElements, index, numMoved);
    setArray(newElements);
}

5.4.1 计算需要移动的元素数量

int numMoved = len - index - 1;

计算从 index + 1 到数组末尾的元素数量,这些元素需要向前移动一个位置。

5.4.2 处理无需移动的情况

if (numMoved == 0)
    setArray(Arrays.copyOf(elements, len - 1));

如果 numMoved 为 0,表示移除的是最后一个元素。直接创建一个新数组,长度为 len - 1,并将前 len - 1 个元素复制到新数组中。

5.4.3 处理需要移动的情况

else {
    Object[] newElements = new Object[len - 1];
    System.arraycopy(elements, 0, newElements, 0, index);
    System.arraycopy(elements, index + 1, newElements, index, numMoved);
    setArray(newElements);
}

  • 创建新数组:Object[] newElements = new Object[len - 1]; 创建一个新数组,长度为 len - 1。
  • 复制前半部分:System.arraycopy(elements, 0, newElements, 0, index); 将原数组 elements 中从索引 0 到 index - 1 的元素复制到新数组 newElements 中。
  • 复制后半部分:System.arraycopy(elements, index + 1, newElements, index, numMoved); 将原数组 elements 中从索引 index + 1 到末尾的元素复制到新数组 newElements 中,从索引 index 开始。
  • 设置新数组:setArray(newElements); 将新数组设置为 CopyOnWriteArrayList 的底层数组。

5.5 返回旧值并释放锁

return oldValue;
} finally {
    lock.unlock();
}
  • return oldValue;:返回被移除的旧值。
  • finally 块:在 finally 块中释放锁,以确保锁在任何情况下都能被释放,避免死锁。

系列文章

1.JDK源码阅读之环境搭建

2.JDK源码阅读之目录介绍

3.jdk源码阅读之ArrayList(上)

4.jdk源码阅读之ArrayList(下)

5.jdk源码阅读之HashMap

6.jdk源码阅读之HashMap(下)

7.jdk源码阅读之ConcurrentHashMap(上)

8.jdk源码阅读之ConcurrentHashMap(下)

9.jdk源码阅读之ThreadLocal

10.jdk源码阅读之ReentrantLock

11.jdk源码阅读之CountDownLatch

12.jdk源码阅读之CyclicBarrier

13.jdk源码阅读之Semaphore

14.jdk源码阅读之线程池(上)

15.jdk源码阅读之线程池(下)

16.jdk源码阅读之ArrayBlockingQueue

17.jdk源码阅读之LinkedBlockingQueue

18.jdk源码阅读之CopyOnWriteArrayList

19.jdk源码阅读之FutureTask

20.jdk源码阅读之CompletableFuture

21.jdk源码阅读之AtomicLong

22.jdk源码阅读之Thread(上)

23.jdk源码阅读之Thread(下)

24.jdk源码阅读之ExecutorService

25.jdk源码阅读之Executors

26.jdk源码阅读之ConcurrentLinkedQueue

27.jdk源码阅读之ConcurrentLinkedDeque

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1959435.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

angular入门基础教程(五)父子组件的数据通信

组件之间的通信是我们业务开发中少不了的,先了解下父子组件的通信 父组件传数据给子组件 前面&#xff0c;我们学会会动态属性的绑定&#xff0c;所以在父组件中给子组件绑定属性&#xff0c;在子组件中就可以使用这个属性了。 父组件中声明然后赋值 export class AppCompon…

C语言 | Leetcode C语言题解之第304题二维区域和检索-矩阵不可变

题目&#xff1a; 题解&#xff1a; typedef struct {int** sums;int sumsSize; } NumMatrix;NumMatrix* numMatrixCreate(int** matrix, int matrixSize, int* matrixColSize) {NumMatrix* ret malloc(sizeof(NumMatrix));ret->sums malloc(sizeof(int*) * (matrixSize …

图论:721. 账户合并(并查集扩展)

文章目录 1、题目链接2、题目描述3、并查集思路3.1、按秩合并3.2、常用并查集代码 4、题目解析 1、题目链接 721. 账户合并 2、题目描述 3、并查集思路 并查集可以在很短的时间内合并不同的集合。它的思想为&#xff0c;一开始将不同单元单独作为一个结点&#xff0c;然后按…

【Qt】修改窗口的标题和图标

以下操作仅对顶层 widget(独⽴窗口),有效。 修改窗口的标题 一.windowTitle属性 1.概念 是一种在用户界面中显示窗口的标题的属性。它可以用来设置窗口的标题栏文本。 2.API API说明windowTitle()获取到控件的窗⼝标题.setWindowTitle(const QString& title)设置控件的…

线性回归和逻辑回归揭示数据的隐藏模式:理论与实践全解析

机器学习之线性回归和逻辑回归 1. 简介1.1 机器学习概述1.2 监督学习的定义与重要性1.3 线性回归和逻辑回归在监督学习中的作用1.3.1 线性回归1.3.2 逻辑回归 2. 线性回归&#xff08;Linear Regression&#xff09;2.1 定义与目标2.1.1 回归问题的定义2.1.2 预测连续目标变量 …

Redis持久化之RDB和AOF详解

持久化是确保 Redis 数据在服务器重启或崩溃时不丢失的关键功能。由于 Redis 是基于内存的数据库&#xff0c;如果不进行持久化&#xff0c;所有数据都存在于内存中&#xff0c;一旦服务器进程退出&#xff0c;内存中的数据就会丢失。持久化机制可以将 Redis 的数据库状态保存到…

Qt 学习第三天:加一个按钮

本章心得&#xff1a; 这个章节有点像写前端的味道了&#xff0c;设置按钮大小&#xff0c;按钮位置&#xff0c;窗口大小......代码全在widget.cpp上写的 #include "widget.h" #include "ui_widget.h" #include <QPushButton>Widget::Widget(QWid…

C++初级学习:⼊⻔基础

本文内容&#xff1a; 1.C参考⽂档&#xff1a;2.C第一个程序3.命名空间3.1namespace的价值3.2namespace的定义3.3命名空间的使用 4.C输⼊&输出5.缺省参数6.函数重载 1.C参考⽂档&#xff1a; https://legacy.cplusplus.com/reference/ https://zh.cppreference.com/w/cp…

实战:Zookeeper 简介和单点部署ZooKeeper

Zookeeper 简介 ZooKeeper是一个开源的分布式协调服务&#xff0c;它是Apache软件基金会下的一个项目&#xff0c;旨在解决分布式系统中的协调和管理问题。以下是ZooKeeper的详细简介&#xff1a; 一、基本定义 ZooKeeper是一个分布式的、开放源码的分布式应用程序协调服务&a…

添加索引导致微服务异常

一、现象 某app部分功能不可用&#xff0c;提示“连接服务器超时&#xff0c;请稍后尝试”。 二、分析 1、分析发现数据库存在大量的TM争用。 2、继续分析发现存在TM行锁的阻塞会话主要是以下几个&#xff1a; 3、查看其阻塞源头是1940 4、而1940进程在这个时间段是在跑新增索…

逆矩阵、秩

在数学的广阔天地中&#xff0c;线性代数扮演着至关重要的角色。它不仅是现代科学和工程学的基石&#xff0c;也是理解复杂数据结构的关键。本文将深入探讨线性代数中的几个核心概念&#xff1a;逆矩阵、秩、列空间和零空间&#xff0c;通过详细的解释和丰富的实例&#xff0c;…

(2024,LoRA压缩和多LoRA快速切换,联合对角化,重构误差)先压缩再提供服务:以极低的开销为数千个 LoRA提供服务

Compress then Serve: Serving Thousands of LoRA Adapters with Little Overhead 公和众与号&#xff1a;EDPJ&#xff08;进 Q 交流群&#xff1a;922230617 或加 VX&#xff1a;CV_EDPJ 进 V 交流群&#xff09; 目录 0. 摘要 1. 简介 3. 基于秩的 LoRA 压缩 3.1 期望特…

ubuntu安装tar安装 nginx最新版本

一、需要先安装依赖 apt install gcc libpcre3 libpcre3-dev zlib1g zlib1g-dev openssl libssl-dev 二、上传安装包 并解压 下载地址 nginx news tar xvf nginx-1.25.2.tar.gz 进入nginx cd nginx-1.25.2 三、编译 ./configure --prefix=/usr/local/nginx --with-htt…

数组模拟单调栈--C++

本题的计算量较大&#xff0c;用暴力算法会超时&#xff0c;的用别的方法&#xff0c;我们假设在左边找第一个比它小的数&#xff0c;那么在左边出现一次的数如果比右边大了&#xff0c;那么就不会在出现了&#xff0c;我们将它删除掉就可以了&#xff0c;用这个方法我们可以的…

算力QoS技术革新:OrionX引领AI行业资源管理新趋势

01 前言 在当今数字化时代&#xff0c;人工智能&#xff08;AI&#xff09;已成为各行业发展的关键推动力。然而&#xff0c;随着AI技术的蓬勃发展&#xff0c;行业对计算资源的需求也日益增长&#xff0c;传统的资源分配方式已无法满足需求。 在这一背景下&#xff0c;算力Q…

基于vue-onlyoffice实现企业office web在线应用

目录 1.背景... 1 2.Onlyoffice介绍... 2 3.Onlyoffice核心api介绍... 2 3.1 ApiDocument 2 3.2 ApiParagraph. 2 3.3 ApiTable. 2 3.4. ApiRange. 3 4.Onlyoffice插件介绍... 3 4.1 插件定义... 3 4.2 插件对象... 3 4.3 插件结构... 4 4.4 插件内嵌使用方式... 4…

Echarts 柱状图实现同时显示百分比+原始值+汇总值

原始效果&#xff1a;柱状图 二开效果&#xff1a; 核心逻辑 同时显示百分比和原始值 label: {show: true,position: inside,formatter: (params) > {const rawValue rawData[params.seriesIndex][params.dataIndex];const percentage Math.round(params.value * 1000) / …

基于springboot+vue+uniapp的校园二手交易小程序

开发语言&#xff1a;Java框架&#xff1a;springbootuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#…

达梦数据库基础操作-查询

一、基础查询 1 &#xff09;单表查询 1. 查看表结构 使用两种方式可查看数据库的表结构&#xff1a; 查询后会显示该表的创建语句以及结构 2. 查询全表 使用 SELECT * 查询全表&#xff0c;此时数据库会返回表所有列 3. 行过滤 使用条件查询进行过滤&#xff0c;…

2024电赛H题参考方案(+视频演示+核心控制代码)——自动行使小车

目录 一、题目要求 二、参考资源获取 三、参考方案 1、环境搭建及工程移植 2、相关模块的移植 4、整体控制方案视频演示 5、视频演示部分核心代码 总结 一、题目要求 小编自认为&#xff1a;此次H题属于控制类题目&#xff0c;相较于往年较为简单&#xff0c;功能也算单一&…