杰瑞科技汇

ConcurrentMap如何保证线程安全且高效?

Of course! Let's dive deep into java.util.concurrent.ConcurrentMap. It's a fundamental and powerful interface in Java's concurrency toolkit.

ConcurrentMap如何保证线程安全且高效?-图1
(图片来源网络,侵删)

What is ConcurrentMap?

ConcurrentMap is an interface introduced in Java 5 as part of the java.util.concurrent package. It extends the standard java.util.Map interface, adding support for concurrent access.

Think of it as a thread-safe version of Map. While you can synchronize a regular HashMap using Collections.synchronizedMap(new HashMap<>()), this approach has significant drawbacks. ConcurrentMap is designed from the ground up to provide better performance and more sophisticated atomic operations in a multi-threaded environment.

Key Idea: Instead of locking the entire map for every operation (like synchronizedMap does), ConcurrentMap uses fine-grained locking, often at the level of individual buckets in its internal hash table. This allows multiple threads to read and write to different parts of the map simultaneously, leading to much higher throughput.


The Problem with synchronizedMap

Before ConcurrentMap, the standard way to make a map thread-safe was:

ConcurrentMap如何保证线程安全且高效?-图2
(图片来源网络,侵删)
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());

This approach has a major flaw: every single method call is synchronized on the map's monitor object. This means:

  • Poor Scalability: If you have many threads, they will all contend for the same lock, effectively serializing all access to the map. This defeats the purpose of having multiple threads.

  • Compound Operations are not Atomic: What if you want to "put a value only if it's not already there"? You might write:

    if (!map.containsKey(key)) {
        map.put(key, value);
    }

    This is a compound operation. Even though containsKey and put are each thread-safe, the sequence of them is not. Another thread could modify the map between your if check and your put call, leading to race conditions.

    ConcurrentMap如何保证线程安全且高效?-图3
    (图片来源网络,侵删)

ConcurrentMap solves both of these problems.


Key Features and Atomic Operations

ConcurrentMap defines several atomic methods that perform compound operations in a single, thread-safe step. These are its superpowers.

putIfAbsent(K key, V value)

Atomically puts the key-value pair into the map only if the key is not already present.

  • Equivalent to: if (!map.containsKey(key)) { map.put(key, value); }
  • Guarantee: The operation is atomic. No other thread can insert the same key in the meantime.

remove(Object key, Object value)

Atomically removes the entry for a key only if it is currently mapped to the given value.

  • Use Case: This is crucial for atomic "compare-and-swap" (CAS) semantics. You ensure you only remove the entry if it hasn't been changed by another thread since you last read it.
  • Equivalent to: if (map.get(key) == value) { map.remove(key); }

replace(K key, V oldValue, V newValue)

Atomically replaces the entry for a key only if it is currently mapped to the given oldValue.

  • Use Case: Updating a value atomically, ensuring you're not overwriting a change made by another thread.
  • Equivalent to: if (map.get(key) == oldValue) { map.put(key, newValue); }

replace(K key, V value)

Atomically replaces the entry for a key only if it is currently mapped to some value.

  • Use Case: A simpler version of replace that doesn't care about the old value, just that one exists.

compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)

Atomically computes a new mapping for the specified key.

  • The remappingFunction is called with the key and its current value (or null if absent).
  • The function's result becomes the new value for the key.
  • This is extremely powerful for complex, conditional updates.

computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)

Atomically computes a value for the specified key if it's not already present.

  • If the key is absent, the mappingFunction is called with the key. The function's result is inserted into the map.
  • Crucially, the function is only executed once, even if multiple threads call this method concurrently for the same absent key. This is a huge performance benefit over the putIfAbsent + compute pattern.

computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)

Atomically computes a new value for the specified key only if it is already present.

  • If the key is present, the remappingFunction is called with the key and its current value. The function's result becomes the new value. If the function returns null, the mapping is removed.

merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)

A very versatile method for atomically updating a value.

  • If the key is not present, it's inserted with the given value.
  • If the key is present, the remappingFunction is called with the existing value and the new value. The function's result becomes the new value for the key.

Common Implementations

The most common and widely used implementation of ConcurrentMap is ConcurrentHashMap.

ConcurrentHashMap

This is the workhorse of concurrent maps in Java. It's highly optimized and used in virtually all high-performance, multi-threaded applications.

How it works (Simplified): A ConcurrentHashMap is internally divided into segments (or, in modern Java versions, uses a technique called volatile and CAS operations). This allows multiple threads to read and write to different segments without blocking each other. Only threads trying to modify the same segment will contend for a lock.

When to use it:

  • When you need a general-purpose, high-performance thread-safe map.
  • When you have high contention from multiple threads.
  • When you frequently use the atomic putIfAbsent, compute, etc., methods.

Example:

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class ConcurrentMapExample {
    public static void main(String[] args) {
        // Create a ConcurrentMap
        ConcurrentMap<String, Integer> wordCounts = new ConcurrentHashMap<>();
        // Simulate multiple threads counting words
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                // Atomically increment the count for "apple"
                // If "apple" is not present, it's added with a value of 1.
                // If it is present, its value is incremented by 1.
                wordCounts.merge("apple", 1, Integer::sum);
            }
        };
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        Thread thread3 = new Thread(task);
        thread1.start();
        thread2.start();
        thread3.start();
        try {
            thread1.join();
            thread2.join();
            thread3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Final count for 'apple': " + wordCounts.get("apple")); // Should be 3000
    }
}

ConcurrentSkipListMap

This is another implementation of ConcurrentMap.

  • Data Structure: It's based on a skip list, not a hash table.
  • Key Feature: It maintains its elements in natural order (or a custom Comparator order).
  • Performance: Offers good average-case performance (O(log n)) for get, put, and remove operations, similar to a TreeMap.
  • Use Case: Use when you need a concurrent map that is sorted.

ConcurrentMap vs. synchronizedMap vs. Hashtable

Feature ConcurrentHashMap Collections.synchronizedMap(new HashMap<>()) Hashtable
Thread Safety Yes Yes Yes
Locking Strategy Fine-grained (per bucket or CAS) Coarse-grained (entire map) Coarse-grained (entire map)
Performance under Contention Excellent. High throughput. Poor. Throughput degrades with more threads. Poor. Throughput degrades with more threads.
Null Keys/Values Yes. One null key, multiple null values. Yes. One null key, multiple null values. No. Throws NullPointerException.
Iteration Weakly consistent. May not reflect all changes. Fail-fast. Throws ConcurrentModificationException if modified during iteration. Fail-fast. Throws ConcurrentModificationException if modified during iteration.
Atomic Operations Yes. putIfAbsent, compute, etc. No. Only individual methods are atomic. No. Only individual methods are atomic.
Legacy Modern, Java 5+ Legacy wrapper Legacy class (pre-Java 2 Collections)

Summary and Best Practices

  • Default Choice: For any new multi-threaded code requiring a Map, ConcurrentHashMap is the default and best choice. It is safer, more performant, and more feature-rich than the alternatives.
  • Use Atomic Methods: Always prefer the atomic methods (putIfAbsent, compute, merge) over performing a sequence of get and put calls. This ensures thread safety and avoids race conditions.
  • Use ConcurrentSkipListMap for Sorted Data: If you need a concurrent map that is sorted, ConcurrentSkipListMap is the perfect fit.
  • Avoid Hashtable and synchronizedMap: Unless you are maintaining legacy code, there is almost no reason to use these in modern Java applications. They are less flexible and perform significantly worse under contention.
分享:
扫描分享到社交APP
上一篇
下一篇