线程安全集合类

本文深入分析了Java中的线程安全集合类,重点关注ConcurrentHashMap在JDK 8和JDK 7的实现区别,以及LinkedBlockingQueue的工作原理。在JDK 8中,ConcurrentHashMap利用ForwardingNode实现扩容,而JDK 7通过Segment数组和锁实现并发。LinkedBlockingQueue的入队、出队操作通过两把锁确保线程安全,与ArrayBlockingQueue相比,其性能更优。此外,还提及CopyOnWriteArrayList的写入时拷贝策略,适合读多写少的场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


ConcurrentHashMap

JDK 8 ConcurrentHashMap源码分析

ForwardingNode在一个桶数组中的元素全部被搬迁完毕的时候会用来标记那个桶数组,表示它已经搬迁完毕了;同时还有一个作用就是当访问桶数组的时候,如果一个桶数组项已经被ForwardingNode标记了,那么就应该在新的桶数组(已经被扩容了的数组)中进行数据的访问;先扩容再红黑;大于8小于6;

构造器方法

// 初始容量,负载因子,并发度
public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
   
   
    if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (initialCapacity < concurrencyLevel)   // Use at least as many bins
    	// 最少保证并发度
        initialCapacity = concurrencyLevel;   // as estimated threads
    // JDK8的加载方式是懒惰加载的,在构造方法中仅仅是计算了table的大小,在第一次使用的时候才会创建出table本身
    long size = (long)(1.0 + (long)initialCapacity / loadFactor);
    int cap = (size >= (long)MAXIMUM_CAPACITY) ?
    	// tableSizeFor保证最终计算出来的大小是2^n,即16, 32, 64等
    	// 因为后续的一些hash算法要求哈希表的大小都必须是2^n才能够正常工作
        MAXIMUM_CAPACITY : tableSizeFor((int)size);
    this.sizeCtl = cap;
}

get方法

// get方法没有加锁,所以效率是很高的,并发度也很高
public V get(Object key) {
   
   
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // spread保证计算出的哈希码是正整数
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
    	// (n - 1) & h就相当于是一个取模运算,效率更高,找到桶数组的下标
    	// tabAt找到桶数组下标处的头结点
        (e = tabAt(tab, (n - 1) & h)) != null) {
   
   
        // 比较头结点的hash码是不是就是给出的key的hash码
        if ((eh = e.hash) == h) {
   
   
        	// hash码是相同的,判断要查找的数和桶数组中头结点的key是不是一样的,如果是的话就直接返回值
        	// 如果用==判断出二者不是同一个对象的话就判断二者的值是不是相等的
        	// 也就是说不管二者是同一个对象还是二者的值是相等的都认为是同一个key
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // hash值为负数就表示该bin在扩容中是treebin(-2)
        // 这时调用相应的find方法去红黑树中查找目标数据
        // 或者头结点是ForwardingNode,ForwardingNode的hash值也是负数(-1)
        // 此时去新的桶数组中查找数据
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        // 如果以上的条件都不成立的话就遍历链表
        while ((e = e.next) != null) {
   
   
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

put方法

public V put(K key, V value) {
   
   
    return putVal(key, value, false);
}

// 如果onlyIfAbsent取值为真,就表示只有第一次put键和值的时候才会将它们放入到map
// 之后遇到相同的key的时候并不会用新值覆盖掉旧的值,而是什么都不做
// 是false就表示会用新值覆盖掉旧值
final V putVal(K key, V value, boolean onlyIfAbsent) {
   
   
	// 普通的HashMap允许存在空的键和值
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    // 代表是链表的长度
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
   
   
        Node<K,V> f; int n, i, fh;
        // 此时表示哈希表还没有创建
        if (tab == null || (n = tab.length) == 0)
        	// 创建hash表,使用的是CAS操作,只有一个线程会创建成功
        	// 如果在创建成功之后put操作还没有执行完毕,在下一轮循环会继续执行put
            tab = initTable();
        // 判断是否有头节点
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
   
   
        	// 通过CAS的方式创建头结点,如果创建失败了会再下一次循环中继续尝试将节点放置于map中
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
		// 检查头节点是不是ForwardingNode
        else if ((fh = f.hash) == MOVED)
        	// 锁住当前的链表帮忙扩容
            tab = helpTransfer(tab, f);
        // 既不是正在扩容,也不是正在初始化table,而是发生桶下标了冲突
        else {
   
   
            V oldVal = null;
            // 只有在发生桶下标冲突的时候才加锁,并且加锁的粒度仅仅是链表的头节点
            synchronized (f) {
   
   
                if (tabAt(tab, i) == f) {
   
   
                	// fh >= 0的一定是普通节点,而不是红黑树的根节点或ForwardingNode
                    if (fh >= 0) {
   
   
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
   
   
                            K ek;
                            // key已经存在了就执行更新操作
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
   
   
                                oldVal = e.val;
                                // 赋值到旧的value值
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            // 已经是最后一个节点了,表示key不存在,新增Node追加到链表的结尾
                            if ((e = e.next) == null) {
   
   
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // 桶数组中的链表已经被置为红黑树了
                    else if (f instanceof TreeBin) {
   
   
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
   
   
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            // 释放锁之后对链表进行优化,binCount != 0表示链表中存在冲突
            if (binCount != 0) {
   
   
                if (binCount >= TREEIFY_THRESHOLD)
                	// 如果binCount链表长度大小已经超过了树化的阈值就将链表转化为红黑树
                	// 注意不是立即树化的,而是先将链表进行扩容,如果还是存在binCount大于阈值的情况再进行树化
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 设置多个累加单元进行计数
    addCount(1L, binCount);
    return null;
}

initTable()方法

// 保证只有一个线程在创建table,其它的线程都是在忙等,并没有阻塞
private final Node<K,V>[] initTable() {
   
   
    Node<K,V>[] tab; int sc;
    // 判断table是不是已经创建了
    while ((tab = table) == null || tab.length == 
Java中实现线程安全的集合主要依赖于Java集合框架中的线程安全类和同步机制。常用的线程安全集合类包括: 参考资源链接:[Java生态体系精华面试题:并发、网络与Spring框架详解](https://wenku.youkuaiyun.com/doc/2mozzpp52f?spm=1055.2569.3001.10343) 1. Vector:这是最早提供的线程安全的List实现,通过synchronized关键字来实现同步访问。虽然它保证了线程安全,但由于其线程同步机制的开销较大,性能较低,现已不推荐使用。 2. Stack:继承自Vector的线程安全堆栈实现,具有先进后出(LIFO)的特性。同样由于使用同步,性能并不理想。 3. Hashtable:是早期提供线程安全的Map实现,通过synchronized方法对所有访问进行了同步。它不支持null键或值。 4. Collections.synchronizedMap:通过将普通的Map包装成一个线程安全的Map来实现。它使用了外部的synchronized语句块对方法进行同步。 5. Collections.synchronizedList、Collections.synchronizedSet:与synchronizedMap类似,这些方法可以将List和Set包装成线程安全的集合。 6. ConcurrentMap:JDK 1.5中引入的一个线程安全的Map接口,提供了一些原子操作如putIfAbsent, remove, replace等,以支持并发操作。ConcurrentHashMap是ConcurrentMap最常用的实现。 7. CopyOnWriteArrayList和CopyOnWriteArraySet:适用于读操作远多于写操作的场景,每次写操作都会复制底层数组,然后在副本上执行修改操作,之后将新数组替换旧数组。这样可以保证读操作的线程安全,但写操作成本较高。 8. BlockingQueue:JDK中的阻塞队列接口,如ArrayBlockingQueue, LinkedBlockingQueue等,它们不仅可以用于线程安全的数据处理,还可以用于线程间的通信。 9. ConcurrentLinkedQueue:一种无阻塞并发队列,适用于高并发场景下的先进先出(FIFO)数据队列。 在实际应用中,选择合适的线程安全集合类非常重要。例如,如果你需要频繁修改数据,建议使用ConcurrentHashMap而不是Hashtable,因为前者在并发写入时性能更优。如果你的应用场景中读操作远多于写操作,那么CopyOnWriteArrayList可能是一个好的选择。而对于需要等待生产者/消费者模式的场合,使用BlockingQueue系列类会更加方便。 了解这些线程安全集合类的特性和适用场景,有助于开发者在多线程编程中做出合适的选择,保证数据的一致性和系统的稳定性。推荐阅读《Java生态体系精华面试题:并发、网络与Spring框架详解》,其中详细解释了这些集合框架的使用及其在并发编程中的应用,帮助你更好地理解和掌握Java集合与并发编程的相关知识。 参考资源链接:[Java生态体系精华面试题:并发、网络与Spring框架详解](https://wenku.youkuaiyun.com/doc/2mozzpp52f?spm=1055.2569.3001.10343)
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值