1

ConcurrentHashMap.compute()I 内递增和递减位于共享内存中的一些 long 值。读取、递增/递减仅在compute同一键的方法内执行。因此,通过锁定 ConcurrentHashMap 段来同步对 long 值的访问,因此递增/递减是原子的。我的问题是:地图上的这种同步是否能保证长期价值的可见性?我可以依靠 Map 的内部同步还是应该做多volatile

我知道,当您明确同步锁定时,可以保证可见性。ConcurrentHashMap但是我对内部结构没有完全的了解。或者也许我今天可以信任它,但明天ConcurrentHashMap的内部结构可能会发生某种变化:独占访问权将被保留,但可见性将消失……这是使我的长期价值不稳定的论据。

下面我将发布一个简化的示例。根据测试,今天没有比赛条件。volatile但是我可以在没有for的情况下长期信任这个代码long value吗?

class LongHolder {

    private final ConcurrentMap<Object, Object> syncMap = new ConcurrentHashMap<>();
    private long value = 0;

    public void increment() {
        syncMap.compute("1", (k, v) -> {
            if (++value == 2000000) {
                System.out.println("Expected final state. If this gets printed, this simple test did not detect visibility problem");
            }
            return null;
        });
    }
}

class IncrementRunnable implements Runnable {

    private final LongHolder longHolder;

    IncrementRunnable(LongHolder longHolder) {
        this.longHolder = longHolder;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000000; i++) {
            longHolder.increment();
        }
    }
}


public class ConcurrentMapExample {
    public static void main(String[] args) throws InterruptedException {
        LongHolder longholder = new LongHolder();
        Thread t1 = new Thread(new IncrementRunnable(longholder));
        Thread t2 = new Thread(new IncrementRunnable(longholder));
        t1.start();
        t2.start();
    }
}

UPD:添加另一个更接近我正在处理的代码的示例。当没有其他人使用该对象时,我想删除地图条目。请注意,长值的读取和写入仅发生在重新映射函数内部ConcurrentHashMap.compute

public class ObjectProvider {

    private final ConcurrentMap<Long, CountingObject> map = new ConcurrentHashMap<>();

    public CountingObject takeObjectForId(Long id) {
        return map.compute(id, (k, v) -> {
            CountingObject returnLock;
            returnLock = v == null ? new CountingObject() : v;

            returnLock.incrementUsages();
            return returnLock;
        });
    }

    public void releaseObjectForId(Long id, CountingObject o) {
        map.compute(id, (k, v) -> o.decrementUsages() == 0 ? null : o);
    }
}

class CountingObject {
    private int usages;

    public void incrementUsages() {
        --usages;
    }

    public int decrementUsages() {
        return --usages;
    }
}

UPD2:我承认我之前没有提供最简单的代码示例,现在发布一个真实的代码:

public class LockerUtility<T> {

    private final ConcurrentMap<T, CountingLock> locks = new ConcurrentHashMap<>();

    public void executeLocked(T entityId, Runnable synchronizedCode) {
        CountingLock lock = synchronizedTakeEntityLock(entityId);
        try {
            lock.lock();
            try {
                synchronizedCode.run();
            } finally {
                lock.unlock();
            }
        } finally {
            synchronizedReturnEntityLock(entityId, lock);
        }

    }

    private CountingLock synchronizedTakeEntityLock(T id) {
        return locks.compute(id, (k, l) -> {
            CountingLock returnLock;
            returnLock = l == null ? new CountingLock() : l;

            returnLock.takeForUsage();
            return returnLock;
        });
    }

    private void synchronizedReturnEntityLock(T lockId, CountingLock lock) {
        locks.compute(lockId, (i, v) -> lock.returnBack() == 0 ? null : lock);
    }

    private static class CountingLock extends ReentrantLock {
        private volatile long usages = 0;

        public void takeForUsage() {
            usages++;
        }

        public long returnBack() {
            return --usages;
        }
    }
}
4

2 回答 2

1

不要被以下文档中的行所迷惑compute

整个方法调用以原子方式执行

这确实适用于副作用,就像你有的那样value++;它仅适用于 的内部数据ConcurrentHashMap

您想念的第一件事是,locking在 中CHM,实现发生了很大变化(正如其他答案所指出的那样)。但即使没有,您对以下内容的理解:

我知道,当您明确同步锁定时,可以保证可见性

有缺陷。JLS表示当the和 the使用相同的锁时,这是可以保证的;在您的情况下显然不会发生;因此,没有任何保证。一般来说,保证(您在此处需要)仅适用于阅读器和作者的配对。readerwriterhappens-before

于 2021-03-10T21:37:55.497 回答
1

不,这种方法行不通,即使是 volatile 也行不通。您必须使用AtomicLong,LongAdder或类似的方法来使其正确地线程安全。 ConcurrentHashMap这些天甚至不能使用分段锁。

此外,您的测试并不能证明任何事情。根据定义,并发问题并非每次都会发生。甚至不是每百万次。

您必须使用适当的并发Long累加器,例如AtomicLongor LongAdder

于 2021-03-10T21:22:22.250 回答