线程集合类

线程安全集合类概述

image-20211016202825663

线程安全集合类可以分为三大类:

  • 遗留的线程安全集合如Hashtable(put,get都是用synchronized修饰,并发性不是很好), Vector

  • 使用Collections装饰的线程安全集合,如:

    • Collections.synchronizedCollection

    • Collections.synchronizedList

    • Collections. synchronizedMap

    • Collections. synchronizedSet

    • Collections . synchronizedNavigableMap

    • Collections.synchronizedNavigableSet

    • Collections.synchronizedSortedMap

    • Collections . synchronizedSortedSet

    • java. util. concurrent.*

以map结合为例解释怎么讲线程不安全的集合转为线程安全的

image-20211016203014544

image-20211016203147021

重点介绍java.util. concurrent. *下的线程安全集合类,可以发现它们有规律,里面包含三类关键词:
Blocking、CopyOnWrite、 Concurrent

  • Blocking(不满足条件的时候阻塞住)大部分实现基于锁,并提供用来阻塞的方法(内部使用的是reentrantLock)
  • CopyOnWrite 之类容器修改开销相对较重(采用拷贝的方式,用于读多写少)
  • Concurrent(并发)类型的容器
    • 内部很多操作使用cas优化,一般可以提供较高吞吐量
    • 弱一致性
      • 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍
        历,这时内容是旧的
      • 求大小弱一致性, size操作未必是100%准确
      • 读取弱一致性

遍历时如果发生了修改,对于非安全容器来讲,使用fail-fast 机制也就是让遍历立刻失败,抛出
ConcurrentModificationException,不再继续遍历

1.ConcurrentHashMap

练习:单词计数

将每个字母生成两百次,之后打乱其在集合中的顺序,在将其输入到文件中,如果中间步骤都正确,那一个字母就应该出现两百次。

image-20211016204136214

在多线程中用Hashmap集合来统计每个单词出现的次数,会出现线程安全问题。

那你该想那可以改为线程安全的ConcurrentHashMap,那也不对,

因为ConcurrentHashMap集合中每个方法是原子的,但是你把多个方法组合到一起,各个方法加起来就不是原子的。

image-20211016204513344

解决方法:可以在方法上加一个synchrnized,使其方法为原子的,但是并发性不是很好

ConcurrentHashMap中有一个方法’computeIfAbsent()’ 作用:如果一个key在map中不存在,则将其key存进去,并且生成一个value,一起存进去,该操作是原子的。之后使用累加器进行原子累加。

image-20211016205444729

map.computeIfAbsent():保证get和put方法的原子性,和累加器一起使用,返回的是上一个操作中的累加器

ConcurrentHashMap原理
I. JDK 7 HashMap并发死链

数组+链表实现,先计算key的hash值:应该放在那个位置。

jdk7中会将后插入的元素放在链表的头部,而jdk8中后插入的元素会被插入到链表的尾部

死链发生在并发下数组扩容时:当数组元素超过数组长度的3/4时,扩容为原数组的2倍,再将原数组的元素前移到新数组里面

桶下标相同的key

transfer

image-20211016210523983

e:当前要去迁移的结点

小结

  • 究其原因,是因为在多线程环境下使用了非线程安全的map集合
  • JDK 8虽然将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),但仍不意味
    着能够在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据)
2.JDK 8 ConcurrentHashMap

重要属性和内部类

image-20211016211430679

sizeCtl:下一次扩容的阈值大小(数组容量的3/4)

ForwardingNode:作用一:用来标记该下标的元素链表已经处理过了

image-20211016211850976

第二个作用:在扩容的时候,有人来get值,发现有ForwardingNode,就会去新数组里面查找

TreeBin:头节点, TreeNode:树上的每个节点

当链表长度达到八,再看数组长度是不是达到64,没有达到就扩容,达到了就将链表转为树。当你删除元素,树上的结点又小于8了,就会在将红黑树转为链表。

重要方法:

image-20211016212325743

构造器分析

可以看到实现了懒惰初始化,在构造方法中仅仅计算了table 的大小,以后在第一次使用时才会真正创建

初始容量,负载因子(3/4,0.75),并发度

image-20211016212423581

1、初始容量要保证并发度那么大

2、懒惰初始化,当你第一次用到的时候才会去初始化数组

3、容量一定是2的n次方

get流程

image-20211016212824309

1、先判断数组长度是否大于0,在定位要查找的桶的下标,去找链表

2、先判断头节点为不为空,再判断头节点的hash码是不是等于key的hash码,是再用equals去比较,如果是,直接返回

3、头节点的hash码为负数:表示当前数组正在扩容(那就去新的数组中去找值)或者是树的头节点(去红黑树里面查找)

4、以上都不符合,就去遍历链表,寻找对应的key

put流程

以下数组简称(table),链表简称(bin)

会把重复的key覆盖掉之前的,并且key,v不能为null

image-20211017084518304

1、看哈希表是不是为空或者长度为0(懒惰初始化)

2、有了哈希表看头结点时不是空的,是创建节点将键值就存放在这

3、再存放键值的时候,数组正处于扩容状态,回去帮忙扩容(锁住某个链表,保证其线程安全)

进到下面的else说明桶下标冲突—加锁,只对链表的头节点加锁。

遍历链表去查找有没有要插入的key,如果有就覆盖掉原来的value,没有就在链表尾部追加

image-20211017085222386

如果此时是红黑树,那就将键值插入到树中

image-20211017085551018

在这里插入图片描述

addCount():里面不仅有增加计数的功能,还有扩容的逻辑

创建哈希表

懒惰初始化,并且创建的时候要保证线程安全,只能有一个线程能创建哈希表,其他线程都是忙等待,并没有阻塞住

image-20211017085946341

用cas的方式将sizectl改为-1(表示正在创建哈希表)

增加hash表中元素的个数

计数值,check链表的长度

LongAdder设置多个累计单元,多线程去做技术增长时,冲突就减少了,增加性能

image-20211017090454617

扩容流程

sizeCtl:扩容的阈值,扩容的时候将其值改为负数

image-20211017090908826

size计算流程

size计算实际发生在put, remove 改变集合元素的操作之中

  • 没有竞争发生,向baseCount累加计数
  • 有竞争发生,新建counterCells, 向其中的一个cell累加计数
    • counterCells初始有两个cell
    • 如果计数竞争比较激烈,会创建新的cell来累加计数

image-20211017091319705

扩容流程transfer

1、原始的tab,扩容后新的nextTab(懒惰初始化,并且长度为之前的2倍)

2、开始以链表为单位,搬迁

3、如果链表头为null,说明该链表已经被处理完了,将链表头替换为fwd(forwardingNode),forwardingNode的哈希码为-1

4、如果链表头是有元素的,那他就上锁,将其链表头锁住。如果链表头的哈希码为正数说明是普通节点,如果是负数,到了这里就看是不是树的头节点

image-20211017091526796

3.JDK 7 ConcurrentHashMap

它维护了-个segment数组,每个segment对应一把锁(jdk8将锁加在链表头)

  • 优点:如果多个线程访问不同的segment,实际是没有冲突的,这与jdk8中是类似的
  • 缺点: Segments数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化

image-20211017092408238

每个HashEntry里面又是数组+链表的结构

image-20211017092612553

整个segment数组和segment[0]的元素不是懒惰加载的,其他位置的元素都是懒惰加载的

LinkedBlockingQueue原理

链表

Node中的结构:

item:将来这个结点要关联的元素

next有三种情况:真实后继,自己(出队时),null

1.基本的入队出队

image-20211017093234225

**初始化链表:**last = head =new Node< E>(null); Dummy节点用来占位(哑元结点), item 为null

image-20211017093520837

入队

新加一个节点: 当一个节点入队last = last.next =node;

image-20211017093705472

再来一个结点时:

image-20211017093810810

出队

image-20211017093836890

image-20211017093941900

image-20211017094037266

image-20211017094322580

2.加锁分析

高明之处在于用了两把锁和dummy节点

  • 用-把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选- -) 执行

  • 用两把锁(锁住的是队列的头和尾),同一时刻,可以允许两个线程同时(- -个生产者与- 一个消费者)执行

    • 消费者与消费者线程仍然串行
    • 生产者与生产者线程仍然串行

    线程安全分析

    • 当节点总数大于2时(包括dummy节点),putLock保证的是last节点的线程安全,takeL ock保证的是
      head节点的线程安全。两把锁保证了’入队和出队没有竞争
    • 当节点总数等于2时(即一个dummy节点,-个正常节点)这时候,仍然是两把锁锁两个对象,不会竞
    • 当节点总数等于1时(就-一个dummy节点)这时take线程会被notEmpty条件阻塞,有竞争,会阻塞

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eT9IFLYp-1634439044019)(…/…/…/…/…/AppData/Roaming/Typora/typora-user-images/image-20211017094841460.png)]

put操作

1、先判断元素是不是null,是就抛出空指针异常。即不允许加入null空元素

c是用来检查空位的。put和take采用的是不同的锁

2、先上一个putLock锁,是可以被打断的

3、当元素的个数等于队列的容量,队列满了,就应该等待(putLock的条件变量notFull,倒过来读,等待不满的状态)(即队列满了,不生产了,休息会)

4、不满就enqueue,将元素加入到队列尾部(并且修改Node里面的指针指向),计数加一

5、如果发现除了自己put的之外,队列还有空位,由自己叫醒其他put线程(使用singnal唤醒一个等待线程)

6、如果生产者队列中只有一个元素了,由生产者线程(put)叫醒消费者线程(take)

image-20211017095642847

take操作

使用takeLock上锁,等待队列不为空去消费,dequeue出队。消费者自己唤醒其他的消费者。但当队列中只有一个空位时,去唤醒生产者线程

image-20211017100155433

3.性能比较

主要列举LinkedBlockingQueue 与ArrayBlockingQueue的性能比较

  • Linked支持有界, Array强制有界.
  • Linked实现是链表,Array实现是数组
  • Linked是懒惰的,而Array需要提前初始化Node数组
  • Linked 每次入队会生成新Node,而Array的Node是提前创建好的
  • Linked两把锁(分别锁住头和尾),Array 一把锁

ConcurrentLinkedQueue

ConcurrentL inkedQueue的设计与LinkedBlockingQueue非常像,也是

  • 两把[锁],同一时刻,可以允许两个线程同时(一个生产者队列未加入元素 与一 个消费者 队列头消费元素)执行

  • dummy节点的引入让两把[锁]将来锁住的是不同对象,避免竞争(头结点是dummy,尾节点是实例,两把锁锁住不同的对象)

  • 只是这[锁]使用了cas来实现,无阻塞

事实上,ConcurrentI inkedQueue应用还是非常广泛的

例如之前讲的Tomcat的Connector结构时,Acceptor 作为生产者向Poller消费者传递事件信息时,正是采用
了ConcurrentI inkedQueue将SocketChannel给Poller使用

image-20211017101233179

CopyOnWriteArrayList

CopyOnWriteArraySet是它的马甲
底层实现采用了写入时拷贝的思想,增删改操作会将底层数组拷贝- -份,更改操作在新数组上执行,这时不
影响其它线程的并发读,读写分离

以新增为例: .

image-20211017101530750

这里的源码版本是Java 11,在Java 1.8中使用的是可重入锁(ReentrantLock)而不是synchronized

其它读操作并未加锁,例如:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dECpaX6V-1634439044024)(…/…/…/…/…/AppData/Roaming/Typora/typora-user-images/image-20211017101813382.png)]

读操作可以并发,写操作的时候要加锁,避免其他的线程写

适用于读多写少。用空间换取线程安全。

弱一致性

image-20211017101941361

image-20211017101949453

读取的是旧数组的元素,仍然可以得到新数组已经删除的元素

迭代器弱一致性

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MINJkmXR-1634439044028)(…/…/…/…/…/AppData/Roaming/Typora/typora-user-images/image-20211017102111445.png)]

在一个线程A去遍历数组的时候,另外一个线程B去删除该数组的元素,线程A得到的还是旧数组的引用

不要觉得弱一致性就不好(做到了读写并发,数据库的多版本并发控制)

  • 数据库的MVCC都是弱一致性的表现
    存中…(img-dECpaX6V-1634439044024)]

读操作可以并发,写操作的时候要加锁,避免其他的线程写

适用于读多写少。用空间换取线程安全。

弱一致性

[外链图片转存中…(img-biFUGAJY-1634439044026)]

[外链图片转存中…(img-Fv1IBGw7-1634439044027)]

读取的是旧数组的元素,仍然可以得到新数组已经删除的元素

迭代器弱一致性

[外链图片转存中…(img-MINJkmXR-1634439044028)]

在一个线程A去遍历数组的时候,另外一个线程B去删除该数组的元素,线程A得到的还是旧数组的引用

不要觉得弱一致性就不好(做到了读写并发,数据库的多版本并发控制)

  • 数据库的MVCC都是弱一致性的表现
  • 并发高和一致性是矛盾的,需要权衡
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中,有几个线程安全的集合类可以用于多线程环境下的操作。其中包括Vector、Stack、HashTable等。这些集合类通过使用Synchronized来保证线程安全性。不过需要注意的是,官方已经标注Vector和HashTable为即将废弃的类,不建议使用它们。 除了上述的线程安全集合类,还有其他一些较新的线程安全集合类可以使用。其中包括CopyOnWriteArrayList、CopyOnWriteArraySet和ConcurrentHashMap等。这些集合类通过特定的机制来实现线程安全性,而且在性能方面也进行了优化。 此外,Java中还有Queue接口,它包含了几个线程安全的实现类,如ConcurrentLinkedQueue和BlockingQueue接口的实现类。这些集合类可以在多线程环境下进行安全的队列操作。 总之,在Java中有多种线程安全的集合类可供选择,可以根据具体的需求和场景选择合适的集合类来保证线程安全性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Java中那些线程安全的集合类](https://blog.csdn.net/weixin_53946852/article/details/122801143)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [Java线程安全的集合](https://blog.csdn.net/weixin_42601136/article/details/107108818)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值