GWT中的意外java.util.ConcurrentModificationException模拟AbstractHashMap

时间:2017-05-30 18:45:42

标签: gwt

我们正在使用gwt 2.8.0遇到java.util.ConcurrentModificationException,同时在GWT模拟的AbstractHashMap类中调用iter.next()(请参阅下面的堆栈跟踪和我们的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();
    }
}

我对ConcurrentModificationException的目的的理解是避免在迭代过程中更改集合。我不认为iter.next()会属于那个类别。此外,我在迭代期间看到集合发生变化的唯一地方是通过迭代器本身完成的。我在这里错过了什么吗?任何帮助将不胜感激!

我们的Stack Trace:

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的第一个建议。我没有看到SimpleEventBus在哪个特定工作的正确工具。然而,我确实无耻地窃取了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();
    }
}

1 个答案:

答案 0 :(得分:2)

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

在浏览集合时以任何方式修改集合抛出ConcurrentModificationException
使用迭代器只能让您在删除项目时避免使用它,但由于没有iterator.add()方法,您无法安全地添加项目。

如果这是服务器端代码,您可以使用ConcurrentHashMap,这可以保证其迭代器在这种情况下不会抛出异常(代价是保证每个项目都将被遍历,如果它是在迭代器创建后添加的。) 但GWT的JRE仿真库尚未支持ConcurrentHashMap,因此您无法在客户端代码中使用它。

您需要采用不同的方式向CallbackRegistry 添加项目。
例如,您可以更改registerCallback()方法,以便将新项目添加到列表/队列而不是地图,然后使用tick()方法将这些项目从队列移到地图之后#39; s遍历现有的 或者,您可以{@ 3}}在Thomas Broyer的评论中指出。