Java集合类学习--HashMap(四)

23 篇文章 0 订阅

思考

HashMap是线程安全的吗?如果多个线程操作同一个HashMap对象会产生哪些非正常现象?
HashMap 是线程不安全的。具体表现在如下两个方面:

1、线程 T1 执行 put / remove 等结构性修改(structural modification)的操作;线程 T2 执行遍历操作,这种情况下会抛出ConcurrentModificationException.

示例代码(以 put 为例):

private static void test() {
  Map<Integer, Integer> map = new HashMap<>();

  Thread t1 = new Thread(() -> {
    for (int i = 0; i < 5000; i++) {
      map.put(i, i);
    }
  });

  Thread t2 = new Thread(() -> {
    for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
      System.out.println(entry);
    }
  });

  t1.start();
  t2.start();
}
// 执行结果:
// 抛出 java.util.ConcurrentModificationException

原因是:

if (modCount != expectedModCount)
    throw new ConcurrentModificationException();

HashMap 的迭代器和集合视图中,都会对该值进行比较。目的是判断是否有其他线程正在对该 HashMap 进行结构性修改,若有则抛会出异常。
PS: 仔细阅读 HashMap 源码的话可以发现,结构性修改的方法中都会有如下一行代码:

++modCount;

该值就是用来记录结构性修改的次数。

2、线程 T1 和 T2 同时执行 put / remove 等结构性修改(structural modification)的操作。以 put 方法为例分析,会发生元素覆盖。

示例代码:

private static void test() throws InterruptedException {
  Map<Integer, Integer> map = new HashMap<>();

  Thread t1 = new Thread(() -> {
    for (int i = 0; i < 5000; i++) {
      map.put(i, i);
    }
  });

  Thread t2 = new Thread(() -> {
    for (int i = 5000; i < 10000; i++) {
      map.put(i, i);
    }
  });

  t1.start();
  t2.start();

  TimeUnit.SECONDS.sleep(20);
  System.out.println(map);
  System.out.println("size: " + map.size());
}
// 输出结果:
// {8192=8192, 8193=8193, 8194=8194, 8195=8195, ...
// size: 9666
// PS: 这是某一次的结果,多次测试的具体结果可能不同,但基本都没有 size=10000 的情况。

这里问题出在 put 方法上,通过前文分析 HashMap 中 put 方法的内部实现原理可以找出原因,这里不再赘述。

这里再说一句,HashMap 的设计就是为了单线程下的高效率,了解线程不安全是为了加深对它的理解,知道在哪些情况不适合使用,若有线程安全的需求,可以考虑使用 ConcurrentHashMap。

链表和红黑树的转换阈值为什么是 8 和 6 ?
首先分析下为什么会有链表和红黑树。理想情况下,HashMap 中每个 bin 所在位置只有一个节点,这样查询效率最高,为 O(1)。而拉出一个链表、或者把链表再转为红黑树,是在散列冲突比较严重时的一种应对措施,目的是为了让 HashMap 在极端情况下仍然能够保持较高的效率。

至于为什么是 8,HashMap 的部分 Implementation notes 如下:

/* Because TreeNodes are about twice the size of regular nodes, we
 * use them only when bins contain enough nodes to warrant use
 * (see TREEIFY_THRESHOLD). And when they become too small (due to
 * removal or resizing) they are converted back to plain bins.  In
 * usages with well-distributed user hashCodes, tree bins are
 * rarely used.  Ideally, under random hashCodes, the frequency of
 * nodes in bins follows a Poisson distribution
 * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
 * parameter of about 0.5 on average for the default resizing
 * threshold of 0.75, although with a large variance because of
 * resizing granularity. Ignoring variance, the expected
 * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
 * factorial(k)). The first values are:
 *
 * 0:    0.60653066
 * 1:    0.30326533
 * 2:    0.07581633
 * 3:    0.01263606
 * 4:    0.00157952
 * 5:    0.00015795
 * 6:    0.00001316
 * 7:    0.00000094
 * 8:    0.00000006
 * more: less than 1 in ten million
 */

由于 TreeNode 的大小是普通节点(Node)的两倍,因此只有当 bin 中包含足够多(即树化的阈值 TREEIFY_THRESHOLD)的节点时才会转为 TreeNode;而当 bin 中节点减少时(删除节点或扩容),又会把红黑树再转为链表。

hashCode 均匀分布时,TreeNode 用到的机会很小。理想情况下,在随机分布的 hashCode 下,bin 中节点的分布遵循泊松分布,并列出了几个数据,可以看到一个 bin 中链表长度达到 8 的概率(0.00000006)不足千万分之一,因此将转换的阈值设为 8.

至于将红黑树转为链表的阈值为 6,网上有说法是为了避免频繁转换。比如,若红黑树转为链表的阈值也是 8,如果一个 HashMap 不停地进行插入和删除元素,链表的个数一直在 8 左右,这种情况会频繁地进行树和链表的相互转换,效率很低。
为什么负载因子是 0.75?
可以参考https://www.jianshu.com/p/64f6de3ffcc1
为什么容量是 2 的次幂?
位运算 n & (length - 1) 和取余运算 n % length 效果是一样的。但是在计算机中,位运算的效率却远高于取余运算。因此,这样做是为了提高运算效率。
一般用什么类型的元素作为 Key?为什么?

面试者通常会回答,使用String或者Integer这样的类。这个时候可以继续追问为什么使用String、Integer呢?这些类有什么特点?如果面试者有很好的思考,可以回答出这些类是Immutable的,并且这些类已经很规范的覆写了hashCode()以及equals()方法。作为不可变类天生是线程安全的,而且可以很好的优化比如可以缓存hash值,避免重复计算等等,那么基本上这道题算是过关了。

一般用 String、Integer 等,它们是不可变的(Immutable),作为不可变类天生是线程安全的。而且重写了 hashCode 方法和 equals 方法。
衡量 hash 算法的好坏?String 类的 hashCode 实现?

hash 方法的设计可以有很多。虽然散列值越均匀越好,但 HashMap 首要目的是追求快,因此 hash 算法的设计要尽可能地快。String 类的 hashCode 方法如下:

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

上面思考是由于面试会被提问,在网络上搜集整理的,当作八股文一样学习一下吧。 O(∩_∩)O哈哈~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值