ConcurrentHashMap源码分析-关键特性使用简单用例实现

ConcurrentHashMap是Java并发包中的一个线程安全的HashMap实现。它通过使用分段锁(segmentation locks)和CAS(Compare And Swap)操作来支持高并发下的键值对存储和检索。下面是一个简化的源码分析,帮助你理解ConcurrentHashMap的工作原理:

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
        for (int i = 0; i < 100; i++) {
            map.put(i, "Value " + i);
        }
        System.out.println(map.get(50));
    }
}

1. 类结构

ConcurrentHashMap类包含一个Segment数组,每个Segment维护一个HashEntry的数组,HashEntry表示实际的键值对。Segment继承自ReentrantLock,因此每个Segment都是一个可重入锁。

1.1 ReentrantLock

Java并发包中的一种同步工具,它提供了一个可重入的互斥锁实现。

  • 互斥性:ReentrantLock确保任何时候只有一个线程可以访问共享资源。当一个线程获取了锁之后,其他线程试图获取锁时会被阻塞,直到该线程释放锁。
    public void method() {
        lock.lock(); // 获取锁
        try {
            // 临界区代码
        } finally {
            lock.unlock(); // 释放锁
        }
  • 可重入性:ReentrantLock允许同一个线程在已经获取了锁的情况下,再次获取相同的锁。这种特性称为“可重入性”或“递归锁”。也就是说,同一个线程可以多次进入同一个代码块,而不会被其他线程干扰。
### 通过内部计数器来实现的。每次线程请求锁时,计数器会递增;每次线程释放锁时,计数器会递减。只有当计数器为零时,其他线程才能够获取该锁。
import java.util.concurrent.locks.ReentrantLock;

public class SimpleReentrantLock {
    private int lockCount = 0; // 计数器
    private boolean isLocked = false; // 表示锁是否已经被获取

    public void lock() {
        // 自旋锁,如果当前线程已经持有锁,则直接返回
        while (isLocked) {
            // 等待,直到锁被释放
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 当前线程获取锁
        isLocked = true;
        lockCount++;
    }

    public void unlock() {
        // 只有当计数器不为零时,才能释放锁
        if (lockCount > 0) {
            lockCount--;
            // 当计数器为零时,表示锁可以释放
            if (lockCount == 0) {
                isLocked = false;
            }
        }
    }

    public boolean isLocked() {
        return isLocked;
    }
}

  • 公平性:ReentrantLock提供了公平和非公平两种获取锁的方式。公平锁意味着等待时间最长的线程将首先获得锁。非公平锁则不保证等待顺序,它可能让新的线程获得锁,即使有其他线程等待的时间更长。
### 公平锁: ReentrantLock 的实现中,有一个内部类 Sync 来维护一个线程等待队列,这个队列是用来管理正在等待获取锁的线程的。会消耗一定的性能
        // 创建一个公平锁
        ReentrantLock fairLock = new ReentrantLock(true);
        // 创建一个非公平锁
        ReentrantLock unfairLock = new ReentrantLock(false);
=================================
public class ReentrantLock {
    private static class Sync {
        // 等待队列
        private final Node waitQueue = new Node();

        // 获取锁的入口方法
        void lock() {
            // 获取当前线程
            Thread current = Thread.currentThread();
            Node node = current.getNode();
            if (node == null) {
                // 创建一个新节点并将其添加到队列的尾部
                node = new Node(current);
                enq(node);
                // 自旋获取锁
                while (!tryAcquire(node)) {
                
                    // 线程阻塞,直到被唤醒
                    parkAndCheckInterrupt();
                }
            } else {
                // 当前线程已经有一个节点,尝试获取锁
                if (!tryAcquire(node)) {
                    // 如果获取锁失败,将当前线程挂起
                    throw new IllegalMonitorStateException();
                }
            }
        }

        // 释放锁
        void unlock() {
            // 获取当前线程的节点
            Node node = Thread.currentThread().getNode();
            if (node == null) {
                throw new IllegalMonitorStateException();
            }
            // 释放锁
            if (!node.waitQueue.isEmpty()) {
                // 如果队列中还有等待的线程,尝试唤醒一个
                Node next = node.waitQueue.next;
                if (next != null) {
                    next.thread.interrupt();
                }
            }
        }

        // 尝试获取锁
        private boolean tryAcquire(Node node) {
            // 尝试获取锁的逻辑
            return false;
        }

        // 将节点添加到队列的尾部
        private Node enq(Node node) {
            // 添加节点的逻辑
            return null;
        }
    }

    // 内部类 Node 用来表示线程等待队列的节点
    private static class Node {
        private Thread thread;
        private Node next;
        private Node prev;

        private Node(Thread thread) {
            this.thread = thread;
        }

        // 其他方法...
    }
}

  • 条件变量:ReentrantLock支持条件变量,允许线程在某个条件发生之前挂起,直到其他线程通知这个条件已经满足。这通过await和signal方法来实现。
### 在这个例子中,Thread A 通过调用 condition.await() 方法挂起,直到 Thread B 通过调用 condition.signal() 方法通知。Thread B 在调用 condition.signal() 之前,会先持有锁,以确保只有持有锁的线程才能唤醒等待线程。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("Thread A: waiting for condition");
                condition.await();
                System.out.println("Thread A: condition met, proceeding");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }).start();

        new Thread(() -> {
            try {
                Thread.sleep(1000); // Simulate some work
                lock.lock();
                try {
                    System.out.println("Thread B: notifying condition");
                    condition.signal();
                } finally {
                    lock.unlock();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

  • 非阻塞式通知:ReentrantLock的实现使用了非阻塞式通知(non-blocking notification),这意味着当一个线程释放锁时,它会将等待队列中的第一个线程唤醒,而不是等待线程主动调用await方法。

  • 内置超时机制:ReentrantLock提供了带超时时间的获取锁方法,如果线程在规定时间内没有获取到锁,则返回一个值表示是否成功获取锁。

在实际使用中,你通常会使用lock()和unlock()方法来获取和释放锁,或者使用tryLock()方法尝试获取锁,并在超时或被中断时返回。ReentrantLock也支持构建函数,允许你指定公平或不公平的获取锁方式。

2. 构造函数

ConcurrentHashMap的构造函数初始化Segment数组,并设置适当的初始容量和负载因子。

public ConcurrentHashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);

    // Find a power of 2 >= initialCapacity
    int c = initialCapacity - 1;
    int sshift = 0;
    int ssize = 1;
    while (ssize < c) {
        sshift++;
        ssize <<= 1;
    }

    // Ensure capacity is a power of 2, and length-2 is a power of 2 as well
    this.length = ssize;
    this.loadFactor = loadFactor;
    this.segments = new Segment[sshift];

    for (int i = 0; i < sshift; i++)
        this.segments[i] = new Segment(ssize, loadFactor);
}

3. 存储数据

put方法用于存储键值对。它首先计算键的哈希码,然后找到对应的Segment,最后使用CAS操作将键值对插入到HashEntry数组中。如果插入成功,则返回旧值;否则,它会自旋尝试直到成功或者超时。

public V put(K key, V value) {
    Segment<K, V> seg = getSegment(key);
    HashEntry<K, V>[] tab = seg.table;
    int hash = hash(key);
    int index = (hash & 0x60000000) == 0 ? hash & 0x7FFFFFFF : tab.length;
    HashEntry<K, V> entry = segtreePut(seg, tab, hash, key, value, true, index);
    return entry == null ? null : entry.value;
}

4. 获取数据

get方法用于检索键的值。它首先计算键的哈希码,然后找到对应的Segment和HashEntry,最后返回对应的值。

public V get(Object key) {
    Segment<K, V> seg = getSegment(key);
    HashEntry<K, V> entry = seg.getEntry(key, false);
    return entry == null ? null : entry.value;
}

5. 扩容机制

ConcurrentHashMap使用一种叫做“渐进式扩容”(biased locking)的机制来避免在扩容时完全锁定整个映射。渐进式扩容是通过使用Node和Segment这两个内部类来实现的。每个Segment代表一个独立的哈希桶数组,它维护了一个子映射。当一个Segment达到其容量阈值时,它会被单独扩容,这个过程是线程安全的,因为它使用了ReentrantLock来保护扩容操作。

6. 总结

ConcurrentHashMap通过分段锁和CAS操作实现了高并发的键值对存储和检索。它的设计使得在多个线程同时访问时,只要访问不同的Segment,就可以避免竞争。此外,它的扩容机制能够避免在扩容时完全锁定整个映射,从而提高了并发性能。

你可能感兴趣的:(java,线程安全)