(一)八股文--Java基础
final有什么用
用于修饰类,属性和方法
被final修饰的类不能被继承
被final修饰的方法不能被重写
被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容。引用指向的内容是可以改变的
final finally finalize区别
final可以修饰类,变量,方法,修饰类表示该类不能被继承,修饰方法表示该方法不能被重写,修饰变量表示该变量是一个常量不能被重新赋值
finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码放在finally中,表示不管是否有异常,该代码块都会执行,一般用来存放一些关闭资源的代码
finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用system.gc()的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断
this关键字的用法
this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针
this的用法在java中大体分为3种:
1.普通的直接引用,this相当于指向对象本身
2.形参和成员名字重名,用this来区分
3.引用本类的构造函数
super关键字的用法
super可以理解为是指向自己父类对象的一个指针
super也有3种用法:
1.普通的直接引用
2.子类中的成员遍历或者方法与父类的成员变量或方法重名时,用super进行区分
3.引用父类构造函数
this和super的区别
super:它引用当前对象的直接父类中的成员
this:它代表当前对象名
super和this类似,区别在于super在子类中调用父类的构造方法,this在本类内调用本类的其他构造方法
super和this均需要放在构造方法内第一行
this和super都指的是对象,所以均不可以在static环境中使用,包括static变量,static方法,static语句块
static存在的主要意义
static的主要意义在于创建独立于具体对象的域变量或者方法,以致于及时没有创建对象,也能使用属性和调用方法
static关键字还有一个比较关键的作用就是用来形成静态代码块以优化程序性能,static可以置于类中的任何地方,类中可以有多个static块,在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次
为什么说static块可以用来优化程序性能?
因为它的特性:只会在类加载的时候执行一次,因此很多时候会将一些只需要进行一次的初始化操作都放在static块中进行
static的独特之处
被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享
在该类被第一次加载的时候,就会去加载被static所修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化
static遍历值在类加载的时候分配空间,所以创建类对象的时候不会重新分配,赋值的话是可以任意赋值的
被static修饰的遍历或者方法是优于对象存在的,也就是说当地一个类加载完毕之后面积是没有创建对象,也可以去访问
break continue returm的区别和作用
break:跳出总上一层循环,不在执行循环(结束当前的循环体)
continue:跳出本次循环,继续执行下次循环(结束正在执行的循环)
return:程序返回,不在执行下面的代码(结束当前的方法,直接返回 )
重载和重写的区别
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,后者实现的是运行时的多态性
重载:发生在同一个类,方法名相同参数列表不同(参数类型不同,个数不同,顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分
重写:发生在父子类,方法名,参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类,如果父类方法访问修饰符为pricate,则子类中就不是重写
==和equals的区别
==:它的作用是判断两个对象的地址是不是相等,即判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)
equals:它的作用也是判断两个对象是否相等,有两种情况:
1.类没有覆盖equals方法,则通过equals比较两个对象,等价于==
2.类覆盖了equals方法,一般,我们都是覆盖equals方法来两个对象的内容相等,若他们的内容相等,则返回true
String中的equals方法是被重写过的,因为Object的equals方法是比较的对象的内存地址,而String的equals比较的是对象的值
当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值,如果有就把它赋给当前引用,如果没有就在常量池中重新创建一个String对象
BIO NIO AIO有什么区别
BIO:Block IO同步阻塞式IO,就是平常使用的传统IO,特点是模式简单使用方便,并发处理能力低
NIO:Non IO同步非阻塞IO,是传统IO的升级,客户端和服务端通过Channel(通道)通讯,实现了多路IO复用
AIO:Asynchronous IO是NIO的升级,也叫NIO2,实现了异步非阻塞IO,异步IO的操作基于事件和回调机制
什么是反射机制
Java反射机制实在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性,这种动态获取的信息以及动态调用对象的方法的功能成为Java语言的反射机制
优点:运行期类型的判断,动态加载类,提高代码灵活度
缺点:性能瓶颈,反射相当于一系列解释操作,通知JVM要做的事情,性能比直接的java代码要慢很多
反射是框架设计的灵魂,在Spring中也用到了反射,最经典的就是XML的配置模式。
Spring通过XML配置模式装载Bean的过程:
1.将程序内所有XML或者Properties配置文件加载到内存中
2.Java类里面解析XML或Properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息
3.使用反射机制,根据这个字符串获得某个类的Class实例
4.动态配置实例的属性
获取反射的方法:
1.通过new对象实现反射机制
2.通过路径实现反射机制
3.通过类名实现反射机制
String有哪些特性
不可变:String是只读字符串,对他进行任何操作都是创建一个新的对象,再把引用指向该对象,不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性
常量池优化:String对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,就直接返回缓存的引用
final:使用final来定义String类,表示String类不能被继承,提高了系统的安全性
在使用HashMap时,用String做key有什么好处
HashMap内部实现是通过key的hashCode来确定value的存储位置,因为字符串是不可变的,所以当创建字符串时,它的hashCode被缓存下来了,不需要再次计算,所以相比于其他对象更快
String StringBuffer StringBuild的区别是什么
String中的对象是不可变的,可以理解为常量,是线程安全的
StringBuffer对方法加了同步锁或者对调用方法加了同步锁,所以是线程安全的
StringBuild没有对方法加同步锁,所以是线程不安全的
每次对String类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String对象,StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用,相同情况下使用StringBuild相比使用StringBuffer仅能获得10%-15%左右的性能,弹药冒多线程不安全的风险
对于三者的总结:
如果要操作少量的数据用String
单线程操作字符串缓冲区下操作大量数据用StringBuilder
多线程操作字符缓冲区下大量数据用StringBuffer
哪些集合类是线程安全的
vector:就比arrayList多了一个同步化机制(线程安全),因为效率较低,不建议使用
statck:堆栈类,先进后出
hashTable:就比hashMap多了一个线程安全
enumeration:枚举,相当于迭代器
Java集合的快速失败机制(fail-fast)
是Java集合的一种错误检测机制,当多个线程对集合进行结构上的改变,有可能会产生fai-fast机制
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个modCount变量,集合在被遍历期间如果内容发送了改变,就会改变modCount的值,每当迭代器使用hashNext/next遍历下一个元素之前,都会检测modCount遍历是否为expectedModCount,是的话就返回遍历,否则抛出异常,终止遍历
解决办法:在遍历过程中,所有涉及到改变modCount值的地方加上synchronized或者使用copyOnWriteArrayList替换ArrayList
怎么确保一个集合不能被修改
使用Collections.unmidufuableCollection(Collection c)创建一个只读集合,这样改变集合的任何操作都会抛出异常
迭代器Iterator是什么
Iterator接口提供遍历任何Collection的接口,我们可以从一个Collection中使用迭代器方法来获取迭代器实例,迭代器取代了Java集合中的Enumeration,迭代器允许调用在在迭代过程中移除元素
Iterator的特点:只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出ConcurrentModificationException异常
Iterator和ListIterator有什么区别
Iterator可以遍历Set和List集合,ListIterator只能遍历List
Iterator只能单向遍历,ListIterator可以双向遍历(向前/后遍历)
ListItrerator实现Iterator接口,添加了一些额外的功能,比如添加一个元素,替换一个元素,获取前面或者后面元素的索引位置
遍历一个List有哪些不同的方式,每种方法的实现原理是什么
1.for循环遍历,基于计数器,在集合外部维护了一个计数器,然后一次读取每一个位置的元素,当读取到后一个元素后停止
2.迭代器遍历,Iterator是面向对象的一个设计模式,目的是屏蔽不停数据集合的特点,统一遍历集合的接口,Java在Collection中支持了Iterator模式
3.foreach循环遍历,foreach内部采用了Iterator的实现方式,使用时不需要显式声明Iterator或计数器,优点是代码简介,不易出错,缺点是只能做简单的遍历,不能在遍历过程中操作数据集合
说一下ArrayList的优缺点
优点:1.ArrayList底层是动态数组,是一种随机访问模式,ArrayLiat实现了RandomAssess接口,因此查找的时候特别快
2.ArrayList在顺序添加一个元素的时候非常方便
缺点:1.删除元素的时候,需要做一次元素复制操作,如果要复制的元素很多,就会比较耗费性能
2.插入元素的时候,有需要做一次元素复制操作,如果要复制的元素很多,就会比较耗费性能
如何实现数组和List之间的转换
数组转List:ArrayList.asList(array)
List转数组:使用List自带的toArray()方法
ArrayList和LinkedList的区别是什么
数据结构实现:ArrayList是动态数据的数据结构实现,LinkedList是双向链表的数据结构实现
随机访问效率:ArrayList比LinkedList在随机访问的时候效率要高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找
增加和删除效率:在非首尾的增加和删除操作,LinkedList要比ArrayList效率要高,因为ArrayList增删操作要影响数组内的其他数据的下标
内存空间占用:LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素
线程安全:ArrayList和LinkedList都是不同步的,也就是不保证线程安全
综上,需要频繁读取集合中的元素时,推荐ArrayList,插入和删除操作较多时,推荐使用LinkedList
ArrayList和Vector的区别是什么
1.都实现了List接口,都是有序集合
2.Vector使用了synchronized来实现线程同步,是线程安全的,而ArrayList是非线程安全的
3.ArrayList在性能方便优于Vector,ArrayList和Vector都会根据实际的需要动态的调整容量,只不过在Vector扩容每次增加一倍,而ArrayList只会增加50%
4.Vector类的所有方法都是同步的,可以由两个线程安全的访问一个Vector对象,但是一个线程访问Vector的话代码在同步操作上耗费大量的时间,ArrayList不是同步的,所以在不需要保证线程安全时建议使用ArrayList
List和Set的区别
1.都继承Collection接口
2.List是有序可重复,可以插入多个null元素,元素都有索引,Set是无序不重复,只允许存入一个null元素,并且必须保证元素的唯一性
3.List支持for循环,可以通过下标来遍历,也可以用迭代器,但Set只能用迭代,因为无序,无法用下标来取得想要的值
4.Set检索元素效率低,删除,插入效率高,并且不会引起元素位置的改变,List查询元素效率高,插入删除元素效率低,会引起其他元素位置改变
说一下HashSet的实现原理
HashSet是基于hashMap实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为Present,因此HashSet的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法完成,HashSet不允许重复的值
HashSet如何检查重复 HashSet是如何保证数据不可重复的
向HashSet中add元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equals方法比较
HashSet中的add方法会使用HashMap的put方法,HashMap中的key是唯一的,HashSet添加进去的值就是作为HashMap的key,并且在HashMap中的K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V,所以才不会重复(HashMap比较key是否相等时先比较hashCode再比较equals)
HashCode和equals的相关规定
1.如果两个对象相等,则hashCode一定也相同
2.两个对象相等,equals返回true
3.两个对象有相同的hashCode值,他们也不一定相等
4.综上,equals方法被覆盖,hashCode方法也必须被覆盖
5.hashCode方法默认行为是对堆上的对象产生独特值,如果没有重写hashCode,则该class的两个对象无论如何多不会相等
BlockIngQueue是什么
BlockingQueue是一个队列,在进行检索或移除一个元素时,它会等待队列变为非空,当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue主要是先生产者-消费者模式,我们不需要担心等待生产者可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理。
相同点:都是返回第一个元素,并在队列中删除返回的对象
不同点:如果没有元素poll()会返回null,而remove()则直接抛异常
说一下HashMap的实现原理
HashMap是基于哈希表的Map接口的非同步实现,此实现提供所有可选的映射操作,并允许使用null值和null键,此类不保证映射的顺序,特别是它不保证该顺序恒久不变
HashMap的数据结构:是一个链表散列的数据结构,即数组+链表的结合体
HashMap基于Hash算法实现的:
1.当我们往HashMap中put元素时,利用key的HashCode重新hash计算当前对象的元素在数组中的下标
2.存储时,如果出现Hash值相同的key,此时有两种情况
(1)如果key相同,则覆盖原始值
(2)如果key不同(出现冲突),则将当前的key-value放入链表中
3.获取时,直接找到hash值对应的下标,再进一步判断key是否相同,从而找到对应值
4.理解了以上过程就不难明白HashMap是如何解决hash冲突的问题。核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步对比
HashMap在JDK1.7和1.8中有哪些不同 HashMap的底层实现
在Java中,保存数据有两种比较简单的数据结构:数组和链表,数组的特点:寻址容易,插入和删除困难,链表的特点:寻址困难,插入和删除容易,所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫拉链法的方式解决hash冲突
1.8之前:采用的是拉链法。拉链法:将链表和数组结合,也就是创建一个链表数组,数组中每一格就是链表,若遇到hash冲突,则将冲突的值加到链表中即可,插入数据方式为头插法
1.8之后:解决hash冲突时有了较大的变化,当链表长度大于阈值(默认为8),将链表转化为红黑树,以减少搜索时间,插入数据方式为尾插法
1.8主要解决或优化的问题:
1.resize扩容优化
2.引入了红黑树,目的是避免单条链表过长影响查询效率
3.解决了多线程死循环问题,但仍是非线程安全的,多线程查询是可能会造成数据丢失问题
HashMap的put方法的具体流程
当我们put的时候,首先计算key的hash值,这里调用了hash方法,hash方法实际就是让key.hashCode与key.hashCode>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以hash函数大概的作用就是:高16bit不变,低16bit和高16bit做一个异或,目的是减少碰撞
1.判断键值对数组是否为空或为null,否则执行resize进行扩容
2.根据键值key计算hash值得到插入的数组索引,
如果table == null,直接新建节点添加,插入成功后判断实际存在的键值对数量size是否超多了大容量threshold,如果超过,就进行扩容
如果不为空,则判断table的首个元素是否跟key一样,
如果相同则覆盖value
否则判断table是否为treeNode,即table是否是红黑树
如果是红黑树,则直接在树中插入键值对
否者遍历table,判断链表长度是否大于8
大于8的话就把链表转为红黑树,在红黑树中执行插入操作
否者进行链表的插入操作,遍历过程中若发现key已经存在直接覆盖value即可
HashMap的扩容操作是怎么实现的
1.在JDK1.8中,resize方法实在hashMap中的键值对大于阈值时或者初始化时,就调用resize方法进行扩容
2.每次扩容的时候们都是扩容2倍
3.扩容后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置,在putVal中,我们在这个函数里面使用了2次resize方法,resize方法表示在进行第一次初始化是会对其进行扩容,或者当该数组的实际大小大于其临界值(第一次为12),这个时候在扩容的同时在会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新计算其hash值,根据hash值对其进行分发,但是在1.8版本中,则是根据在同一个桶的位置中进行判断是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
HashMap是怎么解决Hash冲突的
hash:把任意长度的数组通过散列算法,变换成固定长度的输出,该输出就是hash,这种转换是一种压缩映射,也就是散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的熟人出,所以不可能从散列值来唯一的确定输入值。
所有散列函数都有如下一个基本特征:根据同一散列函数计算出散列值如果不同,那么输入值肯定也不同。但是根据同一散列函数计算出散列值如果相同,输入值不一定相同
什么是hash冲突:当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,就是hash冲突
使用的方法解决冲突:1.使用链地址法(使用散列表)来链接拥有相同hash值的数据
2.使用2次扰动函数(hash函数)来降低hash冲突的概率,是数据分布平均
3.引入红黑树进一步降低遍历的时间复杂度,是遍历更快
为什么HashMap中String,Integer这样的包装类更适合做Key
String,Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的概率。都是final类型,即不可变性,保证了key的不可更改性,不会存在获取hash值不同的情况,内部已重写了equals方法和hashCode方法,不容易出现hash值计算错误的情况
HashMap的长度为什么是2的幂次方
为了能让HashMap存取高效,尽量减少碰撞,每个链表/红黑树长度大致相同。取余操作中如果除数是2的幂次则等价于其除数减一的与操作,也就是说hash%length == hash&(length - 1)的前提是length是2的n次方,并且采用二进制操作&,相对于%能提高运算效率
HashMap和HashTable有什么区别
1.线程安全:HashMap是非线程安全的,HashTable是线程安全的,HashTable内部的方法基本都是经过synchronized修饰(如果要保证线程安全的话就使用ConcurrentHashMap)
2.效率:因为线程安全的问题,HashMap比HashTable效率高一点
3.对Null Key 和 Null Value的支持:HashMap中,null可以作为建,这样的键只有一个,可以有一个或多个键所对应饿值为null,但是在HashTable中只要key为null,就会抛异常
4.初始容量大小和每次扩容大小的不同:(1)创建时如果不指定容量初始值,HashTable默认为11,每次扩容之后,容量变为原来的2n+1,HashMap默认的初始值为16,之后每次扩容,容量变为原来的2倍
(2)创建时如果给定容量初始值,那么HashTable会直接使用你给定的大小,而HashMap会将其扩容为2的幂次方
5.底层数据结构:JDK1.8以后的HashMap在解决hash冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,减少搜索时间
如何决定使用HashMap还是TreeMap
对于Map的插入,删除,定位元素的这类操作,HashMap是最好的选择
当需要对一个有序的key集合进行遍历,TreeMap是最好的选择
HashMap和ConcurrentHashMap的区别
1.ConcurrentHashMap对整个桶数组进行了分割分段,然后在每一个分段上都用了lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了,并发性更好,而HashMap没有锁机制,非线程安全的
2.HashMap的键值对运行为null,但是ConcurrentHashMap都不允许
ConcurrentHashMap和HashTable的区别
ConcurrentHashMap和HashTable的区别主要体现在实现线程安全的方式不同
底层数据结构:JDK1.7的ConcurrentHashMap采用了分段的数组+链表实现
JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑树
HashTable和JDK1.8之前的HashMap的底层数据结构类似都是采用数组+链表的形式,数组是HashMap的主体,链表主要是为了解决hash冲突而存在的
实现线程安全的方式:JDK1.7时,ConcurrentHashMap对整个桶数组进行了分割分段,每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率(默认分配16个分段锁,比HashTable效率提高16倍)
JDK1.8时不需要分段锁的概念,而是直接使用Node数组+链表/红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap
HashTable使用synchronized来保证线程安全,效率非常低,当一个线程访问同步方法时,其他线程也访问同步方法,可能就会进入阻塞或者轮询状态
ConcurrentHashMap底层具体实现知道吗 实现原理是什么
JDK1.7:首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问
采用了Segment + HashEntry的方式实现:一个ConcurrentHashMap里面包含了一个segment数组,Segment的结构和HashMap类型,是一种数组和链表的结构,一个Segment包含了一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护一个HashEntry数组的元素,当没对HashEntry数组的数据进行修改时,必须首先获得对应的Segment的锁。
JDK1.8:放弃了Segment的设计,取而代之的采用了Node + CAS + synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑树的首节点,这样只要hash不冲突,就不会产生并发,效率有提升了N倍
TreeMap和TreeSet在排序中如何比较元素
TreeSet要求存放的对象所属类必须实现Comparable接口,该接口提供了比较元素的compareTo方法,当插入元素时会回调该方法比较元素大小
TreeMap要求存放的键值对映射的键必须实现Comparable接口从而根据键对元素进行排序
双亲委派模型
1.当前类加载器从自己已经加载的类中查询是否此类已经加载,如果加载了则直接返回原来已经加载的类
2.如果没找到,就委托父类加载器去加载,父类加载器也会采用同样的策略,查看自己已经加载过的类是否包含这个类,有就返回,没有就委托父类的父类去加载,一直到启动类加载器,如果父加载器为空,就代表使用启动类加载器作为父类加载器去加载
3.如果启动类加载器加载失败,则会抛异常,然后在调用当前加载器的findClass方法进行加载
好处:1.主要是为了安全行,避免用户自己编写的类动态替换 Java核心类
2.同时避免了类的重复加载,因为JVM区分不同类,不仅仅根据类名,相同的class文件被不用的classLoader加载就是不同的两个类
老生长谈 HashMap的死循环
如果是在单线程下使用HashMap,自然是没有问题,如果后期由于代码优化,这段逻辑引入了多线程并发执行,在一个未知的时间点,会发现CPU占用100%,居高不下,通过查看堆栈,会惊讶的发现线程都在HashMap的get方法上,服务重启之后,问题消失,过段时间可能又复现。
原因分析:
先看看HashMap的数据结构
在内部,HashMap使用了一个Entry数组保存key,value数据,当一对key,value被加入时,会通过一个hash算法得到数组的下标index,算法很简单,根据key的hash值,对数组的大小取模hash&(length - 1),并把结果插入数组该位置,如果该位置已经有元素了,就说明存在hash冲突,这样会在index位置生成链表。
如果存在hash冲突,最惨的情况,就是所有元素都定位到同一个位置,形成一个长长的链表,这样get一个值时,最坏的情况就是需要遍历所有节点,性能变成了O(n),所有元素的hash值算法和HashMap的初始化大小很重要
当插入一个新的节点时,如果不存在相同的key,则会判断当前内部元素是否已经达到了阈值(默认是数组的0.75倍),如果达到了阈值,则会对数组进行扩容,也会对链表的元素进行rehash
解决方案:HashTable和Collections,synchronizedMap(hashMap),不过这两个方案基本上是对读写进行加锁操作,一个线程在读写元素,其他线程必须等待,性能可想而知,所以并发情况还是要使用concurrentedHashMap
深入浅出ConcurrentHashMap
HashMap是我们平时开发过程中用的比较多的集合,但它是非线程安全的,涉及到并发的情况,进行get操作有可能导致死循环,导致CPU利用率接近100%
解决方案:HashTable和Collections,synchronizedMap(hashMap),不过这两个方案基本上是对读写进行加锁操作,一个线程在读写元素,其他线程必须等待,性能可想而知
JDK1.7分析:ConcurrentHashMap采用了分段锁的机制,实现并发的更新操作,底层采用数组+链表的存储结构,其中包含核心静态内部类Segment和HashEntry
1.Segment继承ReentrantLock用来充当互斥锁的角色,每个Segment对象守护每个散列映射表的若干个桶
2.HashEntry用来封装映射表的键值对
3.每个桶是由若干个HashEntry对象连接起来的链表
一个ConcurrentHashMap实例包含由若干个Segment对象组成的数组
JDK1.8分析:抛弃了Segment分段锁机制,采用CAS + synchronized来保证并发更新的安全,底层用数组+ 链表/红黑树的存储结构
1.table默认为null,初始化发生在第一次插入操作,默认大小为16的数组,用来存储Node节点数据,扩容时大小为2的幂次方
2.newxtTable默认为null,扩容时新生成的数组,其大小是原数组的两倍
3.sizeCtl默认为0,用来控制table的初始化和扩容操作

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)