0906(048天 线程集合02 ConcurrentHashMap和CopyOnWrite)

0906(048天 线程集合02 ConcurrentHashMap和CopyOnWrite)

每日一狗(田园犬西瓜瓜

在这里插入图片描述

线程集合ConcurrentHashMap和CopyOnWrite

1. ConcurrentHashMap

概述

为了解决HashMap的线程安全问题,ConcurrentHashMap采用了CAS和synchronized共同

具体实现

ReservationNode占位置且加锁

红黑树不是直接挂载在桶里的

单向链表转换成红黑树后,红黑树会存放到TreeBin中,TreeBin挂载在桶里

属性
private transient volatile Node<K,V>[] nextTable; // 这个就是扩容的时候临时用一些的表,从就数组那边往过搬运,最后在将旧表指向新表

几个节点

普通节点

这就是最一开始的节点,一开始构造双向链表的时候挂载创建的节点

TreeNode树节点

TreeNode就是红黑树的结点,TreeNode不会直接链接到table[i]——桶上面,而是由TreeBin链接,TreeBin会指向红黑树的根结点。

TreeBin树的管理节点

树化后桶和红黑树的中间人,TreeBin会直接链接到table[i]——桶上面,该结点提供了一系列红黑树相关的操作,以及加锁、解锁操作。

ForwardingNode 扩容辅助节点

那个桶在重新分发,就在那个位置上用这个节点进行替换,先把这个玩意插到桶上意思是我这个容器在扩容哦,这个桶上的节点在搬运哦

ReservationNode保留结点

  • 在并发场景下、在从Key不存在到插入的时间间隔内,为了防止哈希槽被其他线程抢占,当前线程会使用一个reservationNode节点放到槽中并加锁,从而保证线程安全
  • hash值固定为-3,不保存实际数据。只在computeIfAbsent和compute这两个函数式API中充当占位符加锁使用
构造器

解决数组初始化数组对象时保证线程安全使用的是cas

初始化数组是在第一次添加数据的时候进行的。

  • volatile变量(sizeCtl):它是一个标记位,用来告诉其他线程这个坑位有没有人在,其线程间的可见性由volatile保证
  • CAS操作:CAS操作保证了设置sizeCtl标记位的原子性,保证了只有一个线程能设置成功,同时保证了只有一个线程来对数组进行初始化
添加数据

桶为空的时候使用cas保证线程安全

在已经存在数据的桶中追加数据使用synchronized保证线程安全,使用头节点充当同步代码块的锁对象。

扩容

ForwardingNode扩容辅助节点

当table需要扩容时,依次遍历table中的每个槽,如果不为null,把所有元素根据hash值放入扩容后的nextTable中,在原table的槽内放置一个ForwardingNode标识我这正在扩容。

ForwardingNode是一种临时结点,在扩容进行中才会出现,hash值固定为-1,且不存储实际数据。

多线程协助扩容

当一个线程再添加数据的时候,刚巧这个数据的hash值算出来的桶上边的标志是-1,即这个桶上存储的是ForwardingNode,就会被拉去当劳工辅助扩容的节点计算。

重新分发节点时看这个节点要不要移动

例子:容积从16扩容到32时,

节点10:0000 1010(2)

节点17:0001 0001(17)

节点33:0010 0001(1)

16 : 0001 0000

就是那个扩容原始长度表示的那个位上是不是1,10和33的哪一位不是1所以就没移动,只有17节点的哪一位是1,他就得加上扩容前数组容积就是这个索引。

扩容时get

算出来的桶没有在扩容就正常返回,在扩容重新分发的话就去新桶去找,这里就用到多态了,得看这个节点是啥

Node那就单链表的find

TreeBin那就是红黑树的find

ForwardingNode那就得定位到新表去调用对应的find

总结

ConcurrentHashMap运用各类CAS操作{Unsafe},将扩容操作的并发性能实现最大化,在扩容过程中,就算有线程调用get查询方法,也可以安全的查询数据,若有线程进行put操作,还会协助扩容,利用sizeCtl标记位和各种volatile变量进行CAS操作达到多线程之间的通信、协助,在迁移过程中只锁一个Node节点,即保证了线程安全,又提高了并发性能。

get操作的线程安全

对于get操作,其实没有线程安全的问题,只有可见性的问题,只需要确保get的数据是线程之间可见的即可在get操作中除了增加了迁移的判断以外,基本与HashMap的get操作无异使用了tabAt方法Unsafe类volatile的方式去获取Node数组中的Node,保证获得到的Node是最新的。

2. CopyOnWriteXXX

思想

这个就很好说了,但凡是遇到了写容器操作,直接就去拷贝一份,改完之后再将原容器的引用指向新的容器。

所以CopyOnWrite容器就是一种读写分离的思想,读和写不同的容器。

这样做就可以对CopyOnWrite容器进行并发的读,而且不需要加锁,因为读取操作遍历的对象是原来的旧对象。

CopyOnWriteArrayList、CopyOnWriteArraySet

CopyOnWriteArrayList的实现原理

写还是要加锁的,要不然就会Copy出来好多个副本,到时候写入那个副本就是一个线程安全问题了。读的时候不需要加锁,如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的ArrayList

public E get(int index) {
    return elementAt(getArray(), index);
}
static <E> E elementAt(Object[] a, int index) {
    return (E) a[index];
}
public boolean add(E e) {
    synchronized (lock) {
        ......
    }
}

CopyOnWriteXXX的应用场景

CopyOnWrite并发容器用于读多写少的并发场景

比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索

注意两点:

  • 减少扩容开销。根据实际需要,初始化一个Set,将数据全局添加到set中,然后再使用addAll之类的方法一行添加集合中的所有元素,以避免每次写时扩容的开销,合理的评估
  • 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。

CopyOnWriteXXX的缺点:

  • 复制是需要额外的空间和时间上的开销
  • 不能保证遍历的数据时最新的

面对第一个复制的修改数据开销问题,可以搞一个临时存储空间,存储需要要改删的数据,隔一段时间更新一次,可以降低在复制上的时空开销

第二个缺点属实是没什么办法,这种拷贝修改覆盖的机制就不可能保证数据的时效性,除非你加锁,在读的时候不允许写,但是这又和这个机制的初衷相悖,但是这个写和写之间可是同步的,这保证了线程安全问题。

3. 容器容错机制

failfast快速失败

  • 如果出现故障,则立即报错。
  • 通常用于非幂等性操作,如:下单操作,如果写入故障,则立即报错,不必重试

使用迭代器遍历集合的同时,如果集合的结构发生改变,会抛出异常ConcurrentModificationException(并发修改异常)。

原理:就是迭代器中维护的预期的expectedModCount和容器中的modCount,他在每一次进行next时会判定修改次数是否与我这边当时创建迭代器的时候传进来的modCount一致,不同则会抛出异常

public E next() {
    checkForComodification();
    ......
}
/ 2 
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

failsafe故障安全

  • 如果出现故障,则可以忽略,因为这种故障不会造成损失或损失在可接受范围内。
  • 通常用于辅助性操作,如:写入监控日志,如果写入故障,则忽略。

当我们对集合结构上做出改变的时候,fail-fast机制直接会抛出异常。但是对弈采用fail-safe机制来说,不会抛出异常。

因为集合的机构被改变的时候,fail-safe机制会拷贝一份数据出来,然后再这份新的数据上进行增删操作,当写删完毕后会将容器的引用指向新的数组。

failover故障切换

  • 如果出现故障,则重试备份操作模式。
  • 通常用于幂等性操作,如:MySql的双Master模式,如果主Master故障,则切换至从Master。
  • 重试通常会带来更多延时。

failback故障恢复

  • 故障切换之后,如果主要操作模式恢复,则自动从备份操作模式恢复主要操作模式。
  • 如:MySql的双Master模式,如果主Master故障,则failover至从Master;当主Master恢复之
    后,则自动切换至主Master。

针对与快速失败提出的解决办法

方案一:使用Iterator接口中提供的remove方法来进行删除操作。

方案二:fail-safe,拷贝修改覆盖

方案三:不就是异常嘛,我处理一下不就行了

monitor监视器对象或管程

Monitor可以翻译成监视器或者管程,是由JVM提供的,当线程执行到对象方法的临界区时,首先去看对象头是不是关联了Monitor,如果关联了加入Monitor的EntryList中,没有关联就改变对象的对象头的Mark Word或者指向一个Monitor,然后Monitor的拥有者就变成了该线程,执行完了之后,Monitor再把线程释放,去EntryList找一个新的拥有者(非公平竞争,不是先来的就先得)

  • 一个对象关联一个Monitor;当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁
  • Java虚拟机给每个对象和class字节码都设置了一个监听器Monitor,用于检测并发代码的重入,同时在Object类中还提供了notify和wait方法来对线程进行控制
  • 重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址
  • 不加synchronized的对象不会关联监视器;
  • 自旋优化:在竞争锁的时候,自旋重试几次,要是此时锁被释放就进入,否则进入阻塞状态

java中每个对象都有唯一的一个monitor,想拥有一个对象的monitor的话有以下三种方式:
1、执行该对象的同步方法public synchronize a () {}

2、执行该对象的同步块synchronize(obj) {}

3、执行某个类的静态同步方法public static synchronize b(){}

Monitor可以类比为一个特殊的房间,这个房间中有一些被保护的数据,Monitor保证每次只能有一个线程能进入这个房间进行访问被保护的数据,进入房间即为持有Monitor,退出房间即为释放Monitor。

当一个线程需要访问受保护的数据(即需要获取对象的Monitor)时,它会首先在entry-set入口队列中排队(这里并不是真正的按照排队顺序),如果没有其他线程正在持有对象的Monitor,那么它会和entry-set队列和wait-set队列中的被唤醒的其他线程进行竞争(即通过CPU调度),选出一个线程来获取对象的Monitor,执行受保护的代码段,执行完毕后释放Monitor,如果已经有线程持有对象的Monitor,那么需要等待其释放Monitor后再进行竞争。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值