Java高手的30k之路|面试宝典|精通并发集合类CopyOnWriteArrayList ConcurrentSkipListMap ConcurrentSkipListSet

CopyOnWriteArrayList

CopyOnWriteArrayList 是 Java 并发包中的一个线程安全的 List 实现,适用于读多写少的场景。它通过在每次修改(如添加、删除元素)时复制整个底层数组,确保并发环境下的线程安全性。

主要特点

  1. 线程安全

    • 通过复制整个数组来确保线程安全,所有的修改操作(添加、删除、设置等)都会创建一个新的数组。
  2. 读操作无锁

    • 读操作不需要加锁,直接访问底层数组,因此读操作非常快,可以并发进行。
  3. 写操作开销大

    • 每次写操作都会复制整个数组,这使得写操作的开销比较大,适用于写操作相对较少的场景。

写操作的复制机制

每次执行写操作时,CopyOnWriteArrayList 会进行以下步骤:

  1. 获取当前数组的副本。
  2. 在副本上执行修改操作。
  3. 将修改后的副本设置为新的底层数组。

这种机制保证了写操作不会影响正在进行的读操作,从而确保了线程安全。

使用场景

CopyOnWriteArrayList 适用于以下场景:

  1. 读多写少

    • 由于写操作的开销较大,而读操作的性能非常好,因此非常适合读多写少的应用场景。
  2. 迭代器稳定性要求高

    • 由于迭代器是基于底层数组的快照创建的,因此在迭代过程中,如果有其他线程进行了修改操作,迭代器仍然可以安全地遍历原来的数据,不会抛出 ConcurrentModificationException

示例代码

import java.util.concurrent.CopyOnWriteArrayList;

public class Main {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        
        // 添加元素
        list.add("A");
        list.add("B");
        list.add("C");

        // 读操作
        for (String s : list) {
            System.out.println(s);
        }

        // 写操作
        list.add("D");

        // 再次读操作
        for (String s : list) {
            System.out.println(s);
        }
    }
}

性能分析

  • 读操作性能:由于读操作不需要加锁,因此性能非常高,适合并发读取场景。
  • 写操作性能:写操作需要复制整个数组,因此当列表较大且写操作频繁时,性能会受到影响。

注意事项

  1. 内存消耗

    • 每次写操作都会创建新的数组,因此会有额外的内存消耗,特别是在大数组频繁写入的情况下。
  2. 写操作性能

    • 由于写操作的开销较大,因此在写操作频繁的场景中,CopyOnWriteArrayList 的性能可能不如其他线程安全的集合类。

线程安全的实现

CopyOnWriteArrayList 通过以下几个关键步骤来实现线程安全:

  1. 不可变性CopyOnWriteArrayList 内部存储元素的数组是不可变的。每次写操作(如添加、删除元素)都会创建一个新的数组副本,而不会修改原有数组。这确保了在读操作进行时,底层数组不会被修改,从而避免了并发问题。

  2. 原子性CopyOnWriteArrayList 使用 volatile 关键字来确保对底层数组的引用是线程可见的,并使用 synchronized 块来确保写操作的原子性。这样可以确保在任何时候,只有一个线程可以进行写操作,从而避免并发修改的问题。

确保只有一个副本

当进行写操作时,CopyOnWriteArrayList 会创建一个新的数组副本,并将这个副本设置为内部的数组引用。通过 volatile 关键字来保证新数组对其他线程可见,从而确保读操作始终使用最新的数组副本。

示例代码分析

以下是 CopyOnWriteArrayList 的部分源码,用于展示其线程安全和副本机制:

import java.util.Arrays;
import java.util.concurrent.locks.ReentrantLock;

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private transient volatile Object[] array;

    final transient ReentrantLock lock = new ReentrantLock();

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

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

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

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

    public boolean remove(Object o) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (len == 0) {
                return false;
            }
            int newlen = len - 1;
            Object[] newElements = new Object[newlen];
            for (int i = 0; i < len; i++) {
                if (o.equals(elements[i])) {
                    System.arraycopy(elements, 0, newElements, 0, i);
                    System.arraycopy(elements, i + 1, newElements, i, newlen - i);
                    setArray(newElements);
                    return true;
                }
            }
            return false;
        } finally {
            lock.unlock();
        }
    }
    
    // 其他方法省略
}

解释

  1. 数组副本 (copy):在 addremove 方法中,当需要进行写操作时,会首先获取当前数组 (elements),然后创建一个新数组 (newElements),并将现有数组的内容复制到新数组中。对于 add 操作,新数组的长度增加一,并将新元素添加到新数组中。对于 remove 操作,新数组的长度减少一,并从中删除指定元素。

  2. 锁机制 (ReentrantLock):写操作使用 ReentrantLock 来确保原子性。锁的使用确保在任何时候,只有一个线程可以进行写操作,其他线程必须等待锁释放后才能进行写操作。这保证了写操作的线程安全性。

  3. volatile 关键字:数组引用 (array) 使用 volatile 关键字,这确保了当写操作完成后,新数组对所有读取线程立即可见。读操作始终访问当前的数组引用,因此不会受到写操作的影响。

CopyOnWriteArrayList in Spring

CopyOnWriteArrayList 在 Spring 框架中也有一些典型的应用场景,特别是在事件监听和处理器管理等需要高并发读写的地方。一个常见的例子是在 Spring 的事件发布机制中,用于管理事件监听器列表,以确保在高并发环境下的线程安全。

应用场景

Spring 的事件机制允许应用程序在事件发生时发布事件,其他组件可以监听这些事件并作出响应。CopyOnWriteArrayList 常用于管理这些事件监听器,因为它可以确保在高并发环境下的安全性,允许多个线程同时读取监听器列表而不会被阻塞。

代码示例

以下是一个使用 CopyOnWriteArrayList 来管理 Spring 事件监听器的示例代码,展示了如何确保高并发下的线程安全。

场景描述

我们创建一个自定义的事件发布者和事件监听器,使用 CopyOnWriteArrayList 来管理监听器列表。当事件发布时,所有监听器都会被通知。

示例代码

首先,我们定义一个自定义事件:

import org.springframework.context.ApplicationEvent;

public class CustomEvent extends ApplicationEvent {
    private final String message;

    public CustomEvent(Object source, String message) {
        super(source);
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

接下来,定义一个事件监听器接口:

public interface CustomEventListener {
    void onCustomEvent(CustomEvent event);
}

然后,我们创建一个事件发布者类,使用 CopyOnWriteArrayList 来管理监听器:

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class CustomEventPublisher {
    private final List<CustomEventListener> listeners = new CopyOnWriteArrayList<>();

    public void addListener(CustomEventListener listener) {
        listeners.add(listener);
    }

    public void removeListener(CustomEventListener listener) {
        listeners.remove(listener);
    }

    public void publishEvent(String message) {
        CustomEvent event = new CustomEvent(this, message);
        for (CustomEventListener listener : listeners) {
            listener.onCustomEvent(event);
        }
    }
}

最后,创建一个示例应用程序,展示如何使用事件发布者和监听器:

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CustomEventApp {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(CustomEventApp.class);
        CustomEventPublisher publisher = context.getBean(CustomEventPublisher.class);

        CustomEventListener listener1 = event -> System.out.println("Listener1 received: " + event.getMessage());
        CustomEventListener listener2 = event -> System.out.println("Listener2 received: " + event.getMessage());

        publisher.addListener(listener1);
        publisher.addListener(listener2);

        publisher.publishEvent("Hello, World!");
        publisher.publishEvent("Spring Events are cool!");
    }

    @Bean
    public CustomEventPublisher customEventPublisher() {
        return new CustomEventPublisher();
    }
}

解释

  1. 自定义事件 (CustomEvent):继承自 ApplicationEvent,用于携带事件信息。
  2. 事件监听器接口 (CustomEventListener):定义一个方法 onCustomEvent 来处理接收到的事件。
  3. 事件发布者 (CustomEventPublisher):使用 CopyOnWriteArrayList 来管理监听器列表,提供添加和移除监听器的方法,并在发布事件时通知所有监听器。
  4. 示例应用程序 (CustomEventApp):配置 Spring 上下文,创建事件发布者和监听器,并演示事件发布和处理过程。

ConcurrentSkipListMap

ConcurrentSkipListMap 和 ConcurrentSkipListSet 分别是基于跳表(Skip List)数据结构实现的并发版本的 TreeMap 和 TreeSet。

ConcurrentSkipListMap 是 Java 并发包 (java.util.concurrent) 中的一个线程安全且有序的 Map 实现。它基于跳表 (SkipList) 数据结构,提供高效的并发访问。

主要特点

  1. 线程安全

    • ConcurrentSkipListMap 是线程安全的,可以在并发环境中高效地进行读取和写入操作。
  2. 有序性

    • 元素按键的自然顺序或自定义比较器的顺序排列,支持高效的范围操作和按顺序的遍历。
  3. 非阻塞算法

    • 使用了无锁的算法 (CAS 操作),避免了传统锁的开销,提升了并发性能。

跳表数据结构

跳表是一种平衡数据结构,具有多层链表,每一层链表都是底层链表的子集。跳表通过多层级链表来实现快速查找、插入和删除操作。

使用场景

ConcurrentSkipListMap 适用于以下场景:

  1. 高并发环境下的有序映射

    • 需要高效地在并发环境中进行插入、删除和查找操作,同时保持键的顺序。
  2. 范围查询

    • 需要进行范围查询(如获取某个键范围内的所有键值对),利用有序特性可以高效地实现。
  3. 优先级队列

    • 可以用作优先级队列,支持按优先级顺序处理任务。

示例代码

import java.util.concurrent.ConcurrentSkipListMap;

public class Main {
    public static void main(String[] args) {
        ConcurrentSkipListMap<String, Integer> map = new ConcurrentSkipListMap<>();

        // 插入元素
        map.put("apple", 10);
        map.put("banana", 20);
        map.put("cherry", 30);

        // 读取元素
        System.out.println("apple: " + map.get("apple"));

        // 遍历元素
        for (String key : map.keySet()) {
            System.out.println(key + ": " + map.get(key));
        }

        // 范围查询
        System.out.println("Keys from apple to cherry: " + map.subMap("apple", "cherry"));
    }
}

性能分析

  • 读写性能ConcurrentSkipListMap 使用无锁算法进行并发访问,读写性能高效,适合高并发场景。
  • 有序性开销:由于保持有序性,写操作(插入和删除)开销略高于无序的 ConcurrentHashMap

注意事项

  1. 内存消耗

    • 跳表的多层结构会占用额外的内存,特别是在存储大量元素时,需要注意内存使用情况。
  2. 写操作开销

    • 相对于无序的并发集合(如 ConcurrentHashMap),有序集合的写操作(插入、删除)开销较大,因为需要维护顺序。

总结

ConcurrentSkipListMap 是在高并发环境中需要有序映射的理想选择,结合了跳表的数据结构和无锁算法,提供了高效的读写性能和良好的有序性。适用于需要频繁进行范围查询或优先级处理的场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值