ConcurrentHashMap 1

总结

互联网大厂比较喜欢的人才特点:对技术有热情,强硬的技术基础实力;主动,善于团队协作,善于总结思考。无论是哪家公司,都很重视高并发高可用技术,重视基础,所以千万别小看任何知识。面试是一个双向选择的过程,不要抱着畏惧的心态去面试,不利于自己的发挥。同时看中的应该不止薪资,还要看你是不是真的喜欢这家公司,是不是能真的得到锻炼。其实我写了这么多,只是我自己的总结,并不一定适用于所有人,相信经过一些面试,大家都会有这些感触。

**另外本人还整理收藏了2021年多家公司面试知识点以及各种技术点整理 **

下面有部分截图希望能对大家有所帮助。

在这里插入图片描述

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

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

unsafe.putOrderedObject(arr,(2 * indexScale) + baseOffset,100);

System.out.println(Arrays.toString(arr));//[2, 5, 100, 8, 10]

}

//反射获取Unsafe对象

public static Unsafe getUnsafe() throws Exception {

Field theUnsafe = Unsafe.class.getDeclaredField(“theUnsafe”);

theUnsafe.setAccessible(true);

return (Unsafe) theUnsafe.get(null);

}

}

3.1、图解说明

JDK 1.7

1、容器初始化

无参构造

//空参构造

public ConcurrentHashMap() {

//调用本类的带参构造

//DEFAULT_INITIAL_CAPACITY = 16

//DEFAULT_LOAD_FACTOR = 0.75f

//int DEFAULT_CONCURRENCY_LEVEL = 16

this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);

}

三个参数的构造:一些非核心逻辑的代码已经省略

//initialCapacity 定义ConcurrentHashMap存放元素的容量

//concurrencyLevel 定义ConcurrentHashMap中Segment[]的大小(为大于concurrencyLevel的最小2的次幂)

public ConcurrentHashMap(int initialCapacity,

float loadFactor, int concurrencyLevel) {

int sshift = 0;

int ssize = 1; //记录Segment[]大小

//计算Segment[]的大小,保证是2的幂次方数

while (ssize < concurrencyLevel) {

++sshift; //4

ssize <<= 1; //2^4

}

//这两个值用于后面计算Segment[]的角标

this.segmentShift = 32 - sshift // 32 -4 = 28

this.segmentMask = ssize - 1; // 16 - 1 = 15

//计算每个Segment中存储元素的个数

int c = initialCapacity / ssize; // c = 16 / 16 = 1

if (c * ssize < initialCapacity)

++c;

//最小Segment中存储元素的个数为2

int cap = MIN_SEGMENT_TABLE_CAPACITY;

//矫正每个Segment中存储元素的个数,保证是2的幂次方,最小为2

while (cap < c)

cap <<= 1;

//创建一个Segment对象,作为其他Segment对象的模板

Segment<K,V> s0 =

new Segment<K,V>(loadFactor, (int)(cap * loadFactor),

(HashEntry<K,V>[])new HashEntry[cap]);

Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];

//利用Unsafe类,将创建的Segment对象存入0角标位置

UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]

this.segments = ss;

}

综上:ConcurrentHashMap中保存了一个默认长度为16的Segment[],每个Segment元素中保存了一个默认长度为2的HashEntry[],我们添加的元素,是存入对应的Segment中的HashEntry[]中。所以ConcurrentHashMap中默认元素的长度是32个,而不是16个

2、图解

3、Segment是什么?

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

}

我们发现Segment是继承自ReentrantLock的,学过线程的兄弟都知道,它可以实现同步操作,从而保证多线程下的安全。因为每个Segment之间的锁互不影响,所以我们也将ConcurrentHashMap中的这种锁机制称之为分段锁,这比HashTable的线程安全操作高效的多。

4、HashEntry是什么?

//ConcurrentHashMap中真正存储数据的对象

static final class HashEntry<K,V> {

final int hash; //通过运算,得到的键的hash值

final K key; // 存入的键

volatile V value; //存入的值

volatile HashEntry<K,V> next; //记录下一个元素,形成单向链表

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

this.hash = hash;

this.key = key;

this.value = value;

this.next = next;

}

}

2、添加元素
1.1、ConcurrentHashMap的put方法

public V put(K key, V value) {

Segment<K,V> s;

if (value == null)

throw new NullPointerException();

//基于key,计算hash值

int hash = hash(key);

//因为一个键要计算两个数组的索引,为了避免冲突,这里取高位计算Segment[]的索引

int j = (hash >>> segmentShift) & segmentMask;

// 此处是重点:我们的hash值的高位 & segment 长度 - 1 得到在segment数组中的位置

// 我们的hash值得低位 & segment 长度 - 1

//判断该索引位的Segment对象是否创建,没有就创建

if ((s = (Segment<K,V>)UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null)

s = ensureSegment(j); // 创建segment对象

return s.put(key, hash, value, false); //调用Segmetn的put方法实现元素添加

}

1.2、ConcurrentHashMap的ensureSegment方法

//创建对应索引位的Segment对象,并返回

private Segment<K,V> ensureSegment(int k) {

final Segment<K,V>[] ss = this.segments;

long u = (k << SSHIFT) + SBASE; // raw offset

Segment<K,V> seg;

//获取,如果为null,即创建

if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { //再次确认,segment槽是否为null

//以0角标位的Segment为模板

Segment<K,V> proto = ss[0]; // 从segment[0] 这个位置拷贝信息

int cap = proto.table.length; // HashEntry数组的长度

float lf = proto.loadFactor; //加载因子

int threshold = (int)(cap * lf); //扩容阈值

HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap]; //创建hashentry

//到此一个segment对象需要的参数准备好了!

//获取,如果为null,即创建

if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))== null) { //再次确认,segment槽是否为null

Segment<K,V> s = new Segment<K,V>(lf, threshold, tab); //在此处创建Segnment对象!

//自旋方式,将创建的Segment对象放到Segment[]中,确保线程安全

while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))== null) {

if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s //cas操作!保证线程安全

break;

}

}

}

//返回

return seg;

}

1.3、Segment的put方法

final V put(K key, int hash, V value, boolean onlyIfAbsent) {

//尝试获取锁,获取成功,node为null,代码向下执行

//如果有其他线程占据锁对象,那么去做别的事情,而不是一直等待,提升效率 (自选等待的过程中,做一些什么?完成对象的创建)

HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);//scanAndLockForPut 稍后分析

V oldValue;

try {

HashEntry<K,V>[] tab = table;

//取hash的低位,计算HashEntry[]的索引

int index = (tab.length - 1) & hash; //hash的低位 & HashEntry[]数组长度 - 1 得到在HashEntry的位置

HashEntry<K,V> first = entryAt(tab, index); //获取索引位的元素对象first

for (HashEntry<K,V> e = first;😉 {

//获取的元素对象不为空

if (e != null) {

K k;

//如果是重复元素,覆盖原值

if ((k = e.key) == key ||

(e.hash == hash && key.equals(k))) {

oldValue = e.value;

if (!onlyIfAbsent) {

e.value = value;

++modCount;

}

break;

}

//如果不是重复元素,获取链表的下一个元素,继续循环遍历链表

e = e.next;

}

else { //如果获取到的元素为空

//当前添加的键值对的HashEntry对象已经创建

if (node != null)

node.setNext(first); //头插法关联即可

else

//创建当前添加的键值对的HashEntry对象

node = new HashEntry<K,V>(hash, key, value, first);

//添加的元素数量递增

int c = count + 1;

//判断是否需要扩容

if (c > threshold && tab.length < MAXIMUM_CAPACITY)

//需要扩容

rehash(node);

else

//不需要扩容

//将当前添加的元素对象,存入数组角标位,完成头插法添加元素

setEntryAt(tab, index, node);

++modCount;

count = c;

oldValue = null;

break;

}

}

} finally {

//释放锁

unlock();

}

return oldValue;

}

1.4、Segment的scanAndLockForPut方法

该方法在线程没有获取到锁的情况下,去完成HashEntry对象的创建,提升效率

但是这个操作个人感觉有点累赘了。

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

//获取头部元素

HashEntry<K,V> first = entryForHash(this, hash);

HashEntry<K,V> e = first;

HashEntry<K,V> node = null;

int retries = -1; // negative while locating node

while (!tryLock()) {

//获取锁失败

HashEntry<K,V> f; // to recheck first below

if (retries < 0) {

//没有下一个节点,并且也不是重复元素,创建HashEntry对象,不再遍历

if (e == null) {

if (node == null) // speculatively create node

node = new HashEntry<K,V>(hash, key, value, null);

retries = 0;

}

else if (key.equals(e.key))

//重复元素,不创建HashEntry对象,不再遍历

retries = 0;

else

//继续遍历下一个节点

e = e.next;

}

else if (++retries > MAX_SCAN_RETRIES) {

//如果尝试获取锁的次数过多,直接阻塞

//MAX_SCAN_RETRIES会根据可用cpu核数来确定

lock();

break;

}

else if ((retries & 1) == 0 &&

(f = entryForHash(this, hash)) != first) {

//如果期间有别的线程获取锁,重新遍历

e = first = f; // re-traverse if entry changed

retries = -1;

}

}

return node;

}

图解 :

到此向ConcurrentHashMap当中插入一个键值对的操作就完成了!

3、扩容机制

private void rehash(HashEntry<K,V> node) {

HashEntry<K,V>[] oldTable = table;

int oldCapacity = oldTable.length;

//两倍容量

int newCapacity = oldCapacity << 1;

threshold = (int)(newCapacity * loadFactor);

//基于新容量,创建HashEntry数组

HashEntry<K,V>[] newTable =

(HashEntry<K,V>[]) new HashEntry[newCapacity];

int sizeMask = newCapacity - 1;

//实现数据迁移

for (int i = 0; i < oldCapacity ; i++) {

HashEntry<K,V> e = oldTable[i];

if (e != null) {

HashEntry<K,V> next = e.next;

int idx = e.hash & sizeMask;

if (next == null) // Single node on list

//原位置只有一个元素,直接放到新数组即可

newTable[idx] = e;

else { // Reuse consecutive sequence at same slot

//=图一=============

HashEntry<K,V> lastRun = e;

int lastIdx = idx;

for (HashEntry<K,V> last = next;

last != null;

last = last.next) {

int k = last.hash & sizeMask;

if (k != lastIdx) {

lastIdx = k;

lastRun = last;

}

}

//=图一=============

//=图二=============

newTable[lastIdx] = lastRun;

//=图二=============

// Clone remaining nodes

总结:绘上一张Kakfa架构思维大纲脑图(xmind)

image

其实关于Kafka,能问的问题实在是太多了,扒了几天,最终筛选出44问:基础篇17问、进阶篇15问、高级篇12问,个个直戳痛点,不知道如果你不着急看答案,又能答出几个呢?

若是对Kafka的知识还回忆不起来,不妨先看我手绘的知识总结脑图(xmind不能上传,文章里用的是图片版)进行整体架构的梳理

梳理了知识,刷完了面试,如若你还想进一步的深入学习解读kafka以及源码,那么接下来的这份《手写“kafka”》将会是个不错的选择。

  • Kafka入门

  • 为什么选择Kafka

  • Kafka的安装、管理和配置

  • Kafka的集群

  • 第一个Kafka程序

  • Kafka的生产者

  • Kafka的消费者

  • 深入理解Kafka

  • 可靠的数据传递

  • Spring和Kafka的整合

  • SpringBoot和Kafka的整合

  • Kafka实战之削峰填谷

  • 数据管道和流式处理(了解即可)

image

image

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

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

其实关于Kafka,能问的问题实在是太多了,扒了几天,最终筛选出44问:基础篇17问、进阶篇15问、高级篇12问,个个直戳痛点,不知道如果你不着急看答案,又能答出几个呢?

若是对Kafka的知识还回忆不起来,不妨先看我手绘的知识总结脑图(xmind不能上传,文章里用的是图片版)进行整体架构的梳理

梳理了知识,刷完了面试,如若你还想进一步的深入学习解读kafka以及源码,那么接下来的这份《手写“kafka”》将会是个不错的选择。

  • Kafka入门

  • 为什么选择Kafka

  • Kafka的安装、管理和配置

  • Kafka的集群

  • 第一个Kafka程序

  • Kafka的生产者

  • Kafka的消费者

  • 深入理解Kafka

  • 可靠的数据传递

  • Spring和Kafka的整合

  • SpringBoot和Kafka的整合

  • Kafka实战之削峰填谷

  • 数据管道和流式处理(了解即可)

[外链图片转存中…(img-3StLxvPW-1715823066242)]

[外链图片转存中…(img-tuk6r0vg-1715823066243)]

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值