前言
并发集合类相关问题的出现频次在大厂的面试中是比较高的,例如安全队列的实现、CopyOnWriteArrayList的原理、ConcurrentHashMap的实现机制等等,相信这篇文章能够帮助你回答好这些问题。
1、多线程中的安全队列一般通过什么实现?
Java提供的线程安全Queue可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQueue,非阻塞队列的典型例子是ConcurrentLinkedQueue。
对于BlockingQueue,想要实现阻塞功能,需要调用put(e) 与take() 方法。而ConcurrentLinkedQueue是基于链接节点、无界、线程安全的非阻塞队列。
阻塞队列
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是从队列里取元素的线程,阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。它的两个核心方法如下所示:
-
1)、支持阻塞的插入方法put(e) :意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。
-
2)、支持阻塞的移除方法take():意思是在队列为空时,获取元素的线程会等待队列变为非空。
2、CopyOnWriteArrayList的了解?
Copy-On-Write 是什么?具体原理?
CopyOnWriteArrayList是一个ArrayList的线程安全的变体;在计算机中就是当你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存的指针指向新的内存,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。
优点
-
1)、数据一致性,为什么?因为加锁了,并发数据不会乱。
-
2)、解决了像ArrayList、Vector这种集合多线程遍历的迭代问题,记住,Vector虽然线程安全,只不过是加了synchronized关键字,迭代问题完全没有解决!
缺点
-
1)、内存占用问题很明显:两个数组同时驻扎在内存中,如果实际应用中,数据比较多、比较大的情况下,占用内存会比较大,针对这个其实可以用ConcurrentHashMap来代替。
-
2)、数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
使用场景
-
1、读多写少(白名单,黑名单,商品类目的访问和更新场景),为什么?因为写的时候会复制新集合。
-
2、集合不大,为什么?因为写的时候会复制新集合。
-
3、实时性要求不高,为什么,因为有可能会读取到旧的集合数据。
3、ConcurrentHashMap 如何实现并发访问?
HashTable 的问题
HashTable我看过他的源码,很简单粗暴,直接在方法上锁,并发度很低,最多同时允许一个线程访问,ConcurrentHashMap就好很多了,1.7和1.8有较大的不同,不过并发度都比前者好太多了。
它有3中类型的锁:
大锁
对 HashTable 对象加锁。
长锁
直接对方法加锁。
读写锁共用
只有一把锁,从头锁到尾。
CHM 的并发优化历程
JDK 5:分段锁,必要时加锁
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器中的其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据的时候,其他段的数据也能被其他线程所访问。
ConcurrentHashMap 实质是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁时的操作锁住的是一个 Segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
其中,每一个 segment 对应一个 table[],在hash(key)中取高位定位 segment 的位置,取低位定位 table 中的位置。
concurrencyLevel的作用?
并行级别、并发数、Segment 数,默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segment,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。
这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。其中的每个 Segment 很像 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。
存在的问题
如果 key 是整数,那么 hash 值的高位对于3万多以下的整数得到的结果都是一样的,都会堆到一个 segment 里面,退化成一个 HashTable,达不到分段优化的目的了。
而对于超过3万多的数值,到大概4,50万时仍然集中到 15,随着数值的慢慢增加,它才会均匀地分布在这个段里。
JDK 6:优化二次 Hash 算法
对 Hash 算法做了优化,具体是使用了 single-word Wang/Jenkins hash 算法,它能够保证高位低位的均匀分布,关键特性:
-
1、雪崩性:更改输入参数的任何一位,就将引起输出有一半以上的位发生变化。
-
2、可逆性:input ==> hash ==> inverse_hash ==> input。
JDK 7:段懒加载,volatile & cas
段没有一开始就实例化,而是需要的时候再去初始化,并且访问的时候会尽可能地使用 volatile & cas 来避免加锁。
在实例化的过程中,有一个问题,由于 segment 本身也涉及到一个可见性的点,当线程 A 把它给初始化了,线程 B 同时也在访问它的时候可能会由于可见性的问题访问不到刚刚被初始化的 segment,也就是因为这一点,在 JDK 7 里面大量使用了对数组的 volatile 访问,这个访问基于 unsafe 这个类。(对应方法为 getObjectVolatile())。
put 方法做了什么?
-
1、首先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的 put。
-
2、虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。
-
3、首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 尝试自旋获取锁。如果重试的次数达到了 MAX_SCAN_RETRIES 就改为阻塞锁获取,保证能获取成功。
-
4、然后将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
-
5、遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value,为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
-
6、最后会调用unlock()解除当前 Segment 的锁。
get 方法做了什么?
-
1、只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。
-
2、由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
-
3、ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。
JDK 8:摒弃段,基于 HashMap 原理的并发实现
1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题:那就是查询遍历链表效率太低。和 1.8 HashMap 结构类似:其中抛弃了原有的 Segment 分段锁,而采用了 synchronized & cas 来保证并发安全性。详细点说就是:
-
1、对于一些不必加锁的地方都使用 volatile 来访问,对于需要加锁的地方尽可能选择最小的范围加锁。加锁仅针对于 table[] 中的 slot 区对应的 entry 元素加锁,而把我们新来的元素放到它后面的链表里面。
-
2、它比起Segment,锁拆分得更细:首先使用无锁操作CAS插入头结点,失败则循环重试,若头结点已存在,则尝试获取头结点的同步锁再进行操作。
而1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率为O(logn),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。
put 方法做了什么?
-
1、判断Node[]数组是否初始化,没有则进行初始化操作。
-
2、通过hash定位到数组的索引坐标,如果没有Node节点则使用CAS进行添加到链表的头结点,添加失败则进入下次循环。
-
3、如果检查到内部正在扩容,就帮助它一块扩容。
- 4、如果链表/红黑二叉树的头元素不为null,则使用synchronized锁住它:
-
1)、如果是Node链表结构则执行链表的添加操作。
-
2)、如果是TreeNode树形结构则执行树的添加操作。
-
-
5、最后会判断链表长度是否已经超过临界值8,超过则将链表转换为红黑树结构。
get 方法做了什么?
-
1、根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
-
2、如果是红黑树那就按照树的方式获取值,否则按照链表的方式遍历获取值。
小结
-
小锁:分段锁(5~7),桶节点锁(8)。
-
短锁:先尝试cas获取,失败再加锁。
-
分离读写锁:读失败再加锁(5~7),volatile 读 CAS 写(7~8)。
CHM 如何计数?
-
JDK 5~7:基于段元素个数求和,二次不同就加锁。
-
JDK 8:引入 CounterCell,本质上也是分段计数。
CHM 的弱一致性是什么?
-
1)、添加元素后不一定马上能读到。
-
2)、清空后可能仍然会有元素。
-
3)、遍历之前的段元素的变化会读到。
-
4)、遍历之后的段元素变化读不到。
-
5)、遍历时元素发生变化不抛异常。
如何进行锁优化?
-
1)、长锁不如短锁:尽可能只锁必要的部分。
-
2)、大锁不如小锁:尽可能对加锁的对象拆分。
-
3)、公锁不如私锁:尽可能将锁的逻辑放到私有代码中。
-
4)、嵌套锁不如扁平锁:尽可能在代码设计时避免嵌套锁。
-
5)、分离读写锁:尽可能将读锁和写锁分离。
-
6)、粗化高频锁:尽可能合并处理频繁过短的锁。
-
7)、消除无用锁:尽可能不加锁,或用 volatile 替代锁。
Contact Me
现如今,Android 行业人才已逐渐饱和化,但高级人才依旧很稀缺,我们经常遇到的情况是,100份简历里只有2、3个比较合适的候选人,大部分的人都是疲于业务,没有花时间来好好学习,或是完全不知道学什么来提高自己的技术。对于 Android 开发者来说,尽早建立起一个完整的 Android 知识框架,了解目前大厂高频出现的常考知识点,掌握面试技巧,是一件非常需要重视的事情。
去年,为了进入一线大厂去做更有挑战的事情,拿到更高的薪资,我提前准备了半年的时间,沉淀了一份 「两年磨一剑」 的体系化精品面试题,而后的半年,我都在不断地进行面试,总共面试了二三十家公司,每一场面试完之后,我都将对应的面试题和详细的答案进行了系统化的总结,并更新到了我的面试项目里,现在,在每一个模块之下,我都已经精心整理出了 超高频和高频的常考 知识点。
在我近一年的大厂实战面试复盘中逐渐对原本的内容进行了大幅度的优化,并且新增了很多新的内容。它可以说是一线互联网大厂的面试精华总结,同时后续还会包含如何写简历和面试技巧的内容,能够帮你省时省力地准备面试,大大降低找到一个好工作的难度。
这份面试项目不同于我 Github 上的 Awesome-Android-Interview 面试项目:https://github.com/JsonChao/Awesome-Android-Interview,Awesome-Android-Interview 已经在 2 年前(2020年 10 月停止更新),内容稍显陈旧,里面也有不少点表述不严谨,总体含金量较低。而我今天要分享的这份面试题库,是我在这两年持续总结、细化、沉淀出来的体系化精品面试题,里面很多的核心题答案在面试的压力下,经过了反复的校正与升华,含金量极高。
在分享之前,有一点要注意的是,一定不要将资料泄露出去!细想一下就明白了:
1、如果暴露出去,拿到手的人比你更快掌握,更早进入大厂,拿到高薪,你进大厂的机会就会变小,毕竟现在好公司就那么多,一个萝卜一个坑。
2、两年前我公开分享的简陋版 Awesome-Android-Interview 面试题库现在还在被各个培训机构当做引流资料,加大了现在 Android 内卷。。
所以,这一点一定要切记。
现在,我已经在我的成长社群里修订好了 《体系化高频核心 Android 面试题库》 中的 ”计算机基础高频核心面试题“ 和 ”Java 和 kotlin 高频核心面试题“ 部分,后续还会为你带来我核心题库中的:
-
“Android基础 高频核心面试题”
-
“基础架构 高频核心面试题”
-
“跨平台 高频核心面试题”
-
“性能优化 高频核心面试题”
-
”Framework 高频核心面试题“
-
”NDK 高频核心面试题“
获取方法:扫描下方的二维码。
出身普通的人,如何真正改变命运?
这是我过去五、六年一直研究的命题。首先,是为自己研究,因为我是从小城镇出来的,通过持续不断地逆袭立足深圳。越是出身普通的人,就越需要有耐心,去进行系统性地全面提升,这方面,我有非常丰富的实践经验和方法论。因此,我开启了 “JsonChao” 的成长社群,希望和你一起完成系统性地蜕变。
星球目前有哪些服务?
-
每周会提供一份让 个人增值,避免踩坑 的硬干货。
-
每日以文字或语音的形式分享我个人学习和实践中的 思考精华或复盘记录。
-
提供 每月 三 次成长、技术或面试指导的咨询服务。
-
更多服务正在研发中...
超哥的知识星球适合谁?
-
如果你希望持续提升自己,获得更高的薪资或是想加入大厂,那么超哥的知识星球会对你有很大的帮助。
-
如果你既努力,又焦虑,特别适合加入超哥的知识星球,因为我经历过同样的阶段,而且最后找到了走出焦虑,靠近梦想的地方。
-
如果你希望改变自己的生活状态,欢迎加入超哥的知识星球,和我一起每日迭代,持续精进。
星球如何定价?
365元每年
每天一元,给自己的成长持续加油💪
为了回馈 JsonChao 的 CSDN 忠实用户,我申请了少量优惠券,先到者先得,错过再无