概念
数组(固定长度且支持基本数据类型,访问快但增删麻烦)与集合(动态长度且只能存对象
,访问需迭代但增删更灵活)区别?
- 数组是固定长度的数据结构,一旦创建长度就无法改变,而集合是动态长度的数据结构,可以根据需要动态增加或减少元素。
- 数组可以包含基本数据类型和对象,而集合只能包含对象。
- 数组可以直接访问元素,而集合需要通过迭代器或其他方法访问元素。
- 使用数组进行增加/删除元素比较麻烦,而使用集合添加/删除新元素很简洁
说说Java中的集合?
Java中的线程安全的集合是什么?(Vector 和 Hashtable 是 java.util 包中仅有的线程安全集合,而 java.util.concurrent 包提供了一系列高效的线程安全集合)
1、在 java.util 包中的线程安全的类主要 2 个,其他都是非线程安全的。
- Vector:线程安全的动态数组,
其内部方法基本都经过synchronized修饰
,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector 内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。 - Hashtable:线程安全的哈希表,HashTable 的加锁方法是
给每个方法加上 synchronized 关键字
,这样锁住的是整个 Table 对象,不支持 null 键和值,由于同步导致的性能开销,所以已经很少被推荐使用,如果要保证线程安全的哈希表,可以用ConcurrentHashMap。
2、java.util.concurrent 包提供的都是线程安全的集合
Collection(集合框架的根接口,定义了基本操作)和Collections(工具类,提供对集合的各种操作(如排序、查找、同步等))的区别
Collection是Java集合框架中的一个接口,它是所有集合类的基础接口。它定义了一组通用的操作和方法,如添加、删除、遍历等,用于操作和管理一组对象。Collection接口有许多实现类,如List、Set和Queue等。
Collections(注意有一个s)是Java提供的一个工具类,位于java.util包中。它提供了一系列静态方法,用于对集合进行操作和算法。Collections类中的方法包括排序、查找、替换、反转、随机化等等。这些方法可以对实现了Collection接口的集合进行操作,如List和Set。
集合遍历的方法有哪些?(普通 for 循环(基于索引)、增强 for 循环(简洁但不能修改结构)、和 Iterator 迭代器(适用于遍历时删除元素))
1、普通 for 循环: 可以使用带有索引的普通 for 循环来遍历 List。
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (int i = 0; i < list.size(); i++) {
String element = list.get(i);
System.out.println(element);
}
2、增强 for 循环(for-each循环): 用于循环访问数组或集合中的元素。
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (String element : list) {
System.out.println(element);
}
3、Iterator 迭代器: 可以使用迭代器来遍历集合,特别适用于需要删除元素的情况。
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class IteratorExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if ("B".equals(element)) {
iterator.remove(); // 安全删除元素
}
}
System.out.println("遍历后列表: " + list);
}
}
List
讲一下java里面list的几种实现,几种实现有什么不同?(ArrayList 和 Vector 适合随机访问,但中间插入删除性能较差,而 LinkedList 插入删除更高效,但随机访问较慢。)
- Vector 和 ArrayList 作为动态数组,其内部元素以数组形式顺序存储的,所以非常适合随机访问的场合。除了尾部插入和删除元素,往往性能会相对较差,比如我们在中间位置插入一个元素,需要移动后续所有元素。
- 而 LinkedList 进行节点插入、删除却要高效得多,但是随机访问性能则要比动态数组慢。
list可以一边遍历一边修改元素吗?(普通 for 循环可以安全修改元素;foreach 循环因基于迭代器可能导致 ConcurrentModificationException;使用迭代器时需借助迭代器 的 remove 或 set 方法来安全修改,不可直接通过List的方法修改)
在 Java 中,List在遍历过程中是否可以修改元素取决于遍历方式和具体的List实现类,以下是几种常见情况:
1、使用普通for循环遍历:可以在遍历过程中修改元素,只要修改的索引不超出List的范围即可
。
import java.util.ArrayList;
import java.util.List;
public class ListTraversalAndModification {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 使用普通for循环遍历并修改元素
for (int i = 0; i < list.size(); i++) {
list.set(i, list.get(i) * 2);
}
System.out.println(list);
}
}
2、使用foreach循环遍历:一般不建议在foreach循环中直接修改正在遍历的List元素,因为这可能会导致意外的结果或ConcurrentModificationException异常。在foreach循环中修改元素可能会破坏迭代器的内部状态,因为foreach循环底层是基于迭代器实现的,在遍历过程中修改集合结构,会导致迭代器的预期结构和实际结构不一致。
import java.util.ArrayList;
import java.util.List;
public class ListTraversalAndModification {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 使用foreach循环遍历并尝试修改元素,会抛出ConcurrentModificationException异常
for (Integer num : list) {
list.set(list.indexOf(num), num * 2);
}
System.out.println(list);
}
}
3、使用迭代器遍历:可以使用迭代器的remove方法来删除元素,但如果要修改元素的值,需要通过迭代器的set方法来进行,而不是直接通过List的set方法,否则也可能会抛出ConcurrentModificationException异常。
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ListTraversalAndModification {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 使用迭代器遍历并修改元素
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer num = iterator.next();
if (num == 2) {
// 使用迭代器的set方法修改元素
iterator.set(4);
}
}
System.out.println(list);
}
}
对于线程安全的List,如CopyOnWriteArrayList,由于其采用了写时复制的机制,在遍历的同时可以进行修改操作,不会抛出ConcurrentModificationException异常,但可能会读取到旧的数据,因为修改操作是在新的副本上进行的。
Arraylist和LinkedList的区别,哪个集合是线程安全的?(ArrayList 底层使用数组适用于随机访问和尾部操作,而 LinkedList 底层使用链表适用于频繁插入/删除但不支持高效随机访问;两者都非线程安全,Vector 是线程安全的。)
1、底层数据结构不同:ArrayList使用数组实现,通过索引进行快速访问元素。LinkedList使用链表实现,通过节点之间的指针进行元素的访问和操作。
2、插入和删除操作的效率不同:ArrayList在尾部的插入和删除操作效率较高,但在中间或开头的插入和删除操作效率较低,需要移动元素。LinkedList在任意位置的插入和删除操作效率都比较高,因为只需要调整节点之间的指针,但是LinkedList是不支持随机访问的,所以除了头结点外插入和删除的时间复杂度都是0(n),效率也不是很高所以LinkedList基本没人用。
3、随机访问的效率不同:ArrayList支持通过索引进行快速随机访问,时间复杂度为O(1)。LinkedList需要从头或尾开始遍历链表,时间复杂度为O(n)。
4、空间占用:ArrayList在创建时需要分配一段连续的内存空间,因此会占用较大的空间。LinkedList每个节点只需要存储元素和指针,因此相对较小。
ArrayList线程安全吗?(不安全)把ArrayList变成线程安全有哪些方法?
不是线程安全的,ArrayList变成线程安全的方式有:
- 1、使用Collections类的synchronizedList方法将ArrayList包装成线程安全的List:
List synchronizedList = Collections.synchronizedList(arrayList); - 2、使用CopyOnWriteArrayList类代替ArrayList,它是一个线程安全的List实现:
CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList<>(arrayList); - 3、使用Vector类代替ArrayList,Vector是线程安全的List实现:
Vector vector = new Vector<>(arrayList);
ArrayList的扩容机制说一下 (ArrayList 基本等同于Vector,但是其线程不安全(没有额外的同步机制)(执行效率高))(无参 0,10,1.5倍)(有参 指定,1.5倍)
- ArrayList中维护了一个Object类型的数组elementData.
- ArrayList 实现了 Serializable 接口,所以它本身是可以序列化的,但ArrayList 的设计者选择将这个数组声明为 transient,这样在序列化时不会直接序列化整个数组,而是通过自定义的方法只序列化实际存储的元素,这样可以节省空间 transient Object[l elementData; //transient 表示瞬间,短暂的,表示该属性不会被序列化
- 当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。
- 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容则直接扩容elementData为1.5倍。
之所以扩容是 1.5 倍,是因为 1.5 可以充分利用移位操作,减少浮点数或者运算时间和运算次数。
// 新容量计算
int newCapacity = oldCapacity + (oldCapacity >> 1);
Map
如何对map进行快速遍历?(1、 for-each + entrySet() 获取键值对,2、for-each + keySet() 遍历键,3、迭代器遍历并支持删除操作。)
使用for-each循环和entrySet()方法:这是一种较为常见和简洁的遍历方式,它可以同时获取Map中的键和值
import java.util.HashMap;
import java.util.Map;
public class MapTraversalExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("key1", 1);
map.put("key2", 2);
map.put("key3", 3);
// 使用for-each循环和entrySet()遍历Map
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
}
}
使用for-each循环和keySet()方法:如果只需要遍历Map中的键,可以使用keySet()方法,这种方式相对简单,性能也较好。
import java.util.HashMap;
import java.util.Map;
public class MapTraversalExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("key1", 1);
map.put("key2", 2);
map.put("key3", 3);
// 使用for-each循环和keySet()遍历Map的键
for (String key : map.keySet()) {
System.out.println("Key: " + key + ", Value: " + map.get(key));
}
}
}
使用迭代器:通过获取Map的entrySet()或keySet()的迭代器,也可以实现对Map的遍历,这种方式在需要删除元素等操作时比较有用。
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
public class MapTraversalExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("key1", 1);
map.put("key2", 2);
map.put("key3", 3);
// 使用迭代器遍历Map
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
}
}
HashMap实现原理介绍一下?(JDK 1.8 数组加链表,JDK 1.8 优化了 HashMap,当链表长度超过 8 时转换为红黑树(O(log n) 查询),少于 6 时恢复为链表,提高查询性能)
在 JDK 1.7 版本之前, HashMap 数据结构是数组和链表,HashMap通过哈希算法将元素的键(Key)映射到数组中的槽位(Bucket)。如果多个键映射到同一个槽位,它们会以链表的形式存储在同一个槽位上,因为链表的查询时间是O(n),所以冲突很严重,一个索引上的链表非常长,效率就很低了。
所以在 JDK 1.8 版本的时候做了优化,当一个链表的长度超过8的时候就转换数据结构,不再使用链表存储,而是使用红黑树,查找时使用红黑树,时间复杂度O(log n),可以提高查询性能,但是在数量较少时,即数量小于6时,会将红黑树转换回链表。
了解的哈希冲突解决方法有哪些?(1、链接法(哈希表加链表,链表存储冲突键值对),2、开放寻址法(寻找空槽存储冲突键值对)
- 链接法:使用链表或其他数据结构来存储冲突的键值对,将它们链接在同一个哈希桶中。
- 开放寻址法:在哈希表中找到另一个可用的位置来存储冲突的键值对,而不是存储在链表中。常见的开放寻址方法包括线性探测、二次探测、再哈希法。
hashmap的put过程介绍一下(先计算键的哈希值确定存储位置,若无冲突则直接存储,若有冲突则遍历链表或红黑树查找相同键并更新值或新增节点,链表长度超阈值8时转红黑树,元素数量超负载因子0.75时触发扩容)
HashMap HashMap的put()方法用于向HashMap中添加键值对,当调用HashMap的put()方法时,会按照以下详细流程执行(JDK8 1.8版本):
-
第一步:根据要添加的键的哈希码计算在数组中的位置(索引)。
-
第二步:检查该位置是否为空(即没有键值对存在)
如果为空,则直接在该位置创建一个新的Entry对象来存储键值对。将要添加的键值对作为该Entry的键和值,并保存在数组的对应位置。将HashMap的修改次数(modCount)加1,以便在进行迭代时发现并发修改。 -
第三步:如果该位置已经存在其他键值对,检查该位置的第一个键值对的哈希码和键是否与要添加的键值对相同?
如果相同,则表示找到了相同的键,直接将新的值替换旧的值,完成更新操作。 -
第四步:如果第一个键值对的哈希码和键不相同,则需要遍历链表或红黑树来查找是否有相同的键:
如果键值对集合是链表结构,从链表的头部开始逐个比较键的哈希码和equals()方法,直到找到相同的键或达到链表末尾。
如果找到了相同的键,则使用新的值取代旧的值,即更新键对应的值。
如果没有找到相同的键,则将新的键值对添加到链表的头部。
如果键值对集合是红黑树结构,在红黑树中使用哈希码和equals()方法进行查找。根据键的哈希码,定位到红黑树中的某个节点,然后逐个比较键,直到找到相同的键或达到红黑树末尾。
如果找到了相同的键,则使用新的值取代旧的值,即更新键对应的值。
如果没有找到相同的键,则将新的键值对添加到红黑树中。 -
第五步:检查链表长度是否达到阈值(默认为8):
如果链表长度超过阈值,且HashMap的数组长度大于等于64,则会将链表转换为红黑树,以提高查询效率。 -
第六步:检查负载因子是否超过阈值(默认为0.75):
如果键值对的数量(size)与数组的长度的比值大于阈值,则需要进行扩容操作。 -
第七步:扩容操作:
创建一个新的两倍大小的数组。
将旧数组中的键值对重新计算哈希码并分配到新数组中的位置。
更新HashMap的数组引用和阈值参数。 -
第八步:完成添加操作。
HashMap一般用什么做Key?为啥String适合做Key呢?
用 string 做 key,因为 String对象是不可变的,一旦创建就不能被修改,这确保了Key的稳定性。
如果Key是可变的,可能会导致hashCode和equals方法的不一致,进而影响HashMap的正确性。
为什么HashMap要用红黑树而不是平衡二叉树?(因为红黑树的“弱平衡”特性在保证较快查找(O(log n))的同时,插入和删除时的旋转调整次数更少,性能更优)
平衡二叉树追求的是一种 “完全平衡” 状态:任何结点的左右子树的高度差不会超过 1,优势是树的结点是很平均分配的。这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树。
红黑树不追求这种完全平衡状态,而是追求一种 “弱平衡” 状态:整个树最长路径不会超过最短路径的 2 倍。优势是虽然牺牲了一部分查找的性能效率,但是能够换取一部分维持树平衡状态的成本。与平衡树不同的是,红黑树在插入、删除等操作,不会像平衡树那样,频繁着破坏红黑树的规则,所以不需要频繁着调整,这也是我们为什么大多数情况下使用红黑树的原因。
hashmap key可以为null吗?(HashMap 的 key 可以为 null,且只能有一个(如果key值一样,那么会覆盖相同key值的value为最新),value可以有多个 null,key 为 null 时哈希值为 0,不调用 key.hashCode() 方法)
可以为 null。
hashMap中使用hash()方法来计算key的哈希值,当key为空时,直接另key的哈希值为0,不走key.hashCode()方法;
- hashMap虽然支持key和value为null,但是null作为key只能有一个,null作为value可以有多个;
- 因为hashMap中,如果key值一样,那么会覆盖相同key值的value为最新,所以key为null只能有一个。
重写HashMap的equal和hashcode方法需要注意什么?(需保证equals()一致的对象hashCode()也一致(存储时,先hashCode(),再equals()),eg:散列冲突的情况)(equals()重写不当hashCode()但未同步,可能导致HashMap判别键不同,存储多个对象相同,或判别键相同,导致数据修改覆盖或查找失败)
HashMap使用Key对象的hashCode()和equals方法去决定key-value对的索引。当我们试着从HashMap中获取值的时候,这些方法也会被用到。如果这些方法没有被正确地实现,在这种情况下,两个不同Key也许会产生相同的hashCode()和equals()输出,HashMap将会认为它们是相同的,然后覆盖它们,而非把它们存储到不同的地方。
同样的,所有不允许存储重复数据的集合类都使用hashCode()和equals()去查找重复,所以正确实现它们非常重要。equals()和hashCode()的实现应该遵循以下规则:
如果o1.equals(o2),那么o1.hashCode() == o2.hashCode()总是为true的。
如果o1.hashCode() == o2.hashCode(),并不意味着o1.equals(o2)会为true。
HashMap的扩容机制介绍一下(0,16,2倍(看阈值,为其0.75,实际12时候要扩容))(扩容时,容量翻倍,元素或者保持原索引,或者移动到“原索引+旧容量”提高,避免重新计算哈希,提高效率并减少冲突)
ConcurrentHashMap怎么实现的?
JDK 1.7 ConcurrentHashMap
在 JDK 1.7 中它使用的是数组加链表的形式实现的,而数组又分为:大数组 Segment 和小数组 HashEntry。 Segment 是一种可重入锁(ReentrantLock),在 ConcurrentHashMap 里扮演锁的角色;HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组,一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素。
JDK 1.7 ConcurrentHashMap 分段锁技术将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。
JDK 1.8 ConcurrentHashMap
在 JDK 1.7 中,ConcurrentHashMap 虽然是线程安全的,但因为它的底层实现是数组 + 链表的形式,所以在数据比较多的情况下访问是很慢的,因为要遍历整个链表,而 JDK 1.8 则使用了数组 + 链表/红黑树的方式优化了 ConcurrentHashMap 的实现,具体实现结构如下:
JDK 1.8 ConcurrentHashMap JDK 1.8 ConcurrentHashMap 主要通过 volatile + CAS 或者 synchronized 来实现的线程安全的。添加元素时首先会判断容器是否为空:
如果为空则使用 volatile 加 CAS 来初始化
如果容器不为空,则根据存储的元素计算该位置是否为空。
如果根据存储的元素计算结果为空,则利用 CAS 设置该节点;
如果根据存储的元素计算结果不为空,则使用 synchronized ,然后,遍历桶中的数据,并替换或新增节点到桶中,最后再判断是否需要转为红黑树,这样就能保证并发访问时的线程安全了。
如果把上面的执行用一句话归纳的话,就相当于是ConcurrentHashMap通过对头结点加锁来保证线程安全的,锁的粒度相比 Segment 来说更小了,发生冲突和加锁的频率降低了,并发操作的性能就提高了。
而且 JDK 1.8 使用的是红黑树优化了之前的固定链表,那么当数据量比较大的时候,查询性能也得到了很大的提升,从之前的 O(n) 优化到了 O(logn) 的时间复杂度。
分段锁怎么加锁的?
在 ConcurrentHashMap 中,将整个数据结构分为多个 Segment,每个 Segment 都类似于一个小的 HashMap,每个 Segment 都有自己的锁,不同 Segment 之间的操作互不影响,从而提高并发性能。
在 ConcurrentHashMap 中,对于插入、更新、删除等操作,需要先定位到具体的 Segment,然后再在该 Segment 上加锁,而不是像传统的 HashMap 一样对整个数据结构加锁。这样可以使得不同 Segment 之间的操作并行进行,提高了并发性能。
#分段锁是可重入的吗?
JDK 1.7 ConcurrentHashMap中的分段锁是用了 ReentrantLock,是一个可重入的锁。
#已经用了synchronized,为什么还要用CAS呢?
ConcurrentHashMap使用这两种手段来保证线程安全主要是一种权衡的考虑,在某些操作中使用synchronized,还是使用CAS,主要是根据锁竞争程度来判断的。
比如:在putVal中,如果计算出来的hash槽没有存放元素,那么就可以直接使用CAS来进行设置值,这是因为在设置元素的时候,因为hash值经过了各种扰动后,造成hash碰撞的几率较低,那么我们可以预测使用较少的自旋来完成具体的hash落槽操作。
当发生了hash碰撞的时候说明容量不够用了或者已经有大量线程访问了,因此这时候使用synchronized来处理hash碰撞比CAS效率要高,因为发生了hash碰撞大概率来说是线程竞争比较强烈。
#ConcurrentHashMap用了悲观锁还是乐观锁?
悲观锁和乐观锁都有用到。
添加元素时首先会判断容器是否为空:
如果为空则使用 volatile 加 CAS (乐观锁) 来初始化。
如果容器不为空,则根据存储的元素计算该位置是否为空。
如果根据存储的元素计算结果为空,则利用 CAS(乐观锁) 设置该节点;
如果根据存储的元素计算结果不为空,则使用 synchronized(悲观锁) ,然后,遍历桶中的数据,并替换或新增节点到桶中,最后再判断是否需要转为红黑树,这样就能保证并发访问时的线程安全了。
#HashTable 底层实现原理是什么?
img
Hashtable的底层数据结构主要是数组加上链表,数组是主体,链表是解决hash冲突存在的。
HashTable是线程安全的,实现方式是Hashtable的所有公共方法均采用synchronized关键字,当一个线程访问同步方法,另一个线程也访问的时候,就会陷入阻塞或者轮询的状态。
#HashTable线程安全是怎么实现的?
因为它的put,get做成了同步方法,保证了Hashtable的线程安全性,每个操作数据的方法都进行同步控制之后,由此带来的问题任何一个时刻只能有一个线程可以操纵Hashtable,所以其效率比较低。
Hashtable 的 put(K key, V value) 和 get(Object key) 方法的源码:
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings(“unchecked”)
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
可以看到,Hashtable是通过使用了 synchronized 关键字来保证其线程安全。
在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。
#hashtable 和concurrentHashMap有什么区别
底层数据结构:
jdk7之前的ConcurrentHashMap底层采用的是分段的数组+链表实现,jdk8之后采用的是数组+链表/红黑树;
HashTable采用的是数组+链表,数组是主体,链表是解决hash冲突存在的。
实现线程安全的方式:
jdk8以前,ConcurrentHashMap采用分段锁,对整个数组进行了分段分割,每一把锁只锁容器里的一部分数据,多线程访问不同数据段里的数据,就不会存在锁竞争,提高了并发访问;jdk8以后,直接采用数组+链表/红黑树,并发控制使用CAS和synchronized操作,更加提高了速度。
HashTable:所有的方法都加了锁来保证线程安全,但是效率非常的低下,当一个线程访问同步方法,另一个线程也访问的时候,就会陷入阻塞或者轮询的状态。
#说一下HashMap和Hashtable、ConcurrentMap的区别
HashMap线程不安全,效率高一点,可以存储null的key和value,null的key只能有一个,null的value可以有多个。默认初始容量为16,每次扩充变为原来2倍。创建时如果给定了初始容量,则扩充为2的幂次方大小。底层数据结构为数组+链表,插入元素后如果链表长度大于阈值(默认为8),先判断数组长度是否小于64,如果小于,则扩充数组,反之将链表转化为红黑树,以减少搜索时间。
HashTable线程安全,效率低一点,其内部方法基本都经过synchronized修饰,不可以有null的key和value。默认初始容量为11,每次扩容变为原来的2n+1。创建时给定了初始容量,会直接用给定的大小。底层数据结构为数组+链表。它基本被淘汰了,要保证线程安全可以用ConcurrentHashMap。
ConcurrentHashMap是Java中的一个线程安全的哈希表实现,它可以在多线程环境下并发地进行读写操作,而不需要像传统的HashTable那样在读写时加锁。ConcurrentHashMap的实现原理主要基于分段锁和CAS操作。它将整个哈希表分成了多Segment(段),每个Segment都类似于一个小的HashMap,它拥有自己的数组和一个独立的锁。在ConcurrentHashMap中,读操作不需要锁,可以直接对Segment进行读取,而写操作则只需要锁定对应的Segment,而不是整个哈希表,这样可以大大提高并发性能。
Set
Set集合有什么特点?如何实现key无重复的?
- set集合特点:Set集合中的元素是唯一的,不会出现重复的元素。
- set实现原理:Set集合通过内部的数据结构(如哈希表、红黑树等)来实现key的无重复。当向Set集合中插入元素时,会先根据元素的hashCode值来确定元素的存储位置,然后再通过equals方法来判断是否已经存在相同的元素,如果存在则不会再次插入,保证了元素的唯一性。
有序的Set是什么?记录插入顺序的集合是什么?
有序的 Set 是TreeSet和LinkedHashSet。TreeSet是基于红黑树实现,保证元素的自然顺序。LinkedHashSet是基于双重链表和哈希表的结合来实现元素的有序存储,保证元素添加的自然顺序
记录插入顺序的集合通常指的是LinkedHashSet,它不仅保证元素的唯一性,还可以保持元素的插入顺序。当需要在Set集合中记录元素的插入顺序时,可以选择使用LinkedHashSet来实现。