1

I've built a collection of atomic maps, sets and lists classes using AtomicReference. I've ran into a problem where when AtomicReference#compareAndSet is called ~20 times a second there is severe lag. (AtomicSet doesn't cause any lag, but that's only because it doesn't call compareAndSet if it already contains the object)

If I comment out the method and simply add synchronized to the method, the lag goes away, but is that a valid substitute and have I over-engineered for something I didn't need to?

I've tried a bunch of different combinations of method layout where I only get the current map and create a new modified map once, then just repeatedly call compareAndSet until it returns true, still lag. I've also tried just calling AtomicReference#set once, which also lags.

Summary: My AtomicReference#compareAndSet calls bog down the main thread, and I'm looking to find out if I've completely misused AtomicReference, should be using synchronized or a fix for my code, if there is one.

For context, I'm running the following code inside a Minecraft Sponge Mixin injection. All it does is inject my java code at the bottom of the defined method (which appears to run at the Minecraft tick speed, 20 times a second):

package net.netcoding.mod.mixins;

import net.minecraft.tileentity.TileEntityHopper;
import net.minecraft.tileentity.TileEntityLockable;
import net.minecraft.util.math.BlockPos;
import net.netcoding.mod.util.Cache;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

@Mixin(TileEntityHopper.class)
public abstract class MixinTileEntityHopper extends TileEntityLockable {

    @Shadow public abstract double getXPos();
    @Shadow public abstract double getZPos();
    @Shadow public abstract double getYPos();

    @Inject(
            method = "update",
            at = @At(
                    value = "TAIL"
            )
    )
    private void mod_update(CallbackInfo ci) {
        BlockPos position = new BlockPos(this.getXPos(), this.getYPos(), this.getZPos());
        Cache.STORED_HOPPERS.put(position, true); // this line calls the AtomicMap#put
    }

}

Here's the AtomicMap class in question (please take a look at the put and put2 methods):

package net.netcoding.core.api.util.concurrent.atomic;

import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;

public abstract class AtomicMap<K, V, M extends AbstractMap<K, V>> extends AbstractMap<K, V> implements Iterable<Map.Entry<K, V>>, Map<K, V> {

    protected final AtomicReference<M> ref;

    /**
     * Create a new concurrent map.
     */
    protected AtomicMap(M type) {
        this.ref = new AtomicReference<>(type);
    }

    @Override
    public final void clear() {
        this.ref.get().clear();
    }

    @Override
    public final boolean containsKey(Object key) {
        return this.ref.get().containsKey(key);
    }

    @Override
    public final boolean containsValue(Object value) {
        return this.ref.get().containsValue(value);
    }

    @Override
    public final Set<Entry<K, V>> entrySet() {
        return this.ref.get().entrySet();
    }

    @Override
    public final V get(Object key) {
        return this.ref.get().get(key);
    }

    @Override
    public final V getOrDefault(Object key, V defaultValue) {
        M current = this.ref.get();
        return current.getOrDefault(key, defaultValue);
    }

    @Override
    public final boolean isEmpty() {
        return this.ref.get().isEmpty();
    }

    @Override
    public Iterator<Entry<K, V>> iterator() {
        return this.entrySet().iterator();
    }

    @Override
    public final Set<K> keySet() {
        return this.ref.get().keySet();
    }

    @SuppressWarnings("unchecked")
    private M newMap(M current) {
        try {
            Map<K, V> map = current.getClass().newInstance();
            map.putAll(current);
            return (M)map;
        } catch (Exception ex) {
            throw new RuntimeException("Unable to create new map instance of " + current.getClass().getSimpleName() + "!");
        }
    }

    public final Stream<Entry<K, V>> parallelStream() {
        return this.entrySet().parallelStream();
    }

    @Override
    public final V put(K key, V value) {
        while (true) {
            M current = this.ref.get();
            M modified = this.newMap(current);
            V old = modified.put(key, value);

            // Causes severe lag if called ~20 times a second
            if (this.ref.compareAndSet(current, modified))
                return old;
        }
    }

    // Causes no lag, but answers in questions about AtomicReference and synchronized
    // all say synchronized can deadlock a thread, and hang until it's complete
    // which is actually the exact opposite of what I am experiencing
    // and I'm not sure this is even a correct way to use AtomicReference
    public synchronized final V put2(K key, V value) {
        return this.ref.get().put(key, value);
    }

    @Override
    public synchronized final void putAll(Map<? extends K, ? extends V> map) {
        this.ref.get().putAll(map);
        /*while (true) {
            M current = this.ref.get();
            M modified = this.newMap(current);
            modified.putAll(map);

            if (this.ref.compareAndSet(current, modified))
                return;
        }*/
    }

    @Override
    public synchronized final V putIfAbsent(K key, V value) {
        return this.ref.get().putIfAbsent(key, value);
        /*while (true) {
            M current = this.ref.get();
            M modified = this.newMap(current);

            if (!modified.containsKey(key) || modified.get(key) == null) {
                V old = modified.put(key, value);

                if (this.ref.compareAndSet(current, modified))
                    return old;
            } else
                return null;
        }*/
    }

    @Override
    public final V remove(Object key) {
        while (true) {
            M current = this.ref.get();

            if (!current.containsKey(key))
                return null;

            M modified = this.newMap(current);
            V old = modified.remove(key);

            if (this.ref.compareAndSet(current, modified))
                return old;
        }
    }

    @Override
    public final boolean remove(Object key, Object value) {
        while (true) {
            M current = this.ref.get();

            if (!current.containsKey(key))
                return false;

            M modified = this.newMap(current);
            V currentValue = modified.get(key);

            if (Objects.equals(currentValue, value)) {
                modified.remove(key);

                if (this.ref.compareAndSet(current, modified))
                    return true;
            } else
                return false;
        }
    }

    @Override
    public final int size() {
        return this.ref.get().size();
    }

    public final Stream<Entry<K, V>> stream() {
        return this.entrySet().stream();
    }

    @Override
    public final Collection<V> values() {
        return this.ref.get().values();
    }

}

ConcurrentMap (implementation of AtomicMap)

package net.netcoding.core.api.util.concurrent;

import net.netcoding.core.api.util.concurrent.atomic.AtomicMap;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

public class ConcurrentMap<K, V> extends AtomicMap<K, V, HashMap<K, V>> {

    /**
     * Create a new concurrent map.
     */
    public ConcurrentMap() {
        super(new HashMap<>());
    }

    /**
     * Create a new concurrent map and fill it with the given map.
     */
    public ConcurrentMap(Map<? extends K, ? extends V> map) {
        super(new HashMap<>(map));
    }

}
Nahydrin
  • 13,197
  • 12
  • 59
  • 101
  • Well, I haven't tried to understand all your code, but even before that I thought that it is worth mentioning that atomic documentation says: ”The specifications of these methods enable implementations to employ efficient machine-level atomic instructions that are available on contemporary processors. However on some platforms, support may entail some form of internal locking. Thus the methods are not strictly guaranteed to be non-blocking -- a thread may block transiently before performing the operation.”. Your synchronized method is just serializing all those calls for all I can tell. – Edwin Dalorzo Jul 21 '20 at 06:01
  • @EdwinDalorzo what do you mean by `Your synchronized method is just serializing all those calls for all I can tell`? – Nahydrin Jul 21 '20 at 18:54

0 Answers0