学习HashMap的实现以及对一些java内存管理的学习

    xiaoxiao2025-08-11  8

    最近听到一些同事在谈论java内存堆栈的事情,突发奇想的想看看自己平时用的java对象的底层实现和jvm如何管理他们的,原谅我现在才想起去看这些,应该前几年就看的,以下也纯粹是个人理解

    项目中最常用的数据结构是Map

    首先,Map是一个接口

    这里主要讲HashMap,ConcurrentHashMap,HashTable

    这几个主要平时用到的对象

    1.HashMap

    一个以键值对数组为存储的对象

    他首先是一个数组,数组里面的元素是键值对Map.Entry<key,value>

    存的时候put方法

    首先判断key是不是为空,为空则始终将null放在数组的第一个位置上

    后执行key的hash值,再然后根据hash值计算应该存在数组的哪个位置上,如果位置上已经有其他的对象,将原来的对象往数组后面移,新的对象放在位子上,如果位置上没有对象,则直接存储

    如果key的hash值相同,会执行keys.equals方法,如果相等,则覆盖,如果不相等,由于HashMap存储对象的时候是由LinkList来存储,所以会将对象放在LinkList的下一个节点上

    在计算应该存储在数组的哪个位置上时,HashMap对key的hashcode进行了二次hash,然后再对数组的长度进行了取模运算,而HashTable是直接将key的hashcode对数组的长度进行取模运算,没又HashMap的二次hash过程

    取得时候get

    同样的判断key是不是为空,如果为空,则直接取数组的第一个位置

    后面对key进行hash,计算存在数组的哪个位置

    如果hash值相同,则会执行keys.equals方法,取出链表中的对象

    HashMap的初始值为16,HashTable的初始长度为11,负载因子都是0.75,就是当存储长度超过总长度的0.75时,会自动执行rfush方法,扩充长度,扩充的长度为初始值

    数组在内存中存的地方是堆,由JVM的GC来进行回收,链表存储的地方是栈,当超出了作用域自动删除

    参考HashMap的存的实现,发现不可变的String和int这种基本数据类型比较适合做为HashMap的key,当然引用的对象也可以作为key,但是需要符合hashcode唯一的特性

    2.ConcurrentHashMap和HashTable

    ConcurrentHashMap和HashTable同为java中的并发容器

    ConcurrentHashMap是HashMap的并发实现,但是ConcurrentHashMap的key和value都不能为null

    ConcurrentHashMap除了继承了HashMap的父类AbstracMap之外还实现了ConcurrentMap接口

    他的putIfAbsent方法当不存在key对应的值时将value作为key存入Map中

    ConcurrentHashMap的核心就在于他默认情况下是用了16个类似HashMap 的结构,其中每一个HashMap拥有一个独占锁。也就是说最终的效果就是通过某种Hash算法,将任何一个元素均匀的映射到某个HashMap的Map.Entry上面,而对某个一个元素的操作就集中在其分布的HashMap上,与其它HashMap无关。这样就支持最多16个并发的写操作 我们把这16个类似于HashMap的操作叫做segment

    首先看put操作

    final V putVal(K key, V value, boolean onlyIfAbsent) {         if (key == null || value == null) throw new NullPointerException();         int hash = spread(key.hashCode());         int binCount = 0;         for (Node<K,V>[] tab = table;;) {             Node<K,V> f; int n, i, fh;             if (tab == null || (n = tab.length) == 0)                 tab = initTable();             else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {                 if (casTabAt(tab, i, null,                              new Node<K,V>(hash, key, value, null)))                     break;                   // no lock when adding to empty bin             }             else if ((fh = f.hash) == MOVED)                 tab = helpTransfer(tab, f);             else {                 V oldVal = null;                 synchronized (f) {                     if (tabAt(tab, i) == f) {                         if (fh >= 0) {                             binCount = 1;                             for (Node<K,V> e = f;; ++binCount) {                                 K ek;                                 if (e.hash == hash &&                                     ((ek = e.key) == key ||                                      (ek != null && key.equals(ek)))) {                                     oldVal = e.val;                                     if (!onlyIfAbsent)                                         e.val = value;                                     break;                                 }                                 Node<K,V> pred = e;                                 if ((e = e.next) == null) {                                     pred.next = new Node<K,V>(hash, key,                                                               value, null);                                     break;                                 }                             }                         }                         else if (f instanceof TreeBin) {                             Node<K,V> p;                             binCount = 2;                             if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,                                                            value)) != null) {                                 oldVal = p.val;                                 if (!onlyIfAbsent)                                     p.val = value;                             }                         }                     }                 }                 if (binCount != 0) {                     if (binCount >= TREEIFY_THRESHOLD)                         treeifyBin(tab, i);                     if (oldVal != null)                         return oldVal;                     break;                 }             }         }         addCount(1L, binCount);         return null;     }

    首先就需要加锁了,同步下是要加锁的,这个毫无疑问。但是需要注意的是是Segment集成的是ReentrantLock,所以这里加的锁也就是独占锁,也就是说同一个Segment在同一时刻只有能一个put操作。 接下来来就是检查是否需要扩容,这和HashMap一样,如果需要的话就扩大一倍,同时进行rehash操作

    其次是get方法

        public V get(Object key) {         Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;         int h = spread(key.hashCode());         if ((tab = table) != null && (n = tab.length) > 0 &&             (e = tabAt(tab, (n - 1) & h)) != null) {             if ((eh = e.hash) == h) {                 if ((ek = e.key) == key || (ek != null && key.equals(ek)))                     return e.val;             }             else if (eh < 0)                 return (p = e.find(h, key)) != null ? p.val : null;             while ((e = e.next) != null) {                 if (e.hash == h &&                     ((ek = e.key) == key || (ek != null && key.equals(ek))))                     return e.val;             }         }         return null;     }

    首先定位到segment,后面的定位其实就是和HashMap是一样的

    HashTable的put

     public synchronized V put(K key, V value) {         // Make sure the value is not null         if (value == null) {             throw new NullPointerException();         }         // Makes sure the key is not already in the hashtable.         Entry<?,?> tab[] = table;         int hash = key.hashCode();         int index = (hash & 0x7FFFFFFF) % tab.length;         @SuppressWarnings("unchecked")         Entry<K,V> entry = (Entry<K,V>)tab[index];         for(; entry != null ; entry = entry.next) {             if ((entry.hash == hash) && entry.key.equals(key)) {                 V old = entry.value;                 entry.value = value;                 return old;             }         }         addEntry(hash, key, value, index);         return null;     }

    HashTable的get

     @SuppressWarnings("unchecked")     public synchronized V get(Object key) {         Entry<?,?> tab[] = table;         int hash = key.hashCode();         int index = (hash & 0x7FFFFFFF) % tab.length;         for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {             if ((e.hash == hash) && e.key.equals(key)) {                 return (V)e.value;             }         }         return null;     }

    所以从底层的代码来说

    Hashtable的可伸缩性的主要障碍是它使用了一个 Map 范围的锁,为了保证插入、删除或者检索操作的完整性必须保持这样一个锁,而且有时候甚至还要为了保证迭代遍历操作的完整性保持这样一个锁。这样一来,只要锁被保持,就从根本上阻止了其他线程访问 Map,即使处理器有空闲也不能访问,这样大大地限制了并发性

    ConcurrentHashMap 的所并不是单一范围的Map锁,取而代之的是由 16个加锁的segment组成的集合,其中每个锁负责保护一个sengment。锁主要由变化性操作put(还有remove操作)控制。具有 16个独立的锁意味着最多可以有 16 个线程可以同时修改 map。这并不一定是说在并发地对 map 进行写操作的线程数少于 16 时,另外的写操作不会被阻塞,16 对于写线程来说是理论上的并发限制数目,但是16 依然比 1 要好得多

    转载请注明原文地址: https://ju.6miu.com/read-1301624.html
    最新回复(0)