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

GWT模拟的AbstractHashMap中出现意外的java.util.ConcurrentModificationException,gwt,Gwt,在gwt模拟的AbstractHashMap类中调用iter.next()时,我们在使用gwt 2.8.0时遇到了java.util.ConcurrentModificationException(请参见下面的堆栈跟踪和回调计时器类)。涉及我们代码的跟踪的最低点在第118行,在方法private void tick()中,是对iter.next()的调用 深入跟踪,我看到AbstractHashMap: @Override public Entry<K, V> next() {

在gwt模拟的AbstractHashMap类中调用iter.next()时,我们在使用gwt 2.8.0时遇到了java.util.ConcurrentModificationException(请参见下面的堆栈跟踪和回调计时器类)。涉及我们代码的跟踪的最低点在第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;
}
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();
    }
}
package com.XXXXX.common.gwt.timer;
导入java.util.ConcurrentModificationException;
导入java.util.HashMap;
导入java.util.Iterator;
导入java.util.Map;
导入java.util.logging.Level;
导入java.util.logging.Logger;
导入com.google.common.base.Optional;
导入com.google.gwt.user.client.Timer;
/**
*一种{@link Timer}包装器,允许在给定的节拍数之后调用回调的注册。
*计时器将仅在当前至少注册了一个{@link TickCallback}时运行,并且在所有
*回调已取消注册。
*
*此类的目的是通过允许GWT应用程序中的所有回调使用相同的方法来减少开销
*Javascript定时器。
*/
公共类回调计时器
{
私有静态最终记录器Logger=Logger.getLogger(CallbackTimer.class.getName());
专用静态最终整数毫微秒=1000;
私人定时器;
private-Map callbackRegistry=new HashMap();
公共回调计时器()
{
计时器=新计时器()
{
@凌驾
公开募捐
{
尝试
{
勾选();
}
捕获(ConcurrentModificationException ConcurrentModificationException)
{
LOGGER.log(Level.WARNING,“中的并发修改异常”+
“CallbackTimer.tick()”,concurrentModificationException);
}
}
};
}
公共无效注册表回调(对象键,回拨)
{
if(callbackRegistry.containsKey(键))
{
LOGGER.fine(“Key”+Key.toString()+”正在被新回调覆盖。”);
}
callbackRegistry.put(键,callback);
callback.markStartTime();
LOGGER.finer(“Key”+Key.toString()+“registed”);
如果(!timer.isRunning())
{
startTimer();
}
}
public void unregisterCallback(对象键)
{
if(callbackRegistry.containsKey(键))
{
callbackRegistry.remove(键);
LOGGER.finer(“Key”+Key.toString()+“unregistered”);
if(callbackRegistry.isEmpty())
{
停止计时器();
}
}
其他的
{
LOGGER.info(“试图注销键“+key.toString()+”,但此键尚未注册。”);
}
}
私有void unregisterCallback(迭代器iter,对象键)
{
iter.remove();
LOGGER.finer(“Key”+Key.toString()+“unregistered”);
if(callbackRegistry.isEmpty())
{
停止计时器();
}
}
公共布尔键已注册(对象键)
{
返回callbackRegistry.containsKey(键);
}
公共TickCallback getCallback(对象键)
{
如果(键已注册(键))
{
返回callbackRegistry.get(key);
}
其他的
{
LOGGER.fine(“Key”+Key.toString()+”未注册;返回null.);
返回null;
}
}
私人空白勾号()
{
long fireTimeMillis=System.currentTimeMillis();
迭代器iter=callbackRegistry.keySet().Iterator();
while(iter.hasNext())
{
Object key=iter.next();//代码堆栈中的最低点
TickCallback=callbackRegistry.get(key);
if(callback.isFireTime(fireTimeMillis))
{
if(Level.FINEST.equals(LOGGER.getLevel()))
{
LOGGER.finest(“为键触发回调”+key.toString());
}
callback.onTick();
callback.markLastFireTime();
}
if(callback.shouldTerminate())
{
LOGGER.finer(“键的回调”+key.toString()+
“已达到指定的运行时间达秒,现在将取消注册。”);
取消注册回调(iter,密钥);
}
}
}
私有void startTimer()
{
计时器计划重复(毫秒);
LOGGER.finer(此+“已启动”);
}
私有void stopTimer()
{
timer.cancel();
LOGGER.finer(此+“已停止”);
}
/**
*在给定时间间隔内运行的任务,可选择指定运行的最大秒数。
*/
公共静态抽象类回调
{
私人长间隔;
私人长启动数毫秒;
私人长枪射击;
专用可选runForMillis;
/**
*@param intervalSeconds
*每次调用{@link#onTick()}之间必须经过的秒数。
*@param runForSeconds
*可选的最大运行秒数,超过该秒后,将有资格执行TickCallback
*要自动取消注册,请传递{@link Optional#缺席()}以指定
*必须手动注销。请使此值与{@param intervalSeconds}相同,以运行
*只回一次电话。
*/
公共回调(int intervalSeconds,可选runForSeconds)
{
this.intervalMillis=intervalSeconds*MILLIS_英寸秒;
this.runForMillis=runForSeconds
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)
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();
    }
}
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();
    }
}