在一次面试中,面试官问我ConCurrentHashMap和HashTable有什么不同。我只是想讨论面试官不相信的一点。我说在ConCurrentHashMap中,任何数量的线程都可以同时执行读取操作,而在HashTable中,一次只能执行一个线程。然后他给出了一个ConCurrentHashMap的场景,假设一个线程在一个段上写入,同时另一个线程正在读取它的值,第二个线程会被阻塞吗??我说不,但他不相信。我在javadoc中检查了一下,上面写着…
检索操作(包括get)通常不会阻塞,因此可能与更新操作(包括put和删除)重叠。检索反映了最近完成的更新操作在开始时保持的结果。
它说检索操作不会阻塞,而是添加了一般
这是什么意思??
为此,我做了一个程序,其中两个线程对ConCurrentHashMap和HashTable以及同步Map执行读写操作:我在同一段上执行读写操作一秒钟
class ReaderThread extends Thread {
private Map<String, Integer> map;
private static boolean b = false;
public ReaderThread(Map<String, Integer> map) {
this.map = map;
}
public void run() {
long startTime = System.nanoTime();
long endTime = 0;
while (!b) {
map.get("A");
endTime = System.nanoTime();
if (endTime - startTime > 1000000000L)
b = true;
}
}
}
class WriterThread extends Thread {
private Map<String, Integer> map;
private static int n = 0;
private static boolean b = false;
public WriterThread(Map<String, Integer> map) {
this.map = map;
}
public void run() {
long startTime = System.nanoTime();
long endTime = 0;
while (!b) {
map.put("A", n++);
endTime = System.nanoTime();
if (endTime - startTime > 1000000000L)
b = true;
}
}
}
public class DiffrentMapReadWritePerformanceTest {
public static void main(String[] args) throws InterruptedException {
Map<String, Integer> map = new ConcurrentHashMap<>();
// Map<String, Integer> map = new Hashtable<>();
// Map<String, Integer> map = new HashMap<>();
// map = Collections.synchronizedMap(map);
Thread readerThread = new ReaderThread(map);
Thread writerThread = new WriterThread(map);
writerThread.start();
readerThread.start();
writerThread.join();
readerThread.join();
System.out.println(map.get("A"));
}
}
和O/P基于不同的映射对象:ConloctHashMap:8649407 Hashtable:5284068同步Map:5438039
因此,输出证明在多线程环境中,与HashTable相比,ConCurrentHashMap更快,但不能证明在写入线程写入时,读取线程没有被阻塞以进行读取。有什么方法可以确认这一点吗??
该JavaDoc注释是为该类的最早版本编写的。它提供了一些实现灵活性并允许演进。
在非常早期的版本中,size()会乐观地对段求和,但回退到锁定以读取计数器。类似地,当映射不存在并且需要检查以解决可能的编译器重新排序时,使用readValueUnderLock。例如,这个问题由Java内存模型解决,该模型同时设计用于保证编译器可以做什么和不可以做什么。
在Java8中,哈希表从粗段重新设计为细粒度的箱。然而,computeIfAbsend
总是锁定以读取或计算值,这是悲观的。在Java9中,通过避免当条目位于箱头时加锁,但如果没有,则加锁进行扫描,从而改进了这一点。在缓存之类的情况下,这可能不够好,因此Caffeine总是在计算之前执行乐观的get
。在这些情况下,这可以显着提高性能。
这意味着检索可能会发生锁定,但随着设计的发展可能不会避免它。模糊的措辞允许在实现更改时保留类契约。
据我所知,我可能错了,我会说你是对的。get操作没有获得任何锁,但它保持了更新的发生顺序。这意味着在get之前发生的任何更新都应该反映在该检索中。另一方面,put操作确实在每个bin的基础上获得了锁,其中bin是通过散列对象值获得的。所以对hashmap的写入/更新需要锁定。正如javadocs中指出的,尽管线程同时访问同一个bin的概率很低。