3

并发编程必备:精通JDK并发容器的使用和策略

 1 month ago
source link: https://blog.51cto.com/xfishup/10680639
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1. JDK并发容器概览

并发编程是现代软件开发中不可或缺的一部分,它允许多线程同时访问和修改数据,但这也引入了复杂性,尤其是在容器(如List、Set、Map和Queue)这些共享数据结构的使用上。为了解决并发环境下的竞态条件和数据同步等问题,Java提供了一系列的线程安全容器,即并发容器。

并发编程必备:精通JDK并发容器的使用和策略_java

1.1. 并发容器的种类与选择

JDK为常用的数据结构提供了相应的并发容器实现,这些并发容器通常位于java.util.concurrent包中。简而言之,List、Set、Map和Queue都有其并发变体。例如:

  • CopyOnWriteArrayList 与 ConcurrentLinkedQueue分别作为并发List和Queue的代表。
  • ConcurrentHashMap 为Map结构提供线程安全的实现。
  • ConcurrentSkipListSet 和 ConcurrentSkipListMap 利用跳表结构实现高效的并发Set和Map。

在选择合适的并发容器时,需要根据实际的使用场景考虑。比如执行更多的读操作还是写操作?是否需要排序功能?是否考虑内存占用和扩展性等因素。

1.2. 并发容器与普通容器的比较

与普通容器相比,如ArrayList 或 HashMap,并发容器提供更高的并发性能。这主要得益于它们内部采用的一些高级技术,如“写时复制”(Copy-On-Write)、“锁分割”(Lock Stripping)和非阻塞数据结构等。普通容器如果在多线程环境下共享访问,必须通过外部同步机制(例如使用Collections.synchronizedList 包装器)来保证线程安全,这往往会导致性能瓶颈。
并发容器更注重在多线程环境下数据结构的操作性能,而不仅仅是线程安全。这是它们被广泛使用在高性能并发应用中的原因。

2. 并发List的实现

在Java中,List是一个有序的集合,它的特点是可以精确的控制每个元素的插入位置,或者访问集合中的元素。

2.1. CopyOnWriteArrayList

当我们谈到并发List的实现时,CopyOnWriteArrayList 是不得不提的一个类。正如其名,这个类用 “写时复制” 的策略来避免并发冲突。这种策略指的是当需要修改List时,它并不直接在原有的数组上进行操作,而是先复制出一个新的数组,然后在新数组上进行修改,最后再将原数组引用指向新数组。

2.1.1 特点

  • 读操作无锁: 由于修改操作不会在原有的数组上进行,因此读操作可以高效地并行执行,不需要加锁。
  • 写操作加锁: 当有新的写操作时,它会锁定整个List,因此写操作是串行的。
  • 内存消耗: 因为每次写操作都需要复制整个底层数组,所以写操作的内存开销会比较大。
import java.util.concurrent.CopyOnWriteArrayList;

public class COWListDemo {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
        cowList.add("Java");
        cowList.add("Concurrency");
        // ... 后续操作
    }
}

2.1.2 使用案例与性能分析

CopyOnWriteArrayList 特别适用于读多写少的并发情况。比如在事件监听器的管理中通常读取操作远多于注册和注销事件,这样可以利用它来存储监听器列表。
但如果写操作频繁,它的性能就会受到影响,因为每次写操作都需要复制整个数组,如果数据量大,会导致大量的临时内存消耗和数组复制成本。

2.2. 使用案例与性能分析

这一部分中,我们将通过一个具体的示例来深入了解 CopyOnWriteArrayList 的性能表现。假设我们有一个Web服务器,它使用 CopyOnWriteArrayList 来管理所有的会话对象。每当有新的用户会话开始时,都会有一个新的会话对象被添加到列表中;当用户会话结束时,则会从列表中移除该会话对象。

import java.util.concurrent.CopyOnWriteArrayList;

public class WebServerSessionManager {
    private CopyOnWriteArrayList<Session> sessions = new CopyOnWriteArrayList<>();

    public void addSession(Session session) {
        sessions.add(session);
    }

    public void removeSession(Session session) {
        sessions.remove(session);
    }

    // ... 其他逻辑
}

class Session {
    // 会话的相关属性和方法
}

在这个使用案例中,我们可以看到如果用户会话频繁创建和销毁,那么 CopyOnWriteArrayList 就可能成为性能瓶颈。在这种情况下,可能需要使用其他并发策略,比如利用 ConcurrentLinkedQueue 等其他更适合频繁修改操作的并发容器。

3. 并发Set的实现

在Java的并发包java.util.concurrent中,提供了为并发环境优化的Set实现,主要包括ConcurrentSkipListSet和CopyOnWriteArraySet。

3.1. ConcurrentSkipListSet

ConcurrentSkipListSet是一个基于ConcurrentSkipListMap的可扩展且线程安全的NavigableSet实现。它利用跳表(skip list)数据结构,为集合中的元素提供了顺序访问,并且支持近似的对数时间成本的搜索、插入和删除操作。

3.1.1 特点

  • 可排序性: 元素会自然顺序排列,也可以通过构造函数指定比较器。
  • 并发性能好: 多线程环境中,读写操作可以并发执行,而不必锁定整个集合。
  • 适用场景: 适用于需要排序的集合处理,特别是在有序并发访问时。
import java.util.concurrent.ConcurrentSkipListSet;

public class CSLSetDemo {
    public static void main(String[] args) {
        ConcurrentSkipListSet<Integer> cslSet = new ConcurrentSkipListSet<>();
        cslSet.add(10);
        cslSet.add(5);
        cslSet.add(20);
        // 元素会自动排序
        System.out.println(cslSet);
    }
}

3.2. CopyOnWriteArraySet

CopyOnWriteArraySet的内部实际上是通过CopyOnWriteArrayList来实现的,它继承了CopyOnWriteArrayList的所有特性,提供了线程安全的Set实现。

3.2.1 特点

  • 线程安全: 保证了Set的基本操作如添加、删除和检查是否包含元素等操作的线程安全。
  • 写操作成本高: 类似于CopyOnWriteArrayList,它在进行写操作时也会复制整个底层数组。
  • 读操作高效: 对于读多写少的场景,该Set提供了高效的遍历操作。
import java.util.concurrent.CopyOnWriteArraySet;

public class COWSetDemo {
    public static void main(String[] args) {
        CopyOnWriteArraySet<String> cowSet = new CopyOnWriteArraySet<>();
        cowSet.add("Java");
        cowSet.add("Concurrency");
        // 由于底层用CopyOnWriteArrayList实现,所以插入顺序得到保留
        System.out.println(cowSet);
    }
}

3.3. 使用案例与场景分析

这里我们深入分析一下ConcurrentSkipListSet和CopyOnWriteArraySet的使用场景。
对于需要维护一个大型且有序的集合,且集合内的消费与生产操作较为频繁的情况,ConcurrentSkipListSet是一个很好的选择,因为它提供了不错的并发性能。
而CopyOnWriteArraySet则更适用于集合大小相对较小,或者读操作远多于写操作的场景,比如注册事件监听器,通常会有大量的读操作(事件调用)和少量的写操作(添加或移除监听器)。

4. 并发Map的实现

Map在软件开发中被广泛使用,主要用于存储键值对。为了支持并发环境,JDK提供了几种线程安全的Map实现。

4.1. ConcurrentHashMap

ConcurrentHashMap是java.util.concurrent包中的一个线程安全的哈希表,适用于高并发场景。它提供了比Hashtable和同步的HashMap(通过Collections.synchronizedMap方法包装)更高的并发性能。

4.1.1 特点

  • 锁分段技术(Segmentation): ConcurrentHashMap在内部使用多个锁来控制对哈希表的不同段(Segment)的访问,这样就允许多线程并发地读写Map,极大提高其并发性能。
  • 高并发遍历操作: 迭代过程中的读取操作可以并行进行,不需要锁定整个Map。
  • 弱一致性迭代器: 迭代器能反映出构造时或迭代开始时的状态,而不会反映出后续的修改。
import java.util.concurrent.ConcurrentHashMap;

public class CHMExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();
        chm.put("key1", 1);
        chm.put("key2", 2);
        // Concurrent updates and accesses are safe
    }
}

4.2. ConcurrentSkipListMap

对于有序映射需求,ConcurrentSkipListMap是个不错的选择。它是一个可排序的并发Map实现,使用跳表(Skip List)数据结构,类似于ConcurrentSkipListSet。

4.2.1 特点

  • 排序的Map: 自然排序或者自定义比较器排序。
  • 并发访问: 支持较大程度的并发。
  • 快速搜索: 对数时间的搜索效能。

4.3. 同步Map(Collections.synchronizedMap)

这是Java早期版本提供的线程安全的Map实现方式,它将非同步的HashMap包装成同步的。虽然这不是一个专门为并发设计的容器,但了解它是如何工作的对于理解并发容器是有帮助的。

4.3.1 特点

  • 全局锁: 对容器中任意操作进行访问时,都需要获取全局锁。
  • 适用性: 适用于访问量不大的并发场景。
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class SynchronizedMapExample {
    public static void main(String[] args) {
        Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
        syncMap.put("key1", 1);
        syncMap.put("key2", 2);
        // Safe updates and accesses in a concurrent context
    }
}

4.4. 性能比较与适用场景

现代并发应用中,选择正确的并发Map至关重要。ConcurrentHashMap在多线程环境中表现出色,尤其是在需要大量并发读写操作时。ConcurrentSkipListMap则适合于需要排序特性的场景。而对于并发级别不高的情况,可以考虑使用Collections.synchronizedMap。

5. 并发Queue

在并发编程中,队列常常用作线程之间的数据传递机制。为此,Java的java.util.concurrent包提供了多种并发队列,既有阻塞队列也有非阻塞队列,用于不同的应用场景。

5.1. 单端阻塞队列

单端阻塞队列指的是队列的插入和移除操作发生在同一端。Java为这种模式提供了不同的实现,如ArrayBlockingQueue和LinkedBlockingQueue。

5.1.1. ArrayBlockingQueue

ArrayBlockingQueue是一个由数组支持的有界阻塞队列。此队列按照先进先出(FIFO)原则对元素进行排序。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ABQExample {
    public static void main(String[] args) {
        BlockingQueue<Integer> abq = new ArrayBlockingQueue<>(10);
        abq.add(1);
        abq.add(2);
        // Producer-consumer operations
    }
}

5.1.2. LinkedBlockingQueue

与ArrayBlockingQueue类似,LinkedBlockingQueue也是有界的,但其内部则通过链表结构实现。默认情况下,LinkedBlockingQueue的容量是Integer.MAX_VALUE。

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class LBQExample {
    public static void main(String[] args) {
        BlockingQueue<Integer> lbq = new LinkedBlockingQueue<>();
        lbq.add(1);
        lbq.add(2);
        // Further operations
    }
}

5.2. 双端阻塞队列

LinkedBlockingDeque是代表性的双端阻塞队列,可在队列的两端插入或移除元素。

5.2.1. LinkedBlockingDeque

import java.util.concurrent.LinkedBlockingDeque;

public class LBDExample {
    public static void main(String[] args) {
        LinkedBlockingDeque<Integer> lbd = new LinkedBlockingDeque<>();
        lbd.addFirst(1);
        lbd.addLast(2);
        // Deque operations
    }
}

5.3. 单端非阻塞队列

ConcurrentLinkedQueue是一种非阻塞的FIFO队列,适合于高并发场景下的插入、移除和访问操作。

5.3.1. ConcurrentLinkedQueue

import java.util.concurrent.ConcurrentLinkedQueue;

public class CLQExample {
    public static void main(String[] args) {
        ConcurrentLinkedQueue<Integer> clq = new ConcurrentLinkedQueue<>();
        clq.add(1);
        clq.add(2);
        // Non-blocking operations
    }
}

5.4. 双端非阻塞队列

与ConcurrentLinkedQueue类似,ConcurrentLinkedDeque支持同时从两端进行非阻塞的插入和移除操作。

5.4.1. ConcurrentLinkedDeque

import java.util.concurrent.ConcurrentLinkedDeque;

public class CLDExample {
    public static void main(String[] args) {
        ConcurrentLinkedDeque<Integer> cld = new ConcurrentLinkedDeque<>();
        cld.addFirst(1);
        cld.addLast(2);
        // Concurrent deque operations
    }
}

5.5. 有界与无界队列的概念与选择

在选择队列的类型时,考虑队列的界限是很重要的。有界队列可以帮助防止资源耗尽,因为它限制了队列可以持有的元素数量。无界队列可能会导致系统内存耗尽,但它们提供了更大的灵活性。
选择哪种类型的队列将取决于你的具体需求。如果你希望队列有助于控制资源消耗,那么有界队列可能是更好的选择。否则,如果你的应用需求更加倾向于队列操作的灵活性和性能,无界队列可能更适合。

6. 并发容器的高级特性

并发容器不仅仅提供了线程安全的数据访问,还引入了一些高级的并发特性,使得它们在多线程环境中更为强大和灵活。

6.1. 迭代器的弱一致性

并发容器的迭代器通常提供了弱一致性(weakly consistent)的特性。这意味着迭代器在遍历的时候不会反映出构造它们之后的修改,只能保证不抛出ConcurrentModificationException异常。

import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;

public class WeaklyConsistentIteratorExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("One", 1);
        map.put("Two", 2);
        
        Iterator<String> iterator = map.keySet().iterator();
        while (iterator.hasNext()) {
            String key = iterator.next();
            if (key.equals("One")) {
                map.remove(key); // Safe to modify the collection
            }
            System.out.println(key);
        }
    }
}

在上面的例子中,我们可以在使用迭代器遍历的同时安全地修改ConcurrentHashMap,而不需要担心遇到迭代器快速失败的问题。

6.2. 锁分段技术

锁分段技术(lock stripping)是并发容器实现中用于提升性能的一种技术。这种技术通过分解锁定的粒度,使得不同线程可以并行访问数据结构的不同部分。ConcurrentHashMap就是使用锁分段技术的典型例子。

6.3. 原子操作与并发策略

许多并发容器还提供了对元素的原子操作,比如atomic putIfAbsent、remove和replace方法。这些方法能够确保操作在没有外部同步的情况下也是线程安全的。
原子操作的使用示例:

import java.util.concurrent.ConcurrentHashMap;

public class AtomicOperationsExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        // Atomically updates the value for the given key if it is present
        map.putIfAbsent("One", 1);
        // ... other atomic operations
    }
}

并发容器的这些高级特性在设计和实现多线程应用程序时至关重要,它们不仅提供了线程安全,还增加了程序的效率和响应能力。

7. 并发容器的实战应用

实际开发中,并发容器的正确使用对于构建健壮、高效的并发应用至关重要。

7.1. 实际业务案例分析

以在线电商平台的库存管理系统为例,该系统要保证对商品库存的读取和更新要高效且线程安全。假设库存信息存放在一个ConcurrentHashMap中,其中商品ID作为键,库存数量作为值。
库存管理系统中会包含增加库存、减少库存和查询库存的操作。由于ConcurrentHashMap提供了原子操作方法,如compute和computeIfAbsent等,我们可以利用这些特性来实现线程安全的库存更新。

import java.util.concurrent.ConcurrentHashMap;

public class InventoryManager {
    private final ConcurrentHashMap<String, Long> stockMap = new ConcurrentHashMap<>();

    public void addStock(String itemId, long quantity) {
        stockMap.compute(itemId, (key, value) -> value == null ? quantity : value + quantity);
    }

    public void deductStock(String itemId, long quantity) {
        stockMap.computeIfPresent(itemId, (key, value) -> value > quantity ? value - quantity : 0);
    }

    public long getStock(String itemId) {
        return stockMap.getOrDefault(itemId, 0L);
    }
}

7.2. 并发容器的性能调优

对于并发容器的性能调优,重点在于根据具体场景选择合适的容器类型和配置参数。例如,选择合适的初始容量和并行级别对ConcurrentHashMap的性能影响很大。
除了选择合适的并发容器外,容器内元素的管理(如保持容器大小的合理性和进行定时清理)也是性能调优的关键。

7.3. 常见问题与解决方案

并发编程经常会遇到的问题包括死锁、资源竞争、线程饥饿等。合理使用并发容器可以在很大程度上减少这些问题的发生。
另外,还应该注意版本控制和后向兼容性。当系统升级并发容器或相关依赖时,需要确保新的更改仍然兼容旧版本的代码,避免因升级导致的问题。
在实际开发中,最佳实践是结合业务逻辑深入理解并发容器的工作机制和性能特性,针对具体的业务需求和并发场景,选择并合理运用合适的并发容器。


Recommend

  • 26

    5 并发容器 5.1 Hashtable、HashMap、TreeMap、HashSet、LinkedHashMap 在...

  • 7

    [TOC]GO的并发编程分享之前我们分享了网络编程,今天我们来看看GO的并发编程分享,我们先来看看他是个啥啥是并发编程呢?指在一台处理器上同时处理多个任务此处说的同时,可不是同一个时间一起手拉手...

  • 5

    嗨!我是一位跑酷达人同时也是一名视频创作者!这一次或许有一点小不同,因为这是我的一个比较印象深刻的一个小分享。因为自小就不怎么爱学习,所以英语真的是我的硬伤~也因为这闹出挺多笑话哈哈。但英语又是很重要的一门语言,让我非常苦恼的是,在生活工作中...

  • 5

    说在前面少女心爆棚的“打工人”,平日里最爱的事情就是收集一堆高颜值又实用的好物件。最近真是有点幸运,无意间在极果上看到这款青萍蓝牙小钟,颜值简直太在线了!它的外表简约却不失大气,而且小小身材功能倒是蛮多的!一机搞定闹钟...

  • 5

    ☕【Java 技术指南】「并发编程专题」Fork/Join 框架基本使用和原理探究(基础篇)李浩宇/Alex关注发布于: 2021 年 09 月 10 日Java 7 开始引入了一种新的 Fork/Join...

  • 6
    • www.cnblogs.com 2 years ago
    • Cache

    Go并发编程--正确使用goroutine

    1. 对创建的gorouting负责 1.1 不要创建一个你不知道何时退出的 goroutine 下面的代码有什么问题? 是不是在我们的程序种经常写类似的代码? // Week03/blog/01/01.gopackage main import ( "log" "net/http" _ "net/http/ppr...

  • 4

    作者:张佐玮(佑祎)在云原生时代下,应用工作负载都是以容器的形式部署在宿主机,共享各类物理资源。随着宿主机硬件性能的增强,单节点的容器部署密度进一步提升,由此带来的进程间 CPU 争用,跨 NUMA 访存等问题也更加严重,影响了应用性能表现。如何...

  • 5

    精通Java事务编程(6)-可串行化隔离级别之真串行 推荐 原创 公众号JavaEdge

  • 3

    Rust并发编程 - 容器类并发原语 Rust 在并发编程方面有一些强大的原语,让你能够写出安全且高效的并发代码。最显著的原语之一是 ownership system...

  • 4

    1. 同步容器的常见问题概览 在使用Java编程时,我们经常会遇到需要在多线程环境下共享和操作数据集合的情况。为了处理这些情况,JDK提供了一系列的同步容器,例如Vector和Collections.synchronizedList。尽管这些同步容器为线程安全提供了一定程度...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK