diff --git a/src/org/openstreetmap/josm/data/osm/AbstractPrimitive.java b/src/org/openstreetmap/josm/data/osm/AbstractPrimitive.java
index bb7127c..f384c67 100644
--- a/src/org/openstreetmap/josm/data/osm/AbstractPrimitive.java
+++ b/src/org/openstreetmap/josm/data/osm/AbstractPrimitive.java
@@ -8,7 +8,6 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -498,16 +497,8 @@ public abstract class AbstractPrimitive implements IPrimitive {
      * @see #visitKeys(KeyValueVisitor)
      */
     @Override
-    public Map<String, String> getKeys() {
-        String[] keys = this.keys;
-        final Map<String, String> result = new HashMap<>(
-                Utils.hashMapInitialCapacity(keys == null ? 0 : keys.length / 2));
-        if (keys != null) {
-            for (int i = 0; i < keys.length; i += 2) {
-                result.put(keys[i], keys[i + 1]);
-            }
-        }
-        return result;
+    public TagMap getKeys() {
+        return new TagMap(keys);
     }
 
     /**
@@ -552,6 +543,21 @@ public abstract class AbstractPrimitive implements IPrimitive {
     }
 
     /**
+     * Copy the keys from a TagMap.
+     * @param keys The new key map.
+     */
+    public void setKeys(TagMap keys) {
+        Map<String, String> originalKeys = getKeys();
+        String[] arr = keys.getTagsArray();
+        if (arr.length == 0) {
+            this.keys = null;
+        } else {
+            this.keys = arr;
+        }
+        keysChangedImpl(originalKeys);
+    }
+
+    /**
      * Set the given value to the given key. If key is null, does nothing. If value is null,
      * removes the key and behaves like {@link #remove(String)}.
      *
diff --git a/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java b/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java
index 9d5cb5d..b0997ce 100644
--- a/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java
+++ b/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java
@@ -914,6 +914,16 @@ public abstract class OsmPrimitive extends AbstractPrimitive implements Comparab
      ------------*/
 
     @Override
+    public final void setKeys(TagMap keys) {
+        boolean locked = writeLock();
+        try {
+            super.setKeys(keys);
+        } finally {
+            writeUnlock(locked);
+        }
+    }
+
+    @Override
     public final void setKeys(Map<String, String> keys) {
         boolean locked = writeLock();
         try {
diff --git a/src/org/openstreetmap/josm/data/osm/Tag.java b/src/org/openstreetmap/josm/data/osm/Tag.java
index 98d710d..0dcf43a 100644
--- a/src/org/openstreetmap/josm/data/osm/Tag.java
+++ b/src/org/openstreetmap/josm/data/osm/Tag.java
@@ -1,6 +1,8 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.data.osm;
 
+import java.util.Map.Entry;
+
 import org.openstreetmap.josm.tools.CheckParameterUtil;
 import org.openstreetmap.josm.tools.Utils;
 
@@ -9,7 +11,7 @@ import org.openstreetmap.josm.tools.Utils;
  * be empty, but not null.
  *
  */
-public class Tag {
+public class Tag implements Entry<String, String> {
 
     private String key;
     private String value;
@@ -57,6 +59,7 @@ public class Tag {
      *
      * @return the key of the tag
      */
+    @Override
     public String getKey() {
         return key;
     }
@@ -66,11 +69,23 @@ public class Tag {
      *
      * @return the value of the tag
      */
+    @Override
     public String getValue() {
         return value;
     }
 
     /**
+     * This is not supported by this implementation.
+     * @param value ignored
+     * @return (Does not return)
+     * @throws UnsupportedOperationException always
+     */
+    @Override
+    public String setValue(String value) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
      * Replies true if the key of this tag is equal to <code>key</code>.
      * If <code>key</code> is null, assumes the empty key.
      *
diff --git a/src/org/openstreetmap/josm/data/osm/TagMap.java b/src/org/openstreetmap/josm/data/osm/TagMap.java
new file mode 100644
index 0000000..1b61eef
--- /dev/null
+++ b/src/org/openstreetmap/josm/data/osm/TagMap.java
@@ -0,0 +1,254 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.osm;
+
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Arrays;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+/**
+ * This class provides a read/write map that uses the same format as {@link AbstractPrimitive#keys}.
+ * It offers good performance for few keys.
+ * It uses copy on write, so there cannot be a {@link ConcurrentModificationException} while iterating through it.
+ *
+ * @author Michael Zangl
+ */
+public class TagMap extends AbstractMap<String, String> {
+    /**
+     * We use this array every time we want to represent an empty map.
+     * This saves us the burden of checking for null every time but saves some object allocations.
+     */
+    private static final String[] EMPTY_TAGS = new String[0];
+
+    /**
+     * An iterator that iterates over the tags in this map. The iterator always represents the state of the map when it was created.
+     * Further changes to the map won't change the tags that we iterate over but they also won't raise any exceptions.
+     * @author Michael Zangl
+     */
+    private static class TagEntryInterator implements Iterator<Entry<String, String>> {
+        /**
+         * The current state of the tags we iterate over.
+         */
+        private final String[] tags;
+        /**
+         * Current tag index. Always a multiple of 2.
+         */
+        private int currentIndex = 0;
+
+        /**
+         * Create a new {@link TagEntryInterator}
+         * @param tags The tags array. It is never changed but should also not be changed by you.
+         */
+        TagEntryInterator(String[] tags) {
+            super();
+            this.tags = tags;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return currentIndex < tags.length;
+        }
+
+        @Override
+        public Entry<String, String> next() {
+            if (!hasNext()) {
+                throw new NoSuchElementException();
+            }
+
+            Tag tag = new Tag(tags[currentIndex], tags[currentIndex + 1]);
+            currentIndex += 2;
+            return tag;
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+
+    }
+
+    /**
+     * This is the entry set of this map. It represents the state when it was created.
+     * @author Michael Zangl
+     */
+    private static class TagEntrySet extends AbstractSet<Entry<String, String>> {
+        private final String[] tags;
+
+        /**
+         * Create a new {@link TagEntrySet}
+         * @param tags The tags array. It is never changed but should also not be changed by you.
+         */
+        TagEntrySet(String[] tags) {
+            super();
+            this.tags = tags;
+        }
+
+        @Override
+        public Iterator<Entry<String, String>> iterator() {
+            return new TagEntryInterator(tags);
+        }
+
+        @Override
+        public int size() {
+            return tags.length / 2;
+        }
+
+    }
+
+    /**
+     * The tags field. This field is guarded using RCU.
+     */
+    private volatile String[] tags;
+
+    /**
+     * Creates a new, empty tag map.
+     */
+    public TagMap() {
+        this(null);
+    }
+
+    /**
+     * Creates a new read only tag map using a key/value/key/value/... array.
+     * <p>
+     * The array that is passed as parameter may not be modified after passing it to this map.
+     * @param tags The tags array. It is not modified by this map.
+     */
+    public TagMap(String[] tags) {
+        if (tags == null || tags.length == 0) {
+            this.tags = EMPTY_TAGS;
+        } else {
+            if (tags.length % 2 != 0) {
+                throw new IllegalArgumentException("tags array length needs to be multiple of two.");
+            }
+            this.tags = tags;
+        }
+    }
+
+    @Override
+    public Set<Entry<String, String>> entrySet() {
+        return new TagEntrySet(tags);
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        return indexOfKey(tags, key) >= 0;
+    }
+
+    @Override
+    public String get(Object key) {
+        String[] tags = this.tags;
+        int index = indexOfKey(tags, key);
+        return index < 0 ? null : tags[index + 1];
+    }
+
+    @Override
+    public boolean containsValue(Object value) {
+        String[] tags = this.tags;
+        for (int i = 1; i < tags.length; i += 2) {
+            if (value.equals(tags[i])) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public synchronized String put(String key, String value) {
+        if (key == null) {
+            throw new NullPointerException();
+        }
+        if (value == null) {
+            throw new NullPointerException();
+        }
+        int index = indexOfKey(tags, key);
+        int newTagArrayLength = tags.length;
+        if (index < 0) {
+            index = newTagArrayLength;
+            newTagArrayLength += 2;
+        }
+
+        String[] newTags = Arrays.copyOf(tags, newTagArrayLength);
+        String old = newTags[index + 1];
+        newTags[index] = key;
+        newTags[index + 1] = value;
+        tags = newTags;
+        return old;
+    }
+
+    @Override
+    public synchronized String remove(Object key) {
+        int index = indexOfKey(tags, key);
+        if (index < 0) {
+            return null;
+        }
+        String old = tags[index + 1];
+        int newLength = tags.length - 2;
+        if (newLength == 0) {
+            tags = EMPTY_TAGS;
+        } else {
+            String[] newTags = new String[newLength];
+            System.arraycopy(tags, 0, newTags, 0, index);
+            System.arraycopy(tags, index + 2, newTags, index, newLength - index);
+            tags = newTags;
+        }
+
+        return old;
+    }
+
+    @Override
+    public synchronized void clear() {
+        tags = EMPTY_TAGS;
+    }
+
+    @Override
+    public int size() {
+        return tags.length / 2;
+    }
+
+    /**
+     * Finds a key in an array that is structured like the {@link #tags} array and returns the position.
+     * <p>
+     * We allow the parameter to be passed to allow for better synchronization.
+     *
+     * @param tags The tags array to search through.
+     * @param key The key to search.
+     * @return The index of the key (a multiple of two) or -1 if it was not found.
+     */
+    private static int indexOfKey(String[] tags, Object key) {
+        for (int i = 0; i < tags.length; i += 2) {
+            if (tags[i].equals(key)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder stringBuilder = new StringBuilder();
+        stringBuilder.append("TagMap[");
+        boolean first = true;
+        for (java.util.Map.Entry<String, String> e : entrySet()) {
+            if (!first) {
+                stringBuilder.append(",");
+            }
+            stringBuilder.append(e.getKey());
+            stringBuilder.append("=");
+            stringBuilder.append(e.getValue());
+            first = false;
+        }
+        stringBuilder.append("]");
+        return stringBuilder.toString();
+    }
+
+    /**
+     * Gets the backing tags array. Do not modify this array.
+     * @return The tags array.
+     */
+    String[] getTagsArray() {
+        return tags;
+    }
+}
