掘金 后端 ( ) • 2024-04-22 14:22

今天 V 哥聊聊并发数据结构的问题,我们知道,并发编程中,保障数据的安全访问是第一要务,JDK 提供了一系列JUC并发数据结构,这些数据结构是线程安全的,可以在多线程环境中使用而无需额外的同步措施。以下是一些主要的并发数据结构:

1、ConcurrentHashMap

一个线程安全的哈希表,用于存储键值对。它在内部使用了分段锁(Segment Locking)或其他形式的并发控制机制,允许多个线程并发读写,同时保持较高的性能。

ConcurrentHashMap 是 Java 并发编程中非常重要的一个线程安全的哈希表实现,它在 java.util.concurrent 包中。ConcurrentHashMap 允许并发读和并发写,旨在提供比同步的 HashMap 更高的并发性能。

实现原理:

  1. 分段锁(Segment Locking):

在 JDK 1.7 及之前的版本中,ConcurrentHashMap 使用了分段锁(Segment Locking)机制。整个哈希表被分割成多个段(Segment),每个段是一个小的哈希表,它们有自己的锁。当多个线程访问不同段的数据时,它们可以并发执行,因为每个段都有自己的锁。

  1. CAS 操作:

ConcurrentHashMap 使用了无锁的 compare-and-swap(CAS)操作来更新数据,这进一步提高了并发性能。

  1. 读取操作无锁:

读取操作通常不需要加锁,因为 ConcurrentHashMap 的设计保证了读取数据的可见性和一致性。

  1. JDK 1.8 的改进:

在 JDK 1.8 中,ConcurrentHashMap 的实现发生了变化,它取消了分段锁,转而使用了 synchronized 关键字来保护哈希表的节点(Node)。同时,它也引入了红黑树来处理哈希碰撞导致的链表过长的问题,提高了最坏情况下的性能。

作用:

ConcurrentHashMap 的主要作用是在多线程环境中提供高效的并发访问。它适用于以下场景:

  • 当多个线程需要访问同一个哈希表时,使用 ConcurrentHashMap 可以减少锁竞争,提高并发性能。
  • 在需要线程安全的集合操作时,ConcurrentHashMap 是一个性能优于同步的 HashMap 的选择。

示例代码:

以下是一个简单的 ConcurrentHashMap 使用示例:

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ConcurrentHashMapExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个ConcurrentHashMap
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // 创建一个线程池
        ExecutorService executor = Executors.newFixedThreadPool(10);

        // 提交10个任务到线程池,每个任务都会更新ConcurrentHashMap
        for (int i = 0; i < 10; i++) {
            final int taskNumber = i;
            executor.submit(() -> {
                map.put("key" + taskNumber, taskNumber);
                System.out.println("Task " + taskNumber + " put value: " + map.get("key" + taskNumber));
            });
        }

        // 关闭线程池
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
    }
}

在这个示例中,我们创建了一个 ConcurrentHashMap 并使用一个线程池来并发地更新它。每个任务都会向哈希表中插入一个键值对,并打印出对应的值。由于 ConcurrentHashMap 是线程安全的,所以这个程序可以正确地运行而不会出现并发问题。

解释:

  • ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();:创建了一个新的 ConcurrentHashMap 实例。
  • ExecutorService executor = Executors.newFixedThreadPool(10);:创建了一个固定大小为10的线程池。
  • executor.submit(() -> {...});:提交了一个 Runnable 任务到线程池,任务中更新了 ConcurrentHashMap。
  • executor.shutdown(); 和 executor.awaitTermination(1, TimeUnit.MINUTES);:关闭线程池并等待所有任务完成。

这个示例展示了如何在多线程环境中安全地使用 ConcurrentHashMap。

实现原理的代码分析:

ConcurrentHashMap 是 Java 中的一个线程安全的哈希表实现,用于存储键值对。在 Java 1.8 之前,ConcurrentHashMap 使用了分段锁机制,而在 Java 1.8 之后,它采用了更为高效的锁分离技术。

Java 1.8 之前的实现原理:

在 Java 1.8 之前,ConcurrentHashMap 使用分段锁(Segment Locking)机制。每个 Segment 是一个可重入的 ReentrantLock,它用于锁定整个哈希表的一个部分。哈希表被分割成多个段,每个段有自己的锁,因此可以同时进行读写操作。

  1. 分段锁:

ConcurrentHashMap 使用分段锁来保护多个哈希表段。每个段有一个自己的锁,这使得在多线程环境中可以并发地读写不同的段。

  1. 写时复制:

在 Java 1.8 之前,ConcurrentHashMap 在进行写操作时,会复制整个段,而不是整个哈希表。这减少了加锁的范围,提高了并发性能。

Java 1.8 之后的实现原理:

在 Java 1.8 中,ConcurrentHashMap 的实现发生了变化,它取消了分段锁,转而使用了 synchronized 关键字来保护哈希表的节点(Node)。同时,它也引入了红黑树来处理哈希碰撞导致的链表过长的问题,提高了最坏情况下的性能。

  1. 锁分离:

在 Java 1.8 中,ConcurrentHashMap 使用了一种称为“锁分离”的技术。它将锁的范围缩小到链表的头部节点,而不是整个哈希表或整个段。这减少了锁竞争,提高了并发性能。

  1. 红黑树:

为了提高哈希表的性能,ConcurrentHashMap 引入了红黑树。当链表的长度超过某个阈值时,链表会被转换为红黑树,这样可以减少搜索时间,提高最坏情况下的性能。

代码分析:

以下是 ConcurrentHashMap 类的一些关键方法的代码分析:

  • put(K key, V value):这个方法用于向 ConcurrentHashMap 中添加一个键值对。

  • get(Object key):这个方法用于从 ConcurrentHashMap 中获取与指定键关联的值。

  • remove(Object key):这个方法用于从 ConcurrentHashMap 中移除与指定键关联的键值对。

这些方法都使用了 synchronized 关键字来保护哈希表的节点。在 Java 1.8 之前,这些方法会使用分段锁来保护整个段。而在 Java 1.8 之后,这些方法会使用锁分离技术来保护链表的头部节点。

这个示例展示了如何在多线程环境中使用 ConcurrentHashMap 来安全地进行键值对的添加、获取和移除操作。由于 ConcurrentHashMap 是线程安全的,所以这个程序可以正确地运行而不会出现并发问题。

2、CopyOnWriteArrayList

CopyOnWriteArrayList是一个线程安全的列表,它在进行修改操作(如添加、删除元素)时会创建底层数组的一个新副本,从而实现读写分离。适用于读多写少的场景。

实现原理:

CopyOnWriteArrayList 是一个线程安全的变体,其中所有对列表的修改(添加、删除、设置元素等)都是在底层数组的一个副加上进行的。这意味着在修改操作发生时,会创建一个新的数组,并将现有的所有元素复制到新数组中,然后在新数组上进行修改。完成修改后,再将内部引用指向新数组。由于写操作是在新数组上进行的,读操作可以安全地访问旧数组,而不会受到写操作的干扰。

  1. 写时复制(Copy-On-Write):

这是 CopyOnWriteArrayList 的核心原理。在发生写操作时,不直接修改原有数组,而是复制出一个新数组,修改完成后,再将内部引用指向新数组。

  1. 读取操作无锁:

由于读操作不需要修改数组,它们可以安全地读取当前的数组,而不需要任何锁。这提高了读取操作的并发性能。

  1. 写操作加锁:

为了确保写操作的原子性和一致性,写操作需要加锁。这是通过在修改方法(如 add, remove, set)中使用 ReentrantLock 实现的。

作用:

CopyOnWriteArrayList 的主要作用是在读多写少的场景中提供线程安全的列表操作,同时尽量减少读操作的锁竞争。它适用于以下场景:

  • 当你需要一个列表,其中大多数操作是读取操作,而写操作相对较少时。
  • 当你可以在发生写操作时接受一定的性能开销,因为你需要复制整个底层数组。

示例代码:

以下是一个简单的 CopyOnWriteArrayList 使用示例:

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

public class CopyOnWriteArrayListExample {
    public static void main(String[] args) {
        // 创建一个CopyOnWriteArrayList
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

        // 添加元素
        list.add("A");
        list.add("B");
        list.add("C");

        // 创建一个线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 提交一个任务到线程池,该任务会修改CopyOnWriteArrayList
        executor.submit(() -> {
            list.add("D");
            list.remove("A");
        });

        // 提交另一个任务到线程池,该任务会读取CopyOnWriteArrayList
        executor.submit(() -> {
            for (String element : list) {
                System.out.println(element);
            }
        });

        // 关闭线程池
        executor.shutdown();
    }
}

在这个示例中,我们创建了一个 CopyOnWriteArrayList 并使用一个线程池来并发地修改和读取它。一个任务尝试添加和删除元素,而另一个任务遍历列表并打印所有元素。由于 CopyOnWriteArrayList 是线程安全的,所以这个程序可以正确地运行而不会出现并发问题。

解释:

  • CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();:创建了一个新的 CopyOnWriteArrayList 实例。
  • list.add("A");list.remove("A");:添加和删除元素的操作。
  • ExecutorService executor = Executors.newFixedThreadPool(2);:创建了一个大小为2的线程池。
  • executor.submit(() -> {...});:提交了一个 Runnable 任务到线程池,任务中修改了 CopyOnWriteArrayList。
  • for (String element : list) {...}:遍历列表并打印元素。
  • executor.shutdown();:关闭线程池。

这个示例展示了如何在多线程环境中使用 CopyOnWriteArrayList 来安全地进行读写操作。由于写操作相对昂贵(因为需要复制数组),所以 CopyOnWriteArrayList 适用于读多写少的场景。

代码分析:

以下是 CopyOnWriteArrayList 类的一些关键方法的代码分析:

  • add(E e):这个方法用于向 CopyOnWriteArrayList 中添加一个元素。

  • get(int index):这个方法用于从 CopyOnWriteArrayList 中获取指定索引的元素。

  • set(int index, E element):这个方法用于将指定索引的元素设置为新元素。

  • remove(int index):这个方法用于从 CopyOnWriteArrayList 中移除指定索引的元素。

这些方法都使用了原子操作来更新数据,并确保了线程安全。在 add、set 和 remove 方法中,会创建一个新数组,并将现有元素复制到新数组中,然后修改新数组。完成修改后,再将内部引用指向新数组。

3、CopyOnWriteArraySet

与 CopyOnWriteArrayList 类似,但它存储的是不包含重复元素的集合。

实现原理:

CopyOnWriteArraySet 是一个线程安全的变体,其中所有对集合的修改(添加、删除元素等)都是在底层数组的一个副加上进行的。这意味着在修改操作发生时,会创建一个新的数组,然后将现有的所有元素复制到新数组中,并在新数组上进行修改。完成修改后,再将内部引用指向新数组。由于写操作是在新数组上进行的,读操作可以安全地访问旧数组,而不会受到写操作的干扰。

  1. 写时复制(Copy-On-Write):

这是 CopyOnWriteArraySet 的核心原理。在发生写操作时,不直接修改原有数组,而是复制出一个新数组,修改完成后,再将内部引用指向新数组。

  1. 读取操作无锁:

由于读操作不需要修改数组,它们可以安全地读取当前的数组,而不需要任何锁。这提高了读取操作的并发性能。

  1. 写操作加锁:

为了确保写操作的原子性和一致性,写操作需要加锁。这是通过在修改方法(如 add, remove)中使用 ReentrantLock 实现的。

作用:

CopyOnWriteArraySet 的主要作用是在读多写少的场景中提供线程安全的集合操作,同时尽量减少读操作的锁竞争。它适用于以下场景:

  • 当你需要一个集合,其中大多数操作是读取操作,而写操作相对较少时。
  • 当你可以在发生写操作时接受一定的性能开销,因为你需要复制整个底层数组。
  • 当你需要一个不允许有重复元素的集合时。

示例代码:

以下是一个简单的 CopyOnWriteArraySet 使用示例:

import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CopyOnWriteArraySetExample {
    public static void main(String[] args) {
        // 创建一个CopyOnWriteArraySet
        CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();

        // 添加元素
        set.add("A");
        set.add("B");
        set.add("C");

        // 创建一个线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 提交一个任务到线程池,该任务会修改CopyOnWriteArraySet
        executor.submit(() -> {
            set.add("D");
            set.remove("A");
        });

        // 提交另一个任务到线程池,该任务会读取CopyOnWriteArraySet
        executor.submit(() -> {
            for (String element : set) {
                System.out.println(element);
            }
        });

        // 关闭线程池
        executor.shutdown();
    }
}

在这个示例中,我们创建了一个 CopyOnWriteArraySet 并使用一个线程池来并发地修改和读取它。一个任务尝试添加和删除元素,而另一个任务遍历集合并打印所有元素。由于 CopyOnWriteArraySet 是线程安全的,所以这个程序可以正确地运行而不会出现并发问题。

解释:

  • CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();:创建了一个新的 CopyOnWriteArraySet 实例。
  • set.add("A");set.remove("A");:添加和删除元素的操作。
  • ExecutorService executor = Executors.newFixedThreadPool(2);:创建了一个大小为2的线程池。
  • executor.submit(() -> {...});:提交了一个 Runnable 任务到线程池,任务中修改了 CopyOnWriteArraySet。
  • for (String element : set) {...}:遍历集合并打印元素。
  • executor.shutdown();:关闭线程池。

这个示例展示了如何在多线程环境中使用 CopyOnWriteArraySet 来安全地进行读写操作。由于写操作相对昂贵(因为需要复制数组),所以 CopyOnWriteArraySet 适用于读多写少的场景,并且需要集合中元素不重复的特性。

代码分析:

以下是 CopyOnWriteArraySet 类的一些关键方法的代码分析:

  • add(E e):这个方法用于向 CopyOnWriteArraySet 中添加一个元素。

  • contains(Object o):这个方法用于检查集合中是否包含指定元素。

  • size():这个方法用于获取集合中元素的数量。

  • clear():这个方法用于清空集合中的所有元素。

这些方法都使用了原子操作来更新数据,并确保了线程安全。在 add 方法中,会创建一个新数组,并将现有元素复制到新数组中。如果新数组中已经存在相同的元素,则不会添加该元素。完成修改后,再将内部引用指向新数组。

4、ConcurrentLinkedQueue

一个线程安全的无界非阻塞队列,基于链表实现。它使用原子操作来保证线程安全,适合在高并发环境下使用。

实现原理:

ConcurrentLinkedQueue 是一个基于链表实现的线程安全的无界非阻塞队列。它使用原子操作来保证线程安全,适合在高并发环境下使用。

  1. 非阻塞队列:

ConcurrentLinkedQueue 实现了 Queue 接口,提供了一组原子操作来支持队列的基本功能,如入队(offer)、出队(poll)等,这些操作都是非阻塞的。

  1. 无界队列:

ConcurrentLinkedQueue 没有容量限制,理论上可以无限增长,直到耗尽内存。

  1. 原子操作:

ConcurrentLinkedQueue 使用了 compare-and-swap(CAS)操作来更新链表节点,这保证了在多线程环境下的线程安全。

  1. 无锁算法:

ConcurrentLinkedQueue 使用了无锁算法,避免了锁竞争带来的性能开销。

作用:

ConcurrentLinkedQueue 的主要作用是在多线程环境中提供一个高效且线程安全的队列。它适用于以下场景:

  • 当你需要一个高并发队列,且队列的容量不需要事先确定时。
  • 当你希望在队列操作中避免锁竞争和阻塞时。

示例代码:

以下是一个简单的 ConcurrentLinkedQueue 使用示例:

import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ConcurrentLinkedQueueExample {
    public static void main(String[] args) {
        // 创建一个ConcurrentLinkedQueue
        ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();

        // 创建一个线程池
        ExecutorService executor = Executors.newFixedThreadPool(10);

        // 提交任务到线程池,生产者线程
        for (int i = 0; i < 5; i++) {
            final int taskNumber = i;
            executor.submit(() -> {
                queue.offer(taskNumber);
                System.out.println("Task " + taskNumber + " added to queue");
            });
        }

        // 提交任务到线程池,消费者线程
        for (int i = 0; i < 5; i++) {
            executor.submit(() -> {
                Integer element = queue.poll();
                if (element != null) {
                    System.out.println("Task " + element + " removed from queue");
                }
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}

在这个示例中,我们创建了一个 ConcurrentLinkedQueue 并使用一个线程池来模拟生产者和消费者。生产者线程向队列中添加元素,而消费者线程从队列中移除元素。由于 ConcurrentLinkedQueue 是线程安全的,所以这个程序可以正确地运行而不会出现并发问题。

解释:

  • ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();:创建了一个新的 ConcurrentLinkedQueue 实例。
  • queue.offer(taskNumber);:生产者线程将元素添加到队列尾部。
  • Integer element = queue.poll();:消费者线程从队列头部移除元素。
  • ExecutorService executor = Executors.newFixedThreadPool(10);:创建了一个大小为10的线程池。
  • executor.submit(() -> {...});:提交了生产者和消费者任务到线程池。
  • executor.shutdown();:关闭线程池。

这个示例展示了如何在多线程环境中使用 ConcurrentLinkedQueue 来安全地进行生产者和消费者操作。由于 ConcurrentLinkedQueue 是无界的且非阻塞的,它适合用于生产者和消费者数量不固定,或者需要高并发处理的场景。

代码分析:

以下是 ConcurrentLinkedQueue 类的一些关键方法的代码分析:

  • offer(E e):这个方法用于向 ConcurrentLinkedQueue 中添加一个元素。如果队列已满,该方法将返回 false。

  • poll():这个方法用于从 ConcurrentLinkedQueue 中移除并返回第一个元素。如果队列为空,该方法将返回 null。

  • peek():这个方法用于返回 ConcurrentLinkedQueue 中第一个元素,但不从队列中移除它。如果队列为空,该方法将返回 null。

  • size():这个方法用于返回 ConcurrentLinkedQueue 中元素的数量。

这些方法都使用了原子操作来更新链表节点,并确保了线程安全。在 offer 方法中,会使用 CAS 操作将新元素添加到链表的尾部。在 poll 方法中,会使用 CAS 操作从链表的头部移除元素。

5、ConcurrentLinkedDeque

一个线程安全的双端队列,也是基于链表实现,适用于需要从两端插入和删除元素的场景。

实现原理:

ConcurrentLinkedDeque 是一个基于链表实现的线程安全的双端队列。它支持在队列的首尾进行插入和删除操作,并且是线程安全的。

  1. 双端队列:

ConcurrentLinkedDeque 实现了 Deque 接口,提供了一组原子操作来支持双端队列的基本功能,如从头部插入(addFirst)、从尾部插入(addLast)、从头部移除(removeFirst)、从尾部移除(removeLast)等。

  1. 无界队列:

ConcurrentLinkedDeque 没有容量限制,理论上可以无限增长,直到耗尽内存。

  1. 原子操作:

ConcurrentLinkedDeque 使用了 compare-and-swap(CAS)操作来更新链表节点,这保证了在多线程环境下的线程安全。

  1. 无锁算法:

ConcurrentLinkedDeque 使用了无锁算法,避免了锁竞争带来的性能开销。

作用:

ConcurrentLinkedDeque 的主要作用是在多线程环境中提供一个高效且线程安全的双端队列。它适用于以下场景:

  • 当你需要一个双端队列,且队列的容量不需要事先确定时。
  • 当你希望在队列操作中避免锁竞争和阻塞时。
  • 当你需要从队列的首尾都可以进行插入和删除操作时。

示例代码:

以下是一个简单的 ConcurrentLinkedDeque 使用示例:


import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ConcurrentLinkedDequeExample {
    public static void main(String[] args) {
        // 创建一个ConcurrentLinkedDeque
        ConcurrentLinkedDeque<String> deque = new ConcurrentLinkedDeque<>();

        // 创建一个线程池
        ExecutorService executor = Executors.newFixedThreadPool(10);

        // 提交任务到线程池,生产者线程
        for (int i = 0; i < 5; i++) {
            final int taskNumber = i;
            executor.submit(() -> {
                deque.addFirst("Task " + taskNumber);
                System.out.println("Task " + taskNumber + " added to the front of the deque");
            });
        }

        // 提交任务到线程池,消费者线程
        for (int i = 0; i < 5; i++) {
            executor.submit(() -> {
                String element = deque.removeLast();
                if (element != null) {
                    System.out.println(element + " removed from the end of the deque");
                }
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}

在这个示例中,我们创建了一个 ConcurrentLinkedDeque 并使用一个线程池来模拟生产者和消费者。生产者线程向队列的头部添加元素,而消费者线程从队列的尾部移除元素。由于 ConcurrentLinkedDeque 是线程安全的,所以这个程序可以正确地运行而不会出现并发问题。

解释:

  • ConcurrentLinkedDeque<String> deque = new ConcurrentLinkedDeque<>();:创建了一个新的 ConcurrentLinkedDeque 实例。
  • deque.addFirst("Task " + taskNumber);:生产者线程将元素添加到队列的头部。
  • String element = deque.removeLast();:消费者线程从队列的尾部移除元素。
  • ExecutorService executor = Executors.newFixedThreadPool(10);:创建了一个大小为10的线程池。
  • executor.submit(() -> {...});:提交了生产者和消费者任务到线程池。
  • executor.shutdown();:关闭线程池。

这个示例展示了如何在多线程环境中使用 ConcurrentLinkedDeque 来安全地进行生产者和消费者操作。由于 ConcurrentLinkedDeque 是无界的且非阻塞的,它适合用于生产者和消费者数量不固定,或者需要高并发处理的场景,并且需要双端队列的特性。

代码分析:

以下是 ConcurrentLinkedDeque 类的一些关键方法的代码分析:

  • addFirst(E e):这个方法用于在 ConcurrentLinkedDeque 的头部添加一个元素。

  • addLast(E e):这个方法用于在 ConcurrentLinkedDeque 的尾部添加一个元素。

  • removeFirst():这个方法用于从 ConcurrentLinkedDeque 的头部移除并返回第一个元素。如果队列为空,该方法将返回 null。

  • removeLast():这个方法用于从 ConcurrentLinkedDeque 的尾部移除并返回最后一个元素。如果队列为空,该方法将返回 null。

这些方法都使用了原子操作来更新链表节点,并确保了线程安全。在 addFirst 和 addLast 方法中,会使用 CAS 操作将新元素添加到链表的头部或尾部。在 removeFirst 和 removeLast 方法中,会使用 CAS 操作从链表的头部或尾部移除元素。

6、BlockingQueue 接口及其实现类

(如 ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue 等)提供了线程安全的队列操作,支持阻塞的插入和获取操作。当队列满时,插入操作会阻塞;当队列空时,获取操作会阻塞。

实现原理:

BlockingQueue 是一个支持阻塞操作的队列。当队列满时,插入操作会阻塞;当队列空时,获取操作会阻塞。它实现了生产者-消费者模式,用于线程间的数据共享。

  1. 阻塞操作:

BlockingQueue 提供了阻塞的 put 和 take 方法,这些方法在队列满或空时会使线程进入等待状态,直到队列有空闲空间或数据可用。

  1. 同步机制:

BlockingQueue 的实现类通常使用锁(如 ReentrantLock)和条件变量(如 Condition)来实现线程同步。

  1. 容量限制:

BlockingQueue 通常有固定的容量限制,但也有一些实现(如 LinkedBlockingQueue)允许指定最大容量,如果没有指定,则默认为 Integer.MAX_VALUE。

作用:

BlockingQueue 的主要作用是在多线程环境中提供一个线程安全的队列,用于生产者和消费者之间的数据传递。它适用于以下场景:

  • 当你需要一个有界或无界队列,并且希望在队列满或空时阻塞线程时。
  • 当你希望在生产者和消费者之间实现同步时。

示例代码:

以下是一个简单的 BlockingQueue 使用示例,使用 ArrayBlockingQueue 作为实现:

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

public class BlockingQueueExample {
    public static void main(String[] args) {
        // 创建一个容量为10的ArrayBlockingQueue
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);

        // 创建一个线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 提交生产者任务到线程池
        executor.submit(() -> {
            try {
                for (int i = 0; i < 20; i++) {
                    queue.put("Task " + i);
                    System.out.println("Task " + i + " added to the queue");
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // 提交消费者任务到线程池
        executor.submit(() -> {
            try {
                for (int i = 0; i < 20; i++) {
                    String task = queue.take();
                    System.out.println(task + " removed from the queue");
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // 关闭线程池
        executor.shutdown();
    }
}

在这个示例中,我们创建了一个 ArrayBlockingQueue 并使用一个线程池来模拟生产者和消费者。生产者线程向队列中添加元素,而消费者线程从队列中移除元素。由于 ArrayBlockingQueue 是线程安全的,所以这个程序可以正确地运行而不会出现并发问题。

解释:

  • BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);:创建了一个容量为10的 ArrayBlockingQueue 实例。
  • queue.put("Task " + i);:生产者线程将元素添加到队列中,如果队列已满,则线程会阻塞。
  • String task = queue.take();:消费者线程从队列中移除元素,如果队列空,则线程会阻塞。
  • ExecutorService executor = Executors.newFixedThreadPool(2);:创建了一个大小为2的线程池。
  • executor.submit(() -> {...});:提交了生产者和消费者任务到线程池。
  • executor.shutdown();:关闭线程池。

这个示例展示了如何在多线程环境中使用 BlockingQueue 来安全地进行生产者和消费者操作。由于 ArrayBlockingQueue 是有界的,它在队列满时会阻塞生产者,在队列空时会阻塞消费者,适合用于需要阻塞队列的场景。

代码分析:

以下是 BlockingQueue 接口的一些关键方法的代码分析:

  • put(E e):这个方法用于向 BlockingQueue 中添加一个元素。如果队列已满,该方法会阻塞,直到队列有空闲空间。

  • take():这个方法用于从 BlockingQueue 中移除并返回第一个元素。如果队列为空,该方法会阻塞,直到队列有数据可用。

  • offer(E e):这个方法用于向 BlockingQueue 中添加一个元素。如果队列已满,该方法将返回 false。

  • poll(long timeout, TimeUnit unit):这个方法用于从 BlockingQueue 中移除并返回第一个元素。如果队列为空,该方法将在指定的时间内阻塞,如果超时则返回 null。

这些方法都使用了锁和条件变量来实现线程同步。在 put 和 offer 方法中,会使用锁来保护队列,并使用条件变量来阻塞线程。在 take 和 poll 方法中,会使用锁来保护队列,并使用条件变量来唤醒等待的线程。

7、ConcurrentSkipListMap 和 ConcurrentSkipListSet

分别是线程安全的有序映射和有序集,基于跳表(Skip List)实现,提供了高效的查找、插入和删除操作。

实现原理:

ConcurrentSkipListMap 是一个线程安全的有序映射,它基于跳表(Skip List)数据结构实现。跳表是一种平衡树结构,它结合了红黑树和有序链表的特点,提供了一种高效的数据结构,可以进行快速的查找、插入和删除操作。

  1. 跳表结构:

跳表包含多层索引,每一层索引都是有序的链表。最底层是最简单的有序链表,高层索引包含指向下一层索引的指针。通过这些指针,可以快速跳过大量节点,从而提高查找、插入和删除操作的效率。

  1. 线程安全:

ConcurrentSkipListMap 使用 ReentrantLock 来保证线程安全。当多个线程同时进行修改操作时,它们会竞争锁。

  1. 读取操作无锁:

读取操作(如 get)通常不需要加锁,因为跳表的结构保证了读取操作的可见性和一致性。 作用:

ConcurrentSkipListMap 的主要作用是在多线程环境中提供高效的并发访问,同时保持元素的自然顺序。它适用于以下场景:

  • 当你需要一个线程安全的有序映射,且需要在多线程环境中进行频繁的读写操作时。
  • 当你需要根据元素的自然顺序进行快速查找、插入和删除操作时。

示例代码:

以下是一个简单的 ConcurrentSkipListMap 使用示例:


import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ConcurrentSkipListMapExample {
    public static void main(String[] args) {
        // 创建一个ConcurrentSkipListMap
        ConcurrentSkipListMap<String, Integer> map = new ConcurrentSkipListMap<>();

        // 创建一个线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 提交任务到线程池,生产者线程
        executor.submit(() -> {
            for (int i = 0; i < 20; i++) {
                map.put("Key " + i, i);
                System.out.println("Task " + i + " added to the map");
            }
        });

        // 提交任务到线程池,消费者线程
        executor.submit(() -> {
            for (int i = 0; i < 20; i++) {
                Integer value = map.get("Key " + i);
                if (value != null) {
                    System.out.println("Task " + value + " found in the map");
                }
            }
        });

        // 关闭线程池
        executor.shutdown();
    }
}

在这个示例中,我们创建了一个 ConcurrentSkipListMap 并使用一个线程池来模拟生产者和消费者。生产者线程向映射中添加元素,而消费者线程从映射中查找元素。由于 ConcurrentSkipListMap 是线程安全的,所以这个程序可以正确地运行而不会出现并发问题。

解释:

  • ConcurrentSkipListMap<String, Integer> map = new ConcurrentSkipListMap<>();:创建了一个新的 ConcurrentSkipListMap 实例。
  • map.put("Key " + i, i);:生产者线程将键值对添加到映射中。
  • Integer value = map.get("Key " + i);:消费者线程从映射中查找特定键对应的值。
  • ExecutorService executor = Executors.newFixedThreadPool(2);:创建了一个大小为2的线程池。
  • executor.submit(() -> {...});:提交了生产者和消费者任务到线程池。
  • executor.shutdown();:关闭

代码分析:

以下是 ConcurrentSkipListMap 和 ConcurrentSkipListSet 类的一些关键方法的代码分析:

  1. ConcurrentSkipListMap 类:
  • put(K key, V value):这个方法用于向 ConcurrentSkipListMap 中添加一个键值对。
  • get(Object key):这个方法用于从 ConcurrentSkipListMap 中获取与指定键关联的值。
  • remove(Object key):这个方法用于从 ConcurrentSkipListMap 中移除与指定键关联的键值对。

这些方法都使用了 ReentrantLock 来保护跳表的修改操作,并确保线程安全。在 put、get 和 remove 方法中,会使用跳表的数据结构进行查找、插入和删除操作。

  1. ConcurrentSkipListSet 类:
  • add(E e):这个方法用于向 ConcurrentSkipListSet 中添加一个元素。
  • contains(Object o):这个方法用于检查集合中是否包含指定元素。
  • remove(Object o):这个方法用于从 ConcurrentSkipListSet 中移除指定元素。

这些方法同样使用了 ReentrantLock 来保护跳表的修改操作,并确保线程安全。在 add、contains 和 remove 方法中,会使用跳表的数据结构进行查找、插入和删除操作。

8、CountDownLatch

一个同步辅助类,允许一个或多个线程等待其他线程完成操作,可用于实现并发同步。

实现原理:

CountDownLatch 是一个同步辅助类,用于实现线程之间的等待/通知模式。它允许一个或多个线程等待直到一系列操作在其他线程中完成。

  1. 计数器:

CountDownLatch 使用一个计数器来跟踪完成操作的线程数量。初始时,计数器的值等于线程的数量。

  1. 阻塞等待:

当调用 CountDownLatch 的 await 方法时,当前线程会阻塞,直到计数器值为零。

  1. 计数器递减:

其他线程通过调用 CountDownLatch 的 countDown 方法来递减计数器的值。每个线程在完成自己的操作后调用此方法。

作用:

CountDownLatch 的主要作用是在多线程环境中提供一个同步点,使得主线程可以等待其他线程完成各自的任务后再继续执行。它适用于以下场景:

  • 当你需要等待多个线程完成各自的任务后,才能继续执行后续操作时。
  • 当你需要确保所有线程都完成了自己的任务,再进行下一步操作时。

示例代码:

以下是一个简单的 CountDownLatch 使用示例:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CountDownLatchExample {
    public static void main(String[] args) {
        // 创建一个CountDownLatch,初始值为5
        CountDownLatch latch = new CountDownLatch(5);

        // 创建一个线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);

        // 提交任务到线程池,每个任务都会递减latch的计数器
        for (int i = 0; i < 5; i++) {
            final int taskNumber = i;
            executor.submit(() -> {
                System.out.println("Task " + taskNumber + " is running");
                latch.countDown();
                System.out.println("Task " + taskNumber + " is completed");
            });
        }

        // 主线程等待latch的计数器归零后继续执行
        try {
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("All tasks are completed, main thread continues");

        // 关闭线程池
        executor.shutdown();
    }
}

在这个示例中,我们创建了一个 CountDownLatch 并使用一个线程池来模拟5个并发任务。每个任务在完成自己的操作后都会递减 CountDownLatch 的计数器。主线程在调用 latch.await() 方法时会阻塞,直到计数器归零。当所有任务完成后,主线程继续执行。

解释:

  • CountDownLatch latch = new CountDownLatch(5);:创建了一个初始值为5的 CountDownLatch 实例。
  • latch.countDown();:每个线程在完成任务后调用此方法递减计数器的值。
  • latch.await();:主线程在调用此方法时会阻塞,直到计数器的值为零。
  • ExecutorService executor = Executors.newFixedThreadPool(5);:创建了一个大小为5的线程池。
  • executor.submit(() -> {...});:提交了5个任务到线程池。
  • executor.shutdown();:关闭线程池。

这个示例展示了如何在多线程环境中使用 CountDownLatch 来确保主线程等待所有子线程完成任务后再继续执行。由于 CountDownLatch 提供了线程间的同步点,它适合用于需要等待多个线程完成任务的场景。

代码分析:

以下是 CountDownLatch 类的一些关键方法的代码分析:

  • CountDownLatch(int count): 这个构造方法用于创建一个 CountDownLatch 对象,并初始化计数器的值。

  • await(): 这个方法用于使当前线程等待,直到计数器的值为零。如果计数器的值不为零,当前线程会阻塞。

  • countDown(): 这个方法用于递减计数器的值。每个线程在完成自己的操作后调用此方法。

  • getCount(): 这个方法用于获取当前计数器的值。

  • isLatchOpen(): 这个方法用于检查计数器的值是否为零。如果计数器的值为零,则返回 true,否则返回 false。

这些方法都使用了 Object 类的 wait() 和 notify() 方法来实现线程间的同步。当线程到达 await 方法时,它会调用 wait() 方法,这会导致线程进入等待状态。当其他线程调用 countDown() 方法并递减计数器的值时,会调用 notify() 方法来唤醒等待的线程。

9、CyclicBarrier

一个允许一组线程互相等待的同步辅助类,直到所有线程都达到某个屏障点后才继续执行。

实现原理:

CyclicBarrier 是一个同步辅助类,用于让一组线程到达一个屏障(barrier)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。CyclicBarrier 的名称中的 “Cyclic” 指的是它可以被重用。当所有参与者到达屏障时,它们会执行 barrierAction 指定的动作,这个动作只会被最后一个到达屏障的线程执行。

  1. 屏障计数器:

CyclicBarrier 使用一个内部计数器来跟踪到达屏障的线程数量。

  1. 阻塞线程:

当线程到达屏障时,它会阻塞,直到计数器的值为零。

  1. 重用性:

CyclicBarrier 允许在屏障打开后重新使用它,而不是每次使用后都必须创建一个新的。

作用:

CyclicBarrier 的主要作用是在多线程环境中提供一个线程间的同步点,使得一组线程在完成各自的任务后,能够同时继续执行后续操作。它适用于以下场景:

  • 当你需要让一组线程等待直到所有线程都完成某个任务后,才能继续执行后续操作时。
  • 当你需要确保一组线程同时开始执行某个操作时。

示例代码:

以下是一个简单的 CyclicBarrier 使用示例:

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        // 创建一个CyclicBarrier,初始值为5
        CyclicBarrier barrier = new CyclicBarrier(5);

        // 创建一个线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);

        // 提交任务到线程池,每个任务都会到达屏障并执行后续操作
        for (int i = 0; i < 5; i++) {
            final int taskNumber = i;
            executor.submit(() -> {
                System.out.println("Task " + taskNumber + " is running");
                try {
                    barrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    Thread.currentThread().interrupt();
                }
                System.out.println("Task " + taskNumber + " is completed");
            });
        }

        // 主线程等待所有任务完成后继续执行
        try {
            barrier.await();
        } catch (InterruptedException | BrokenBarrierException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("All tasks are completed, main thread continues");

        // 关闭线程池
        executor.shutdown();
    }
}

在这个示例中,我们创建了一个 CyclicBarrier 并使用一个线程池来模拟5个并发任务。每个任务在完成自己的操作后会到达屏障并等待其他任务也到达屏障。当所有任务都到达屏障时,它们会继续执行后续操作。主线程在调用 barrier.await() 方法时会阻塞,直到所有子线程都到达屏障。

解释:

  • CyclicBarrier barrier = new CyclicBarrier(5);:创建了一个初始值为5的 CyclicBarrier 实例。
  • barrier.await();:每个线程在完成任务后调用此方法到达屏障并等待其他线程。
  • ExecutorService executor = Executors.newFixedThreadPool(5);:创建了一个大小为5的线程池。
  • executor.submit(() -> {...});:提交了5个任务到线程池。
  • executor.shutdown();:关闭线程池。

这个示例展示了如何在多线程环境中使用 CyclicBarrier 来确保一组线程在完成各自的任务后,能够同时继续执行后续操作。由于 CyclicBarrier 提供了线程间的同步点,它适合用于需要线程同步执行的场景。

代码分析:

以下是 CyclicBarrier 类的一些关键方法的代码分析:

  • CyclicBarrier(int parties): 这个构造方法用于创建一个 CyclicBarrier 对象,并指定屏障的参与者数量。

  • await(): 这个方法用于使当前线程等待,直到计数器的值为零。如果计数器的值不为零,当前线程会阻塞。

  • await(long timeout, TimeUnit unit): 这个方法与 await() 类似,但它允许设置一个超时时间。如果其他线程在超时时间内还没有到达屏障,当前线程将返回 false。

  • reset(): 这个方法用于重置屏障,将其计数器的值重置为初始值。

  • getNumberWaiting(): 这个方法用于获取当前等待在屏障上的线程数量。

  • getParties(): 这个方法用于获取屏障的参与者数量。

  • isBroken(): 这个方法用于检查屏障是否被破坏。如果屏障被破坏,所有等待的线程都会被中断。

这些方法都使用了 Object 类的 wait() 和 notify() 方法来实现线程间的同步。当线程到达 await 方法时,它会调用 wait() 方法,这会导致线程进入等待状态。当其他线程到达屏障时,会调用 notify() 方法来唤醒等待的线程。

10、Semaphore

一个计数信号量,可以用来限制可以同时访问某个特定资源的线程数量。

实现原理:

Exchanger 是一个同步辅助类,用于实现两个线程间的数据交换。当两个线程都到达 Exchanger 指定的交换点时,它们可以交换彼此的数据。如果只有一个线程到达交换点,它会阻塞,直到另一个线程也到达交换点。

  1. 交换点:

Exchanger 使用一个内部同步机制来跟踪到达交换点的线程数量。

  1. 阻塞等待:

当线程到达交换点时,它会阻塞,直到另一个线程也到达交换点。

  1. 数据交换:

当两个线程都到达交换点时,它们可以交换彼此的数据。

作用:

Exchanger 的主要作用是在多线程环境中提供一个线程间的同步点,使得两个线程可以交换数据。它适用于以下场景:

  • 当你需要实现两个线程间的数据交换时。
  • 当你需要确保两个线程在某个点同步执行时。

示例代码:

以下是一个简单的 Exchanger 使用示例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Exchanger;

public class ExchangerExample {
    public static void main(String[] args) {
        // 创建一个Exchanger
        Exchanger<String> exchanger = new Exchanger<>();

        // 创建一个线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 提交任务到线程池,第一个线程会生成数据并等待交换
        executor.submit(() -> {
            String data = "Data from the first thread";
            try {
                String receivedData = exchanger.exchange(data);
                System.out.println("Received data from the second thread: " + receivedData);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // 提交任务到线程池,第二个线程会生成数据并交换
        executor.submit(() -> {
            String data = "Data from the second thread";
            try {
                String receivedData = exchanger.exchange(data);
                System.out.println("Received data from the first thread: " + receivedData);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // 关闭线程池
        executor.shutdown();
    }
}

在这个示例中,我们创建了一个 Exchanger 并使用一个线程池来模拟两个线程。第一个线程在交换点等待,并准备好交换数据。第二个线程在交换点准备好数据,并等待第一个线程到达交换点。当两个线程都到达交换点时,它们可以交换数据。

解释:

  • Exchanger<String> exchanger = new Exchanger<>();:创建了一个新的 Exchanger 实例。
  • String data = "Data from the first thread";:第一个线程准备的数据。
  • exchanger.exchange(data);:第一个线程在交换点等待,并准备好交换数据。
  • String receivedData = exchanger.exchange(data);:第二个线程在交换点准备好数据,并等待第一个线程到达交换点。
  • ExecutorService executor = Executors.newFixedThreadPool(2);:创建了一个大小为2的线程池。
  • executor.submit(() -> {...});:提交了两个任务到线程池。
  • executor.shutdown();:关闭线程池。

这个示例展示了如何在多线程环境中使用 Exchanger 来确保两个线程在某个点同步执行并交换数据。由于 Exchanger 提供了线程间的同步点,它适合用于需要线程间数据交换的场景。

代码分析:

以下是 Semaphore 类的一些关键方法的代码分析:

  • Semaphore(int permits): 这个构造方法用于创建一个 Semaphore 对象,并指定信号量的初始值。

  • acquire(): 这个方法用于尝试获取一个资源。如果信号量的值大于零,当前线程可以继续执行;如果信号量的值等于零,当前线程将阻塞。

  • acquire(int permits): 这个方法与 acquire() 类似,但它允许指定要获取的资源数量。

  • release(): 这个方法用于释放一个资源。它会增加信号量的值,从而允许其他被阻塞的线程继续执行。

  • tryAcquire(): 这个方法用于尝试获取一个资源,但不阻塞。如果信号量的值大于零,当前线程可以继续执行;如果信号量的值等于零,该方法将返回 false。

  • tryAcquire(int permits): 这个方法与 tryAcquire() 类似,但它允许指定要获取的资源数量。

这些方法都使用了 Object 类的 wait() 和 notify() 方法来实现线程间的同步。当线程到达 acquire 方法时,它会调用 wait() 方法,这会导致线程进入等待状态。当其他线程调用 release 方法并释放资源时,会调用 notify() 方法来唤醒等待的线程。

11、Exchanger

一个用于在并发线程之间交换数据的工具,适用于遗传算法、流水线设计等场景。

Exchanger 是一个用于线程间交换数据的同步辅助类,它允许两个线程在某个点交换它们的数据。当一个线程准备好数据时,它会等待另一个线程准备好数据,然后它们可以交换数据。如果一个线程准备好数据而另一个线程还没有准备好,那么第一个线程会阻塞,直到第二个线程准备好数据。

实现原理:

Exchanger 的实现原理是基于 Object 类的 wait() 和 notify() 方法。当一个线程到达交换点时,它会调用 exchange() 方法,该方法会尝试将该线程的数据与另一个线程的数据交换。如果另一个线程还没有准备好数据,那么第一个线程会阻塞,直到第二个线程到达交换点并准备好数据。

  1. 交换点:

Exchanger 使用一个内部同步机制来跟踪到达交换点的线程数量。

  1. 阻塞等待:

当线程到达交换点时,它会阻塞,直到另一个线程也到达交换点。

  1. 数据交换:

当两个线程都到达交换点时,它们可以交换彼此的数据。

作用:

Exchanger 的主要作用是在多线程环境中提供一个线程间的同步点,使得两个线程可以交换数据。它适用于以下场景:

  • 当你需要实现两个线程间的数据交换时。
  • 当你需要确保两个线程在某个点同步执行时。

示例代码:

以下是一个简单的 Exchanger 使用示例:


import java.util.concurrent.Exchanger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExchangerExample {
    public static void main(String[] args) {
        // 创建一个Exchanger
        Exchanger<String> exchanger = new Exchanger<>();

        // 创建一个线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 提交任务到线程池,第一个线程会生成数据并等待交换
        executor.submit(() -> {
            String data = "Data from the first thread";
            try {
                String receivedData = exchanger.exchange(data);
                System.out.println("Received data from the second thread: " + receivedData);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // 提交任务到线程池,第二个线程会生成数据并交换
        executor.submit(() -> {
            String data = "Data from the second thread";
            try {
                String receivedData = exchanger.exchange(data);
                System.out.println("Received data from the first thread: " + receivedData);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // 关闭线程池
        executor.shutdown();
    }
}

在这个示例中,我们创建了一个 Exchanger 并使用一个线程池来模拟两个线程。第一个线程在交换点等待,并准备好交换数据。第二个线程在交换点准备好数据,并等待第一个线程到达交换点。当两个线程都到达交换点时,它们可以交换数据。

解释:

  • Exchanger<String> exchanger = new Exchanger<>();:创建了一个新的 Exchanger 实例。
  • String data = "Data from the first thread";:第一个线程准备的数据。
  • String receivedData = exchanger.exchange(data);:第一个线程在交换点等待,并准备好交换数据。
  • String receivedData = exchanger.exchange(data);:第二个线程在交换点准备好数据,并等待第一个线程到达交换点。
  • ExecutorService executor = Executors.newFixedThreadPool(2);:创建了一个大小为2的线程池。
  • executor.submit(() -> {...});:提交了两个任务到线程池。
  • executor.shutdown();:关闭线程池。

这个示例展示了如何在多线程环境中使用 Exchanger 来确保两个线程在某个点同步执行并交换数据。由于 Exchanger 提供了线程间的同步点,它适合用于需要线程间数据交换的场景。

在这个示例中,两个线程分别准备了一些数据,并尝试通过 Exchanger 进行交换。如果一个线程到达交换点时,另一个线程还没有准备好数据,那么第一个线程会阻塞,直到第二个线程也到达交换点并准备好数据。当两个线程都到达交换点时,它们可以交换数据,然后继续执行。

代码分析:

在 Java 中,Exchanger 类的实现原理基于 Object 类的 wait() 和 notify() 方法。以下是 Exchanger 类的一些关键方法的实现原理:

  • boolean exchange(V x):这个方法允许一个线程尝试交换数据。如果另一个线程已经准备好数据,那么这两个线程将交换数据。如果另一个线程还没有准备好数据,那么当前线程会阻塞,直到另一个线程到达交换点并准备好数据。

  • boolean exchange(V x, long timeout, TimeUnit unit):这个方法与 exchange(V x) 类似,但它允许设置一个超时时间。如果另一个线程在超时时间内还没有准备好数据,那么当前线程将返回 false。

  • V exchange(V x, Phaser phaser):这个方法允许一个线程尝试交换数据,并且使用一个 Phaser 来管理线程的同步。如果另一个线程已经准备好数据,那么这两个线程将交换数据。如果另一个线程还没有准备好数据,那么当前线程会阻塞,直到另一个线程到达交换点并准备好数据。

这些方法都使用了 Object 类的 wait()notify() 方法来实现线程间的同步。当一个线程到达交换点时,它会调用 wait() 方法,这会导致线程进入等待状态。当另一个线程到达交换点并调用 notify() 方法时,第一个线程会被唤醒,并且可以继续执行。

12、Phaser

一个可重用的同步屏障,适用于类似于 CyclicBarrier 的场景,但提供了更灵活的注册和注销机制。

实现原理:

Phaser 是一个可重用的同步屏障,它允许一组线程互相等待,直到它们都到达某个屏障点。与 CyclicBarrier 类似,Phaser 允许重用同一个屏障,这意味着在所有线程到达屏障点后,可以重新使用该屏障。

  1. 屏障计数器:

Phaser 使用一个内部计数器来跟踪到达屏障点的线程数量。

  1. 阻塞等待:

当线程到达屏障点时,它会阻塞,直到所有线程都到达屏障点。

  1. 重用性:

Phaser 允许在屏障点被触发后重新使用它,而不是每次使用后都必须创建一个新的。

作用:

Phaser 的主要作用是在多线程环境中提供一个线程间的同步点,使得一组线程在完成各自的任务后,能够同时继续执行后续操作。它适用于以下场景:

  • 当你需要让一组线程等待直到所有线程都完成某个任务后,才能继续执行后续操作时。
  • 当你需要确保一组线程同时开始执行某个操作时。

示例代码:

以下是一个简单的 Phaser 使用示例:


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Phaser;

public class PhaserExample {
    public static void main(String[] args) {
        // 创建一个Phaser,初始值为5
        Phaser phaser = new Phaser(5);

        // 创建一个线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);

        // 提交任务到线程池,每个任务都会到达屏障点并执行后续操作
        for (int i = 0; i < 5; i++) {
            final int taskNumber = i;
            executor.submit(() -> {
                System.out.println("Task " + taskNumber + " is running");
                try {
                    phaser.arriveAndAwaitAdvance();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                System.out.println("Task " + taskNumber + " is completed");
            });
        }

        // 主线程等待所有任务完成后继续执行
        try {
            phaser.arriveAndAwaitAdvance();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("All tasks are completed, main thread continues");

        // 关闭线程池
        executor.shutdown();
    }
}

在这个示例中,我们创建了一个 Phaser 并使用一个线程池来模拟5个并发任务。每个任务在完成自己的操作后会到达屏障点并等待其他任务也到达屏障点。当所有任务都到达屏障点时,它们会继续执行后续操作。主线程在调用 phaser.arriveAndAwaitAdvance() 方法时会阻塞,直到所有子线程都到达屏障点。

解释:

  • Phaser phaser = new Phaser(5);:创建了一个初始值为5的 Phaser 实例。
  • phaser.arriveAndAwaitAdvance();:每个线程在完成任务后调用此方法到达屏障点并等待其他线程。
  • ExecutorService executor = Executors.newFixedThreadPool(5);:创建了一个大小为5的线程池。
  • executor.submit(() -> {...});:提交了5个任务到线程池。
  • executor.shutdown();:关闭线程池。

这个示例展示了如何在多线程环境中使用 Phaser 来确保一组线程在完成各自的任务后,能够同时继续执行后续操作。由于 Phaser 提供了线程间的同步点,它适合用于需要线程同步执行的场景。

代码分析:

以下是 Phaser 类的一些关键方法的代码分析:

  • Phaser(int parties): 这个构造方法用于创建一个 Phaser 对象,并指定屏障的参与者数量。

  • arrive(): 这个方法用于使当前线程到达屏障点。每次调用此方法时,计数器的值会递减。

  • arriveAndAwaitAdvance(): 这个方法与 arrive() 类似,但它会阻塞当前线程,直到计数器的值变为零。

  • awaitAdvance(): 这个方法用于阻塞当前线程,直到屏障点被触发。

  • getRegisteredParties(): 这个方法用于获取当前注册在屏障上的线程数量。

  • getArrivedParties(): 这个方法用于获取已经到达屏障的线程数量。

  • isTerminated(): 这个方法用于检查屏障是否已经终止。如果屏障已经终止,返回 true;否则返回 false。

  • forceTermination(): 这个方法用于强制终止屏障。它将计数器的值设置为零,并唤醒所有等待的线程。

这些方法都使用了 Object 类的 wait()notify() 方法来实现线程间的同步。当线程到达 arriveAndAwaitAdvance() 方法时,它会调用 wait() 方法,这会导致线程进入等待状态。当其他线程到达屏障点并调用 arrive() 方法时,会调用 notify() 方法来唤醒等待的线程。

最后

以上就是 V哥给大家整理的12个并发相关的数据结构,这些并发数据结构是 Java 并发编程的基础,它们在 java.util.concurrent 包(J.U.C)中提供。使用这些数据结构可以帮助开发者编写出高效且线程安全的并发程序,分布式应用开发的项目中,你会使用到的。