线程安全集合类

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


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的并发包还提供了以下功能: 1. 并发执行框架:java.util.concurrent包中的Executor框架提供了一种使用线程池的方式来执行并发任务。它将任务的提交与任务的执行解耦,可以通过线程池来管理和复用线程,并提供了任务执行的调度和控制的功能。 2. 并发工具类:并发包中还提供了一些工具类,如CountDownLatch、CyclicBarrier、Semaphore等,用于帮助实现更加复杂的并发控制。这些工具类可以用于同步多个线程的执行,控制线程的并发数量,并在一些条件满足后触发线程的执行等功能。 3. 原子操作类:并发包中提供了一系列原子操作类,如AtomicInteger、AtomicLong等,用于在多线程环境下对变量进行原子操作。这些类通过使用CAS(Compare and Swap)操作来保证变量的原子性,避免了使用synchronized关键字进行同步操作带来的性能开销。 4. 并发线程安全工具类:并发包中还提供了一些线程安全的辅助类,如CopyOnWriteArrayList、ConcurrentHashMap等,用于替代传统的非线程安全集合类。这些类通过使用一些特定的并发算法来保证多线程环境下的线程安全性,能够在并发读写的情况下提供较好的性能。 总之,java的并发包不仅提供了高性能的线程安全集合类,还提供了一些并发执行框架、并发工具类、原子操作类以及线程安全工具类,用于帮助开发者在多线程环境下更加方便、高效地编写并发代码。这些功能的引入使得开发者能够更好地处理并发程序的编写与调试,提高了程序的性能和可靠性。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值