1

内部でConcurrentHashMap.compute()は、共有メモリにある long 値をインクリメントおよびデクリメントします。読み取り、インクリメント/デクリメントはcompute、同じキーのメソッド内でのみ実行されます。そのため、long 値へのアクセスは ConcurrentHashMap セグメントをロックすることによって同期されるため、インクリメント/デクリメントはアトミックです。私の質問は: マップ上のこの同期は、長い値の可視性を保証しますか? Map の内部同期に依存できますか、それとも long 値にする必要があります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:私が取り組んでいるコードに近い別の例を追加します。誰もオブジェクトを使用していないときに、マップ エントリを削除したいと考えています。long 値の読み取りと書き込みは、次の再マッピング関数内でのみ行われることに注意してください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