这些Java并发容器,你都了解吗?

前言

在多线程环境下,数据的并发访问和修改是无法避免的问题。

为了解决这个问题,Java 提供了一系列并发容器,这些容器在内部已经处理了并发问题,使得我们可以在多线程环境下安全地访问和修改数据。


并发容器

1.ConcurrentHashMap 并发版 HashMap

最常见的并发容器之一,可以用作并发场景下的缓存。底层依然是哈希表,但在 JAVA 8 中有了不小的改变,而 JAVA 7 和 JAVA 8 都是用的比较多的版本,因此经常会将这两个版本的实现方式做一些比较(比如面试中)。

一个比较大的差异就是,JAVA 7 中采用分段锁来减少锁的竞争,JAVA 8 中放弃了分段锁,采用 CAS(一种乐观锁),同时为了防止哈希冲突严重时退化成链表(冲突时会在该位置生成一个链表,哈希值相同的对象就链在一起),会在链表长度达到阈值(8)后转换成红黑树(比起链表,树的查询效率更稳定)。

示例
arduino 复制代码
import java.util.concurrent.*;
​
public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        // Creating a ConcurrentHashMap
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>();
​
        // Adding elements to the ConcurrentHashMap
        map.put("Key1", "Value1");
        map.put("Key2", "Value2");
        map.put("Key3", "Value3");
​
        // Printing the ConcurrentHashMap
        System.out.println("ConcurrentHashMap: " + map);
    }
}
​

2.CopyOnWriteArrayList 并发版 ArrayList

并发版 ArrayList,底层结构也是数组,和 ArrayList 不同之处在于:当新增和删除元素时会创建一个新的数组,在新的数组中增加或者排除指定对象,最后用新增数组替换原来的数组。

CopyOnWriteArrayList 的主要特性是,每当列表修改时,例如添加或删除元素,它都会创建列表的一个新副本。原始列表和新副本都可以进行并发读取,这样就可以在不锁定整个列表的情况下进行并发读取。这种方法在读取操作远多于写入操作的场景中非常有用。

适用场景:由于读操作不加锁,写(增、删、改)操作加锁,因此适用于读多写少的场景。

局限:由于读的时候不会加锁(读的效率高,就和普通 ArrayList 一样),读取的当前副本,因此可能读取到脏数据。如果介意,建议不用。

看看源码感受下:

示例
typescript 复制代码
import java.util.concurrent.*;
​
public class CopyOnWriteArrayListExample {
    public static void main(String[] args) {
        // 创建一个 CopyOnWriteArrayList
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
​
        // 向 CopyOnWriteArrayList 添加元素
        list.add("Element1");
        list.add("Element2");
        list.add("Element3");
​
        // 打印 CopyOnWriteArrayList
        System.out.println("CopyOnWriteArrayList: " + list);
    }
}

3.CopyOnWriteArraySet 并发 Set

基于 CopyOnWriteArrayList 实现(内含一个 CopyOnWriteArrayList 成员变量),也就是说底层是一个数组,意味着每次 add 都要遍历整个集合才能知道是否存在,不存在时需要插入(加锁)。

CopyOnWriteArraySet 的工作原理与 CopyOnWriteArrayList 类似。每当发生修改操作(如添加或删除元素)时,它都会创建集合的一个新副本。原始集合和新副本都可以进行并发读取,这样就可以在不锁定整个集合的情况下进行并发读取。这种方法在读取操作远多于写入操作的场景中非常有用。

适用场景:在 CopyOnWriteArrayList 适用场景下加一个,集合别太大(全部遍历伤不起)。

示例
arduino 复制代码
import java.util.concurrent.*;
​
public class CopyOnWriteArraySetExample {
    public static void main(String[] args) {
        // 创建一个 CopyOnWriteArraySet
        CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<String>();
​
        // 向 CopyOnWriteArraySet 添加元素
        set.add("Element1");
        set.add("Element2");
        set.add("Element3");
​
        // 打印 CopyOnWriteArraySet
        System.out.println("CopyOnWriteArraySet: " + set);
    }
}
​

4.ConcurrentLinkedQueue 并发队列 (基于链表)

基于链表实现的并发队列,使用乐观锁 (CAS) 保证线程安全。因为数据结构是链表,所以理论上是没有队列大小限制的,也就是说添加数据一定能成功。

ConcurrentLinkedQueue 是 Java 并发包的一部分,它是基于链接节点的无界线程安全队列。它按照 FIFO(先进先出)的原则对元素进行排序。

ConcurrentLinkedQueue 的主要优点是它允许完全并发的插入,并且使用了一种高效的"wait-free"算法。

示例
arduino 复制代码
import java.util.concurrent.*;
​
public class ConcurrentLinkedQueueExample {
    public static void main(String[] args) {
        // 创建一个 ConcurrentLinkedQueue
        ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<String>();
​
        // 向 ConcurrentLinkedQueue 添加元素
        queue.add("Element1");
        queue.add("Element2");
        queue.add("Element3");
​
        // 打印 ConcurrentLinkedQueue
        System.out.println("ConcurrentLinkedQueue: " + queue);
    }
}
​

5.ConcurrentLinkedDeque 并发队列 (基于双向链表)

基于双向链表实现的并发队列,可以分别对头尾进行操作,因此除了先进先出 (FIFO),也可以先进后出(FILO),当然先进后出的话应该叫它栈了。

ConcurrentLinkedDeque 是 Java 并发包的一部分,它是一个基于链接节点的无界并发双端队列。在 ConcurrentLinkedDeque 中,添加、删除等操作可以在队列的两端进行,使其具有更高的并发性。

示例
arduino 复制代码
import java.util.concurrent.*;
​
public class ConcurrentLinkedDequeExample {
    public static void main(String[] args) {
        // 创建一个 ConcurrentLinkedDeque
        ConcurrentLinkedDeque<String> deque = new ConcurrentLinkedDeque<String>();
​
        // 向 ConcurrentLinkedDeque 添加元素
        deque.add("Element1");
        deque.addFirst("Element2");
        deque.addLast("Element3");
​
        // 打印 ConcurrentLinkedDeque
        System.out.println("ConcurrentLinkedDeque: " + deque);
    }
}
​

6.ConcurrentSkipListMap 基于跳表的并发 Map

ConcurrentSkipListMap 是 Java 并发包的一部分,它是一个线程安全的排序映射表。它使用跳表的数据结构来保证元素的有序性和并发性。

跳表是一种可以进行二分查找的有序链表。ConcurrentSkipListMap 提供了预期的平均 log(n) 时间成本来执行 containsKeygetputremove 操作,并且它的并发性通常优于基于树的算法。

SkipList 即跳表,跳表是一种空间换时间的数据结构,通过冗余数据,将链表一层一层索引,达到类似二分查找的效果

示例
arduino 复制代码
import java.util.concurrent.*;
​
public class ConcurrentSkipListMapExample {
    public static void main(String[] args) {
        // 创建一个 ConcurrentSkipListMap
        ConcurrentSkipListMap<String, String> map = new ConcurrentSkipListMap<String, String>();
​
        // 向 ConcurrentSkipListMap 添加元素
        map.put("Key1", "Value1");
        map.put("Key2", "Value2");
        map.put("Key3", "Value3");
​
        // 打印 ConcurrentSkipListMap
        System.out.println("ConcurrentSkipListMap: " + map);
    }
}
​

7.ConcurrentSkipListSet 基于跳表的并发 Set

类似 HashSet 和 HashMap 的关系,ConcurrentSkipListSet 里面就是一个 ConcurrentSkipListMap,

ConcurrentSkipListSet 是 Java 并发包的一部分,它是一个线程安全的排序集合。它使用跳表的数据结构来保证元素的有序性和并发性。

跳表是一种可以进行二分查找的有序链表。ConcurrentSkipListSet 提供了预期的平均 log(n) 时间成本来执行 containsaddremove 操作,并且它的并发性通常优于基于树的算法。

示例
arduino 复制代码
import java.util.concurrent.*;
​
public class ConcurrentSkipListSetExample {
    public static void main(String[] args) {
        // 创建一个 ConcurrentSkipListSet
        ConcurrentSkipListSet<String> set = new ConcurrentSkipListSet<String>();
​
        // 向 ConcurrentSkipListSet 添加元素
        set.add("Element1");
        set.add("Element2");
        set.add("Element3");
​
        // 打印 ConcurrentSkipListSet
        System.out.println("ConcurrentSkipListSet: " + set);
    }
}
​

8.ArrayBlockingQueue 阻塞队列 (基于数组)

ArrayBlockingQueue 是 Java 并发包的一部分,它是一个基于数组的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。

ArrayBlockingQueue 在尝试插入元素到已满队列或从空队列中移除元素时,会导致线程阻塞,直到有空间或元素可用。

基于数组实现的可阻塞队列,构造时必须制定数组大小,往里面放东西时如果数组满了便会阻塞直到有位置(也支持直接返回和超时等待),通过一个锁 ReentrantLock 保证线程安全。

乍一看会有点疑惑,读和写都是同一个锁,那要是空的时候正好一个读线程来了不会一直阻塞吗?

答案就在 notEmpty、notFull 里,这两个出自 lock 的小东西让锁有了类似 synchronized + wait + notify 的功能。传送门 → 终于搞懂了 sleep/wait/notify/notifyAll

示例
arduino 复制代码
import java.util.concurrent.*;
​
public class ArrayBlockingQueueExample {
    public static void main(String[] args) {
        // 创建一个 ArrayBlockingQueue
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
​
        // 向 ArrayBlockingQueue 添加元素
        try {
            queue.put("Element1");
            queue.put("Element2");
            queue.put("Element3");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
​
        // 打印 ArrayBlockingQueue
        System.out.println("ArrayBlockingQueue: " + queue);
    }
}
​

9.LinkedBlockingQueue 阻塞队列 (基于链表)

LinkedBlockingQueue 是 Java 并发包的一部分,它是一个基于链表的可选有界阻塞队列。此队列按照 FIFO(先进先出)的原则对元素进行排序。

LinkedBlockingQueue 在尝试插入元素到已满队列或从空队列中移除元素时,会导致线程阻塞,直到有空间或元素可用。

基于链表实现的阻塞队列,想比与不阻塞的 ConcurrentLinkedQueue,它多了一个容量限制,如果不设置默认为 int 最大值。

示例
arduino 复制代码
import java.util.concurrent.*;
​
public class LinkedBlockingQueueExample {
    public static void main(String[] args) {
        // 创建一个 LinkedBlockingQueue
        LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<String>(3);
​
        // 向 LinkedBlockingQueue 添加元素
        try {
            queue.put("Element1");
            queue.put("Element2");
            queue.put("Element3");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
​
        // 打印 LinkedBlockingQueue
        System.out.println("LinkedBlockingQueue: " + queue);
    }
}
​

10.LinkedBlockingDeque 阻塞队列 (基于双向链表)

LinkedBlockingDeque 是 Java 并发包的一部分,它是一个基于链表的可选有界阻塞双端队列。此队列按照 FIFO(先进先出)的原则对元素进行排序。

LinkedBlockingDeque 在尝试插入元素到已满队列或从空队列中移除元素时,会导致线程阻塞,直到有空间或元素可用。双端队列的优势在于可以从两端插入或移除元素。

类似 LinkedBlockingQueue,但提供了双向链表特有的操作。

示例
arduino 复制代码
import java.util.concurrent.*;
​
public class LinkedBlockingDequeExample {
    public static void main(String[] args) {
        // 创建一个 LinkedBlockingDeque
        LinkedBlockingDeque<String> deque = new LinkedBlockingDeque<String>(3);
​
        // 向 LinkedBlockingDeque 添加元素
        try {
            deque.putFirst("Element1");
            deque.putLast("Element2");
            deque.putFirst("Element3");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
​
        // 打印 LinkedBlockingDeque
        System.out.println("LinkedBlockingDeque: " + deque);
    }
}
​

11.PriorityBlockingQueue 线程安全的优先队列

PriorityBlockingQueue 是 Java 并发包的一部分,它是一个无界的并发队列。它使用了和类 java.util.PriorityQueue 一样的排序规则,并且能够确保在并发环境下的线程安全。

PriorityBlockingQueue 中的元素按照自然顺序或者由比较器提供的顺序进行排序。队列不允许使用 null 元素。

构造时可以传入一个比较器,可以看做放进去的元素会被排序,然后读取的时候按顺序消费。某些低优先级的元素可能长期无法被消费,因为不断有更高优先级的元素进来。

示例
arduino 复制代码
import java.util.concurrent.*;
​
public class PriorityBlockingQueueExample {
    public static void main(String[] args) {
        // 创建一个 PriorityBlockingQueue
        PriorityBlockingQueue<String> queue = new PriorityBlockingQueue<String>();
​
        // 向 PriorityBlockingQueue 添加元素
        queue.add("Element1");
        queue.add("Element2");
        queue.add("Element3");
​
        // 打印 PriorityBlockingQueue
        System.out.println("PriorityBlockingQueue: " + queue);
    }
}
​

12.SynchronousQueue 数据同步交换的队列

SynchronousQueue 是 Java 并发包的一部分,它是一个不存储元素的阻塞队列。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素,反之亦然。

这种特性使 SynchronousQueue 成为线程之间传递数据的好工具。它可以看作是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。

一个虚假的队列,因为它实际上没有真正用于存储元素的空间,每个插入操作都必须有对应的取出操作,没取出时无法继续放入。

示例
arduino 复制代码
import java.util.concurrent.SynchronousQueue;
​
public class Main {
    
    public static void main(String[] args) {
        SynchronousQueue<Integer> queue = new SynchronousQueue<>();
        new Thread(()->{
            try{
                for(int i=0;;i++){
                    System.out.println("放入:" + i);
                    queue.put(i);
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }).start();
​
        new Thread(()->{
            try{
                while(true){
                    System.out.println("取出:" + queue.take());
                    Thread.sleep((long)(Math.random()*2000));
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }).start();
    }
}

运行结果:

erlang 复制代码
取出:0
放入:0
取出:1
放入:1
放入:2
取出:2
取出:3
放入:3
取出:4
放入:4
...
...

可以看到,写入的线程没有任何 sleep,可以说是全力往队列放东西,而读取的线程又很不积极,读一个又 sleep 一会。输出的结果却是读写操作成对出现。

JAVA 中一个使用场景就是 Executors.newCachedThreadPool(),创建一个缓存线程池。

13.LinkedTransferQueue 基于链表的数据交换队列

LinkedTransferQueue 是 Java 并发包的一部分,它是一个由链表结构组成的无界转移阻塞队列。队列按照 FIFO(先进先出)的原则对元素进行排序。

LinkedTransferQueue 的一个特性是,它可以尝试将元素直接转移给消费者,如果没有等待的消费者,元素就会被添加到队列的尾部,等待消费者来获取。

实现了接口 TransferQueue,通过 transfer 方法放入元素时,如果发现有线程在阻塞在取元素,会直接把这个元素给等待线程。如果没有人等着消费,那么会把这个元素放到队列尾部,并且此方法阻塞直到有人读取这个元素。和 SynchronousQueue 有点像,但比它更强大。

示例
arduino 复制代码
import java.util.concurrent.*;
​
public class LinkedTransferQueueExample {
    public static void main(String[] args) {
        // 创建一个 LinkedTransferQueue
        LinkedTransferQueue<String> queue = new LinkedTransferQueue<String>();
​
        // 启动一个新线程来从 LinkedTransferQueue 取出元素
        new Thread(() -> {
            try {
                System.out.println("Taken: " + queue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
​
        // 向 LinkedTransferQueue 添加一个元素
        try {
            queue.transfer("Element");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
​

14.DelayQueue 延时队列

DelayQueue 是 Java 并发包的一部分,它是一个无界阻塞队列,只有在延迟期满时才能从中提取元素。此队列的头部是延迟期满后保存时间最长的元素。如果延迟都还没有期满,则队列没有头部,并且 poll 将返回 null

元素在 DelayQueue 中的顺序是按照其到期时间的先后顺序进行排序的,越早到期的元素越排在队列前面。延迟队列常用于实现定时任务功能。

可以使放入队列的元素在指定的延时后才被消费者取出,元素需要实现 Delayed 接口。

示例
arduino 复制代码
import java.util.concurrent.*;
​
public class DelayQueueExample {
    public static void main(String[] args) {
        // 创建一个 DelayQueue
        DelayQueue<DelayedElement> queue = new DelayQueue<DelayedElement>();
​
        // 向 DelayQueue 添加一个元素,延迟 3 秒
        queue.put(new DelayedElement(3000, "Element"));
​
        // 从 DelayQueue 获取元素
        try {
            DelayedElement element = queue.take();
            System.out.println("Taken: " + element);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
​
class DelayedElement implements Delayed {
    private long delayTime; // 延迟时间
    private long expire;  // 到期时间
    private String element; // 元素数据
​
    public DelayedElement(long delay, String element) {
        this.delayTime = delay;
        this.element = element;
        this.expire = System.currentTimeMillis() + delay;
    }
​
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }
​
    @Override
    public int compareTo(Delayed o) {
        return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
    }
​
    @Override
    public String toString() {
        return element;
    }
}
​

总结

从上面的介绍总总结有以下几种容器类

  1. ConcurrentHashMap:并发版 HashMap
  2. CopyOnWriteArrayList:并发版 ArrayList
  3. CopyOnWriteArraySet:并发 Set
  4. ConcurrentLinkedQueue:并发队列 (基于链表)
  5. ConcurrentLinkedDeque:并发队列 (基于双向链表)
  6. ConcurrentSkipListMap:基于跳表的并发 Map
  7. ConcurrentSkipListSet:基于跳表的并发 Set
  8. ArrayBlockingQueue:阻塞队列 (基于数组)
  9. LinkedBlockingQueue:阻塞队列 (基于链表)
  10. LinkedBlockingDeque:阻塞队列 (基于双向链表)
  11. PriorityBlockingQueue:线程安全的优先队列
  12. SynchronousQueue:读写成对的队列
  13. LinkedTransferQueue:基于链表的数据交换队列
  14. DelayQueue:延时队列

Java 并发容器为处理多线程环境下的数据访问和修改提供了强大的工具。

通过了解和学习这些并发容器,我们可以更好地理解并发编程,更有效地处理并发问题。

无论你是正在学习 Java,还是已经在使用 Java 进行开发,我都强烈建议你深入了解这些并发容器,它们将在你的并发编程之路上起到重要的作用。


写在最后

感谢您的支持和鼓励! 😊🙏

如果大家对相关文章感兴趣,可以关注公众号"架构殿堂",会持续更新AIGC,java基础面试题, netty, spring boot, spring cloud等系列文章,一系列干货随时送达!

相关推荐
努力的小郑19 分钟前
Canal 不难,难的是用好:从接入到治理
后端·mysql·性能优化
Victor3561 小时前
MongoDB(87)如何使用GridFS?
后端
Victor3561 小时前
MongoDB(88)如何进行数据迁移?
后端
小红的布丁1 小时前
单线程 Redis 的高性能之道
redis·后端
GetcharZp1 小时前
Go 语言只能写后端?这款 2D 游戏引擎刷新你的认知!
后端
宁瑶琴3 小时前
COBOL语言的云计算
开发语言·后端·golang
普通网友3 小时前
阿里云国际版服务器,真的是学生党的性价比之选吗?
后端·python·阿里云·flask·云计算
IT_陈寒4 小时前
Vue的这个响应式问题,坑了我整整两小时
前端·人工智能·后端
Soofjan5 小时前
Go 内存回收-GC 源码1-触发与阶段
后端
shining5 小时前
[Golang]Eino探索之旅-初窥门径
后端