HashTable详解(面试)
1.HashTable的线程安全:
HashTable
被认为是线程安全的,因为它的几乎所有公共方法都是同步的(使用 synchronized
关键字)。这意味着在一个时间点内,只有一个线程可以访问 HashTable
的一个实例的任一方法,这避免了多线程环境中的并发修改异常和数据不一致的问题。下面是一些详细说明:
1.synchronized 关键字
在 HashTable
的实现中,关键操作如 put
、get
、remove
等方法都被声明为 synchronized
。这是 Java 提供的一种基本的线程同步机制,可以确保同一时刻只有一个线程能执行这个方法。如果有多个线程尝试访问同一个 HashTable
的同步方法,那么这些线程会被阻塞,直到持有锁的线程释放锁。
2.线程安全的代价
尽管 synchronized
提供了线程安全,但它也带来了性能上的代价。每次只允许一个线程操作 HashTable
,这意味着即使是读取操作(通常很快并且不会修改数据),也必须等待其他线程完成它们的操作。这在高并发的应用中可能成为性能瓶颈。
3.Hashtable 与 ConcurrentHashMap
在 Java 中,虽然 HashTable
提供了线程安全,但它的设计已经较为过时。Java 1.5 引入了 ConcurrentHashMap
,这是一个更现代的线程安全哈希表实现,它使用了分段锁(Segmentation)。ConcurrentHashMap
允许多个读取操作和一定数量的更新操作并发进行,显著提高了性能,尤其是在多处理器系统上。
4.HashTable线程安全的原因(代码分析):
public synchronized V put(K key, V value) {
// 确保键和值不为 null
if (key == null || value == null) {
throw new NullPointerException();
}
// 计算 key 的哈希码
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % table.length;
// 遍历链表,查找键是否已存在
for (Entry<K,V> e = table[index]; e != null; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
// 如果键不存在,添加新的条目
addEntry(hash, key, value, index);
return null;
}
2.HashTable计算一次hash:
HashTable
是 Java 早期的一个组件,它为每个键计算哈希码,然后通过一定的哈希函数(通常是取模运算)将这个哈希码映射到一个数组索引上。这种方法简单直接,但在遇到哈希冲突时,其处理方式较为基础,通常采用链地址法来解决冲突,即在同一个数组索引处形成一个链表来存储具有相同哈希索引的所有元素。
3.HashTable示例代码:
package mianshi_exercise;
import java.util.Hashtable;
import java.util.Map;
public class hashtable {
public static void main(String[] args) {
// 创建一个 Hashtable
Hashtable<String, Integer> table = new Hashtable<>();
// 使用多线程向 Hashtable 添加元素
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
table.put("Thread1:" + i, i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i = 1000; i++) {
table.put("Thread2:" + i, i);
}
});
// 启动线程
thread1.start();
thread2.start();
// 等待线程完成
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出 Hashtable 的大小
System.out.println("Total entries in Hashtable: " + table.size());
// 遍历并打印 Hashtable 的所有键值对
for (Map.Entry<String, Integer> entry : table.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
输出结果:
Thread2:804: 804
Thread2:803: 803
Thread2:802: 802
Thread2:801: 801
Thread2:800: 800
Thread1:739: 739
Thread1:738: 738
Thread1:737: 737
Thread1:736: 736
Thread1:735: 735
Thread1:734: 734
Thread1:733: 733
Thread1:732: 732
Thread1:731: 731
Thread1:730: 730
1. 线程调度的不确定性
多线程环境中,线程的执行顺序和具体的调度由操作系统的调度器控制,这意味着两个并发运行的线程的执行顺序是不确定的。因此,thread1
和 thread2
向 Hashtable
添加元素的顺序取决于它们各自获得CPU时间的时刻。这就是为什么输出中的条目顺序看起来是随机的,因为这两个线程是并发执行和更新 Hashtable
的。
2. 线程竞争
尽管 Hashtable
的 put
方法是同步的,但这只保证了单个方法调用的线程安全,即在任何给定时间只有一个线程可以执行 put
操作。但是,这并不意味着两个线程中的操作是连续执行的。例如,thread1
可能添加了几个元素,然后操作系统切换到 thread2
,它接着添加了自己的一些元素,然后再切换回 thread1
,如此这般。
3. Hashtable 的内部结构
Hashtable
使用哈希表存储键值对。每个键通过哈希函数计算出一个索引,键值对存储在这个索引指向的位置。这个过程也影响了键值对在表中的物理存储位置,而且因为哈希冲突和处理方式的不同,即使是连续添加的元素,也可能被存储在哈希表的任意位置。在遍历 Hashtable
时,元素的遍历顺序将是它们在内部存储结构中的顺序,而不是添加顺序。
4. 遍历顺序
当您打印 Hashtable
的内容时,使用的是 entrySet()
方法来遍历所有的条目。这个遍历的顺序是基于哈希表中的桶的顺序,而不是键值对插入的顺序。因此,打印出来的结果是根据哈希表的内部状态而定的,而与插入顺序无关。
输出中键值对的顺序是由多个因素共同作用的结果,包括线程的并发执行、操作系统的线程调度策略、Hashtable
的同步机制、以及哈希表本身的内部结构。这些因素共同导致了输出的无序性。如果需要保持插入顺序,可以考虑使用如 LinkedHashMap
这样的数据结构,它在保持哈希表的查找效率的同时,还能通过链表保持元素的插入顺序。
4.使用 ConcurrentHashMap
和 Collections.synchronizedMap()
代码示例:
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
public class SynchronizedOrderedMap {
public static void main(String[] args) throws InterruptedException {
// 创建一个线程安全的 LinkedHashMap
Map<String, Integer> map = Collections.synchronizedMap(new LinkedHashMap<>());
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
map.put("Thread1:" + i, i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
map.put("Thread2:" + i, i);
}
});
thread1.start();
thread2.start();
// 等待线程完成
thread1.join();
thread2.join();
// 输出 Map 的内容
synchronized (map) {
map.forEach((key, value) -> System.out.println(key + ": " + value));
}
}
}
输出结果:
Thread2:989: 989
Thread2:990: 990
Thread2:991: 991
Thread2:992: 992
Thread2:993: 993
Thread2:994: 994
Thread2:995: 995
Thread2:996: 996
Thread2:997: 997
Thread2:998: 998
Thread2:999: 999
在 LinkedHashMap
中,键值对的插入顺序并不是基于桶的顺序,而是基于链表的顺序。这一点与普通的 HashMap
的工作方式不同,后者仅基于哈希表桶的顺序。让我们详细探讨这一点。
LinkedHashMap
的工作原理
LinkedHashMap
继承自 HashMap
并添加了一个维护顺序的双向链表。这个链表确保了元素的迭代顺序,它可以是插入顺序(默认)或访问顺序(如果在构造器中指定了 accessOrder
为 true
)。
- 哈希桶机制:和
HashMap
一样,LinkedHashMap
通过键的哈希值确定其在哈希表中的桶位置。如果多个键具有相同的哈希值或不同的哈希值导致的哈希碰撞,它们将被放在同一个桶中。 - 双向链表维护顺序:
LinkedHashMap
的每个条目(entry)不仅存储在哈希表中,还被插入到一个双向链表中。这个链表不关心键的哈希值或桶的位置;它仅按照条目被添加到LinkedHashMap
中的顺序(或根据访问顺序)来维护顺序。
插入顺序和迭代顺序
- 插入顺序:在
LinkedHashMap
的默认配置下,条目将按照它们被添加到映射中的顺序来排列。这意味着即使两个键映射到相同的桶,它们在链表中的顺序是根据它们插入的顺序来确定的。 - 访问顺序:如果构造
LinkedHashMap
时指定了accessOrder
为true
,迭代顺序将是最近最少使用(LRU)顺序。也就是说,最近被访问的条目会被移动到链表的末尾。
实际应用
因此,在 LinkedHashMap
中,键值对的插入顺序与桶的顺序无关。即使两个条目落在同一个桶中,它们在迭代时的顺序仍然反映了它们被插入映射的顺序。这一点对于需要保持插入顺序的情况非常有用,比如缓存实现、保持插入顺序的记录等。
总结来说,LinkedHashMap
是 HashMap
的一个扩展,它通过一个内部的双向链表来额外维护键值对的顺序,而这个顺序与键值对在哈希表中的桶位置无关。
5.HashMap和HashTable的区别
HashMap
和 HashTable
都是 Java 集合框架中的组件,用于存储键值对,但它们有几个关键的区别,这些区别影响了它们的使用场景和性能:
1. 线程安全
HashTable
:是线程安全的,因为它的几乎所有公共方法都是同步的。这使得HashTable
在多线程环境中不会出现数据一致性的问题,但同时也意味着性能可能会受到影响,因为多个线程不能并发地访问HashTable
。HashMap
:不是线程安全的,没有同步措施来控制对数据结构的并发访问。如果在多线程环境中使用HashMap
,而不适当地同步,可能会导致数据不一致。为了在多线程环境中使用,可以用Collections.synchronizedMap
包装HashMap
或者使用ConcurrentHashMap
。
2. 性能
HashTable
:由于同步方法,HashTable
的性能通常比HashMap
差,特别是在高并发的场景中。HashMap
:由于没有同步开销,HashMap
的性能通常优于HashTable
。
3. null 键和 null 值
HashTable
:不允许键或值为 null。尝试插入 null 键或 null 值会抛出NullPointerException
。HashMap
:允许一个 null 键和多个 null 值,这使得它在某些使用场景下更为灵活。
4. 继承的类
HashTable
:继承自Dictionary
类。HashMap
:继承自AbstractMap
类并实现了Map
接口。
5. 迭代器
HashTable
:迭代器(Enumerator
)在迭代过程中不是快速失败的;如果迭代过程中修改了HashTable
,迭代器的行为可能是不确定的。HashMap
:提供的迭代器(Iterator
)是快速失败的,这意味着在迭代过程中如果修改了HashMap
,迭代器会立即抛出ConcurrentModificationException
。
6. 推荐使用
HashTable
:由于现代 Java 集合框架提供了更好的替代品(如ConcurrentHashMap
),通常不再推荐使用HashTable
。HashMap
:是现代 Java 应用中使用最广泛的映射实现之一,除非需要并发访问,否则通常推荐使用HashMap
。
总结
选择 HashTable
还是 HashMap
取决于您的具体需求。如果您需要保证线程安全,可以考虑 ConcurrentHashMap
。如果线程安全不是问题,并且需要高性能及接受 null 值,HashMap
是更好的选择。