当前位置: 首页>后端>正文

并发容器 Map、Set、List

这个绝不是我对JUC的最后一版笔记,AQS的原理因为笔记里没有需要看视频,所以后期看过视频一定会更新的
还有一些内容写的我并不是很满意,后期会再去更新的


并发容器 Map、Set、List,第1张

并发包中的 ConcurrentLinkedQueue 和 LinkedBlockingQueue 有什么区别

Concurrent 类型基于 lock-free,在常见的多线程访问场景,一般可以提供较高吞吐量

而 LinkedBlockingQueue 内部则是基于锁,并提供了 BlockingQueue 的等待性方法。

不知道你有没有注意到,java.util.concurrent 包提供的容器(Queue、List、Set)、Map,
从命名上可以大概区分为 Concurrent*、CopyOnWrite和 Blocking等三类,同样是线程安全容器,
可以简单认为:

Concurrent 类型没有类似 CopyOnWrite 之类容器相对较重的修改开销。

但是,凡事都是有代价的,Concurrent 往往提供了较低的遍历一致性。你可以这样理解所谓的弱一致性,
例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历。

弱一致性的另外一个体现是,size 等操作准确性是有限的,未必是 100% 准确。

与此同时,读取的性能具有一定的不确定性。
————————————————
版权声明:本文为CSDN博主「TimeFriends」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_44590469/article/details/129295697
并发容器 Map、Set、List,第2张

CopyOnWriteArrayList

CopyOnWriteArrayList 是 Java 中的一种线程安全的 List,它是一个可变的数组,支持并发读和写。
与普通的 ArrayList 不同,它的读取操作不需要加锁,因此具有很高的并发性能。

2.1 应用场景

CopyOnWriteArrayList 的应用场景主要有两个方面:

1. 读多写少的场景

由于 CopyOnWriteArrayList 的读操作不需要加锁,因此它非常适合在读多写少的场景中使用。例
如,一个读取频率比写入频率高得多的缓存,使用 CopyOnWriteArrayList 可以提高读取性能,并减
少锁竞争的开销。

2. 不需要实时更新的数据

由于 CopyOnWriteArrayList 读取的数据可能不是最新的,因此它适合于不需要实时更新的数据。例
如,在日志应用中,为了保证应用的性能,日志记录的操作可能被缓冲,并不是实时写入文件系统,
而是在某个时刻批量写入。这种情况下,使用 CopyOnWriteArrayList 可以避免多个线程之间的竞
争,提高应用的性能。

2.2 CopyOnWriteArrayList使用

基本使用

和 ArrayList 在使用方式方面很类似。

1 // 创建一个 CopyOnWriteArrayList 对象
2 CopyOnWriteArrayList phaser = new CopyOnWriteArrayList();
3 // 新增
4 copyOnWriteArrayList.add(1);
5 // 设置(指定下标)
6 copyOnWriteArrayList.set(0, 2);
7 // 获取(查询)
8 copyOnWriteArrayList.get(0);
9 // 删除
10 copyOnWriteArrayList.remove(0);
11 // 清空
12 copyOnWriteArrayList.clear();
13 // 是否为空
14 copyOnWriteArrayList.isEmpty();
15 // 是否包含
16 copyOnWriteArrayList.contains(1);
17 // 获取元素个数
18 copyOnWriteArrayList.size();
19

2.3 CopyOnWriteArrayList原理

CopyOnWriteArrayList 内部使用了一种称为“写时复制”的机制。当需要进行写操作时,它会创建
一个新的数组,并将原始数组的内容复制到新数组中,然后进行写操作。因此,读操作不会被写操作
阻塞,读操作返回的结果可能不是最新的,但是对于许多应用场景来说,这是可以接受的。此外,由
于读操作不需要加锁,因此它可以支持更高的并发度。

CopyOnWriteArrayList 的缺陷

CopyOnWriteArrayList 有几个缺点:
由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者
full gc
不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然 CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;
CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用。因为谁也没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。

2.4 扩展知识:迭代器的 fail-fast 与 fail-safe 机制

在 Java 中,迭代器(Iterator)在迭代的过程中,如果底层的集合被修改(添加或删除元素),不同
的迭代器对此的表现行为是不一样的,可分为两类:Fail-Fast(快速失败)和 Fail-Safe(安全失
败)。

fail-fast 机制

fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作
时,就可能会产生 fail-fast 事件。例如:当某一个线程A通过 iterator 去遍历某集合的过程中,若该
集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生 fail-fast 事件。
在 java.util 包中的集合,如 ArrayList、HashMap 等,它们的迭代器默认都是采用 Fail-Fast 机制。

我的疑问

这部分是存在问题的,看一下我的代码

 @Test
    public void test33() throws InterruptedException {
        List<Integer> lists=new ArrayList<>(50);
        for (int i = 0; i < 50; i++) {
            lists.add(i);
        }
        Thread t=new Thread(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 5; i++) {
                        Iterator<Integer> iterator = lists.iterator();
                        while (iterator.hasNext()){
                            System.out.print(iterator.next());
                        }
                        System.out.println();
                    }

                }catch (Exception e){
                    System.out.println("异常");
                    System.out.println(e.getMessage());
                }
            }
        };
        t.start();
        Thread.sleep(2);
        lists.set(1,100);
        t.join();
    }

然后运行结果


并发容器 Map、Set、List,第3张

根本没有异常,我还catch了,真无语,一个月要扣我740的课程咋能有这种低等错误呢?

fail-fast解决方案

方案一:在遍历过程中所有涉及到改变modCount 值的地方全部加上synchronized 或者直接使用

Collection#synchronizedList,这样就可以解决问题,但是不推荐,因为增删造成的同步锁可能会阻塞遍历操
作。

方案二:使用CopyOnWriteArrayList 替换 ArrayList,推荐使用该方案(即fail-safe)。

fail-safe机制

任何对集合结构的修改都会在一个复制的集合上进行,因此不会抛出
ConcurrentModificationException。在 java.util.concurrent 包中的集合,如
CopyOnWriteArrayList、ConcurrentHashMap 等,它们的迭代器一般都是采用 Fail-Safe 机制。

缺点:

采用 Fail-Safe 机制的集合类都是线程安全的,但是它们无法保证数据的实时一致性,它们只能保证数据的最终一致性。在迭代过程中,如果集合被修改了,可能读取到的仍然是旧的数据。
Fail-Safe 机制还存在另外一个问题,就是内存占用。由于这类集合一般都是通过复制来实现读写分离的,因此它们会创建出更多的对象,导致占用更多的内存,甚至可能引起频繁的垃圾回收,严重影响性能。

2. ConcurrentHashMap

ConcurrentHashMap 是 Java 中线程安全的哈希表,它支持高并发并且能够同时进行读写操作。
在JDK1.8之前,ConcurrentHashMap使用分段锁以在保证线程安全的同时获得更大的效率。
JDK1.8开始舍弃了分段锁,使用自旋+CAS+synchronized关键字来实现同步。官方的解释中:一是节省内存空间 ,二是分段锁需要更多的内存空间,而大多数情况下,并发粒度达不到设置的粒度,竞争概率较小,反而导致更新的长时间等待(因为锁定一段后整个段就无法更新了)三是提高GC效率。

2.1 应用场景

ConcurrentHashMap 的应用场景包括但不限于以下几种:

  1. 共享数据的线程安全:在多线程编程中,如果需要进行共享数据的读写,可以使用 ConcurrentHashMap 保证线程安全。
  2. 缓存:ConcurrentHashMap 的高并发性能和线程安全能力,使其成为一种很好的缓存实现方案。在多线程环境下,使用 ConcurrentHashMap 作为缓存的数据结构,能够提高程序的并发性能,同时保证数据的一致性。

2.2 ConcurrentHashMap使用

基本用法

1 // 创建一个 ConcurrentHashMap 对象
2 ConcurrentHashMap<Object, Object> concurrentHashMap = new ConcurrentHashMap<>();
3 // 添加键值对
4 concurrentHashMap.put("key", "value");
5 // 添加一批键值对
6 concurrentHashMap.putAll(new HashMap());
7 // 使用指定的键获取值
8 concurrentHashMap.get("key");
9 // 判定是否为空
10 concurrentHashMap.isEmpty();
11 // 获取已经添加的键值对个数
12 concurrentHashMap.size();
13 // 获取已经添加的所有键的集合
14 concurrentHashMap.keys();
15 // 获取已经添加的所有值的集合
16 concurrentHashMap.values();
17 // 清空
18 concurrentHashMap.clear();
19

其他方法:

  1. V putIfAbsent(K key, V value)
    如果 key 对应的 value 不存在,则 put 进去,返回 null。否则不 put,返回已存在的 value。
  2. boolean remove(Object key, Object value)
    如果 key 对应的值是 value,则移除 K-V,返回 true。否则不移除,返回 false。
  3. boolean replace(K key, V oldValue, V newValue)
    如果 key 对应的当前值是 oldValue,则替换为 newValue,返回 true。否则不替换,返回 false。

补充by syo~

Map< ,AtomicInteger>
如果是这样,后面对AtomicInteger类型对象进行更高,那么map里面的数据也会改
Map<
,自定义类>
若后面对这个类对象进行更改,那么map里面的数据也会跟着更改

3. ConcurrentSkipListMap

这个不是很全回头我再找资料吧,笔记不全得听课
ConcurrentSkipListMap 是 Java 中的一种线程安全、基于跳表实现的有序映射(Map)数据结构。
它是对 TreeMap 的并发实现,支持高并发读写操作。
ConcurrentSkipListMap适用于需要高并发性能、支持有序性和区间查询的场景,能够有效地提高系
统的性能和可扩展性。

3.1 跳表

跳表是一种基于有序链表的数据结构,支持快速插入、删除、查找操作,其时间复杂度为O(log n),
比普通链表的O(n)更高效。
https://cmps-people.ok.ubc.ca/ylucet/DS/SkipList.html
图一
图二
图三

跳表的特性有这么几点:

一个跳表结构由很多层数据结构组成。
每一层都是一个有序的链表,默认是升序。也可以自定义排序方法。
最底层链表(图中所示Level1)包含了所有的元素。
如果每一个元素出现在LevelN的链表中(N>1),那么这个元素必定在下层链表出现。
每一个节点都包含了两个指针,一个指向同一级链表中的下一个元素,一个指向下一层级别链表中的相同值元
素。
跳表的查找
跳表的插入

跳表插入数据的流程如下:

  1. 找到元素适合的插入层级K,这里的K采用随机的方式。若K大于跳表的总层级,那么开辟新的一层,否则在对应
    的层级插入。
  2. 申请新的节点。
  3. 调整对应的指针。
    假设我要插入元素13,原有的层级是3级,假设K=4:
    倘若K=2:

3.2 ConcurrentSkipListMap使用

基本用法

1 public class ConcurrentSkipListMapDemo {
2 public static void main(String[] args) {
3 ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();
4 
5 // 添加元素
6 map.put(1, "a");
7 map.put(3, "c");
8 map.put(2, "b");
9 map.put(4, "d");
10 
11 // 获取元素
12 String value1 = map.get(2);
13 System.out.println(value1); // 输出:b
14 
15 // 遍历元素
16 for (Integer key : map.keySet()) {
17 String value = map.get(key);
18 System.out.println(key + " : " + value);
19 }
20 
21 // 删除元素
22 String value2 = map.remove(3);
23 System.out.println(value2); // 输出:c
24 }
25 }

https://www.xamrdz.com/backend/3kj1941842.html

相关文章: