You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by na...@apache.org on 2026/10/12 15:42:53 UTC

(ignite) branch master updated: IGNITE-29099 Avoid boxing of cache ID and message direct type on the cache operation hot path (#13662)

This is an automated email from the ASF dual-hosted git repository.

NSAmelchev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new bb7f60f26cf IGNITE-29099 Avoid boxing of cache ID and message direct type on the cache operation hot path (#13662)
bb7f60f26cf is described below

commit bb7f60f26cfeede822ae008e38bb832f63b6c9b0
Author: Nikita Amelchev <ns...@gmail.com>
AuthorDate: Mon Oct 12 18:42:45 2026 +0300

    IGNITE-29099 Avoid boxing of cache ID and message direct type on the cache operation hot path (#13662)
---
 .../processors/cache/GridCacheSharedContext.java   |   7 +-
 .../util/collection/CopyOnWriteIntMap.java         | 164 +++++++++++++++++++
 .../internal/util/collection/IntHashMap.java       |  71 +++++++--
 .../tcp/TcpCommunicationMetricsListener.java       |  42 +----
 .../util/collection/CopyOnWriteIntMapTest.java     | 176 +++++++++++++++++++++
 .../internal/util/collection/IntHashMapTest.java   |  59 +++++++
 .../ignite/testsuites/IgniteBasicTestSuite2.java   |   2 +
 7 files changed, 473 insertions(+), 48 deletions(-)

diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheSharedContext.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheSharedContext.java
index aae8f54411c..ce09ab360ca 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheSharedContext.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheSharedContext.java
@@ -23,7 +23,6 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.ignite.IgniteCheckedException;
 import org.apache.ignite.IgniteLogger;
@@ -59,6 +58,7 @@ import org.apache.ignite.internal.processors.cache.version.GridCacheVersionManag
 import org.apache.ignite.internal.processors.cluster.IgniteChangeGlobalStateSupport;
 import org.apache.ignite.internal.processors.timeout.GridTimeoutProcessor;
 import org.apache.ignite.internal.util.GridIntList;
+import org.apache.ignite.internal.util.collection.CopyOnWriteIntMap;
 import org.apache.ignite.internal.util.future.GridCompoundFuture;
 import org.apache.ignite.internal.util.future.GridEmbeddedFuture;
 import org.apache.ignite.internal.util.future.GridFinishedFuture;
@@ -136,7 +136,7 @@ public class GridCacheSharedContext<K, V> {
     private PartitionsEvictManager evictMgr;
 
     /** Cache contexts map. */
-    private final ConcurrentHashMap<Integer, GridCacheContext<K, V>> ctxMap;
+    private final CopyOnWriteIntMap<GridCacheContext<K, V>> ctxMap = new CopyOnWriteIntMap<>();
 
     /** Tx metrics. */
     private final TransactionMetricsAdapter txMetrics;
@@ -252,8 +252,6 @@ public class GridCacheSharedContext<K, V> {
 
         txMetrics = new TransactionMetricsAdapter(kernalCtx);
 
-        ctxMap = new ConcurrentHashMap<>();
-
         kernalCtx.systemView().registerView(new ScanQuerySystemView<>(ctxMap.values()));
 
         locStoreCnt = new AtomicInteger();
@@ -537,6 +535,7 @@ public class GridCacheSharedContext<K, V> {
     /**
      * @param cacheCtx Cache context to remove.
      */
+    @SuppressWarnings("unchecked")
     void removeCacheContext(GridCacheContext cacheCtx) {
         int cacheId = cacheCtx.cacheId();
 
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/collection/CopyOnWriteIntMap.java b/modules/core/src/main/java/org/apache/ignite/internal/util/collection/CopyOnWriteIntMap.java
new file mode 100644
index 00000000000..c648942fa5e
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/collection/CopyOnWriteIntMap.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.collection;
+
+import java.util.AbstractCollection;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Objects;
+import java.util.function.IntFunction;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Thread-safe {@link IntMap} for data that is looked up on hot paths but changes rarely: reads never block or box,
+ * every write copies the map. With a noticeable share of writes it is slower than a concurrent map.
+ */
+public class CopyOnWriteIntMap<V> implements IntMap<V> {
+    /** Current map, never modified after publication: writes replace it with a copy. */
+    private volatile IntHashMap<V> map = new IntHashMap<>();
+
+    /** Live view over the current map. */
+    private final Collection<V> vals = new AbstractCollection<V>() {
+        @Override public Iterator<V> iterator() {
+            return map.valuesIterator();
+        }
+
+        @Override public int size() {
+            return map.size();
+        }
+    };
+
+    /** {@inheritDoc} */
+    @Override public V get(int key) {
+        return map.get(key);
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean containsKey(int key) {
+        return map.containsKey(key);
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean containsValue(V val) {
+        return map.containsValue(val);
+    }
+
+    /** {@inheritDoc} */
+    @Override public <E extends Throwable> void forEach(EntryConsumer<V, E> act) throws E {
+        map.forEach(act);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int size() {
+        return map.size();
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isEmpty() {
+        return map.isEmpty();
+    }
+
+    /** {@inheritDoc} */
+    @Override public int[] keys() {
+        return map.keys();
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The collection reflects the current state; its iterator traverses a snapshot taken at creation time and does not
+     * support removal.
+     */
+    @Override public Collection<V> values() {
+        return vals;
+    }
+
+    /** {@inheritDoc} */
+    @Override public synchronized V put(int key, V val) {
+        IntHashMap<V> copy = map.copy();
+
+        V old = copy.put(key, val);
+
+        map = copy;
+
+        return old;
+    }
+
+    /** {@inheritDoc} */
+    @Override public synchronized V putIfAbsent(int key, V val) {
+        IntHashMap<V> map = this.map;
+
+        return map.containsKey(key) ? map.get(key) : put(key, val);
+    }
+
+    /** {@inheritDoc} */
+    @Override public synchronized @Nullable V computeIfAbsent(int key, IntFunction<? extends V> mappingFunction) {
+        // The default implementation is a lookup followed by a put, the lock makes the pair atomic.
+        return IntMap.super.computeIfAbsent(key, mappingFunction);
+    }
+
+    /** {@inheritDoc} */
+    @Override public synchronized V remove(int key) {
+        IntHashMap<V> map = this.map;
+
+        return map.containsKey(key) ? removeFromCopy(map, key) : null;
+    }
+
+    /**
+     * Removes the mapping if the key is mapped to the given value.
+     *
+     * @param key Key.
+     * @param val Expected value.
+     * @return {@code True} if the mapping was removed.
+     */
+    public synchronized boolean remove(int key, V val) {
+        IntHashMap<V> map = this.map;
+
+        if (!map.containsKey(key) || !Objects.equals(map.get(key), val))
+            return false;
+
+        removeFromCopy(map, key);
+
+        return true;
+    }
+
+    /**
+     * @param map Current map, must contain the key.
+     * @param key Key.
+     * @return Removed value.
+     */
+    private V removeFromCopy(IntHashMap<V> map, int key) {
+        IntHashMap<V> copy = map.copy();
+
+        V old = copy.remove(key);
+
+        this.map = copy;
+
+        return old;
+    }
+
+    /** {@inheritDoc} */
+    @Override public synchronized void clear() {
+        map = new IntHashMap<>();
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return map.toString();
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/collection/IntHashMap.java b/modules/core/src/main/java/org/apache/ignite/internal/util/collection/IntHashMap.java
index 38bb575f5cf..cdf62389403 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/util/collection/IntHashMap.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/collection/IntHashMap.java
@@ -20,7 +20,9 @@ package org.apache.ignite.internal.util.collection;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Iterator;
 import java.util.List;
+import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.stream.Collectors;
 
@@ -93,18 +95,33 @@ public class IntHashMap<V> implements IntMap<V> {
 
     /** Default constructor. */
     public IntHashMap() {
-        entries = (Entry<V>[])new Entry[INITIAL_CAPACITY];
+        this(INITIAL_CAPACITY);
     }
 
     /** Create map with preallocated array. */
     public IntHashMap(int cap) {
-        int entriesSize = tableSize(cap);
+        this((Entry<V>[])new Entry[tableSize(cap)], 0);
+    }
 
-        compactThreshold = (int)(COMPACT_LOAD_FACTOR * (entriesSize >> 1));
+    /**
+     * @param entries Table, owned by the map.
+     * @param size Number of entries in the table.
+     */
+    private IntHashMap(Entry<V>[] entries, int size) {
+        init(entries, size);
+    }
 
-        scaleThreshold = (int)(entriesSize * SCALE_LOAD_FACTOR);
+    /**
+     * @param entries Table, owned by the map.
+     * @param size Number of entries in the table.
+     */
+    private void init(Entry<V>[] entries, int size) {
+        this.entries = entries;
+        this.size = size;
 
-        entries = (Entry<V>[])new Entry[entriesSize];
+        compactThreshold = (int)(COMPACT_LOAD_FACTOR * (entries.length >> 1));
+
+        scaleThreshold = (int)(entries.length * SCALE_LOAD_FACTOR);
     }
 
     /**
@@ -116,6 +133,44 @@ public class IntHashMap<V> implements IntMap<V> {
         other.forEach(this::put);
     }
 
+    /** @return Iterator over the values without copying them. The map must not be modified while iterating. */
+    Iterator<V> valuesIterator() {
+        Entry<V>[] entries = this.entries;
+
+        return new Iterator<V>() {
+            /** Index of the next non-empty slot. */
+            private int idx = nextIdx(0);
+
+            @Override public boolean hasNext() {
+                return idx < entries.length;
+            }
+
+            @Override public V next() {
+                if (idx >= entries.length)
+                    throw new NoSuchElementException();
+
+                V val = entries[idx].val;
+
+                idx = nextIdx(idx + 1);
+
+                return val;
+            }
+
+            /** @return Index of the first non-empty slot starting from the given one. */
+            private int nextIdx(int from) {
+                while (from < entries.length && entries[from] == null)
+                    from++;
+
+                return from;
+            }
+        };
+    }
+
+    /** @return Shallow copy of the same table size: the entries are immutable and shared with this map. */
+    IntHashMap<V> copy() {
+        return new IntHashMap<>(entries.clone(), size);
+    }
+
     /** {@inheritDoc} */
     @Override public V get(int key) {
         int idx = find(key);
@@ -230,11 +285,7 @@ public class IntHashMap<V> implements IntMap<V> {
 
     /** {@inheritDoc} */
     @Override public void clear() {
-        entries = (Entry<V>[])new Entry[INITIAL_CAPACITY];
-
-        compactThreshold = 0;
-        scaleThreshold = 0;
-        size = 0;
+        init((Entry<V>[])new Entry[INITIAL_CAPACITY], 0);
     }
 
     /** {@inheritDoc} */
diff --git a/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/TcpCommunicationMetricsListener.java b/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/TcpCommunicationMetricsListener.java
index 4bf17f1a969..afc30e9e9d5 100644
--- a/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/TcpCommunicationMetricsListener.java
+++ b/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/TcpCommunicationMetricsListener.java
@@ -30,6 +30,7 @@ import org.apache.ignite.internal.managers.communication.GridIoMessage;
 import org.apache.ignite.internal.managers.communication.IgniteMessageFactoryImpl;
 import org.apache.ignite.internal.processors.metric.MetricRegistryImpl;
 import org.apache.ignite.internal.processors.metric.impl.LongAdderMetric;
+import org.apache.ignite.internal.util.collection.CopyOnWriteIntMap;
 import org.apache.ignite.internal.util.collection.IntHashMap;
 import org.apache.ignite.internal.util.collection.IntMap;
 import org.apache.ignite.lang.IgniteBiTuple;
@@ -113,11 +114,8 @@ public class TcpCommunicationMetricsListener {
     /** Counters of sent and received messages by direct type. */
     private final IntMap<IgniteBiTuple<LongAdderMetric, LongAdderMetric>> msgCntrsByType;
 
-    /** Method to synchronize access to message type map. */
-    private final Object msgTypeMapMux = new Object();
-
-    /** Message type map. */
-    private volatile Map<Short, String> msgTypeMap;
+    /** Message class names by direct type, filled on the first message of each type. */
+    private final IntMap<String> msgTypeMap = new CopyOnWriteIntMap<>();
 
     /**
      * @param ignite Ignite instance.
@@ -341,14 +339,10 @@ public class TcpCommunicationMetricsListener {
             if (metric.name().startsWith(prefix)) {
                 short directType = Short.parseShort(metric.name().substring(prefix.length()));
 
-                Map<Short, String> msgTypeMap0 = msgTypeMap;
-
-                if (msgTypeMap0 != null) {
-                    String typeName = msgTypeMap0.get(directType);
+                String typeName = msgTypeMap.get(directType);
 
-                    if (typeName != null)
-                        res.put(typeName, ((LongMetric)metric).value());
-                }
+                if (typeName != null)
+                    res.put(typeName, ((LongMetric)metric).value());
             }
         }
 
@@ -430,28 +424,8 @@ public class TcpCommunicationMetricsListener {
     private void updateMessageTypeMap(Message msg) {
         short typeId = msg.directType();
 
-        Map<Short, String> msgTypeMap0 = msgTypeMap;
-
-        if (msgTypeMap0 == null || !msgTypeMap0.containsKey(typeId)) {
-            synchronized (msgTypeMapMux) {
-                if (msgTypeMap == null) {
-                    msgTypeMap0 = new HashMap<>();
-
-                    msgTypeMap0.put(typeId, msg.getClass().getName());
-
-                    msgTypeMap = msgTypeMap0;
-                }
-                else {
-                    if (!msgTypeMap.containsKey(typeId)) {
-                        msgTypeMap0 = new HashMap<>(msgTypeMap);
-
-                        msgTypeMap0.put(typeId, msg.getClass().getName());
-
-                        msgTypeMap = msgTypeMap0;
-                    }
-                }
-            }
-        }
+        if (!msgTypeMap.containsKey(typeId))
+            msgTypeMap.putIfAbsent(typeId, msg.getClass().getName());
     }
 
     /**
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/collection/CopyOnWriteIntMapTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/collection/CopyOnWriteIntMapTest.java
new file mode 100644
index 00000000000..b001befcf28
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/util/collection/CopyOnWriteIntMapTest.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.util.collection;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Random;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test for copy-on-write int map. Every write copies the map, so the sizes are kept small on purpose.
+ */
+public class CopyOnWriteIntMapTest {
+    /** */
+    @Test
+    public void putGetRemove() {
+        CopyOnWriteIntMap<String> map = new CopyOnWriteIntMap<>();
+
+        assertTrue(map.isEmpty());
+        assertNull(map.get(1));
+        assertFalse(map.containsKey(1));
+
+        assertNull(map.put(1, "one"));
+        assertEquals("one", map.put(1, "uno"));
+        assertNull(map.put(-2, "two"));
+
+        assertEquals(2, map.size());
+        assertEquals("uno", map.get(1));
+        assertEquals("two", map.get(-2));
+        assertTrue(map.containsKey(-2));
+        assertTrue(map.containsValue("two"));
+        assertFalse(map.containsValue("one"));
+
+        assertEquals("uno", map.remove(1));
+        assertNull(map.remove(1));
+
+        assertEquals(1, map.size());
+        assertFalse(map.containsKey(1));
+
+        map.clear();
+
+        assertTrue(map.isEmpty());
+        assertNull(map.get(-2));
+    }
+
+    /** */
+    @Test
+    public void putIfAbsentAndComputeIfAbsent() {
+        CopyOnWriteIntMap<String> map = new CopyOnWriteIntMap<>();
+
+        assertNull(map.putIfAbsent(1, "one"));
+        assertEquals("one", map.putIfAbsent(1, "uno"));
+        assertEquals("one", map.get(1));
+
+        assertEquals("one", map.computeIfAbsent(1, k -> "computed"));
+        assertEquals("2", map.computeIfAbsent(2, String::valueOf));
+        assertEquals("2", map.get(2));
+    }
+
+    /** */
+    @Test
+    public void compareWithReferenceImplementation() {
+        CopyOnWriteIntMap<Integer> map = new CopyOnWriteIntMap<>();
+        Map<Integer, Integer> ref = new HashMap<>();
+
+        Random rnd = new Random(0);
+
+        for (int i = 0; i < 10_000; i++) {
+            int key = rnd.nextInt(500);
+
+            switch (rnd.nextInt(3)) {
+                case 0:
+                    assertEquals(ref.put(key, i), map.put(key, i));
+
+                    break;
+
+                case 1:
+                    assertEquals(ref.putIfAbsent(key, i), map.putIfAbsent(key, i));
+
+                    break;
+
+                default:
+                    assertEquals(ref.remove(key), map.remove(key));
+            }
+
+            assertEquals(ref.get(key), map.get(key));
+            assertEquals(ref.size(), map.size());
+        }
+    }
+
+    /** */
+    @Test
+    public void valuesIsLiveView() {
+        CopyOnWriteIntMap<String> map = new CopyOnWriteIntMap<>();
+
+        Collection<String> vals = map.values();
+
+        assertSame(vals, map.values());
+        assertTrue(vals.isEmpty());
+
+        map.put(1, "one");
+
+        assertEquals(1, vals.size());
+        assertTrue(vals.contains("one"));
+
+        map.remove(1);
+
+        assertTrue(vals.isEmpty());
+    }
+
+    /** */
+    @Test
+    public void iteratorSeesSnapshot() {
+        CopyOnWriteIntMap<String> map = new CopyOnWriteIntMap<>();
+
+        map.put(1, "one");
+
+        Iterator<String> it = map.values().iterator();
+
+        map.put(2, "two");
+        map.remove(1);
+
+        assertEquals("one", it.next());
+        assertFalse(it.hasNext());
+    }
+
+    /** */
+    @Test
+    public void conditionalRemove() {
+        CopyOnWriteIntMap<String> map = new CopyOnWriteIntMap<>();
+
+        String val = "one";
+
+        map.put(1, val);
+
+        assertFalse(map.remove(1, "other"));
+        assertEquals(val, map.get(1));
+
+        assertTrue(map.remove(1, new String(val)));
+        assertFalse(map.containsKey(1));
+        assertFalse(map.remove(1, val));
+    }
+
+    /** */
+    @Test(expected = UnsupportedOperationException.class)
+    public void throwExceptionForValuesModification() {
+        CopyOnWriteIntMap<String> map = new CopyOnWriteIntMap<>();
+
+        map.put(1, "one");
+
+        map.values().iterator().remove();
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/collection/IntHashMapTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/collection/IntHashMapTest.java
index 0991774df6b..2bdabfc46fd 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/util/collection/IntHashMapTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/util/collection/IntHashMapTest.java
@@ -19,7 +19,10 @@ package org.apache.ignite.internal.util.collection;
 
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
 import java.util.Map;
+import java.util.Set;
 import java.util.stream.IntStream;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.junit.Test;
@@ -30,6 +33,7 @@ import static org.apache.ignite.testframework.GridTestUtils.assertThrows;
 import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
 import static org.hamcrest.Matchers.is;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertThat;
 
 /**
@@ -100,6 +104,61 @@ public class IntHashMapTest extends AbstractBaseIntMapTest {
         assertEquals(256, realCapacityForInitialSize(155));
     }
 
+    /** */
+    @Test
+    public void shouldNotResizeOnFirstPut() {
+        IntHashMap<String> map = new IntHashMap<>();
+
+        map.put(1, "1");
+
+        assertEquals(INITIAL_CAPACITY, ((Object[])U.field(map, "entries")).length);
+
+        map.clear();
+        map.put(1, "1");
+
+        assertEquals(INITIAL_CAPACITY, ((Object[])U.field(map, "entries")).length);
+    }
+
+    /** */
+    @Test
+    public void shouldIterateValues() {
+        IntHashMap<String> map = new IntHashMap<>();
+
+        assertFalse(map.valuesIterator().hasNext());
+
+        for (int i = 0; i < 20; i++)
+            map.put(i, String.valueOf(i));
+
+        Set<String> vals = new HashSet<>();
+
+        for (Iterator<String> it = map.valuesIterator(); it.hasNext(); )
+            vals.add(it.next());
+
+        assertEquals(new HashSet<>(map.values()), vals);
+        assertEquals(20, vals.size());
+    }
+
+    /** */
+    @Test
+    public void shouldCopyIndependently() {
+        IntHashMap<String> map = new IntHashMap<>();
+
+        for (int i = 0; i < 20; i++)
+            map.put(i, String.valueOf(i));
+
+        IntHashMap<String> copy = map.copy();
+
+        assertEquals(20, copy.size());
+        assertEquals("7", copy.get(7));
+
+        copy.remove(7);
+        copy.put(100, "100");
+
+        assertEquals("7", map.get(7));
+        assertFalse(map.containsKey(100));
+        assertEquals(20, map.size());
+    }
+
     /**
      *
      */
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBasicTestSuite2.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBasicTestSuite2.java
index 4a97a041dc2..a6f9e966907 100644
--- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBasicTestSuite2.java
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBasicTestSuite2.java
@@ -84,6 +84,7 @@ import org.apache.ignite.internal.processors.metastorage.persistence.Distributed
 import org.apache.ignite.internal.processors.metastorage.persistence.DmsDataWriterTest;
 import org.apache.ignite.internal.processors.metastorage.persistence.InMemoryCachedDistributedMetaStorageBridgeTest;
 import org.apache.ignite.internal.util.collection.BitSetIntSetTest;
+import org.apache.ignite.internal.util.collection.CopyOnWriteIntMapTest;
 import org.apache.ignite.internal.util.collection.ImmutableIntSetTest;
 import org.apache.ignite.internal.util.collection.IntHashMapTest;
 import org.apache.ignite.internal.util.collection.IntRWHashMapTest;
@@ -140,6 +141,7 @@ import org.junit.runners.Suite;
     MetricConfigurationTest.class,
     SwapPathConstructionSelfTest.class,
     BitSetIntSetTest.class,
+    CopyOnWriteIntMapTest.class,
     ImmutableIntSetTest.class,
     IntHashMapTest.class,
     IntRWHashMapTest.class,