阿里面试官经常问的 HashMap 和 ConcurrentHashMap,相信看完这篇没有人能再难住你。

Java核心架构进阶知识点

面试成功其实都是必然发生的事情,因为在此之前我做足了充分的准备工作,不单单是纯粹的刷题,更多的还会去刷一些Java核心架构进阶知识点,比如:JVM、高并发、多线程、缓存、Spring相关、分布式、微服务、RPC、网络、设计模式、MQ、Redis、MySQL、设计模式、负载均衡、算法、数据结构、kafka、ZK、集群等。而这些也全被整理浓缩到了一份pdf——《Java核心架构进阶知识点整理》,全部都是精华中的精华,本着共赢的心态,好东西自然也是要分享的

image

image

image

内容颇多,篇幅却有限,这就不在过多的介绍了,大家可根据以上截图自行脑补

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

通过源码深度剖析 ConcurrentHashMap,透彻地比较了 Java 7 和 Java 8 之间的不同。

老读者就请肆无忌惮地点赞吧,微信搜索【沉默王二】关注这个在九朝古都洛阳苟且偷生的程序员。

本文 GitHub github.com/itwanger 已收录,里面还有我精心为你准备的一线大厂面试题。

HashMap 是 Java 中非常强大的数据结构,使用频率非常高,几乎所有的应用程序都会用到它。但 HashMap 不是线程安全的,不能在多线程环境下使用,该怎么办呢?

1)Hashtable,一个老掉牙的同步哈希表,t 竟然还是小写的,一看就非常不专业:

public class Hashtable<K,V>

extends Dictionary<K,V>

implements Map<K,V>, Cloneable, java.io.Serializable {

public synchronized V put(K key, V value) {}

public synchronized int size() {}

public synchronized V get(Object key) {}

}

里面的方法全部是 synchronized,同步的力度非常大,对不对?这样的话,性能就没法保证了。pass。

2)Collections.synchronizedMap(new HashMap<String, String>()),可以把一个 HashMap 包装成同步的 SynchronizedMap:

private static class SynchronizedMap<K,V>

implements Map<K,V>, Serializable {

public int size() {

synchronized (mutex) {return m.size();}

}

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);}

}

}

可以看得出,SynchronizedMap 确实比 Hashtable 改进了,synchronized 不再放在方法上,而是放在方法内部,作为同步块出现,但仍然是对象级别的同步锁,读和写操作都需要获取锁,本质上,仍然只允许一个线程访问,其他线程被排斥在外。

3)ConcurrentHashMap,本篇的主角,唯一正确的答案。Concurrent 这个单词就是并发、并行的意思,所以 ConcurrentHashMap 就是一个可以在多线程环境下使用的 HashMap。

ConcurrentHashMap 一直在进化,Java 7 和 Java 8 就有很大的不同。Java 7 版本的 ConcurrentHashMap 是基于分段锁的,就是将内部分成不同的 Segment(段),每个段里面是 HashEntry 数组。

来看一下 Segment:

static final class Segment<K,V> extends ReentrantLock implements Serializable {

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

transient int count;

transient int modCount;

transient int threshold;

final float loadFactor;

}

再来看一下 HashEntry:

static final class HashEntry<K,V> {

final K key; // 声明 key 为 final 型

final int hash; // 声明 hash 值为 final 型

volatile V value; // 声明 value 为 volatile 型

final HashEntry<K,V> next; // 声明 next 为 final 型

HashEntry(K key, int hash, HashEntry<K,V> next, V value) {

this.key = key;

this.hash = hash;

this.next = next;

this.value = value;

}

}

和 HashMap 非常相似,唯一的区别就是 value 是 volatile 的,保证 get 时候的可见性。

Segment 继承自 ReentrantLock,所以不会像 Hashtable 那样不管是 put 还是 get 都需要 synchronized,锁的力度变小了,每个线程只锁一个 Segment,对其他线程访问的 Segment 没有影响。

Java 8 和之后的版本在此基础上做了很大的改进,不再采用分段锁的机制了,而是利用 CAS(Compare and Swap,即比较并替换,实现并发算法时常用到的一种技术)和 synchronized 来保证并发,虽然内部仍然定义了 Segment,但仅仅是为了保证序列化时的兼容性,代码注释上就可以看得出来:

/**

  • Stripped-down version of helper class used in previous version,

  • declared for the sake of serialization compatibility.

*/

static class Segment<K,V> extends ReentrantLock implements Serializable {

final float loadFactor;

Segment(float lf) { this.loadFactor = lf; }

}

底层结构和 Java 7 也有所不同,更接近 HashMap(数组+双向链表+红黑树):

来看一下新版 ConcurrentHashMap 定义的关键字段:

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>

implements ConcurrentMap<K,V>, Serializable {

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

private transient volatile Node<K,V>[] nextTable;

private transient volatile int sizeCtl;

}

1)table,默认为 null,第一次 put 的时候初始化,默认大小为 16,用来存储 Node 节点,扩容时大小总是 2 的幂次方。

顺带看一下 Node 的定义:

static class Node<K,V> implements Map.Entry<K,V> {

final int hash;

final K key;

volatile V val;

volatile Node<K,V> next;

// …

}

hash 和 key 是 final 的,和 HashMap 的 Node 一样,因为 key 是不会发生变化的。val 和 next 是 volatile 的,保证多线程环境下的可见性。

2)nextTable,默认为 null,扩容时新生成的数组,大小为原数组的两倍。

3)sizeCtl,默认为 0,用来控制 table 的初始化和扩容操作。-1 表示 table 正在初始化;-(1+线程数) 表示正在被多个线程扩容。

Map 最重要的方法就是 put,ConcurrentHashMap 也不例外:

public V put(K key, V value) {

return putVal(key, value, false);

}

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);

…省略部分代码

}

addCount(1L, binCount);

return null;

}

1)spread() 是一个哈希算法,和 HashMap 的 hash() 方法类似:

static final int spread(int h) {

return (h ^ (h >>> 16)) & HASH_BITS;

}

2)如果是第一次 put 的话,会调用 initTable() 对 table 进行初始化。

private final ConcurrentHashMap.Node<K,V>[] initTable() {

ConcurrentHashMap.Node<K,V>[] tab; int sc;

while ((tab = table) == null || tab.length == 0) {

if ((sc = sizeCtl) < 0)

Thread.yield(); // lost initialization race; just spin

else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {

try {

if ((tab = table) == null || tab.length == 0) {

int n = (sc > 0) ? sc : DEFAULT_CAPACITY;

@SuppressWarnings(“unchecked”)

ConcurrentHashMap.Node<K,V>[] nt = (ConcurrentHashMap.Node<K,V>[])new ConcurrentHashMap.Node<?,?>[n];

table = tab = nt;

sc = n - (n >>> 2);

}

} finally {

sizeCtl = sc;

}

break;

}

}

return tab;

}

外层用了一个 while 循环,如果发现 sizeCtl 小于 0 的话,就意味着其他线程正在初始化,yield 让出 CPU。

第一次 put 的时候会执行 U.compareAndSetInt(this, SIZECTL, sc, -1),把 sizeCtl 赋值为 -1,表示当前线程正在初始化。

private static final Unsafe U = Unsafe.getUnsafe();

private static final long SIZECTL

= U.objectFieldOffset(ConcurrentHashMap.class, “sizeCtl”);

U 是一个 Unsafe(可以提供硬件级别的原子操作,可以获取某个属性在内存中的位置,也可以修改对象的字段值)对象,compareAndSetInt() 是 Unsafe 的一个本地(native)方法,它就负责把 ConcurrentHashMap 的 sizeCtl 修改为指定的值(-1)。

初始化后的 table 大小为 16(DEFAULT_CAPACITY)。

不是第一次 put 的话,会调用 tabAt() 取出 key 位置((n - 1) & hash)上的值(f):

static final <K,V> ConcurrentHashMap.Node<K,V> tabAt(ConcurrentHashMap.Node<K,V>[] tab, int i) {

总结

总的来说,面试是有套路的,一面基础,二面架构,三面个人。

最后,小编这里收集整理了一些资料,其中包括面试题(含答案)、书籍、视频等。希望也能帮助想进大厂的朋友

三面蚂蚁金服成功拿到offer后,他说他累了

三面蚂蚁金服成功拿到offer后,他说他累了

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

e<K,V> tabAt(ConcurrentHashMap.Node<K,V>[] tab, int i) {

总结

总的来说,面试是有套路的,一面基础,二面架构,三面个人。

最后,小编这里收集整理了一些资料,其中包括面试题(含答案)、书籍、视频等。希望也能帮助想进大厂的朋友

[外链图片转存中…(img-HFDeDaYF-1715716167260)]

[外链图片转存中…(img-VDCk9KZH-1715716167261)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值