diff --git src/org/openstreetmap/gui/jmapviewer/Tile.java src/org/openstreetmap/gui/jmapviewer/Tile.java
index 82ab19e..edd6554 100644
--- src/org/openstreetmap/gui/jmapviewer/Tile.java
+++ src/org/openstreetmap/gui/jmapviewer/Tile.java
@@ -48,9 +48,9 @@ public class Tile {
     protected int zoom;
     protected BufferedImage image;
     protected String key;
-    protected boolean loaded = false;
-    protected boolean loading = false;
-    protected boolean error = false;
+    protected volatile boolean loaded = false; // field accessed by multiple threads without any monitors, needs to be volatile
+    protected volatile boolean loading = false;
+    protected volatile boolean error = false;
     protected String error_message;
 
     /** TileLoader-specific tile metadata */
diff --git src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
index 15d4f45..b4a0fbd 100644
--- src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
+++ src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
@@ -25,6 +25,7 @@ import java.util.logging.Logger;
 import org.apache.commons.jcs.access.behavior.ICacheAccess;
 import org.apache.commons.jcs.engine.behavior.ICacheElement;
 import org.openstreetmap.gui.jmapviewer.FeatureAdapter;
+import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.cache.ICachedLoaderListener.LoadResult;
 import org.openstreetmap.josm.data.preferences.IntegerProperty;
 
@@ -282,7 +283,7 @@ public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements
             }
         } catch (Exception e) {
             log.log(Level.WARNING, "JCS - Error while loading object from cache: {0}; {1}", new Object[]{e.getMessage(), getUrl()});
-            log.log(Level.FINE, "Stacktrace", e);
+            Main.warn(e);
             for (ICachedLoaderListener l: listeners) {
                 l.loadingFinished(cacheData, LoadResult.FAILURE);
             }
@@ -381,7 +382,8 @@ public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements
             cache.put(getCacheKey(), createCacheEntry(new byte[]{}), attributes);
             return handleNotFound();
         } catch (Exception e) {
-            log.log(Level.WARNING, "JCS - Exception during download " + getUrl(), e);
+            log.log(Level.WARNING, "JCS - Exception during download {0}",  getUrl());
+            Main.warn(e);
         }
         log.log(Level.WARNING, "JCS - Silent failure during download: {0}", getUrl());
         return false;
diff --git src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
index e0fd382..a627c90 100644
--- src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
+++ src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
@@ -4,8 +4,11 @@ package org.openstreetmap.josm.data.imagery;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.net.URL;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.ThreadPoolExecutor;
@@ -36,9 +39,12 @@ import org.openstreetmap.josm.data.preferences.IntegerProperty;
 public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, BufferedImageCacheEntry> implements TileJob, ICachedLoaderListener  {
     private static final Logger log = FeatureAdapter.getLogger(TMSCachedTileLoaderJob.class.getCanonicalName());
     private Tile tile;
-    private TileLoaderListener listener;
     private volatile URL url;
 
+    // we need another deduplication of Tile Loader listeners, as for each submit, new TMSCachedTileLoaderJob was created
+    // that way, we reduce calls to tileLoadingFinished, and general CPU load due to surplus Map repaints
+    private static final ConcurrentMap<String,Set<TileLoaderListener>> inProgress = new ConcurrentHashMap<>();
+
     /**
      * Limit definition for per host concurrent connections
      */
@@ -132,7 +138,17 @@ public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, Buffe
             Map<String, String> headers) {
         super(cache, connectTimeout, readTimeout, headers);
         this.tile = tile;
-        this.listener = listener;
+        if (listener != null) {
+            String deduplicationKey = getCacheKey();
+            synchronized (inProgress) {
+                Set<TileLoaderListener> newListeners = inProgress.get(deduplicationKey);
+                if (newListeners == null) {
+                    newListeners = new HashSet<>();
+                    inProgress.put(deduplicationKey, newListeners);
+                }
+                newListeners.add(listener);
+            }
+        }
     }
 
     @Override
@@ -226,33 +242,46 @@ public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, Buffe
 
     @Override
     public void loadingFinished(CacheEntry object, LoadResult result) {
+        Set<TileLoaderListener> listeners;
+        synchronized (inProgress) {
+            listeners = inProgress.remove(getCacheKey());
+        }
+
         try {
-            tile.finishLoading(); // whatever happened set that loading has finished
-            switch(result){
-            case FAILURE:
-                tile.setError("Problem loading tile");
-                // no break intentional here
-            case SUCCESS:
-                handleNoTileAtZoom();
-                if (object != null) {
-                    byte[] content = object.getContent();
-                    if (content != null && content.length > 0) {
-                        tile.loadImage(new ByteArrayInputStream(content));
+            if(!tile.isLoaded()) { //if someone else already loaded tile, skip all the handling
+                tile.finishLoading(); // whatever happened set that loading has finished
+                switch(result){
+                case FAILURE:
+                    tile.setError("Problem loading tile");
+                    // no break intentional here
+                case SUCCESS:
+                    handleNoTileAtZoom();
+                    if (object != null) {
+                        byte[] content = object.getContent();
+                        if (content != null && content.length > 0) {
+                            tile.loadImage(new ByteArrayInputStream(content));
+                        }
                     }
+                    // no break intentional here
+                case REJECTED:
+                    // do nothing
                 }
-                // no break intentional here
-            case REJECTED:
-                // do nothing
             }
-            if (listener != null) {
-                listener.tileLoadingFinished(tile, result.equals(LoadResult.SUCCESS));
+
+            // always check, if there is some listener interested in fact, that tile has finished loading
+            if (listeners != null) { // listeners might be null, if some other thread notified already about success
+                for(TileLoaderListener l: listeners) {
+                    l.tileLoadingFinished(tile, result.equals(LoadResult.SUCCESS));
+                }
             }
         } catch (IOException e) {
             log.log(Level.WARNING, "JCS TMS - error loading object for tile {0}: {1}", new Object[] {tile.getKey(), e.getMessage()});
             tile.setError(e.getMessage());
             tile.setLoaded(false);
-            if (listener != null) {
-                listener.tileLoadingFinished(tile, false);
+            if (listeners != null) { // listeners might be null, if some other thread notified already about success
+                for(TileLoaderListener l: listeners) {
+                    l.tileLoadingFinished(tile, false);
+                }
             }
         }
     }
