一个Java8中ConcurrentHashMap的CAS锁引发的问题

Java架构师交流群:793825326

java版本:jdk1.8

IDE:idea 18

使用java8运行如下代码:

ConcurrentHashMap<String,Integer> map=new ConcurrentHashMap<>(16);
map.computeIfAbsent("AaAa", key->map.computeIfAbsent("BBBB",key2->42));
System.out.println("success");

这段代码将不会执行到System.out.println("success");因为上面的操作造成了死循环,如果将“AaAa”或者“BBBB”改成其他的东西,比如cccc,代码就可以正常执行了。

是什么原因导致了这个问题呢,这就涉及到ConcurrentHashMap使用的线程安全策略了。如果对这块不了解的,可以看下我的另外一篇博文https://blog.youkuaiyun.com/dap769815768/article/details/96596287

我们跟踪进computeIfAbsent方法内看下为何会卡住:

public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
    if (key == null || mappingFunction == null)
        throw new NullPointerException();
    int h = spread(key.hashCode());
    V val = null;
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
            Node<K,V> r = new ReservationNode<K,V>();
            synchronized (r) {
                if (casTabAt(tab, i, null, r)) {
                    binCount = 1;
                    Node<K,V> node = null;
                    try {
                        if ((val = mappingFunction.apply(key)) != null)
                            node = new Node<K,V>(h, key, val, null);
                    } finally {
                        setTabAt(tab, i, node);
                    }
                }
            }
            if (binCount != 0)
                break;
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            boolean added = false;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek; V ev;
                            if (e.hash == h &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                val = e.val;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                if ((val = mappingFunction.apply(key)) != null) {
                                    added = true;
                                    pred.next = new Node<K,V>(h, key, val, null);
                                }
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        binCount = 2;
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> r, p;
                        if ((r = t.root) != null &&
                            (p = r.findTreeNode(h, key, null)) != null)
                            val = p.val;
                        else if ((val = mappingFunction.apply(key)) != null) {
                            added = true;
                            t.putTreeVal(h, key, val);
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (!added)
                    return val;
                break;
            }
        }
    }
    if (val != null)
        addCount(1L, binCount);
    return val;
}

在第一次调用该方法的时候,代码会进入到这句:

else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
    Node<K,V> r = new ReservationNode<K,V>();
    synchronized (r) {
        if (casTabAt(tab, i, null, r)) {
            binCount = 1;
            Node<K,V> node = null;
            try {
                if ((val = mappingFunction.apply(key)) != null)
                    node = new Node<K,V>(h, key, val, null);
            } finally {
                setTabAt(tab, i, node);
            }
        }
    }
    if (binCount != 0)
        break;
}

这句执行到

val = mappingFunction.apply(key)

这句,会执行

key->map.computeIfAbsent("BBBB",key2->42)

来获取到值,以便完成最终的数据插入。这就会导致重新进入到computeIfAbsent方法里面,这个时候的table在31索引(”AaAa“和”BBBB“计算出来单的索引都是31)位置上是有值的,它是一个ReservationNode<K,V>节点,它的作用相当于一个占位符,并没有其他的作用,它的key和value全为null。

因此第二次进入到computeIfAbsent方法内,由于synchronized是可重入锁,所以代码并不会阻塞,会执行到下面的代码里面去:

else {
    boolean added = false;
    synchronized (f) {
        if (tabAt(tab, i) == f) {
            if (fh >= 0) {
                binCount = 1;
                for (Node<K,V> e = f;; ++binCount) {
                    K ek; V ev;
                    if (e.hash == h &&
                        ((ek = e.key) == key ||
                         (ek != null && key.equals(ek)))) {
                        val = e.val;
                        break;
                    }
                    Node<K,V> pred = e;
                    if ((e = e.next) == null) {
                        if ((val = mappingFunction.apply(key)) != null) {
                            added = true;
                            pred.next = new Node<K,V>(h, key, val, null);
                        }
                        break;
                    }
                }
            }
            else if (f instanceof TreeBin) {
                binCount = 2;
                TreeBin<K,V> t = (TreeBin<K,V>)f;
                TreeNode<K,V> r, p;
                if ((r = t.root) != null &&
                    (p = r.findTreeNode(h, key, null)) != null)
                    val = p.val;
                else if ((val = mappingFunction.apply(key)) != null) {
                    added = true;
                    t.putTreeVal(h, key, val);
                }
            }
        }
    }
    if (binCount != 0) {
        if (binCount >= TREEIFY_THRESHOLD)
            treeifyBin(tab, i);
        if (!added)
            return val;
        break;
    }
}

这个循环唯一的推出条件是binCount!=0,这就要求

a)if (fh >= 0)
b)else if (f instanceof TreeBin)

这两个条件至少有一个成立,其中fh = f.hash,普通的节点,如果正在被transfer,也就是扩容操作,hash会被设置为-1,这个时候这个线程检测到hash为-1,会帮忙扩容。但这里的f为占位节点,它的默认hash为-3,这可以根据它的构造方法看到

static final class ReservationNode<K,V> extends Node<K,V> {
    ReservationNode() {
        super(RESERVED, null, null, null);
    }

    Node<K,V> find(int h, Object k) {
        return null;
    }
}

其中

static final int RESERVED  = -3;

所以第一个条件不会满足,第二个条件很明显也不会满足,那么这个循环将永远不会结束,于是线程就卡在这里了。

这个问题和锁无关,属于设计问题,由于线程安全使用的策略是CAS,也就是自旋操作,导致必须符合条件,才能正常退出循环,但是恰好因为占位节点的hash值被设计成了-3,所以这个循环无论无何也无法达到退出条件了。

因此,官方也不建议在存入数据的时候嵌入其他的存数据的操作。

<think>好的,我现在需要回答用户关于多线程环境下两个线程竞争同一把可能引发问题及解决方法。首先,我得回忆一下相关的多线程知识,特别是的竞争可能带来的问题。 首先,用户提到了死、性能瓶颈或其他同步问题。我需要逐一分析这些情况。死通常发生在多个线程互相持有对方需要的资源时,比如两个线程各自持有一把,并试图获取对方的,导致互相等待。这种情况下,程序会卡住,无法继续执行。我需要解释死发生的条件,以及如何避免,比如按照顺序获取或使用超时机制。 然后是性能瓶颈。当多个线程频繁竞争同一把时,的争用会导致线程频繁切换,增加上下文切换的开销,降低系统吞吐量。这时候可能需要减少的粒度,或者使用无数据结构,比如CAS操作,或者采用读写(ReentrantReadWriteLock)来区分读写操作,减少不必要的阻塞。 同步问题方面,如果线程没有正确同步,可能会导致数据不一致,比如竞态条件。这需要确保临界区的操作是原子的,所有访问共享资源的代码都必须正确加。此外,线程安全的设计模式,比如不可变对象或线程局部存储(ThreadLocal),也能帮助减少同步需求。 接下来,我需要结合用户提供的引用内容。引用[1]提到多线程共享存储空间带来的访问冲突问题,引用[2]讨论了线程安全需要正确的同步机制,引用[3]提到Java中的关键字,引用[4]指向Redis处理多线程竞争的解决方案。这些引用支持我的分析,比如使用synchronized或volatile来保证可见性和原子性,或者使用分布式在分布式系统中处理竞争。 可能还需要提到具体的解决方案,比如避免嵌套分解、分段等技术,或者使用高级并发工具类如CountDownLatch、CyclicBarrier等来管理线程协作。此外,死检测工具和合理的超时设置也是实际开发中常用的方法。 在结构上,应该先列出问题,再给出解决方法,确保每个部分都有对应的解释和示例。同时,引用相关文献来增强回答的权威性,比如引用[2]中关于同步机制重要性的部分,引用[3]中的Java关键字应用。 最后,生成相关问题时要围绕多线程竞争的扩展问题,比如死预防的具体方法,优化的其他技术,或者分布式系统中的处理,这样用户能进一步深入理解。</think>### 多线程环境下两线程竞争同一把可能导致的问题及解决方法 #### 1. 可能引发问题 1. **死** 当两个线程互相持有对方需要的且互不释放时,会导致永久阻塞。例如: ```java // 线程A synchronized(lock1) { synchronized(lock2) { ... } } // 线程B synchronized(lock2) { synchronized(lock1) { ... } } ``` 此时若线程A持有`lock1`并请求`lock2`,线程B持有`lock2`并请求`lock1`,则形成死[^2]。 2. **性能瓶颈** 高并发场景下,多个线程频繁竞争同一把会导致大量线程阻塞,增加**上下文切换**的开销,降低系统吞吐量[^1]。 3. **数据不一致(同步问题)** 若未正确同步共享资源,可能导致**竞态条件**。例如: ```java // 未同步的计数器 int count = 0; public void increment() { count++; // 非原子操作 } ``` 多个线程同时调用`increment()`时,结果可能小于预期值。 --- #### 2. 解决方法 1. **避免死** - **固定的获取顺序**:所有线程按相同顺序获取(如先`lock1`后`lock2`)[^2]。 - **使用超时机制**:通过`tryLock(timeout)`尝试获取,超时后释放已有并重试。 - **死检测工具**:借助`jstack`或可视化工具分析线程状态。 2. **优化性能** - **减小粒度**:将大拆分为多个小(如`ConcurrentHashMap`的分段)。 - **无编程**:使用原子类(`AtomicInteger`)或CAS操作。 - **读写分离**:用`ReentrantReadWriteLock`区分读/写,允许多线程并发读[^3]。 3. **确保数据同步** - **显式同步**:使用`synchronized`或`Lock`保护临界区: ```java public synchronized void safeIncrement() { count++; } ``` - **线程安全设计**:优先使用不可变对象(如`String`)或线程封闭(`ThreadLocal`)。 4. **分布式扩展** 在分布式系统中(如Redis),可通过**RedLock算法**或`Redisson`客户端实现跨进程协调[^4]。 --- ### 相关问题 1. 如何检测和诊断Java程序中的死? 2. 除了分段,还有哪些优化高并发场景下竞争的方法? 3. 分布式系统中如何保证的可靠性和高性能? 4. 无编程如何通过CAS实现线程安全? 5. `volatile`关键字如何解决多线程可见性问题?[^3] --- [^1]: 引用[1] : 引用[2] : 引用[3] : 引用[4]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值