CopyOnWriteArrayList:深入理解Java中的线程安全List原理和应用

简介: CopyOnWriteArrayList:深入理解Java中的线程安全List原理和应用

1️⃣ 什么是CopyOnWrite(写时复制)

CopyOnWrite,也被称为写时复制(Copy-On-Write,简称COW),是程序设计领域中的一种优化策略。这种策略的核心思想是,当多个调用者(或线程)同时访问同一份资源时,他们会共同获取一个指向该资源的指针。只要没有调用者尝试修改这份资源,所有的调用者都可以继续访问同一个资源。但是,一旦有调用者尝试修改资源,系统就会复制一份该资源的副本给这个调用者,而其他调用者所见到的仍然是原来的资源。这个过程对其他的调用者都是透明的,他们并不知道资源已经被复制。


在Java中,CopyOnWriteArrayList和CopyOnWriteArraySet就是使用了这种策略的两个类。这两个类都位于java.util.concurrent包下,是线程安全的集合类。当需要修改集合中的元素时,它们不会直接在原集合上进行修改,而是复制一份新的集合,然后在新的集合上进行修改。修改完成后,再将指向原集合的引用指向新的集合。这种设计使得读操作可以在不加锁的情况下进行,从而提高了并发性能。


总的来说,CopyOnWrite是一种适用于读多写少场景的优化策略,它通过复制数据的方式实现了读写分离,提高了并发性能。但是,它也存在一些潜在的性能问题,如内存占用增加、写操作性能下降以及频繁的垃圾回收。因此,在使用时需要根据具体场景进行权衡和选择。


2️⃣什么是CopyOnWriteArrayList

CopyOnWriteArrayList是Java并发包java.util.concurrent中的一个类,它实现了List接口。如其名所示,


CopyOnWriteArrayList是Java中的一个类,位于java.util.concurrent包下。它是ArrayList的一个线程安全的变体,其中所有可变操作(如add和set等)都是通过创建底层数组的新副本来实现的,因此被称为“写时复制”的列表。


由于CopyOnWriteArrayList在遍历时不会对列表进行任何修改,因此它绝对不会抛出ConcurrentModificationException的异常。它在修改操作(如add、set等)时,会复制一份底层数组,然后在新的数组上进行修改,修改完成后再将指向底层数组的引用切换到新的数组。这种设计使得读操作可以在不加锁的情况下进行,从而提高了并发性能,这个特性使得它在多线程环境下进行遍历操作时更为安全。


然而,CopyOnWriteArrayList并没有“扩容”的概念。每次写操作(如add或remove)都需要复制一个全新的数组,这在写操作较为频繁时可能会导致性能问题,因为复制整个数组的操作是相当耗时的。因此,在使用CopyOnWriteArrayList时,需要特别注意其适用场景,一般来说,它更适合于读多写少的场景。


3️⃣CopyOnWriteArrayList的工作原理

CopyOnWriteArrayList是ArrayList的一个线程安全的变体。读操作可以在不加锁的情况下进行,从而提高了并发性能。


具体来说,CopyOnWriteArrayList内部有一个可重入锁(ReentrantLock)来保证线程安全,但这个锁只在写操作时才会被使用。当进行修改操作时,线程会先获取锁,然后复制底层数组,并在新数组上执行修改。修改完成后,通过volatile关键字修饰的引用来确保新的数组对所有线程可见。由于读操作不需要获取锁,因此多个线程可以同时进行读操作,而不会相互干扰。

3.1 读写分离的设计模式的几个优点

  • 读操作性能很高

由于读操作不需要获取锁,因此多个线程可以同时进行读操作,而不会相互干扰。这使得在高并发场景下,CopyOnWriteArrayList的读操作性能非常出色。

  • 数据一致性

由于写操作是通过复制底层数组并在新数组上执行修改来实现的,因此不会出现多个线程同时修改同一个元素的情况。这保证了数据的一致性。

  • 适用于读多写少的场景

由于写操作需要复制整个底层数组,因此在写操作较为频繁的场景下,CopyOnWriteArrayList的性能可能会受到较大影响。但在读多写少的场景下,它可以充分发挥其优势。

3.2 存在的性能问题

  • 内存占用

每次写操作都需要复制整个底层数组,这会导致内存占用增加。特别是在列表较大时,这种内存开销可能会变得非常显著。

  • 写操作性能下降

由于每次写操作都需要复制整个数组,并在新数组上执行修改,因此写操作的性能可能会受到较大影响。特别是在高并发场景下,这种性能下降可能会更加明显。

  • 频繁的垃圾回收

由于写操作会创建新的数组,因此可能导致频繁的垃圾回收。这可能会对系统的整体性能产生影响。

总的来说,CopyOnWriteArrayList是一种适用于读多写少场景的线程安全列表实现。它通过复制底层数组的方式实现了读写分离,提高了读操作的并发性能。但在使用时需要根据具体场景进行权衡和选择,以避免潜在的性能问题。


3️⃣CopyOnWriteArrayList使用场景

CopyOnWriteArrayList适用于读多写少的场景。在这种场景下,由于读操作不需要获取锁,因此可以充分发挥多核CPU的并行计算能力,提高系统的吞吐量。然而,在写操作较为频繁的场景下,CopyOnWriteArrayList的性能可能会受到较大影响。


4️⃣CopyOnWriteArrayList的应用

下面是一个使用CopyOnWriteArrayList的代码,它模拟了一个简单的新闻发布系统。在这个系统中,多个线程可以并发地添加新闻和读取新闻列表。由于读操作远多于写操作,因此使用CopyOnWriteArrayList是合适的。

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

// 新闻类
class News {
    private String title;
    private String content;

    public News(String title, String content) {
        this.title = title;
        this.content = content;
    }

    public String getTitle() {
        return title;
    }

    public String getContent() {
        return content;
    }

    @Override
    public String toString() {
        return "News{" +
                "title='" + title + '\'' +
                ", content='" + content + '\'' +
                '}';
    }
}

// 新闻发布系统类
public class NewsPublisherSystem {
    // 使用CopyOnWriteArrayList存储新闻列表
    private final List<News> newsList = new CopyOnWriteArrayList<>();

    // 添加新闻
    public void addNews(News news) {
        newsList.add(news);
        System.out.println("新闻已添加: " + news);
    }

    // 获取新闻列表
    public List<News> getNewsList() {
        return newsList;
    }

    // 模拟多线程添加和读取新闻
    public void simulate() {
        ExecutorService executor = Executors.newFixedThreadPool(10);

        // 提交5个添加新闻的任务
        for (int i = 0; i < 5; i++) {
            final int index = i;
            executor.submit(() -> {
                for (int j = 0; j < 10; j++) {
                    News news = new News("新闻标题" + index + "-" + j, "新闻内容" + index + "-" + j);
                    addNews(news);
                    try {
                        // 模拟新闻发布的延迟
                        TimeUnit.MILLISECONDS.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        // 提交5个读取新闻列表的任务
        for (int i = 0; i < 5; i++) {
            executor.submit(() -> {
                for (int j = 0; j < 20; j++) {
                    System.out.println("当前新闻列表: " + getNewsList());
                    try {
                        // 模拟读取新闻列表的延迟
                        TimeUnit.MILLISECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        // 关闭执行器服务
        executor.shutdown();
        try {
            if (!executor.awaitTermination(1, TimeUnit.MINUTES)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) {
        NewsPublisherSystem system = new NewsPublisherSystem();
        system.simulate();
    }
}

NewsPublisherSystem类维护了一个CopyOnWriteArrayList来存储新闻对象。addNews方法用于添加新闻到列表中,而getNewsList方法用于获取当前的新闻列表。


在simulate方法中,我们创建了一个固定大小的线程池,并提交了10个任务:其中5个任务用于添加新闻,另外5个任务用于读取新闻列表。每个添加新闻的任务会创建并添加10条新闻,而每个读取新闻列表的任务会读取新闻列表20次。


由于使用了CopyOnWriteArrayList,多个线程可以同时读取新闻列表,而不会有线程安全问题。当添加新闻时,CopyOnWriteArrayList会复制底层数组,从而保证读取操作不会受到写操作的影响。


请注意,由于CopyOnWriteArrayList在写操作时会复制整个底层数组,因此在新闻列表非常大且写操作频繁的情况下,性能可能会受到影响。在这种情况下,可能需要考虑其他并发数据结构或同步策略。然而,在本案例中,由于读操作远多于写操作,使用CopyOnWriteArrayList是合适的。

5️⃣总结

CopyOnWriteArrayList是Java并发编程中一个重要的线程安全列表实现。它通过复制底层数组的方式实现了读写分离,提高了读操作的并发性能。然而,它也存在一些潜在的性能问题,如内存占用增加、写操作性能下降以及频繁的垃圾回收。因此,在使用CopyOnWriteArrayList时,需要根据具体的使用场景进行权衡和选择。在读多写少的场景下,CopyOnWriteArrayList可以发挥出色的性能;而在写操作较为频繁的场景下,可能需要考虑其他线程安全的列表实现。


目录
打赏
0
3
3
0
39
分享
相关文章
|
27天前
|
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
157 60
【Java并发】【线程池】带你从0-1入门线程池
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
66 23
|
23天前
|
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
93 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
134 14
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
61 13
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
|
2月前
|
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
135 17
|
3月前
|
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
Java 多线程 面试题
Java 多线程 相关基础面试题
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。