Java八股--集合(上)

📌Java八股--集合(上)

Java后端各科最全八股自用整理,获取方式见

集合

https://www.teqng.com/2021/07/27/hashmap%E8%BF%9E%E7%8E%AF18%E9%97%AE/

https://cloud.tencent.com/developer/article/1672781

https://www.teqng.com/2021/07/27/hashmap%E8%BF%9E%E7%8E%AF18%E9%97%AE/

vector和数组的区别

有哪些集合类

常见集合类有哪些

ArrayList和LinkedList的区别

问Java的集合,哪些有序,哪些无序,集合都问了个遍

ArrayList和LinkedList的区别,LinkedList是单向的还是双向的,为什么这么设计?

Arraylist是线程安全的吗?如何实现线程安全

https://segmentfault.com/a/1190000039264628

Java 集合可分为Collection和Map两种体系。

  • Collection接口:单列数据,定义了存取一组对象的方法的集合
    • List:元素有序、可重复的集合
    • Set:元素无序、不可重复的集合
  • Map接口:双列数据,保存具有映射关系“key-value对”的集合、

1、Collection接口:单列集合,用来存储一个一个的对象
线程安全 数据结构 支持随机访问 适用 内存空间占用

  • List接口:存储有序的、可重复的数据。
    • ArrayList[1.2]:作为List接口的主要实现类;线程不安全的【无同步】,效率高;底层使用Object[] elementData存储,支持随机访问。 【用数组来存储】
      【ArrayList维护成本低,LinkedList维护成本高】
    • LinkedList[1.2]:对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
    • Vector[1.0]:作为List接口的古老实现类;线程安全的【线程同步】,效率低;层使用Object[] elementData存储
  • Set接口:存储无序的、不可重复的数据 -->高中讲的“集合”
    • HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值。使用 Iterator 遍历 HashSet 得到的结果是不确定的。HashSet 查找的时间复杂度为 O(1)
      • LinkedHashSet:作为HashSet的子类;内部使用双向链表维护元素的插入顺序,遍历其内部数据时,可以按照添加的顺序遍历。对于频繁的遍历操作,LinkdHashSet效率高于HashSet.
    • TreeSet:基于红黑树实现,可以照添加对象【只能是同类的对象】的指定属性进行排序。TreeSet时间复杂度则为O(logN)。

2、Map[1.2]:双列数据,存储key-value对的数据

  • HashMap[1.2]:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value
    • LinkedHashMap[1.4]:保证在遍历map元素时,可以照添加的顺序实现遍历。原因:在原的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类执行效率高于HashMap。
  • TreeMap[1.2]:基于红黑树实现,保证照添加的key-value对进行排序【有序】,实现排序遍历。此时考虑key的自然排序或定制排序
  • Hashtable[1.0]:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
    • Properties:常用来处理配置文件。key和value都是String类型

ArrayList的排序底层是怎么实现的

CopyOnWriteArrayList是为了解决什么问题?为什么用了这样数据结构?问了为什么要加锁?加什么锁?

CopyOnWriteArrayList的底层?

ArrayList和LinkedList讲一讲,是线程安全的嘛?ArrayList初始化大小,怎么从0变到10的?(扩容算法实现的)

线程安全的集合

CopyOnWriteArrayList如何保证的线程安全

HashMap是在collection下面的吗,不是在map下面

1、HashMap底层原理

HashMap数据结构

在JDK1.7中和JDK1.8中有所区别:

  • 在JDK1.7中,由“数组+链表”组成,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。
  • 在JDK1.8中,有“数组+链表+红黑树”组成。当链表过长,则会严重影响HashMap的性能,红黑树搜索时间复杂度是O(logn),而链表是O(n)。因此,JDK1.8对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:
    • 当链表超过8且数组长度(数据总量)超过64才会转为红黑树
    • 将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。

HashMap在jdk7中实现原理:
在这里插入图片描述

HashMap在jdk8中相较于jdk7在底层实现方面的不同:

  1. new HashMap():底层没创建一个长度为16的数组
  2. jdk 8底层的数组是:Node[],而非Entry[]
  3. 首次调用put()方法时,底层创建长度为16的数组
  4. jdk7底层结构只:数组+链表。jdk8中底层结构:数组+链表+红黑树。
    • 4.1 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
    • 4.2 当数组的某一个索引位置上的元素以链表形式存在的数据个数>8,且当前数组的长度>64时,此时此索引位置上的所有数据改为使用红黑树存储。

2、HashMap底层典型属性属性

  • DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
  • DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
  • threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12
  • TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
  • MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64

【为什么要提前扩容??】

如果每个节点全是链表,这样数组是不满的,但是要避免这种情况,所以要提前扩容,提高数组的利用率,并且使得链表数少,因子0.75性能刚好中和

3、HashMap的是如何将一个数据放进去的呢?put过程?(元素添加过程)

map.put(key1,value1)

首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。

  • 如果此位置上的数据为空,此时的key1-value1添加成功。 ----情况1
  • 如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
    • 如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2
    • 如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同【不一定内容相同!】,继续比较,调用key1所在类的equals(key2)方法,比较:
      • 如果equals()返回false:此时key1-value1添加成功。----情况3
      • 如果equals()返回true:使用value1替换value2。

补充:关于情况2和情况3,此时key1-value1和原来的数据以链表的方式存储。

在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。

4、为了减少hash冲突,一个好的hash函数很重要,HashMap中使用的hash函数是什么呢?

hashCode方法是Object类中的方法,所有的类都可以对其进行使用。对于 key 调用hashCode()得到初始hash值h,然后将h无符号右移 16 位然后和h做异或运算得到最终的hash值。

还有平方取中法,伪随机数法和取余数法。这三种效率都比较低。而无符号右移 16 位异或运算效率是最高的。

hash 算法(JDK 8)

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

5、HashMap为什么从头插改为了尾插

6、讲讲自己对 HashMap的理解,以及和 Weakhashmap 的区别?

WeakHashMap与HashMap类似,不同之处在于,WeakHashMap中的Key采用的是“弱引用”,只要key不再被外部引用,它就可以被垃圾回收器回收。而HashMap中的key采用“强引用”,当HashMap中的key没有被外部引用时,只有在这个key从HashMap中删除后,才可以被垃圾回收器回收。

7、HashMap是有序的吗?HashMap的扩容机制、HashMap的负载因子为什么是0.75?为什么长度是2的幂次方?

8、为什么LinkedHashMap是有序的?如何保证它的有序性

7、为什么HashMap是线程不安全的?并发情况下会发生什么?

10. HashMap 的查找时间复杂度?

不管插入还是查找,由key获取hash值然后定位到桶的时间复杂度都是O(1),那么真正决定时间复杂度的实际上是桶里面链表/红黑树的情况。

如果桶里面没有元素,那么直接将元素插入/或者直接返回未查找到,时间复杂度就是O(1),如果里面有元素,那么就沿着链表进行遍历,时间复杂度就是O(n),链表越短时间复杂度越低,如果是红黑树的话那就是O(logn)。 所以平均复杂度很难说,只能说在最优的情况下是O(1)

11、为什么HashMap 不用B+树?

12、java 1.8 中hashmap的改进

HashMap在jdk8中相较于jdk7在底层实现方面的不同:

  1. jdk 8底层的数组是:Node[],而非Entry[]
  2. new HashMap():底层没创建一个长度为16的数组,首次调用put()方法时,底层创建长度为16的数组
  3. jdk7底层结构只:数组+链表。jdk8中底层结构:数组+链表+红黑树。
    4.1 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)

14、HashMap扩容机制,扩容时如何保证可操作

DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
threshold:扩容的阈值=容量*加载因子:16 * 0.75 => 12
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64

JDK7 HashMap扩容

扩容过程:
[1] 创建一个新的Entry空数组,长度是原数组的2倍;
[2] 取出旧数组元素然后遍历以该元素为头的单向链表元素,依据每个被遍历元素的 hash 值计算其在新数组中的下标,然后将其插入到新的哈希表中,直到所有的 Entry 对象都转移到了新的哈希表中为止。

分为两步
 扩容:创建一个新的Entry空数组,长度是原数组的2倍。
 ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
		//新的容量为旧数组容量的两倍
        resize(2 * table.length); 
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    createEntry(hash, key, value, bucketIndex);
}

//扩容方法
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    //如果老的数组容量大于最大值,即2的30次方,则将其容量设置为Integer.MAX_VALUE返回
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    //根据新的容量创建Entry数组
    Entry[] newTable = new Entry[newCapacity];
    
    //将就数组的值rehash到新数组中去
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    //更新阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

//JDK1.7扩容最核心的方法,newTable为新容量数组大小
void transfer(HashMapEntry[] newTable) {
	
	//新容量数组桶大小为旧的table的2倍
	int newCapacity = newTable.length;
	
	//遍历旧的数组桶table
	for (HashMapEntry<K,V> e : table) {
		
		//如果这个数组位置上有元素且存在哈希冲突的链表结构则继续遍历链表
		while(null != e) {
			
			//取当前数组索引位上单向链表的下一个元素
			Entry<K,V> next = e.next;
			
			//多线程在此阻塞!!!
			if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
			
			//重新依据hash值计算元素在扩容后数组中的索引位置
			int i = indexFor(e.hash, newCapacity);
			
			//将数组i的元素赋值给当前链表元素的下一个节点
			e.next = newTable[i];
			
			//将链表元素放入数组位置
			newTable[i] = e;
			
			//将当前数组索引位上单向链表的下一个元素赋值给e进行新的一圈链表遍历
			e = next;
		}
	}
}

在这里插入图片描述

多线程 死循环
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

JDK8 HashMap扩容

https://xie.infoq.cn/article/b2878d0aed9bc3f384422de59
https://segmentfault.com/a/1190000039302830

时机
[1] HashMap中put入第一个元素,初始化数组table。
[2] HashMap中的元素个数(size)超过阈值(threshold)时就会自动扩容。

1、创建一个新的Node空数组,长度是原数组的2倍;
2、遍历旧数组,

  • [1] 如果旧数组元素只有一个元素的话,那么将该元素重新哈希到新数组中;
  • [2] 如果是红黑树节点的话,
    if ((e.hash & oldCap) == 0),节点加到loHead链表;否则,加到hiHead链表;
    将loHead链表放在数组的原位置,然后进行红黑树链表节点转换处理;
    hiHead链表放在原索引+oldCap的位置,然后进行红黑树链表节点转换处理。
  • [3] 如果是链表节点,
    if ((e.hash & oldCap) == 0),节点加到loHead链表;否则,加到hiHead链表;
    将loHead链表放在数组的原位置,hiHead链表放在原索引+oldCap的位置。
    在这里插入图片描述

【HashMap绝对是最常问的集合之一】

16、保存1000个元素,怎么确定hashmap初始长度

又 1024 < 1333 < 2048,所以最好使用2048作为初始容量。让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

17、为什么要提前扩容??

如果每个节点全是链表,这样数组是不满的,但是要避免这种情况,所以要提前扩容,提高数组的利用率,并且使得链表数少,因子0.75性能刚好中和。

18、讲讲HashMap put的整个过程?

JDK7 put方法
① 如果定位到的数组位置没有元素 就直接插入。
② 如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的 key 比较,如果 key 相同就直接覆盖,不同就采用头插法插入元素。

JDK 8流程如下:
1、首先根据 key 的值计算 hash 值,找到该元素在数组中的下标;
3、如果该位置没有元素就直接插入;
4、如果该位置有元素就要和插入的key比较,如果key相同,覆盖掉value;
5、如果 key不相同,
判断该节点是否是树节点,如果是就将这个key-value挂在红黑树上;
否则判断该节点是链表节点,如果链表长度小于8,那么插入元素;如果该链表大于 8 ,且数组容量小于64,就进行扩容;如果链表节点大于 8 且数组容量大于 64,则将这个结构转换为红黑树,再插入元素。

JDK 8流程如下:
1、首先根据 key 的值计算hash值,找到该元素在数组中的下标;
3、如果该位置没有元素就直接插入;
4、如果该位置有元素就要和插入的key比较,如果key相同,覆盖掉value;
5、如果key不相同,则判断该节点是否是树节点,如果是就将这个key-value挂在红黑树上;
6、否则判断该节点是链表节点,判断该链表是否大于 8 ,如果大于 8 且数组容量小于64,就进行扩容;如果链表节点大于 8 且数组容量大于 64,则将这个结构转换为红黑树;否则,链表插入key,若 key 存在,就覆盖掉 value。

在这里插入图片描述

19、除了拉链法和红黑树你还能想到什么解决哈希冲突的问题?

解决哈希冲突的方法

2.1 开放定址法

  • 2.1.1 线行探查法
  • 2.1.2 平方探查法
  • 2.1.3 双散列函数探查法

2.2 链地址法(拉链法)
2.3 再哈希法
2.4 建立公共溢出区

解决Hash冲突方法有:开放定址法、再哈希法、链地址法(HashMap中常见的拉链法)、简历公共溢出区。

  1. 开放定址法,如果p=H(key)出现冲突时,则以p为基础,再次hash,p1=H§,如果p1再次出现冲突,则以p1为基础,以此类推,直到找到一个不冲突的哈希地址pi。因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素,而且因为存在再次hash,所以只能在删除的节点上做标记,而不能真正删除节点
  2. 再哈希法(双重散列,多重散列),提供多个不同的hash函数,R1=H1(key1)发生冲突时,再计算R2=H2(key1),直到没有冲突为止。这样做虽然不易产生堆集,但增加了计算的时间。
  3. 链地址法(拉链法),将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行,链表法适用于经常进行插入和删除的情况。
  4. 建立公共溢出区,将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区
    注意开放定址法和再哈希法的区别是
  5. 开放定址法只能使用同一种hash函数进行再次hash,再哈希法可以调用多种不同的hash函数进行再次hash

20、为什么小于6是链表,大于8变成红黑树;

通过查看源码可以发现,默认是链表长度达到 8 就转成红黑树,而当长度降到 6 就转换回去,这体现了时间和空间平衡的思想,最开始使用链表的时候,空间占用是比较少的,而且由于链表短,所以查询时间也没有太大的问题。可是当链表越来越长,需要用红黑树的形式来保证查询的效率。对于何时应该从链表转化为红黑树,需要确定一个阈值,这个阈值默认为 8,并且在源码中也对选择 8 这个数字做了说明,原文如下
在这里插入图片描述

上面这段话的意思是,如果 hashCode 分布良好,也就是 hash 计算的结果离散好的话,那么红黑树这种形式是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,桶(bins)中的节点数概率(链表长度)符合泊松分布,当桶中节点数(链表长度)为8的时候,概率仅为 0.00000006。这是一个小于千万分之一的概率,通常我们的 Map 里面是不会存储这么多的数据的,所以通常情况下,并不会发生从链表向红黑树的转换。可以看到链表中元素个数为 8 时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了 8,是根据概率统计而选择的。

但是,HashMap 决定某一个元素落到哪一个桶里,是和这个对象的 hashCode 有关的,JDK 并不能阻止我们用户实现自己的哈希算法,如果我们故意把哈希算法变得不均匀,例如:
在这里插入图片描述

事实上,链表长度超过 8 就转为红黑树的设计,更多的是为了防止用户自己实现了不好的哈希算法时导致链表过长,从而导致查询效率低,而此时转为红黑树更多的是一种保底策略,用来保证极端情况下查询的效率。

通常如果 hash 算法正常的话,那么链表的长度也不会很长,那么红黑树也不会带来明显的查询时间上的优势,反而会增加空间负担。所以通常情况下,并没有必要转为红黑树,所以就选择了概率非常小,小于千万分之一概率,也就是长度为 8 的概率,把长度 8 作为转化的默认阈值。

所以如果平时开发中发现 HashMap 或是 ConcurrentHashMap 内部出现了红黑树的结构,这个时候往往就说明我们的哈希算法出了问题,需要留意是不是我们实现了效果不好的 hashCode 方法,并对此进行改进,以便减少冲突。

21、HashMap扩容为什么是扩为两倍?

https://www.cnblogs.com/tyux/p/16010172.html

核心目的是:实现节点均匀分布,减少 hash 冲突。

计算索引位置的公式为:(n - 1) & hash,当 n 为 2 的 N 次方时,n - 1 为低位全是 1 的值,此时任何值跟 n - 1 进行 & 运算的结果为该值的低 N 位,达到了和取模同样的效果,实现了均匀分布。实际上,这个设计就是基于公式:x mod 2^n = x & (2^n - 1),因为 & 运算比 mod 具有更高的效率。

如下图,当 n 不为 2 的 N 次方时,hash 冲突的概率明显增大。

HashMap的初始容量是2的n次幂,扩容也是2倍的形式进行扩容,是因为容量是2的n次幂,可以使得添加的元素均匀分布在HashMap中的数组上,减少hash碰撞,避免形成链表的结构,使得查询效率降低

22、为什么在解决 hash 冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?

因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。

因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。

23、HashMap默认加载因子是多少?为什么是 0.75,不是 0.6 或者 0.8 ?

回答这个问题前,我们来先看下HashMap的默认构造函数:

int threshold; // 容纳键值对的最大值 
final float loadFactor; // 负载因子 
int modCount; 
int size; 

Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳键值对的最大值。threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。

默认的loadFactor是0.75,0.75是对空间和时间效率的一个平衡选择,一般不要修改,除非在时间和空间比较特殊的情况下 :

  • 如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值 。
  • 相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。

我们来追溯下作者在源码中的注释(JDK1.7):

  • As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.

  • 翻译过来大概的意思是:作为一般规则,默认负载因子(0.75)在时间和空间成本上提供了很好的折衷。较高的值会降低空间开销,但提高查找成本(体现在大多数的HashMap类的操作,包括get和put)。设置初始大小时,应该考虑预计的entry数在map及其负载系数,并且尽量减少rehash操作的次数。如果初始容量大于最大条目数除以负载因子,rehash操作将不会发生。

24、HashMap 中 key 的存储索引是怎么计算的?

首先根据key的值计算出hashcode的值,然后根据hashcode计算出hash值,最后通过hash&(length-1)计算得到存储的位置。看看源码的实现:

// jdk1.7
方法一:
static int hash(int h) {
    int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

    h ^= k.hashCode(); // 为第一步:取hashCode值
    h ^= (h >>> 20) ^ (h >>> 12); 
    return h ^ (h >>> 7) ^ (h >>> 4);
}

方法二:
static int indexFor(int h, int length) {  //jdk1.7的源码,jdk1.8没有这个方法,但实现原理一样
     return h & (length-1);  //第三步:取模运算
}

// jdk1.8
static final int hash(Object key) {   
     int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
	 
    /* 
     h = key.hashCode() 为第一步:取hashCode值
     h ^ (h >>> 16)  为第二步:高位参与运算
    */
}

这里的 Hash 算法本质上就是三步:取key的 hashCode 值、根据 hashcode 计算出hash值、通过取模计算下标。其中,JDK1.7和1.8的不同之处,就在于第二步。我们来看下详细过程,以JDK1.8为例,n为table的长度。
左列!!
在这里插入图片描述

25、HashMap为什么线程不安全?

HashMap是否线程安全

在这里插入图片描述

  • 多线程下扩容死循环。JDK1.7中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此,JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。
  • 多线程的put可能导致元素的丢失。多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在JDK 1.7和 JDK 1.8 中都存在。
  • put和get并发时,可能导致get为null。线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题。此问题在JDK 1.7和 JDK 1.8 中都存在。

具体分析可见我的这篇文章:面试官:HashMap 为什么线程不安全?

26、HashMap如何保证线程安全?

因此多线程环境下保证 HashMap 的线程安全性,主要有如下几种方法:

  1. 使用 java.util.Hashtable 类,此类是线程安全的。
  2. 使用 java.util.concurrent.ConcurrentHashMap,此类是线程安全的。
  3. 使用 java.util.Collections.synchronizedMap() 方法包装 HashMap object,得到线程安全的Map,并在此Map上进行操作。
  4. 自己在程序的关键方法或者代码段加锁,保证安全性,当然这是严重的不推荐。
    在这里插入图片描述

队列,堆栈的底层实现

Java 队列和堆栈的底层实现都是基于数组或链表的。

  • 队列的底层实现:可以使用数组或链表,其中数组实现的队列被称为循环队列。循环队列的实现使用了取模运算,以避免队列满时需要移动元素。Java 中的 Queue 接口有多种实现,包括 LinkedList 和 ArrayDeque。
  • 堆栈的底层实现:也可以使用数组或链表。使用数组实现的堆栈被称为定长堆栈,因为它们的大小是固定的。使用链表实现的堆栈被称为可变堆栈,因为它们可以动态增长。Java 中的 Stack 类使用了数组实现,而 Deque 接口的实现类 LinkedList 也可以用作堆栈。

更多后端全部八股点击👉👉【闲鱼】https://m.tb.cn/h.5yHpgkY?tk=O8bhWpn1NBD CZ8908 「我在闲鱼发布了【京985计算机硕士自用后端八股文出售,不同于市面上的几块钱八】」
点击链接直接打开

Java后端各科最全八股自用整理,获取方式见


整理不易🚀🚀,关注和收藏后拿走📌📌欢迎留言🧐👋📣
欢迎专注我的公众号AdaCoding 和 Github:AdaCoding123
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值