意外的Java.util.ConcurrentModification Exception在GWT中模拟了AbsTrac



我们正在使用GWT 2.8.0的Java.util.concurrentModificationException,同时调用GWT中的iter.next((模拟abstracthashmap类(请参阅下面的stack Trace和我们的CallbackTimer类(。涉及我们代码的跟踪中的最低点是在第118行中,在方法private void tick()中,调用iter.next((。

钻入痕迹,我看到Abstracthashmap:

@Override
public Entry<K, V> next() {
  checkStructuralChange(AbstractHashMap.this, this);
  checkElement(hasNext());
  last = current;
  Entry<K, V> rv = current.next();
  hasNext = computeHasNext();
  return rv;
}

调用ConcurrentModificationDetector.CheckstructuralChange:

public static void checkStructuralChange(Object host, Iterator<?> iterator) {
    if (!API_CHECK) {
      return;
    }
if (JsUtils.getIntProperty(iterator, MOD_COUNT_PROPERTY)
    != JsUtils.getIntProperty(host, MOD_COUNT_PROPERTY)) {
  throw new ConcurrentModificationException();
    }
}

我对ContrentModification Exception的目的的理解是避免在迭代时进行收集的变化。我认为iter.next((不会属于该类别。此外,我看到该系列在迭代期间发生变化的唯一地方通过迭代器本身进行。我在这里错过了什么吗?任何帮助将不胜感激!

我们的堆栈跟踪:

java.util.ConcurrentModificationException
    at Unknown.Throwable_1_g$(Throwable.java:61)
    at Unknown.Exception_1_g$(Exception.java:25)
    at Unknown.RuntimeException_1_g$(RuntimeException.java:25)
    at Unknown.ConcurrentModificationException_1_g$(ConcurrentModificationException.java:25)
    at Unknown.checkStructuralChange_0_g$(ConcurrentModificationDetector.java:54)
    at Unknown.next_79_g$(AbstractHashMap.java:106)
    at Unknown.next_78_g$(AbstractHashMap.java:105)
    at Unknown.next_81_g$(AbstractMap.java:217)
    at Unknown.tick_0_g$(CallbackTimer.java:118)
    at Unknown.run_47_g$(CallbackTimer.java:41)
    at Unknown.fire_0_g$(Timer.java:135)
    at Unknown.anonymous(Timer.java:139)
    at Unknown.apply_65_g$(Impl.java:239)
    at Unknown.entry0_0_g$(Impl.java:291)
    at Unknown.anonymous(Impl.java:77)

callbacktimer.java的源代码在这里:

package com.XXXXX.common.gwt.timer;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.google.common.base.Optional;
import com.google.gwt.user.client.Timer;

/**
 * A {@link Timer} wrapper which allows for the registration of callbacks to be invoked after a given number of ticks.
 * The timer will only run if at least one {@link TickCallback} is currently registered and will stop running when all
 * callbacks have been unregistered.
 *
 * The intent of this class is to reduce overhead by allowing all callbacks in a GWT application to use the same
 * Javascript timer.
 */
public class CallbackTimer
{
    private static final Logger LOGGER = Logger.getLogger(CallbackTimer.class.getName());
    private static final int MILLIS_IN_SEC = 1000;
    private Timer timer;
    private Map<Object, TickCallback> callbackRegistry = new HashMap<>();
    public CallbackTimer()
    {
        timer = new Timer()
        {
            @Override
            public void run()
            {
                try
                {
                    tick();
                }
                catch(ConcurrentModificationException concurrentModificationException)
                {
                    LOGGER.log(Level.WARNING, "Concurrent Modification Exception in " +
                        "CallbackTimer.tick()", concurrentModificationException);
                }
            }
        };
    }
    public void registerCallback(Object key, TickCallback callback)
    {
        if (callbackRegistry.containsKey(key))
        {
            LOGGER.fine("Key " + key.toString() + " is being overwritten with a new callback.");
        }
        callbackRegistry.put(key, callback);
        callback.markStartTime();
        LOGGER.finer("Key " + key.toString() + " registered.");
        if (!timer.isRunning())
        {
            startTimer();
        }
    }
    public void unregisterCallback(Object key)
    {
        if (callbackRegistry.containsKey(key))
        {
            callbackRegistry.remove(key);
            LOGGER.finer("Key " + key.toString() + " unregistered.");
            if (callbackRegistry.isEmpty())
            {
                stopTimer();
            }
        }
        else
        {
            LOGGER.info("Attempted to unregister key " + key.toString() + ", but this key has not been registered.");
        }
    }
    private void unregisterCallback(Iterator<Object> iter, Object key)
    {
        iter.remove();
        LOGGER.finer("Key " + key.toString() + " unregistered.");
        if (callbackRegistry.isEmpty())
        {
            stopTimer();
        }
    }
    public boolean keyIsRegistered(Object key)
    {
        return callbackRegistry.containsKey(key);
    }
    public TickCallback getCallback(Object key)
    {
        if (keyIsRegistered(key))
        {
            return callbackRegistry.get(key);
        }
        else
        {
            LOGGER.fine("Key " + key.toString() + " is not registered; returning null.");
            return null;
        }
    }
    private void tick()
    {
        long fireTimeMillis = System.currentTimeMillis();
        Iterator<Object> iter = callbackRegistry.keySet().iterator();
        while (iter.hasNext())
        {
            Object key = iter.next();//Lowest point in stack for our code
            TickCallback callback = callbackRegistry.get(key);
            if (callback.isFireTime(fireTimeMillis))
            {
                if (Level.FINEST.equals(LOGGER.getLevel()))
                {
                    LOGGER.finest("Firing callback for key " + key.toString());
                }
                callback.onTick();
                callback.markLastFireTime();
            }
            if (callback.shouldTerminate())
            {
                LOGGER.finer("Callback for key " + key.toString() +
                    " has reached its specified run-for-seconds and will now be unregistered.");
                unregisterCallback(iter, key);
            }
        }
    }
    private void startTimer()
    {
        timer.scheduleRepeating(MILLIS_IN_SEC);
        LOGGER.finer(this + " started.");
    }
    private void stopTimer()
    {
        timer.cancel();
        LOGGER.finer(this + " stopped.");
    }

    /**
     * A task to run on a given interval, with the option to specify a maximum number of seconds to run.
     */
    public static abstract class TickCallback
    {
        private long intervalMillis;
        private long startedAtMillis;
        private long millisRunningAtLastFire;
        private Optional<Long> runForMillis;
        /**
         * @param intervalSeconds
         *          The number of seconds which must elapse between each invocation of {@link #onTick()}.
         * @param runForSeconds
         *          An optional maximum number of seconds to run for, after which the TickCallback will be eligible
         *          to be automatically unregistered.  Pass {@link Optional#absent()} to specify that the TickCallback
         *          must be manually unregistered.  Make this value the same as {@param intervalSeconds} to run the
         *          callback only once.
         */
        public TickCallback(int intervalSeconds, Optional<Integer> runForSeconds)
        {
            this.intervalMillis = intervalSeconds * MILLIS_IN_SEC;
            this.runForMillis = runForSeconds.isPresent() ?
                    Optional.of((long)runForSeconds.get() * MILLIS_IN_SEC) : Optional.<Long>absent();
        }
        private void markStartTime()
        {
            millisRunningAtLastFire = 0;
            startedAtMillis = System.currentTimeMillis();
        }
        private void markLastFireTime()
        {
            millisRunningAtLastFire += intervalMillis;
        }
        private boolean isFireTime(long nowMillis)
        {
            return nowMillis - (startedAtMillis + millisRunningAtLastFire) >= intervalMillis;
        }
        private boolean shouldTerminate()
        {
            return runForMillis.isPresent() && System.currentTimeMillis() - startedAtMillis >= runForMillis.get();
        }
        /**
         * A callback to be run every time intervalSeconds seconds have past since this callback was registered.
         */
        public abstract void onTick();
    }
}

更新2017-06-08

我最终遵循Walen的第一个建议。我没有看到简单的eventbus是该特定工作的正确工具。但是,我确实无耻地窃取了SEBS的方法来集成新添加/删除的回调:

package com.XXXXX.common.gwt.timer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.google.common.base.Optional;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Timer;
/**
 * A {@link Timer} wrapper which allows for the registration of callbacks to be invoked after a given number of ticks.
 * The timer will only run if at least one {@link TickCallback} is currently registered and will stop running when all
 * callbacks have been unregistered.
 *
 * The intent of this class is to reduce overhead by allowing all callbacks in a GWT application to use the same
 * Javascript timer.
 */
public class CallbackTimer
{
    private static final Logger LOGGER = Logger.getLogger(CallbackTimer.class.getName());
    private static final int MILLIS_IN_SEC = 1000;
    private Timer timer;
    private Map<Object, TickCallback> callbackRegistry = new HashMap<>();
    private List<Command> deferredDeltas = new ArrayList<>();
    public CallbackTimer()
    {
        timer = new Timer()
        {
            @Override
            public void run()
            {
                tick();
            }
        };
    }
    public void registerCallback(final Object key, final TickCallback callback)
    {
        deferredDeltas.add(new Command()
        {
            @Override
            public void execute()
            {
                activateCallback(key, callback);
            }
        });
        if (!timer.isRunning())
        {
            startTimer();
        }
    }
    private void activateCallback(Object key, TickCallback callback)
    {
        if (callbackRegistry.containsKey(key))
        {
            LOGGER.fine("Key " + key.toString() + " is being overwritten with a new callback.");
        }
        callbackRegistry.put(key, callback);
        callback.markStartTime();
        LOGGER.finer("Key " + key.toString() + " registered.");
    }
    public void unregisterCallback(final Object key)
    {
        deferredDeltas.add(new Command()
        {
            @Override
            public void execute()
            {
                deactivateCallback(key);
            }
        });
    }
    private void deactivateCallback(Object key)
    {
        if (callbackRegistry.containsKey(key))
        {
            callbackRegistry.remove(key);
            LOGGER.fine("Key " + key.toString() + " unregistered.");
            if (callbackRegistry.isEmpty())
            {
                stopTimer();
            }
        }
        else
        {
            LOGGER.info("Attempted to unregister key " + key.toString() + ", but this key has not been registered.");
        }
    }
    private void handleQueuedAddsAndRemoves()
    {
        for (Command c : deferredDeltas)
        {
            c.execute();
        }
        deferredDeltas.clear();
    }
    public boolean keyIsRegistered(Object key)
    {
        return callbackRegistry.containsKey(key);
    }
    private void tick()
    {
        handleQueuedAddsAndRemoves();
        long fireTimeMillis = System.currentTimeMillis();
        for (Map.Entry<Object, TickCallback> objectTickCallbackEntry : callbackRegistry.entrySet())
        {
            Object key = objectTickCallbackEntry.getKey();
            TickCallback callback = objectTickCallbackEntry.getValue();
            if (callback.isFireTime(fireTimeMillis))
            {
                if (Level.FINEST.equals(LOGGER.getLevel()))
                {
                    LOGGER.finest("Firing callback for key " + key.toString());
                }
                callback.onTick();
                callback.markLastFireTime();
            }
            if (callback.shouldTerminate())
            {
                LOGGER.finer("Callback for key " + key.toString() +
                    " has reached its specified run-for-seconds and will now be unregistered.");
                unregisterCallback(key);
            }
        }
    }
    private void startTimer()
    {
        timer.scheduleRepeating(MILLIS_IN_SEC);
        LOGGER.finer(this + " started.");
    }
    private void stopTimer()
    {
        timer.cancel();
        LOGGER.finer(this + " stopped.");
    }

    /**
     * A task to run on a given interval, with the option to specify a maximum number of seconds to run.
     */
    public static abstract class TickCallback
    {
        private long intervalMillis;
        private long startedAtMillis;
        private long millisRunningAtLastFire;
        private Optional<Long> runForMillis;
        /**
         * @param intervalSeconds The number of seconds which must elapse between each invocation of {@link #onTick()}.
         * @param runForSeconds An optional maximum number of seconds to run for, after which the TickCallback will be
         * eligible
         * to be automatically unregistered.  Pass {@link Optional#absent()} to specify that the TickCallback
         * must be manually unregistered.  Make this value the same as {@param intervalSeconds} to run the
         * callback only once.
         */
        protected TickCallback(int intervalSeconds, Optional<Integer> runForSeconds)
        {
            this.intervalMillis = intervalSeconds * MILLIS_IN_SEC;
            this.runForMillis = runForSeconds.isPresent() ?
                Optional.of((long) runForSeconds.get() * MILLIS_IN_SEC) : Optional.<Long>absent();
        }
        private void markStartTime()
        {
            millisRunningAtLastFire = 0;
            startedAtMillis = System.currentTimeMillis();
        }
        private void markLastFireTime()
        {
            millisRunningAtLastFire += intervalMillis;
        }
        private boolean isFireTime(long nowMillis)
        {
            return nowMillis - (startedAtMillis + millisRunningAtLastFire) >= intervalMillis;
        }
        private boolean shouldTerminate()
        {
            return runForMillis.isPresent() && System.currentTimeMillis() - startedAtMillis >= runForMillis.get();
        }
        /**
         * A callback to be run every time intervalSeconds seconds have past since this callback was registered.
         */
        public abstract void onTick();
    }
}

您的问题似乎是在tick()方法试图穿越其keySet的同时,将新项目(新密钥(添加到地图中。

在遍历时以任何方式修改集合使用迭代器只能避免在删除项目时避免这种情况,但是由于没有iterator.add()方法,因此无法安全地添加项目。

如果这是服务器端代码,则可以使用ConcurrentHashMap,该CC_6保证其迭代器不会在这种情况下抛出例外(以 not 保证每个项目都将是遍历,如果在创建迭代后添加(。
但是ConcurrentHashMap尚未得到GWT的JRE仿真库的支持,因此您不能在客户端代码中使用它。

您需要提出另一种将项目添加到CallbackRegistry 的方式。
例如,您可以更改registerCallback()方法,以便将新项目添加到列表/队列而不是地图,然后让tick()方法在遍历现有的项目后将这些项目从队列移至地图。<<br>另外,您可以使用Thomas Broyer评论中指出的SimpleEventBus。

最新更新