- Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。
- 线程安全性不同,Hashtable 线程安全
HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。
Hashtable中,key和value都不允许出现null值,HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。
Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。
哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。
- 标记----清除算法:分为标记和清除两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。这种算法有两个不足的地方,一个是效率问题,标记和清除两个过程效率都不高,另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片过多可能会导致以后程序运行过程中需要分配较大对象时,无法找到足够连续的内存而不得不提前触发另一次垃圾收集动作。
- 复制算法:为了解决效率问题,出现了复制算法,这种算法把内存分为大小相等的两块,每次只是用其中的一块。当这一块内存用完了,就将还存活的对象复制到另一块上面,然后把已经使用过的那一块内存整个清理掉,这样使得每次清理的只是半个内存区域,内存分配时就不用考虑内存碎片等复杂的情况,只要移动堆顶的指针,按顺序分配内存即可,实现简单,运行高效,但是缺点是,把内存缩小为原来的一半,代价有点高。目前商业虚拟机都采用这种收集算法来回收新生代,不过内存并不是按1:1这样的比例划分的,而是将内存分为较大的Eden空间和两块较小的Survivor空间,每次只使用Eden和其中的一块Survivor区,当回收时,将Eden和survivor区中还存活的对象复制到另一个Survivor区中,最后清理掉Eden和Survivor空间。
- 标记----整理算法:和标记清除算法类似,只不过后续步骤不是直接对可回收对象进行清理,而是让存活的对象都向一端移动,然后直接清理到边界以外的内存空间。
- 分代收集算法:当前商业虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存划分为几块,一般把堆划分为新生代和老年代,这样就可以各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现大批对象死去,只有少量存活,就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集,而老年代中对象存活率高,就必须使用标记----清理或者标记----整理算法进行回收。
1、数据丢失:
acks=1的时候(只保证写入leader成功),如果刚好leader挂了。数据会丢失。
acks=0的时候,使用异步模式的时候,该模式下kafka无法保证消息,有可能会丢。
2、brocker如何保证不丢失:
acks=all : 所有副本都写入成功并确认。
retries = 一个合理值。
min.insync.replicas=2 消息至少要被写入到这么多副本才算成功。
unclean.leader.election.enable=false 关闭unclean leader选举,即不允许非ISR中的副本被选举为leader,以避免数据丢失。
3、Consumer如何保证不丢失
如果在消息处理完成前就提交了offset,那么就有可能造成数据的丢失。
enable.auto.commit=false 关闭自动提交offset
处理完数据之后手动提交。
Zookeeper通过Paxos选举算法实现数据强一致性
1.使用mapPartitions代替大部分map操作,或者连续使用的map操作
2.broadcast join和普通join:将小的RDD进行collect操作然后设置为broadcast变量
3.先filter在join
4.使用reduceBykey替换groupBykey
5.内存不足时可以降低cache级别使用rdd.persist(StorageLevel.MEMORY_AND_DISK_SER)代替rdd.cache()
6.spark使用hbase的时候,spark和hbase搭建在同一个集群
7.spark.driver.memory (default 1g) 这个参数用来设置Driver的内存
8.spark.rdd.compress (default false)这个参数在内存吃紧的时候,又需要persist数据有良好的性能,就可以设置这个参数为true,这样在使用persist(StorageLevel.MEMORY_ONLY_SER)的时候,就能够压缩内存中的rdd数据。减少内存消耗,就是在使用的时候会占用CPU的解压时间。
9.spark.serializer (default org.apache.spark.serializer.JavaSerializer ) KryoSerializer比JavaSerializer快
10.spark.memory.storageFraction (default 0.5) 这个参数设置内存表示 Executor内存中 storage/(storage+execution),虽然spark-1.6.0+的版本内存storage和execution的内存已经是可以互相借用的了,但是借用和赎回也是需要消耗性能的,所以如果明知道程序中storage是多是少就可以调节一下这个参数。
- 假设我们对数组{7, 1, 3, 5, 13, 9, 3, 6, 11}进行快速排序。
- 首先在这个序列中找一个数作为基准数,为了方便可以取第一个数。
- 遍历数组,将小于基准数的放置于基准数左边,大于基准数的放置于基准数右边。
- 此时得到类似于这种排序的数组{3, 1, 3, 5, 6, 7, 9, 13, 11}。
- 在初始状态下7是第一个位置,现在需要把7挪到中间的某个位置k,也即k位置是两边数的分界点。
- 那如何做到把小于和大于基准数7的值分别放置于两边呢,我们采用双指针法,从数组的两端分别进行比对。
- 先从最右位置往左开始找直到找到一个小于基准数的值,记录下该值的位置(记作 i)。
- 再从最左位置往右找直到找到一个大于基准数的值,记录下该值的位置(记作 j)。
- 如果位置i
(j-1)的位置往前和(i+1)的位置往后重复上面比对基准数然后交换的步骤。 - 如果执行到i==j,表示本次比对已经结束,将最后i的位置的值与基准数做交换,此时基准数就找到了临界点的位置k,位置k两边的数组都比当前位置k上的基准值或都更小或都更大。
- 上一次的基准值7已经把数组分为了两半,基准值7算是已归位(找到排序后的位置)。
- 通过相同的排序思想,分别对7两边的数组进行快速排序,左边对[left, k-1]子数组排序,右边则是[k+1, right]子数组排序。
- 利用递归算法,对分治后的子数组进行排序。
public class Test {
public static void main(String[] args) {
int[] arr = {7, 2, 1, 3, 8, 6, 4, 5};
quickSort(arr, 0, arr.length - 1);
for (int anArr : arr) {
System.out.println(anArr);
}
}
private static void quickSort(int[] arr, int low, int high) {
int i, j, temp, t;
if (low > high) {
return;
}
i = low;
j = high;
//temp就是基准位
temp = arr[low];
while (i < j) {
//先看右边,依次往左递减
while (temp <= arr[j] && i < j) {
j--;
}
//再看左边,依次往右递增
while (temp >= arr[i] && i < j) {
i++;
}
//如果满足条件则交换
if (i < j) {
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
}
}
//最后将基准为与i和j相等位置的数字交换
arr[low] = arr[i];
arr[i] = temp;
//递归调用左半数组
quickSort(arr, low, j - 1);
//递归调用右半数组
quickSort(arr, j + 1, high);
}
}
- 诊断内存的消耗
- 高性能序列化类库
- 优化数据结构
- 对多次使用的RDD进行持久化或Checkpoint
- 使用序列化的持久化级别
- GC调优
- 提高并行度
- 广播共享数据
- 数据本地化
- reduceByKey和groupByKey的选择
- shuffle性能优化
Flink使用netty,spark使用Akka
静态代理:
1.可以做到在不修改目标对象的功能前提下,对目标功能扩展.
2.缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多.同时,一旦接口增加方法,目标对象与代理对象都要维护.动态代理:
1.代理对象,不需要实现接口
2.代理对象的生成,是利用JDK的API,动态的在内存中构建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)
3.动态代理也叫做:JDK代理,接口代理Cglib代理
Cglib代理,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展.
JDK的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口,如果想代理没有实现接口的类,就可以使用Cglib实现.
Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展java类与实现java接口.它广泛的被许多AOP的框架使用,例如Spring AOP和synaop,为他们提供方法的interception(拦截)
Cglib包的底层是通过使用一个小而块的字节码处理框架ASM来转换字节码并生成新的类.不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉.
12、Java的内存溢出和内存泄漏
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
hadoop组件:common(工具类)、HDFS、MapReduce、YARN
Yarn中有三种调度器可以选择:FIFO Scheduler (队列调度器),Capacity Scheduler(容器调度器),FairS cheduler(公平调度器)
Local 模式是最简单的一种Spark运行方式,它采用单节点多线程(cpu)方式运行
Spark可以通过部署与Yarn的架构类似的框架来提供自己的集群模式,该集群模式的架构设计与HDFS和Yarn大相径庭,都是由一个主节点多个从节点组成,在Spark 的Standalone模式中,主,即为master;从,即为worker
Spark on Yarn 模式就是将Spark应用程序跑在Yarn集群之上,通过Yarn资源调度将executor启动在container中,从而完成driver端分发给executor的各个任务。将Spark作业跑在Yarn上,首先需要启动Yarn集群,然后通过spark-shell或spark-submit的方式将作业提交到Yarn上运行
Yarn的两种模式:一种为 client;一种为 cluster,可以通过- -deploy-mode 进行指定,也可以直接在 - -master 后面使用 yarn-client和yarn-cluster进行指定
俩种模式的区别:在于driver端启动在本地(client),还是在Yarn集群内部的AM中(cluster)
- 返回值类型不同:reduceByKey返回的是RDD[(K, V)],而groupByKey返回的是RDD[(K, Iterable[V])],举例来说这两者的区别。比如含有一下数据的rdd应用上面两个方法做求和:(a,1),(a,2),(a,3),(b,1),(b,2),(c,1);reduceByKey产生的中间结果(a,6),(b,3),(c,1);而groupByKey产生的中间结果结果为((a,1)(a,2)(a,3)),((b,1)(b,2)),(c,1),(以上结果为一个分区中的中间结果)可见groupByKey的结果更加消耗资源
- 作用不同,reduceByKey作用是聚合,异或等,groupByKey作用主要是分组,也可以做聚合(分组之后)
- map端中间结果对键对应的值得聚合方式不同
【写过程】
1、Client先从缓存中定位region,如果没有缓存则需访问zookeeper,从.META.表获取要写入的region信息
2、找到小于rowkey并且最接近rowkey的startkey对应的region
3、将更新写入WAL中。当客户端发起put/delete请求时,考虑到写入内存会有丢失数据的风险,因此在写入缓存前,HBase会先写入到Write Ahead Log(WAL)中(WAL存储在HDFS中),那么即使发生宕机,也可以通过WAL还原初始数据。
4、将更新写入memstore中,当增加到一定大小,达到预设的Flush size阈值时,会触发flush memstore,把memstore中的数据写出到hdfs上,生成一个storefile。
5、随着Storefile文件的不断增多,当增长到一定阈值后,触发compact合并操作,将多个storefile合并成一个,同时进行版本合并和数据删除。
6、storefile通过不断compact合并操作,逐步形成越来越大的storefile。
7、单个stroefile大小超过一定阈值后,触发split操作,把当前region拆分成两个,新拆分的2个region会被hbase master分配到相应的2个regionserver上。
【读过程】
1、Client先从缓存中定位region,如果没有缓存则需访问zookeeper,查询.-ROOT-.表,获取.-ROOT-.表所在的regionserver地址。
2、通过查询.-ROOT-.的region服务器 获取 含有.-META-.表所在的regionserver地址。
3、clinet会将保存着regionserver位置信息的元数据表.META.进行缓存,然后在表中确定待检索rowkey所在的regionserver信息。
4、client会向在.META.表中确定的regionserver发送真正的数据读取请求。
5、先从memstore中找,如果没有,再到storefile上读。
采用2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)进行,共需内存 2^32 * 2 bit=1 GB内存,还可以接受。然后扫描这2.5亿个整数,查看Bitmap中相对应位,如果是00变01,01变10,10保持 不变。扫描后,查看bitmap,把对应位是01的整数输出即可。
- CDH支持的存储组件更丰富
- HDP支持的数据分析组件更丰富
- HDP对多维分析及可视化有了支持,引入Druid和Superset
- HDP的HBase数据使用Phoenix的jdbc查询;CDH的HBase数据使用映射Hive到Impala的jdbc查询,但分析数据可以存储Impala内部表,提高查询响应
- 多维分析Druid纳入集群,会方便管理;但可视化工具Superset可以单独安装使用
- CDH没有时序数据库,HDP将Druid作为时序数据库使用
问题:log4j的日志文件肯定是会根据规则进行滚动的:当*.log满了就会滚动把前文件更名为*.log.1,然后重新进行*.log文件打印。这样flume就会把*.log.1文件当作新文件,又重新读取一遍,导致重复。
原因:flume会把重命名的文件重新当作新文件读取是因为正则表达式的原因,因为重命名后的文件名仍然符合正则表达式。所以第一,重命名后的文件仍然会被flume监控;第二,flume是根据文件inode&&文件绝对路径 、文件是否为null&&文件绝对路径,这样的条件来判断是否是同一个文件。
解决办法:
1.修改 ReliableTaildirEventReader
修改 ReliableTaildirEventReader 类的 updateTailFiles方法。去除tf.getPath().equals(f.getAbsolutePath()) 。只用判断文件不为空即可,不用判断文件的名字,因为log4j 日志切分文件会重命名文件。
if (tf == null || !tf.getPath().equals(f.getAbsolutePath())) { 修改为: if (tf == null) {
2.修改TailFile
修改TailFile 类的 updatePos方法。inode 已经能够确定唯一的 文件,不用加 path 作为判定条件
if (this.inode == inode && this.path.equals(path)) { 修改为: if (this.inode == inode) {
3.将修改过的代码打包为自定义source的jar
可以直接打包taildirSource组件即可,然后替换该组件的jar
如果是第一层某个代理失败,那么可以考虑由第一层的其他节点来接管故障节点。如果是第二层代理停止运行,则为了防止数据丢失,只能让每一个第一层代理具有多个冗余的Avro sink,然后把这些sink安排到同一个sink组中,如果第二层代理中的某个代理出现问题,则该事件会被传递给该层sink组的其他代理来完成,以此来实现故障转移和负载均衡。
class 的生命周期:
加载一个Class需要完成以下3件事:
- 通过Class的全限定名获取Class的二进制字节流
- 将Class的二进制内容加载到虚拟机的方法区
- 在内存中生成一个java.lang.Class对象表示这个Class
获取Class的二进制字节流这个步骤有多种方式:
- 从zip中读取,如:从jar、war、ear等格式的文件中读取Class文件内容
- 从网络中获取,如:Applet
- 动态生成,如:动态代理、ASM框架等都是基于此方式
- 由其他文件生成,典型的是从jsp文件生成相应的Class
- 通过网络拉取运行所需的资源,并反序列化(由于多个task运行在多个Executor中,都是并行运行的,或者并发运行的,一个stage的task,处理的RDD是一样的,这是通过广播变量来完成的)
- 获取shuffleManager,从shuffleManager中获取shuffleWriter(shuffleWriter用于后面的数据处理并把返回的数据结果写入磁盘)
- 调用rdd.iterator(),并传入当前task要处理的partition(针对RDD的某个partition执行自定义的算子或逻辑函数,返回的数据都是通过上面生成的ShuffleWriter,经过HashPartitioner[默认是这个]分区之后写入对应的分区backet,其实就是写入磁盘文件中)
- 封装数据结果为MapStatus ,发送给MapOutputTracker,供ResultTask拉取。(MapStatus里面封装了ShuffleMaptask计算后的数据和存储位置地址等数据信息。其实也就是BlockManager相关信息,BlockManager 是Spark底层的内存,数据,磁盘数据管理的组件)
- ResultTask拉取ShuffleMapTask的结果数据(经过2/3/4步骤之后的结果)
public class Singleton
{
private static class SingletonHolder
{
public final static Singleton instance = new Singleton();
}
public static Singleton getInstance()
{
return SingletonHolder.instance;
}
}
Kudu的大部分场景和Hbase类似,其设计降低了随机读写性能,提高了扫描性能,在大部分场景下,Kudu在拥有接近Hbase的随机读写性能的同时,还有远超Hbase的扫描性能。
区别于Hbase等存储引擎,Kudu有如下优势:
- 快速的OLAP类查询处理速度
- 与MapReduce、Spark等Hadoop生态圈常见系统高度兼容,其连接驱动由官方支持维护
- 与Impala深度集成,相比HDFS+Parquet+Impala的传统架构,Kudu+Impala在绝大多数场景下拥有更好的性能。
- 强大而灵活的一致性模型,允许用户对每个请求单独定义一致性模型,甚至包括强序列一致性。
- 能够同时支持OLTP和OLAP请求,并且拥有良好的性能。
- Kudu集成在ClouderaManager之中,对运维友好。
- 高可用。采用Raft Consensus算法来作为master失败后选举模型,即使选举失败,数据仍然是可读的。
- 支持结构化的数据,纯粹的列式存储,省空间的同时,提供更高效的查询速度。
当producer向leader发送数据时,可以通过request.required.acks参数来设置数据可靠性的级别:
0:这意味着producer无需等待来自broker的确认而继续发送下一批消息。这种情况下数据传输效率最高,
但是数据可靠性确是最低的。
1(默认):这意味着producer在ISR中的leader已成功收到的数据并得到确认后发送下一条message。
如果leader宕机了,则会丢失数据。
-1(或者是all):producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成,可靠性最高。
但是这样也不能保证数据不丢失,比如当ISR中只有leader时(前面ISR那一节讲到,
ISR中的成员由于某些情况会增加也会减少,最少就只剩一个leader),这样就变成了acks=1的情况。如果要提高数据的可靠性,在设置request.required.acks=-1的同时,也要min.insync.replicas这个参数
(可以在broker或者topic层面进行设置)的配合,这样才能发挥最大的功效。min.insync.replicas这个参数设定
ISR中的最小副本数是多少,默认值为1,当且仅当request.required.acks参数设置为-1时,此参数才生效。
如果ISR中的副本数少于min.insync.replicas配置的数量时,客户端会返回异常:
org.apache.kafka.common.errors.NotEnoughReplicasExceptoin: Messages are rejected since there are fewer in-sync replicas than required
全局索引 global index是默认的索引格式。适用于多读少写的业务场景。写数据的时候会消耗大量开销,因为索引表也要更新,而索引表是分布在不同的数据节点上的,跨节点的数据传输带来了较大的性能消耗。全局索引必须是查询语句中所有列都包含在全局索引中,它才会生效。
本地索引 Local index适用于写操作频繁的场景。和全局索引一样,Phoenix也会在查询的时候自动选择是否使用本地索引。本地索引之所以是本地,只要是因为索引数据和真实数据存储在同一台机器上,这样做主要是为了避免网络数据传输的开销。
local index和global index比较
1.索引数据
global index单独把索引数据存到一张表里,保证了原始数据的安全,侵入性小
local index把数据写到原始数据里面,侵入性强,原表的数据量=原始数据+索引数据,使原始数据更大
2.性能方面global index要多写出来一份数据,写的压力就大一点,但读的速度就非常快
local index只用写一份索引数据,节省不少空间,但多了一步通过rowkey查找数据,写的速度非常快,读的速度就没有直接取自己的列族数据快。
- 第一个block副本放在client结点所在机架的datanode里(如果client不在集群范围内,则这第一个node是随机选取的,当然系统会尝试不选择哪些太满或者太忙的node)。
- 第二个block副本放置在与第一个datanode节点相同的机架中的另一个datanode中(随机选择)。
- 第三个block副本放置于另一个随机远端机架的一个随机datanode中。
如果还有更多的副本就随机放在集群的node里。
将第一、二个block副本放置在同一个机架中,当用户发起数据读取请求时可以较快地读取,从而保证数据具有较好的本地性。
第三个及更多的block副本放置于其他机架,当整个本地结点都失效时,HDFS将自动通过远端机架上的数据副本将数据副本的娄得恢复到标准数据。
- 底层数据结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构(当链表长度大于8,转为红黑树)。
- JDK1.8中resize()方法在表为空时,创建表;在表不为空时,扩容;而JDK1.7中resize()方法负责扩容,inflateTable()负责创建表。
- 1.8中没有区分键为null的情况,而1.7版本中对于键为null的情况调用putForNullKey()方法。但是两个版本中如果键为null,那么调用hash()方法得到的都将是0,所以键为null的元素都始终位于哈希表table[0]中。
- 当1.8中的桶中元素处于链表的情况,遍历的同时最后如果没有匹配的,直接将节点添加到链表尾部;而1.7在遍历的同时没有添加数据,而是另外调用了addEntry()方法,将节点添加到链表头部。
- 1.7中新增节点采用头插法,1.8中新增节点采用尾插法。这也是为什么1.8不容易出现环型链表的原因。
- 1.7中是通过更改hashSeed值修改节点的hash值从而达到rehash时的链表分散,而1.8中键的hash值不会改变,rehash时根据(hash&oldCap)==0将链表分散。
- 1.8rehash时保证原链表的顺序,而1.7中rehash时有可能改变链表的顺序(头插法导致)。
- 在扩容的时候:1.7在插入数据之前扩容,而1.8插入数据成功之后扩容。
ConCurrentHashMap 1.8 相比 1.7的话,主要改变为:
去除
Segment + HashEntry + Unsafe
的实现,
改为Synchronized + CAS + Node + Unsafe
的实现
其实 Node 和 HashEntry 的内容一样,但是HashEntry是一个内部类。
用 Synchronized + CAS 代替 Segment ,这样锁的粒度更小了,并且不是每次都要加锁了,CAS尝试失败了在加锁。put()方法中 初始化数组大小时,1.8不用加锁,因为用了个
sizeCtl
变量,将这个变量置为-1,就表明table正在初始化。下面简单介绍下主要的几个方法的一些区别:
1. put() 方法
JDK1.7中的实现:
ConCurrentHashMap 和 HashMap 的put()方法实现基本类似,所以主要讲一下为了实现并发性,ConCurrentHashMap 1.7 有了什么改变
需要定位 2 次 (segments[i],segment中的table[i])
由于引入segment的概念,所以需要
- 先通过key的
rehash值的高位
和segments数组大小-1
相与得到在 segments中的位置- 然后在通过
key的rehash值
和table数组大小-1
相与得到在table中的位置没获取到 segment锁的线程,没有权力进行put操作,不是像HashTable一样去挂起等待,而是会去做一下put操作前的准备:
- table[i]的位置(你的值要put到哪个桶中)
- 通过首节点first遍历链表找有没有相同key
- 在进行1、2的期间还不断自旋获取锁,超过
64次
线程挂起!JDK1.8中的实现:
- 先拿到根据
rehash值
定位,拿到table[i]的首节点first
,然后:
- 如果为
null
,通过CAS
的方式把 value put进去- 如果
非null
,并且first.hash == -1
,说明其他线程在扩容,参与一起扩容- 如果
非null
,并且first.hash != -1
,Synchronized锁住 first节点,判断是链表还是红黑树,遍历插入。2. get() 方法
JDK1.7中的实现:
由于变量
value
是由volatile
修饰的,java内存模型中的happen before
规则保证了 对于 volatile 修饰的变量始终是写操作
先于读操作
的,并且还有 volatile 的内存可见性
保证修改完的数据可以马上更新到主存中,所以能保证在并发情况下,读出来的数据是最新的数据。如果get()到的是null值才去加锁。
JDK1.8中的实现:
- 和 JDK1.7类似
3. resize() 方法
JDK1.7中的实现:
- 跟HashMap的 resize() 没太大区别,都是在 put() 元素时去做的扩容,所以在1.7中的实现是获得了锁之后,在单线程中去做扩容(1.
new个2倍数组
2.遍历old数组节点搬去新数组
)。JDK1.8中的实现:
- jdk1.8的扩容支持并发迁移节点,从old数组的尾部开始,如果该桶被其他线程处理过了,就创建一个 ForwardingNode 放到该桶的首节点,hash值为-1,其他线程判断hash值为-1后就知道该桶被处理过了。
4. 计算size
JDK1.7中的实现:
- 先采用不加锁的方式,计算两次,如果两次结果一样,说明是正确的,返回。
- 如果两次结果不一样,则把所有 segment 锁住,重新计算所有 segment的
Count
的和JDK1.8中的实现:
由于没有segment的概念,所以只需要用一个
baseCount
变量来记录ConcurrentHashMap 当前节点的个数
。
- 先尝试通过CAS 修改
baseCount
- 如果多线程竞争激烈,某些线程CAS失败,那就CAS尝试将
CELLSBUSY
置1,成功则可以把baseCount变化的次数
暂存到一个数组counterCells
里,后续数组counterCells
的值会加到baseCount
中。
- 如果
CELLSBUSY
置1失败又会反复进行CASbaseCount
和 CAScounterCells
数组