eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

Partner – LambdaTest – NPI EA (cat= Testing)
announcement - icon

Distributed systems often come with complex challenges such as service-to-service communication, state management, asynchronous messaging, security, and more.

Dapr (Distributed Application Runtime) provides a set of APIs and building blocks to address these challenges, abstracting away infrastructure so we can focus on business logic.

In this tutorial, we'll focus on Dapr's pub/sub API for message brokering. Using its Spring Boot integration, we'll simplify the creation of a loosely coupled, portable, and easily testable pub/sub messaging system:

>> Flexible Pub/Sub Messaging With Spring Boot and Dapr

1. Overview

When managing key-value pairs in a Java application, we often find ourselves considering two main options: Hashtable and ConcurrentHashMap.

While both collections offer the advantage of thread safety, their underlying architectures and capabilities significantly differ. Whether we’re building a legacy system or working on modern, microservices-based cloud applications, understanding these nuances is critical for making the right choice.

In this tutorial, we’ll dissect the differences between Hashtable and ConcurrentHashMap, delving into their performance metrics, synchronization features, and various other aspects to help us make an informed decision.

2. Hashtable

Hashtable is one of the oldest collection classes in Java and has been present since JDK 1.0. It provides key-value storage and retrieval APIs:

Hashtable<String, String> hashtable = new Hashtable<>();
hashtable.put("Key1", "1");
hashtable.put("Key2", "2");
hashtable.putIfAbsent("Key3", "3");
String value = hashtable.get("Key2");

The primary selling point of Hashtable is thread safety, which is achieved through method-level synchronization.

Methods like put(), putIfAbsent(), get(), and remove() are synchronized. Only one thread can execute any of these methods at a given time on a Hashtable instance, ensuring data consistency.

3. ConcurrentHashMap

ConcurrentHashMap is a more modern alternative, introduced with the Java Collections Framework as part of Java 5.

Both Hashtable and ConcurrentHashMap implement the Map interface, which accounts for the similarity in method signatures:

ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>();
concurrentHashMap.put("Key1", "1");
concurrentHashMap.put("Key2", "2");
concurrentHashMap.putIfAbsent("Key3", "3");
String value = concurrentHashMap.get("Key2");

4. Differences

In this section, we’ll examine key aspects that set Hashtable and ConcurrentHashMap apart, including concurrency, performance, and memory usage.

4.1. Concurrency

As we discussed earlier, Hashtable achieves thread safety through method-level synchronization.

ConcurrentHashMap, on the other hand, provides thread safety with a higher level of concurrency. It allows multiple threads to read and perform limited writes simultaneously without locking the entire data structure. This is especially useful in applications that have more read operations than write operations.

4.2. Performance

While both Hashtable and ConcurrentHashMap guarantee thread safety, they differ in performance due to their underlying synchronization mechanisms.

Hashtable locks the entire table during a write operation, thereby preventing other reads or writes. This could be a bottleneck in a high-concurrency environment.

ConcurrentHashMap, however, allows concurrent reads and limited concurrent writes, making it more scalable and often faster in practice.

Differences in performance numbers may not be noticeable for small datasets. However, ConcurrentHashMap often shows its strength with larger datasets and higher levels of concurrency.

To substantiate performance numbers, let’s run benchmark tests using JMH (the Java Microbenchmark Harness), which uses 10 threads to simulate concurrent activity and performs three warm-up iterations followed by five measurement iterations. It measures the average time taken by each benchmark method, indicating the average execution time:

@Benchmark
@Group("hashtable")
public void benchmarkHashtablePut() {
    for (int i = 0; i < 10000; i++) {
        hashTable.put(String.valueOf(i), i);
    }
}

@Benchmark
@Group("hashtable")
public void benchmarkHashtableGet(Blackhole blackhole) {
    for (int i = 0; i < 10000; i++) {
        Integer value = hashTable.get(String.valueOf(i));
        blackhole.consume(value);
    }
}

@Benchmark
@Group("concurrentHashMap")
public void benchmarkConcurrentHashMapPut() {
    for (int i = 0; i < 10000; i++) {
        concurrentHashMap.put(String.valueOf(i), i);
    }
}

@Benchmark
@Group("concurrentHashMap")
public void benchmarkConcurrentHashMapGet(Blackhole blackhole) {
    for (int i = 0; i < 10000; i++) {
        Integer value = concurrentHashMap.get(String.valueOf(i));
        blackhole.consume(value);
    }
}

Here are the test results:

Benchmark                                                        Mode  Cnt   Score   Error
BenchMarkRunner.concurrentHashMap                                avgt    5   1.788 ± 0.406
BenchMarkRunner.concurrentHashMap:benchmarkConcurrentHashMapGet  avgt    5   1.157 ± 0.185
BenchMarkRunner.concurrentHashMap:benchmarkConcurrentHashMapPut  avgt    5   2.419 ± 0.629
BenchMarkRunner.hashtable                                        avgt    5  10.744 ± 0.873
BenchMarkRunner.hashtable:benchmarkHashtableGet                  avgt    5  10.810 ± 1.208
BenchMarkRunner.hashtable:benchmarkHashtablePut                  avgt    5  10.677 ± 0.541

Benchmark results provide insights into the average execution times of specific methods for both Hashtable and ConcurrentHashMap.

Lower scores indicate better performance, and the results show that, on average, ConcurrentHashMap outperforms Hashtable for both get() and put() operations. 

4.3. Hashtable Iterators

Hashtable iterators are “fail-fast”, which means that if the structure of the Hashtable is modified after an iterator has been created, the iterator will throw a ConcurrentModificationException. This mechanism helps prevent unpredictable behavior by failing quickly when concurrent modifications are detected.

In the example below, we have a Hashtable containing three key-value pairs, and we initiate two threads:

  • iteratorThread: iterates through the Hashtable keys and prints them with 100 milliseconds delay
  • modifierThread: waits for 50 milliseconds and then adds a new key-value pair to the Hashtable

When modifierThread adds a new key-value pair to the Hashtable, iteratorThread throws a ConcurrentModificationException, indicating that the Hashtable structure was modified while the iteration was in progress:

Hashtable<String, Integer> hashtable = new Hashtable<>();
hashtable.put("Key1", 1);
hashtable.put("Key2", 2);
hashtable.put("Key3", 3);
AtomicBoolean exceptionCaught = new AtomicBoolean(false);

Thread iteratorThread = new Thread(() -> {
    Iterator<String> it = hashtable.keySet().iterator();
    try {
        while (it.hasNext()) {
            it.next();
            Thread.sleep(100);
        }
    } catch (ConcurrentModificationException e) {
        exceptionCaught.set(true);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

Thread modifierThread = new Thread(() -> {
    try {
        Thread.sleep(50);
        hashtable.put("Key4", 4);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

iteratorThread.start();
modifierThread.start();

iteratorThread.join();
modifierThread.join();

assertTrue(exceptionCaught.get());

4.4. ConcurrentHashMap Iterators

In contrast to Hashtable, which uses “fail-fast” iterators, ConcurrentHashMap employs “weakly consistent” iterators.

These iterators can withstand concurrent modifications to the original map, reflecting the state of the map at the time the iterator was created. They might also reflect further changes but aren’t guaranteed to do so. Therefore, we can modify ConcurrentHashMap in one thread while iterating over it in another without getting a ConcurrentModificationException.

The example below demonstrates the weakly consistent nature of iterators in ConcurrentHashMap:

  • iteratorThread: iterates through the ConcurrentHashMap keys and prints them with 100 milliseconds delay
  • modifierThread: waits for 50 milliseconds and then adds a new key-value pair to the ConcurrentHashMap

Unlike Hashtable “fail-fast” iterators, the weakly consistent iterator here doesn’t throw a ConcurrentModificationException. The iterator in iteratorThread continues without any issues, showcasing how ConcurrentHashMap is designed for high-concurrency scenarios:

ConcurrentHashMap<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();
concurrentHashMap.put("Key1", 1);
concurrentHashMap.put("Key2", 2);
concurrentHashMap.put("Key3", 3);
AtomicBoolean exceptionCaught = new AtomicBoolean(false);

Thread iteratorThread = new Thread(() -> {
    Iterator<String> it = concurrentHashMap.keySet().iterator();
    try {
        while (it.hasNext()) {
            it.next();
            Thread.sleep(100);
        }
    } catch (ConcurrentModificationException e) {
        exceptionCaught.set(true);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

Thread modifierThread = new Thread(() -> {
    try {
        Thread.sleep(50);
        concurrentHashMap.put("Key4", 4);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

iteratorThread.start();
modifierThread.start();

iteratorThread.join();
modifierThread.join();

assertFalse(exceptionCaught.get());

4.5. Memory

Hashtable uses a simple data structure, essentially an array of linked lists. Each bucket in this array stores one key-value pair, so there’s only the overhead of the array itself and the linked list nodes. There are no additional internal data structures to manage the concurrency level, load factor, or other advanced functionalities. Thus Hashtable consumes less memory overall.

ConcurrentHashMap is more complex and consists of an array of segments, which is essentially a separate Hashtable. This allows it to perform certain operations concurrently but also consumes additional memory for these segment objects.

For each segment, it maintains extra information, such as count, threshold, load factor, etc., which increases its memory footprint. It dynamically adjusts the number of segments and their sizes to accommodate more entries and reduce collision, which means it has to keep additional metadata to manage these, leading to further memory consumption.

5. Conclusion

In this article, we learned the differences between Hashtable and ConcurrentHashMap.

Both Hashtable and ConcurrentHashMap serve the purpose of storing key-value pairs in a thread-safe manner. However, we saw that ConcurrentHashMap usually has the upper hand in terms of performance and scalability due to its advanced synchronization features.

Hashtable is still useful and might be preferable in legacy systems or scenarios where method-level synchronization is explicitly required. Understanding the specific needs of our application can help us make a more informed decision between these two.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook Jackson – NPI EA – 3 (cat = Jackson)