Skip to content

Reflection, Generics Type Erasure, ConcurrentHashMap Internals, and Thread Contention

How ConcurrentHashMap Achieves Thread Safety

Section titled “How ConcurrentHashMap Achieves Thread Safety”

Unlike Hashtable, which synchronizes on the entire map for every operation, ConcurrentHashMap (Java 8+) avoids locking the whole structure:

  • Writes use CAS (compare-and-swap) operations where possible, and fall back to a synchronized block scoped to just the head node of the affected bucket — not the whole map.
  • Reads are largely lock-free, relying on volatile reads of the underlying Node array so readers always see a consistent (if possibly slightly stale) view without blocking on writers.
  • Buckets that accumulate too many collisions are converted from a linked list to a red-black tree (same optimization as HashMap since Java 8), keeping worst-case lookup at O(log n) instead of O(n).
Hashtable: one lock guards the entire map
ConcurrentHashMap: lock scope = one bucket, most reads lock-free

This is why ConcurrentHashMap scales far better than Hashtable or Collections.synchronizedMap() under concurrent access — contention is spread across buckets instead of serializing every operation through a single lock.

The Reflection API (java.lang.reflect) lets code inspect and manipulate classes, methods, fields, and constructors at runtime — including private members, bypassing normal access checks via setAccessible(true).

Class<?> clazz = Employee.class;
Method[] methods = clazz.getDeclaredMethods();
Field field = clazz.getDeclaredField("salary");
field.setAccessible(true);
field.set(employeeInstance, 75000);
  • Frameworks (Spring, Hibernate) instantiating beans/entities and reading annotations without the application code knowing the concrete type ahead of time.
  • Serialization libraries (Jackson, Gson) mapping JSON fields to object fields generically.
  • Dependency injection containers wiring @Autowired fields.
  • Testing tools accessing private state for assertions.
  • Noticeably slower than direct method/field access (JIT can’t inline as effectively).
  • Breaks encapsulation and bypasses compile-time type checking — errors that would normally be caught by the compiler surface as runtime exceptions instead.

Stream API Internal Working and Lazy Evaluation

Section titled “Stream API Internal Working and Lazy Evaluation”

Intermediate operations (filter, map, sorted, …) don’t process any data when called — they just record a stage in a pipeline built on top of a Spliterator. Nothing actually runs until a terminal operation (collect, forEach, reduce, …) is invoked.

When the terminal operation runs, each element flows through the entire pipeline one at a time (this is sometimes called “vertical” execution), rather than running each stage across the whole collection before moving to the next stage. This is what makes short-circuiting operations like findFirst() or limit() efficient — the pipeline can stop pulling elements from the source as soon as enough have satisfied the terminal operation, instead of eagerly processing everything upfront.

List<String> result = names.stream()
.filter(n -> {
System.out.println("filtering " + n);
return n.startsWith("A");
})
.map(n -> {
System.out.println("mapping " + n);
return n.toUpperCase();
})
.findFirst()
.orElse(null);

For a name that doesn’t start with “A”, you’ll see filtering printed but never mapping — and once a match is found, no further elements are pulled from the source at all.

Java generics exist only at compile time. The compiler uses generic type information for type checking, then erases it when generating bytecode — replacing type parameters with Object (or their bound, if one is specified) and inserting casts where needed.

List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass()); // true

Both have the same runtime Class object — List — because the <String>/<Integer> information doesn’t exist at runtime.

Consequences of erasure:

  • You can’t create a generic array directly (new T[10] doesn’t compile).
  • You can’t use instanceof with a parameterized type (obj instanceof List<String> doesn’t compile — only obj instanceof List).
  • The compiler generates bridge methods to preserve polymorphism when a generic method is overridden with a more specific type.

CompletableFuture vs Traditional Multithreading

Section titled “CompletableFuture vs Traditional Multithreading”
Traditional (Thread/ExecutorService + Future) CompletableFuture
Future.get() blocks the calling thread Non-blocking composition via thenApply/thenCompose
No built-in way to chain follow-up work Fluent chaining (thenApply, thenAccept, thenCompose)
Combining multiple results requires manual coordination thenCombine/allOf/anyOf compose multiple futures directly
Exception handling requires try/catch around get() exceptionally/handle integrate into the chain
You manage your own ExecutorService Uses the common ForkJoinPool by default, or a supplied executor

Diagnosing Thread Contention in Production

Section titled “Diagnosing Thread Contention in Production”

Where memory leak investigation focuses on heap dumps, thread contention investigation focuses on thread dumps:

Terminal window
jstack <pid>

or, without needing the PID resolved separately:

Terminal window
jcmd <pid> Thread.print

Look for:

  • Many threads in BLOCKED state, all waiting on the same lock/monitor — a strong signal of lock contention on a hot resource.
  • The thread currently holding the contended lock (shown in the dump) — that’s the one to investigate first, not the blocked ones.
  • Repeated dumps a few seconds apart, to distinguish a genuinely stuck thread from one that’s just slow.

Tools: VisualVM’s thread view, async-profiler’s lock-profiling mode, and Java Flight Recorder for capturing contention events with lower overhead than repeated thread dumps.

  • ConcurrentHashMap scales under load because it locks at bucket granularity (and reads lock-free), unlike Hashtable’s single map-wide lock.
  • Reflection trades performance and compile-time safety for the flexibility frameworks need to work generically over unknown types.
  • Streams are lazy: nothing executes until a terminal operation runs, and elements flow through the whole pipeline one at a time — this is what makes findFirst()/limit() avoid unnecessary work.
  • Generics are erased at compile time — List<String> and List<Integer> are the same class at runtime, which is why generic arrays and instanceof with type parameters aren’t allowed.
  • For thread contention (as opposed to memory leaks), reach for thread dumps (jstack/jcmd Thread.print) rather than heap dumps — look for clusters of BLOCKED threads and identify who’s holding the contended lock.