HashMap高并发

HashMap,ConcurrentHashMap,volatitle

table:entry->entry->entry

entry:<key,value>,next

数组与链表的结合体,链式数组

拥有两个参数loadFactor和initialCapacity,分别称作加载因子0.75和初始化容量16。

HashMap在没有数据进入,即put时,是不会创建数组的。

在这里,为了确保table容量满足要求,却又必须为2的幂。

所以阈值就是table的容量与负载因子的乘积。

put方法

在添加数据时put,为了确保所对应的key的hash值不超出table的长度,所以需要与table.length-1与操作,即成功取得hash的length长度,即低多少位。

在往table中添加数据时,当数组为空时,可以直接插入。

当向table中添加entry时,首先要判断entry数量是否超过阈值,其次要判断当前位置是否为空,若为空,可以直接添加。

get方法

在从table中进行取值操作时,如果传入的key值为null时,则需要从table[0]中进行取值操作。

在通过key获取entry时,首先需要判断的key的hash与遍历所得到的hash是否相同,再判断两个key是否相等,才能够返回entry。

扩容

在添加entry时,若table的长度超过阈值,且table当前位置不为空。

  1. 容量直接进行拓展两倍
    1. {将旧的数据与长度保存起来}
    2. {判断旧的table容量是否已经处于为设置的阈值,若处于阈值,则将阈值变为int类型的最大值,若没有则创建新的table数组}->将旧的table数据都传输到新创建的table中。
    3. 将阈值进行新的设置,即newCapacity*loadFactor 最大容量为所设置的阈值的<<1

高并发问题

问题出现在,table已经满了,需要进行拓展,即resize。

当某一个线程完成插入操作,而另外一个线程也要进行插入至同一位置时,当A线程更新完成后,但B线程正在进行transfer,若继续运行将造成,同一位置的链表由单向链表变为双向链表。那么在进行get操作时,将会陷入死循环。

如何解决高并发问题ConcurrentHashMap

按照以往的常规操作将使用synchronized方法,但是将导致所有线程在加锁的过程中阻塞,为了解决这样的问题,推荐使用concurrentHashMap。

在concurrentHashMap中,我们将以segment作为一个基本的组成单元。

一个segement就可以当作是一个table,而table的基本单元为entry。而concurrentHashMap就是一些segment的集合,可以称作segments。

如此我们可以有清晰地了解。

concurrentHashMap:segments:segment->segment->segment

相信我们可以有了一个了解,concurrentHashMap就可以看作为一个二级的哈希表。

Segment

通过观看segment的内部类,我们可以看到segemnt类继承ReentrantLock,进行锁的设置。

在观看segment的内部参数时,我们会发现其与HashMap中的table作用是一样的,但是它将使用volatitle进行修饰:

transient volatile HashEntry<K,V>[] table;

我们具体观看HashEntry的内容:

static final HashEntry<K,V>{
    final int hssh;
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next;
}

由此可见,对于value与链表都是由volatile进行修饰。

volatile

相较于synchronized重量级锁来说,volatile属于轻量级锁,不会引起线程上下的切换与调度,但是同步性较差,使用易出错。

可见性

当多个线程同时访问同一个变量时,一个线程修改这个变量的值,其他线程能够立刻看到变量的值。

相较于多线程情况下,synchronized能够保证一个线程获取到锁后然后执行同步代码,并在释放锁之前将变量刷新到内存中。但变量被volatile修饰就将意味着线程对于本地变量的存储无效,一旦修改,将立刻刷新到内存中,其他线程想要读取共享变量就会直接从内存中读取。

有序性

程序执行的顺序按照代码的先后顺序执行。

在线程内,所有操作是有序的,但在线程中观察另外一个线程,操作是无序的。

因为JAVA为了效率是允许编译器进行重排序的,这将对于多线程造成比较大的影响,而通过volatile可以保证有序性。当然,synchronized与lock也可以使得某一时刻只有一个线程执行某一变量。

可见性原理

通过上文,我们可以明确感知到了,每一个线程都是有着对于变量存储利用的自主性。

但是因为多线程的存在,且多线程对于共享变量如何使用又产生了新的问题。

为了确保在不同线程使用共享变量的过程中,不出现数据更新过慢,导致的数据引用出错的问题。JAVA提出,每个线程都将使用属于自己的本地内存,而不再使用主内存,本地内存是对于线程中所需数据的复制拷贝。需要注意的是这里的内存与实际的物理内存没有关系。

volatitle原理

保证可见性,提供有序性,但是无法保证原子性。

  1. 确保使用volatile进行修饰的指令不会在重排序中,排到前面指令的后面,也不会排在后面指令的前面。
  2. 若为写操作,则对于共享变量的修改直接写入主存中,且对于其他线程的缓存设为无效。
public class Singleton {
      private volatile static Singleton instance;
      
      private Singleton() {
      }
      
      public static Singleton getInstance() {
          if (instance == null) {
              synchronized (Singleton.class) {
                 if (instance == null) {
                     instance = new Singleton();
                 }
             }
         }
         
         return instance;
     }
 }

单例模式的双重加锁,注意这里synchronized锁的是类,如果锁的是对象的话,多个线程调用同一对象方法会阻塞,但是不同对象则不会阻塞。在这里使用类锁是为了使得只有单一线程才可成功进入临界区。之所以使用volatile是因为,这里volatile是对instance对象进行上锁,两者之间不会相互影响,且在创建instance后,将会立刻写入主内存中,保证可见性。

ConcurrentHashMap:put方法

首先通过key获取哈希值,通过哈希值的低位来获取segment。

获取锁,发现获取失败,使用scanAndLockForPut()自旋获取锁,

  • 尝试获取锁失败;
  • 若尝试次数小于0,观看segment是否为空,若空添加新的entry;
  • 若尝试次数大于所设定的最大尝试次数,则直接加锁。

在成功获取锁后:

  • 遍历旧的table,进行key对比,若相等则将旧的value进行替换。
  • 若table为空,添加新的entry,并判断是否需要扩展。
  • 释放锁

ConcurrentHashMap:get方法

因为concurrentHashMap为二级哈希表,所以需要通过key来hash来定位segment,再通过hash来获取value。

问题解决,因为使用volatile使得共享变量都是可见的,且操作都是针对于segemnt的,因此可以解决线程的安全问题。

参考推荐:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值