JAVA中的Map

HashMap

HashMap类在之前的文章有较为详细的介绍,作为最常用的Map数据结构,了解HashMap也是了解JAVA中其他Map的基础。

HashTable

HashTable是一个遗留类,现较少使用。使用单线程使用Map结构一般使用HashMap,多线程则使用ConcurrencyHashMap。不过可以通过了解HashTable了解到为什么它被淘汰以及对在并发编程中线程安全与效率的安全性的进步。
下面从HashTable与HashMap的对比了解HashTable。

  • 线程安全
    HashMap是线程不安全的类,多线程下会造成并发冲突,但单线程下运行效率较高;HashTable是线程安全的类,但很多方法都是用synchronized修饰,因此导致并发效率低下,单线程环境效率也十分低。
  • null值
    HashMap允许有一个键为null,允许多个值为null;但HashTable不允许键或值为null。
  • 容量
    HashMap底层数组长度必须为2的幂,这样做是为了hash准备,默认为16;而HashTable底层数组长度可以为任意值,这就造成了hash算法散射不均匀,容易造成hash冲突,默认为11。
  • 哈希算法
    在对HashMap的介绍中认识到,HashMap由于有“容量为2的n次幂”的前提,哈希算法得以通过使用位与运算代替取模运算,减少运算消耗;而HashTable的哈希算法取得哈希值后,再通过对容量取模得到下标,使用取模运算无疑使性能下降。
  • 扩容
    HashMap(1.8)创建一个为原先2倍的数组,然后对原数组进行遍历以及rehash(扩容的rehash计算代价很小),用尾插法拷贝旧链表到新链表;HashTable扩容则和将创建一个原长度2倍的数组,对原数组遍历及rehash(计算代价比HashMap大)后,使用头插法拷贝旧链表,会导致拷贝后的链表反序。
    为什么HashMap1.8扩容后的rehash计算代价小?同样可参考之前的文章
  • 使用数据结构
    HashMap(1.8)是由数组+链表+红黑树形成。HashTable一直都是数组+链表。

总结:HashTable线程安全,初始容量11,扩容容量*2,使用数组+链表。效率低,原因有:频繁加锁、容量任意导致容易冲突、哈希算法取模开销大。

ConcurrentHashMap

ConcurrentHashMap最大的特点就是它是一个线程安全的Map,且效率比同为线程安全的HashTable高,在目前多线程使用Map的情况下一般会选用。

HashTable和ConcurrentHashMap

之所以ConcurrentHashMap的效率更高,是因为ConcurrentHashMap将数组分成了不同的Segment(段),在并发访问Map时,HashTable会将整个Map加锁,而ConcurrentHashMap则只会对数据所对应的段加锁。ConcurrentHashMap的段时这样定义的:
图片说明
可以看到与HashTable不同的是ConcurrentHashMap将数组分为16个段(默认值,可以初始化时显式指定,后期无法更改),每个Segment里面可以近似看成一个HashMap,每个Segment块都有自己独立的ReentrantLock锁,所以并发操作时每个Segment互不影响。因此锁的粒度降低了,提高了并发度。同时多线程读时不会阻塞,因此效率大大提高。

ConcurrentHashMap 1.7与1.8的区别

在上文对ConcurrentHashMap与HashTable的比较中我们对ConcurrentHashMap已经有一定了解并且知道为什么它能够取缔HashTable。下面继续介绍ConcurrentHashMap中1.7与1.8的区别。

  • 数据结构

1.7中使用数组+链表,1.8使用数组+链表+红黑树。这点和HashMap相似。好处是降低了查询复杂度(O(n)->O(logn))

  • 锁的粒度

1.7使用了segment粒度加锁,每个segment内是一个小的HashMap。1.8进一步降低锁的粒度,实现Node粒度的锁。简单理解即1.7的segment锁会锁数组中的多个元素,而1.8的锁直接锁的就是数组的一个元素。因此并发度进一步提高。

  • 同步的实现

1.7使用ReentrantLock+Segment方式实现同步,1.8则使用synchronized +CAS 方式实现。原因有下:

  • 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差。在粗粒度加锁中ReentrantLock可能通过Condition来定义各个低粒度的边界,使得锁粒度更细、更加灵活。而在本身就是低粒度的加锁中,Condition的优势没有了。
  • 基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然。
  • 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存。

ConcurrentHashMap源码解析(1.8)

put方法工作过程

  1. key-value的检验,不允许为空,否则抛出异常;
  2. 用while(true)无限重试put动作,直到put成功;
  3. 如果数组没有初始化过则先进行初始化工作;
  4. 如果数组中该hashcode对应位置不存在元素,则将该元素以CAS方式放入,对应的casTabAt()方法;
  5. 如果数组中存在元素,用synchronized块保证同步操作,循环判断链表中是否存在该value,存在则返回oldValue,不存在则插入链表末端,put成功跳出循环;
  6. 如果是红黑树,则将元素放入红黑树节点中;
  7. put完成后根据链表长度是否大于8且数组长度是否大于64来判断是否将链表转换成红黑树;
  8. 返回oldValue值;
// ConcurrentHashMap.put()
public V put(K var1, V var2) {
        return this.putVal(var1, var2, false);
    }

    final V putVal(K var1, V var2, boolean var3) {
        if (var1 != null && var2 != null) {
            int var4 = spread(var1.hashCode());
            int var5 = 0;
            ConcurrentHashMap.Node[] var6 = this.table;

            while(true) {
                int var8;
                while(var6 == null || (var8 = var6.length) == 0) {
                    var6 = this.initTable();
                }

                ConcurrentHashMap.Node var7;
                int var9;
                if ((var7 = tabAt(var6, var9 = var8 - 1 & var4)) == null) {
                    if (casTabAt(var6, var9, (ConcurrentHashMap.Node)null, new ConcurrentHashMap.Node(var4, var1, var2, (ConcurrentHashMap.Node)null))) {
                        break;
                    }
                } else {
                    int var10 = var7.hash;
                    if (var7.hash == -1) {
                        var6 = this.helpTransfer(var6, var7);
                    } else {
                        Object var11 = null;
                        synchronized(var7) {
                            if (tabAt(var6, var9) == var7) {
                                if (var10 < 0) {
                                    if (var7 instanceof ConcurrentHashMap.TreeBin) {
                                        var5 = 2;
                                        ConcurrentHashMap.TreeNode var18;
                                        if ((var18 = ((ConcurrentHashMap.TreeBin)var7).putTreeVal(var4, var1, var2)) != null) {
                                            var11 = var18.val;
                                            if (!var3) {
                                                var18.val = var2;
                                            }
                                        }
                                    }
                                } else {
                                    var5 = 1;
                                    ConcurrentHashMap.Node var13 = var7;

                                    while(true) {
                                        if (var13.hash == var4) {
                                            Object var14 = var13.key;
                                            if (var13.key == var1 || var14 != null && var1.equals(var14)) {
                                                var11 = var13.val;
                                                if (!var3) {
                                                    var13.val = var2;
                                                }
                                                break;
                                            }
                                        }

                                        ConcurrentHashMap.Node var15 = var13;
                                        if ((var13 = var13.next) == null) {
                                            var15.next = new ConcurrentHashMap.Node(var4, var1, var2, (ConcurrentHashMap.Node)null);
                                            break;
                                        }

                                        ++var5;
                                    }
                                }
                            }
                        }

                        if (var5 != 0) {
                            if (var5 >= 8) {
                                this.treeifyBin(var6, var9);
                            }

                            if (var11 != null) {
                                return var11;
                            }
                            break;
                        }
                    }
                }
            }

            this.addCount(1L, var5);
            return null;
        } else {
            throw new NullPointerException();
        }
    }

get()方法

  1. 判断数组是否为空,为空直接返回null;
  2. 根据hash值查找,如果hash值相等且key也相等,则直接返回value值;
  3. 如果key的hash值小于0,说明是红黑树,则进入红黑树查找到该节点值;
  4. 否则进入循环链表读取该value值并返回;
    //ConcurrentHashMap.get()
     public V get(Object var1) {
         int var8 = spread(var1.hashCode());
         ConcurrentHashMap.Node[] var2 = this.table;
         ConcurrentHashMap.Node var3;
         int var5;
         if (this.table != null && (var5 = var2.length) > 0 && (var3 = tabAt(var2, var5 - 1 & var8)) != null) {
             int var6 = var3.hash;
             Object var7;
             if (var3.hash == var8) {
                 var7 = var3.key;
                 if (var3.key == var1 || var7 != null && var1.equals(var7)) {
                     return var3.val;
                 }
             } else if (var6 < 0) {
                 ConcurrentHashMap.Node var4;
                 return (var4 = var3.find(var8, var1)) != null ? var4.val : null;
             }
             while((var3 = var3.next) != null) {
                 if (var3.hash == var8) {
                     var7 = var3.key;
                     if (var3.key == var1 || var7 != null && var1.equals(var7)) {
                         return var3.val;
                     }
                 }
             }
         }
         return null;
    }

LinkedHashMap

LinkedHashMap继承自HashMap,在HashMap基础上,通过维护一条双向链表,解决了HashMap不能随时保持遍历顺序和插入顺序一致的问题。在实现上,LinkedHashMap 很多方法直接继承自 HashMap,底层仍然是基于数组+链表+红黑树组成,仅为维护双向链表重写了部分方法。
总结:当我们希望有顺序地(按照插入的顺序)去存储key-value时,就需要使用LinkedHashMap了。

TreeMap

TreeMap也是存储键值对,但它不是以哈希的方法进行存储了。它通过维护一个红黑树的数据结构,实现有序地存储键值对。TreeMap可以对元素的排序方式,可以按照默认的排序方式,也可以自己指定排序方式。
指定自定义的排序方式方法如下:

  • 要自定义排序的类(也就是存储到TreeMap中的key的类)实现java.lang.Comparable接口,实现其compareTo()方法,从而自定义key之间的大小关系。
  • 写一个比较器类(如MyCompatator)实现java.util.Comparator接口,并实现compare()方法,然后将MyCompatator类实例对象作为TreeMap的构造方法参数进行传参。
    总结:TreeMap即有Map的特性,即根据key检索value,也能实现键值对的有序存储。具体的使用可以参考此链接

collections.synchronizedMap代理类

collections.synchronizedMap对传入的Map进行包装后,返回一个线程安全的Map。因此它是一个实现了Map接口的代理类(从JDK动态代理中我们了解了代理模式)。可以从下面源码中了解到它的实现方式是对Map接口中的方法使用synchronized包装,以保证对被代理的Map的操作是线程安全的。

private static class SynchronizedMap<K,V>
    implements Map<K,V>, Serializable {
    // use serialVersionUID from JDK 1.2.2 for interoperability
    private static final long serialVersionUID = 1978198479659022715L;

    private final Map<K,V> m;     // Backing Map
        final Object      mutex;    // Object on which to synchronize

    SynchronizedMap(Map<K,V> m) {
            if (m==null)
                throw new NullPointerException();
            this.m = m;
            mutex = this;
        }

    SynchronizedMap(Map<K,V> m, Object mutex) {
            this.m = m;
            this.mutex = mutex;
        }

    public int size() {
        synchronized(mutex) {return m.size();}
        }
    public boolean isEmpty(){
        synchronized(mutex) {return m.isEmpty();}
        }
    public boolean containsKey(Object key) {
        synchronized(mutex) {return m.containsKey(key);}
        }
    public boolean containsValue(Object value){
        synchronized(mutex) {return m.containsValue(value);}
        }
    public V get(Object key) {
        synchronized(mutex) {return m.get(key);}
        }

    public V put(K key, V value) {
        synchronized(mutex) {return m.put(key, value);}
        }
    public V remove(Object key) {
        synchronized(mutex) {return m.remove(key);}
        }
    public void putAll(Map<? extends K, ? extends V> map) {
        synchronized(mutex) {m.putAll(map);}
        }
    public void clear() {
        synchronized(mutex) {m.clear();}
    }

    private transient Set<K> keySet = null;
    private transient Set<Map.Entry<K,V>> entrySet = null;
    private transient Collection<V> values = null;

    public Set<K> keySet() {
            synchronized(mutex) {
                if (keySet==null)
                    keySet = new SynchronizedSet<K>(m.keySet(), mutex);
                return keySet;
            }
    }

    public Set<Map.Entry<K,V>> entrySet() {
            synchronized(mutex) {
                if (entrySet==null)
                    entrySet = new SynchronizedSet<Map.Entry<K,V>>(m.entrySet(), mutex);
                return entrySet;
            }
    }

    public Collection<V> values() {
            synchronized(mutex) {
                if (values==null)
                    values = new SynchronizedCollection<V>(m.values(), mutex);
                return values;
            }
        }

    public boolean equals(Object o) {
            if (this == o)
                return true;
            synchronized(mutex) {return m.equals(o);}
        }
    public int hashCode() {
            synchronized(mutex) {return m.hashCode();}
        }
    public String toString() {
        synchronized(mutex) {return m.toString();}
        }
        private void writeObject(ObjectOutputStream s) throws IOException {
        synchronized(mutex) {s.defaultWriteObject();}
        }
    }

结合上述源码,回过头思考在JDK动态代理一文中说到的代理模式,collections.synchronizedMap正是一种静态代理。从代理模式的角度来说,它与被代理对象(即传入的Map对象)同时实现了Map接口,在代理类重写Map中方法中,以put()为例,用synchronized包装被代理对象实现的put()方法,从而通过这种方式使线程不安全被代理类在使用代理后,变得线程安全。

参考

https://zhuanlan.zhihu.com/p/268025265?ivk_sa=1024320u
https://blog.csdn.net/fanrenxiang/article/details/80435459
https://blog.csdn.net/qq_39767955/article/details/81563956
https://blog.csdn.net/xingxiupaioxue/article/details/88062163
https://blog.csdn.net/u012860938/article/details/95613684
https://blog.csdn.net/YingHuaNanHai/article/details/81266690
https://www.cnblogs.com/cruze/p/3689249.html

全部评论

相关推荐

2025-12-16 22:19
已编辑
南昌市第三中学 Java
个人背景:27届本科&nbsp;江西普通一本院校个人经历:小厂->用友->蔚来->美团->腾讯不知不觉已经有了五段实习经历,也快在外面漂泊一年半了,在今年也完成了两年前自己想进大厂的目标,可能在别人看来确实就是一段比较传奇的过程,一步一步都在向上走,也会有很多人来问我相关学习实习的一些问题,我看到了也会尽量去回复,但现在我想给大家说的并不是千篇一律的学习路线,而是我认为更为重要的——勇气与抉择。下面我来分享一下这些年的心路历程最初学习背景:我跟很多人一样,都是刚进入大学才开始接触计算机,也刚刚拥有自己的电脑,在刚开始学习的过程没有任何人来帮助我,给予我相关的指导,完全是自己摸索出来的一条学习路线,不会有如今这样有很多完善好的速成路线,而家里人都在想让我考研,似乎本科以我的学历就业是不现实的。我也很早意识到了学历对于我的限制,所以萌生出了大一就开始实习的想法,但这个想法在当时基本上是不存在。所有人都在抨击我(这里感兴趣的话可以看我最早发的帖子),有的人说本科想进大厂痴人说梦,有的人劝我以我的学历考研才是上策,有的人说我屁都不懂就来卷,总之我很难说去看到有支持的。我大一的时候还没卷成如今这样很多大一实习,当我想找到是否有跟我一样下定决心一步一步往上走的人,我当时是没有找到的,要么是秋招的哀嚎,要么就直接是零实习进大厂(现在我知道,这里所谓的普通学历0实习进大厂的水分有很多,排除真正意义上的运气和实力,其他基本上全是造假作弊,大家自己心知肚明,也要放平心态)这就导致了一个没有先例的情况,很多人也都是拿没有先例来抨击我,包括家里人也不支持我去实习,可能很多人的积极性就会下降,但我从来不会信所谓的不可能,如果没有先例,那我就会是第一个,他们不行,是因为他们没能力,他们坚持不下去。勇气是很重要的,当你发现你身边没有人像你一样,就很少会有人相信你,看好你,但好在,我不在乎。最初实习阶段:在最初3000沟通只有零星几个面试的时候,那感觉确实很不好受,沉没成本太大,得到的正反馈却太少,当时基本上都是一天学八个小时从来不间断,没有周末没有节假日,甚至过年我都在学习,这就导致我现在都会因为我周末偶尔休息的时候会有负罪感,我感觉已经是种病了,我也知道我也可以休息会但控制不了。当时我出去实习口袋里有1w块(这是我高中三年加大一一年存下来的,基本上是很抠很抠,一个月生活费有时候有一千多有时候就五六百,但也算得上是成功攒了一点钱)但第一次总会是很害怕,担心租房被骗,担心工作能力不行,担心被公司坑,担心学校原因导致不能实习等等,基本上在前面几段实习是根本不攒钱的,代课已经花了一万多,加上租房来回,基本上只能说堪堪不负支出,后来远赴北京,作为一个南方人,有很多不适应的地方,但现在回过头来一想,已经在北京呆了一年多了。我知道很多人要么担心学校因素,要么担心赚的还没花的多,种种因素导致了实习的困难,我也很害怕,我的钱会不会最终全部打水漂,学校会不会爆雷,我以后还能顺利实习吗等等。但对于我来说,我能对自己狠下心,我能接受通勤时间一个半小时只为节省那么几百块的房租钱,我能控制自己的消费的欲望,我能每个月大把大把把钱给代课,这可能就是我能够初期实习顺利的原因,这需要勇气,也需要对自己狠。实习中的抉择:在有了两段实习经历后,我的目标就朝着大厂进发,在去蔚来的中途,我oc了七八家中小厂公司,这里面不乏一些待遇极其优越的公司(有一家我真的差点就去了),但我最终还是都拒了,因为我清楚的明白想往上走的,只有公司title会帮你说话,没有人有义务理解你的困难你的坚持,好在最后去了蔚来,也算如愿以偿。从蔚来到美团倒是没有过多纠结,因为在最开始的梦中情厂就是美团,但从美团去腾讯这个决定或许是我人生中的转折点。美团多次挽留我,帮我沟通问hr,基本上就是一定能转暑期然后成功转正,仿佛这年薪40w的工作已经触手可得,所以在拿到腾讯offer的那一刻并没有多高兴,因为我意识到这可能是我此生最接近大厂的一次机会,可能大部分人都会选择留在美团,我也认为这一定是一个好的选择。我能够走到如今,是永远相信自己的判断,我的每一步都是在赌一个好的未来,只不过,这次赌注大了点而已,或许未来我再也进不了这些所谓的大厂,但我赌的不是选择错对,我赌我不后悔。所谓信念支撑:都说人要为自己而活,但我或许做不到,毕竟我身处人情社会,有许多爱我的人在等着我成长,我也不能接受因为能力而再次放弃一段感情,最近喜欢一段歌词:爱我的人相信我我一直在努力改变所有失败为你们而存在爱我的人感谢你你们的爱就算人生不是精彩我也要勇敢的姿态最后的最后,我想给大家传递的从来都不是一个普通学历进入大厂的意气风发,我想给大家传递的,是一股相信自己能够向上的信念和可能性。在没有打比赛能力,没有开源能力,没有学历等各个限制下,我帮大家试出了一条能够向上的路。如果没有先例,那我会是第一个。我们不需要弄虚作假,只靠自己一步一步脚踏实地,哪怕慢一点,不赌自己是否成功,只赌自己不后悔,问心无愧。最后送给大家,也送给自己一段话结束2025:生活可能没你想的那么好,也不会像你想的那么糟,人的脆弱和坚强,都超乎了你的想象,有时候可能脆弱的一句话就泪流满面,有时候你发现自己咬咬牙已经走了很长的路了
等闲_:感觉咱们双非的同学都有一个共性,想证明双非也是能进大厂的,我之前所有的标签都喜欢带着双非,仿佛这样可以像别人证明自己的实力,现在我却不再想证明双非到底能不能进大厂,我的生活的所有者是我自己,享受生活,接受结果
2025年终总结
点赞 评论 收藏
分享
评论
2
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务