[util] get rid of explicit reference to internal Unsafe class in ConcurrentHashMap
authorNikolay Chashnikov <Nikolay.Chashnikov@jetbrains.com>
Thu, 13 Aug 2020 15:02:40 +0000 (18:02 +0300)
committerintellij-monorepo-bot <intellij-monorepo-bot-no-reply@jetbrains.com>
Thu, 13 Aug 2020 16:05:59 +0000 (16:05 +0000)
Use method handle to call its methods instead. This is needed to be able to compile intellij.platform.core.impl module using JDK 11 (IDEA-248086).

The change doesn't affect performance of ConcurrentHashMap much, and anyway we plan to create a variant of it which uses var handles (see IDEA-244473), so this implementation will be used only in places which stay on Java 8.

GitOrigin-RevId: d379a7c747aa720566249d31f16d9d9e0fe13ea5

platform/core-impl/src/com/intellij/concurrency/ConcurrentHashMap.java
platform/platform-tests/testSrc/com/intellij/concurrency/ConcurrentHashMapTest.kt [new file with mode: 0644]

index 16108a737515fc3f20793aa8a408f13e241c9ff0..9d6843793ad245766327b158e5530c3c6e759c34 100644 (file)
@@ -15,6 +15,10 @@ import org.jetbrains.annotations.NotNull;
 
 import java.io.ObjectStreamField;
 import java.io.Serializable;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Field;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.util.*;
@@ -735,16 +739,29 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
 
     @SuppressWarnings("unchecked")
     static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
-        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
+        try {
+            Object o = getObjectVolatileHandle.invokeExact((Object) tab, ((long)i << ASHIFT) + ABASE);
+            return (Node<K,V>) o;
+        } catch (Throwable throwable) {
+            throw new RuntimeException(throwable);
+        }
     }
 
     static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                         Node<K,V> c, Node<K,V> v) {
-        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
+        try {
+            return (boolean) compareAndSwapObjectHandle.invokeExact((Object)tab, ((long)i << ASHIFT) + ABASE, (Object) c, (Object) v);
+        } catch (Throwable throwable) {
+            throw new RuntimeException(throwable);
+        }
     }
 
     static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
-        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
+        try {
+            putObjectVolatileHandle.invokeExact((Object)tab, ((long)i << ASHIFT) + ABASE, (Object) v);
+        } catch (Throwable throwable) {
+            throw new RuntimeException(throwable);
+        }
     }
 
     /* ---------------- Fields -------------- */
@@ -2129,7 +2146,7 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
         while ((tab = table) == null || tab.length == 0) {
             if ((sc = sizeCtl) < 0)
                 Thread.yield(); // lost initialization race; just spin
-            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
+            else if (compareAndSwapInt(this, SIZECTL, sc, -1)) {
                 try {
                     if ((tab = table) == null || tab.length == 0) {
                         int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@@ -2160,13 +2177,13 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
     private final void addCount(long x, int check) {
         CounterCell[] as; long b, s;
         if ((as = counterCells) != null ||
-            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
+            !compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
             CounterCell a; long v; int m;
             boolean uncontended = true;
             if (as == null || (m = as.length - 1) < 0 ||
                 (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                 !(uncontended =
-                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
+                  compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                 fullAddCount(x, uncontended);
                 return;
             }
@@ -2184,11 +2201,11 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
                         sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                         transferIndex <= 0)
                         break;
-                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
+                    if (compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                         transfer(tab, nt);
                 }
-                else if (U.compareAndSwapInt(this, SIZECTL, sc,
-                                             (rs << RESIZE_STAMP_SHIFT) + 2))
+                else if (compareAndSwapInt(this, SIZECTL, sc,
+                                           (rs << RESIZE_STAMP_SHIFT) + 2))
                     transfer(tab, null);
                 s = sumCount();
             }
@@ -2208,7 +2225,7 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
                 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                     sc == rs + MAX_RESIZERS || transferIndex <= 0)
                     break;
-                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
+                if (compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                     transfer(tab, nextTab);
                     break;
                 }
@@ -2231,7 +2248,7 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
             Node<K,V>[] tab = table; int n;
             if (tab == null || (n = tab.length) == 0) {
                 n = (sc > c) ? sc : c;
-                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
+                if (compareAndSwapInt(this, SIZECTL, sc, -1)) {
                     try {
                         if (table == tab) {
                             @SuppressWarnings("unchecked")
@@ -2248,7 +2265,7 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
                 break;
             else if (tab == table) {
                 int rs = resizeStamp(n);
-                if (U.compareAndSwapInt(this, SIZECTL, sc,
+                if (compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                     transfer(tab, null);
             }
@@ -2289,7 +2306,7 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
                     i = -1;
                     advance = false;
                 }
-                else if (U.compareAndSwapInt
+                else if (compareAndSwapInt
                          (this, TRANSFERINDEX, nextIndex,
                           nextBound = (nextIndex > stride ?
                                        nextIndex - stride : 0))) {
@@ -2306,7 +2323,7 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
                     sizeCtl = (n << 1) - (n >>> 1);
                     return;
                 }
-                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
+                if (compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                     if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                         return;
                     finishing = advance = true;
@@ -2435,7 +2452,7 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
                     if (cellsBusy == 0) {            // Try to attach new Cell
                         CounterCell r = new CounterCell(x); // Optimistic create
                         if (cellsBusy == 0 &&
-                            U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
+                            compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                             boolean created = false;
                             try {               // Recheck under lock
                                 CounterCell[] rs; int m, j;
@@ -2457,14 +2474,14 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
                 }
                 else if (!wasUncontended)       // CAS already known to fail
                     wasUncontended = true;      // Continue after rehash
-                else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
+                else if (compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                     break;
                 else if (counterCells != as || n >= NCPU)
                     collide = false;            // At max size or stale
                 else if (!collide)
                     collide = true;
                 else if (cellsBusy == 0 &&
-                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
+                         compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                     try {
                         if (counterCells == as) {// Expand table unless stale
                             CounterCell[] rs = new CounterCell[n << 1];
@@ -2481,7 +2498,7 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
                 h = ThreadLocalRandom.advanceProbe(h);
             }
             else if (cellsBusy == 0 && counterCells == as &&
-                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
+                     compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                 boolean init = false;
                 try {                           // Initialize table
                     if (counterCells == as) {
@@ -2496,7 +2513,7 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
                 if (init)
                     break;
             }
-            else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
+            else if (compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
                 break;                          // Fall back on using base
         }
     }
@@ -2692,7 +2709,7 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
          * Acquires write lock for tree restructuring.
          */
         private final void lockRoot() {
-            if (!U.compareAndSwapInt(this, LOCKSTATE, 0, WRITER))
+            if (!compareAndSwapInt(this, LOCKSTATE, 0, WRITER))
                 contendedLock(); // offload to separate method
         }
 
@@ -2710,14 +2727,14 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
             boolean waiting = false;
             for (int s;;) {
                 if (((s = lockState) & ~WAITER) == 0) {
-                    if (U.compareAndSwapInt(this, LOCKSTATE, s, WRITER)) {
+                    if (compareAndSwapInt(this, LOCKSTATE, s, WRITER)) {
                         if (waiting)
                             waiter = null;
                         return;
                     }
                 }
                 else if ((s & WAITER) == 0) {
-                    if (U.compareAndSwapInt(this, LOCKSTATE, s, s | WAITER)) {
+                    if (compareAndSwapInt(this, LOCKSTATE, s, s | WAITER)) {
                         waiting = true;
                         waiter = Thread.currentThread();
                     }
@@ -2733,32 +2750,36 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
          * search when lock not available.
          */
         final Node<K,V> find(int h, Object k) {
-            if (k != null) {
-                for (Node<K,V> e = first; e != null; ) {
-                    int s; K ek;
-                    if (((s = lockState) & (WAITER|WRITER)) != 0) {
-                        if (e.hash == h &&
-                            ((ek = e.key) == k || (ek != null && isEqual((K)k,ek,hashingStrategy))))
-                            return e;
-                        e = e.next;
-                    }
-                    else if (U.compareAndSwapInt(this, LOCKSTATE, s,
-                                                 s + READER)) {
-                        TreeNode<K,V> r, p;
-                        try {
-                            p = ((r = root) == null ? null :
-                                 r.findTreeNode(h, k, null));
-                        } finally {
-                            Thread w;
-                            if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
-                                (READER|WAITER) && (w = waiter) != null)
-                                LockSupport.unpark(w);
+            try {
+                if (k != null) {
+                    for (Node<K,V> e = first; e != null; ) {
+                        int s; K ek;
+                        if (((s = lockState) & (WAITER|WRITER)) != 0) {
+                            if (e.hash == h &&
+                                ((ek = e.key) == k || (ek != null && isEqual((K)k,ek,hashingStrategy))))
+                                return e;
+                            e = e.next;
+                        }
+                        else if (compareAndSwapInt(this, LOCKSTATE, s,
+                                                   s + READER)) {
+                            TreeNode<K,V> r, p;
+                            try {
+                                p = ((r = root) == null ? null :
+                                     r.findTreeNode(h, k, null));
+                            } finally {
+                                Thread w;
+                                if ((int) getAndAddIntHandle.invokeExact((Object)this, LOCKSTATE, -READER) ==
+                                    (READER|WAITER) && (w = waiter) != null)
+                                    LockSupport.unpark(w);
+                            }
+                            return p;
                         }
-                        return p;
                     }
                 }
+                return null;
+            } catch (Throwable throwable) {
+                throw new RuntimeException(throwable);
             }
-            return null;
         }
 
         /**
@@ -3142,9 +3163,10 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
         private static final long LOCKSTATE;
         static {
             try {
-                LOCKSTATE = U.objectFieldOffset
-                    (TreeBin.class.getDeclaredField("lockState"));
-            } catch (ReflectiveOperationException e) {
+                MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
+                MethodHandle objectFieldOffset = publicLookup.findVirtual(U.getClass(), "objectFieldOffset", MethodType.methodType(long.class, Field.class));
+                LOCKSTATE = (long) objectFieldOffset.invoke(U, TreeBin.class.getDeclaredField("lockState"));
+            } catch (Throwable e) {
                 throw new Error(e);
             }
         }
@@ -6129,7 +6151,7 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
     }
 
     // Unsafe mechanics
-    private static final sun.misc.Unsafe U = AtomicFieldUpdater.getUnsafe();
+    private static final Object U = AtomicFieldUpdater.getUnsafe();
     private static final long SIZECTL;
     private static final long TRANSFERINDEX;
     private static final long BASECOUNT;
@@ -6138,26 +6160,42 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
     private static final int ABASE;
     private static final int ASHIFT;
 
+    private static MethodHandle putObjectVolatileHandle;
+    private static MethodHandle getObjectVolatileHandle;
+    private static MethodHandle compareAndSwapObjectHandle;
+    private static MethodHandle compareAndSwapIntHandle;
+    private static MethodHandle compareAndSwapLongHandle;
+    private static MethodHandle getAndAddIntHandle;
+
     static {
         try {
-            SIZECTL = U.objectFieldOffset
-                (ConcurrentHashMap.class.getDeclaredField("sizeCtl"));
-            TRANSFERINDEX = U.objectFieldOffset
-                (ConcurrentHashMap.class.getDeclaredField("transferIndex"));
-            BASECOUNT = U.objectFieldOffset
-                (ConcurrentHashMap.class.getDeclaredField("baseCount"));
-            CELLSBUSY = U.objectFieldOffset
-                (ConcurrentHashMap.class.getDeclaredField("cellsBusy"));
-
-            CELLVALUE = U.objectFieldOffset
-                (CounterCell.class.getDeclaredField("value"));
-
-            ABASE = U.arrayBaseOffset(Node[].class);
-            int scale = U.arrayIndexScale(Node[].class);
+            MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
+            MethodHandle objectFieldOffset = publicLookup.findVirtual(U.getClass(), "objectFieldOffset", MethodType.methodType(long.class, Field.class));
+            SIZECTL = (long) objectFieldOffset.invoke(U,
+                ConcurrentHashMap.class.getDeclaredField("sizeCtl"));
+            TRANSFERINDEX = (long) objectFieldOffset.invoke(U,
+                ConcurrentHashMap.class.getDeclaredField("transferIndex"));
+            BASECOUNT = (long) objectFieldOffset.invoke(U,
+                ConcurrentHashMap.class.getDeclaredField("baseCount"));
+            CELLSBUSY = (long) objectFieldOffset.invoke(U,
+                ConcurrentHashMap.class.getDeclaredField("cellsBusy"));
+
+            CELLVALUE = (long) objectFieldOffset.invoke(U,
+                CounterCell.class.getDeclaredField("value"));
+
+            ABASE = (int) publicLookup.findVirtual(U.getClass(), "arrayBaseOffset", MethodType.methodType(int.class, Class.class)).invoke(U, Node[].class);
+            int scale = (int) publicLookup.findVirtual(U.getClass(), "arrayIndexScale", MethodType.methodType(int.class, Class.class)).invoke(U, Node[].class);
             if ((scale & (scale - 1)) != 0)
                 throw new Error("array index scale not a power of two");
             ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
-        } catch (ReflectiveOperationException e) {
+
+            putObjectVolatileHandle = publicLookup.findVirtual(U.getClass(), "putObjectVolatile", MethodType.methodType(void.class, Object.class, long.class, Object.class)).bindTo(U);
+            getObjectVolatileHandle = publicLookup.findVirtual(U.getClass(), "getObjectVolatile", MethodType.methodType(Object.class, Object.class, long.class)).bindTo(U);
+            compareAndSwapObjectHandle = publicLookup.findVirtual(U.getClass(), "compareAndSwapObject", MethodType.methodType(boolean.class, Object.class, long.class, Object.class, Object.class)).bindTo(U);
+            compareAndSwapIntHandle = publicLookup.findVirtual(U.getClass(), "compareAndSwapInt", MethodType.methodType(boolean.class, Object.class, long.class, int.class, int.class)).bindTo(U);
+            compareAndSwapLongHandle = publicLookup.findVirtual(U.getClass(), "compareAndSwapLong", MethodType.methodType(boolean.class, Object.class, long.class, long.class, long.class)).bindTo(U);
+            getAndAddIntHandle = publicLookup.findVirtual(U.getClass(), "getAndAddInt", MethodType.methodType(int.class, Object.class, long.class, int.class)).bindTo(U);
+        } catch (Throwable e) {
             throw new Error(e);
         }
 
@@ -6166,7 +6204,21 @@ final class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
         Class<?> ensureLoaded = LockSupport.class;
     }
 
-  //////////////// IJ specific
+    private static boolean compareAndSwapInt(Object object, long offset, int expected, int value) {
+        try {
+            return (boolean) compareAndSwapIntHandle.invokeExact(object, offset, expected, value);
+        } catch (Throwable throwable) {
+            throw new RuntimeException(throwable);
+        }
+    }
+
+    private static boolean compareAndSwapLong(Object object, long offset, long expected, long value) {
+        try {
+            return (boolean) compareAndSwapLongHandle.invokeExact(object, offset, expected, value);
+        } catch (Throwable throwable) {
+            throw new RuntimeException(throwable);
+        }
+    }
 
   @Override
   public int computeHashCode(final K object) {
diff --git a/platform/platform-tests/testSrc/com/intellij/concurrency/ConcurrentHashMapTest.kt b/platform/platform-tests/testSrc/com/intellij/concurrency/ConcurrentHashMapTest.kt
new file mode 100644 (file)
index 0000000..0f361f5
--- /dev/null
@@ -0,0 +1,117 @@
+// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.concurrency
+
+import org.assertj.core.api.Assertions.*
+import org.junit.Test
+
+class ConcurrentHashMapTest {
+  @Test
+  fun `put and get`() {
+    val map = ConcurrentHashMap<Int, Int>()
+    assertThat(map.size).isEqualTo(0)
+    assertThat(map.isEmpty()).isTrue()
+    map[1] = 0
+    map[2] = 1
+    assertThat(map.size).isEqualTo(2)
+    assertThat(map.isEmpty()).isFalse()
+    assertThat(map[1]).isEqualTo(0)
+    assertThat(map[2]).isEqualTo(1)
+    assertThat(map.getOrDefault(2, 4)).isEqualTo(1)
+    assertThat(map[3]).isNull()
+    assertThat(map.getOrDefault(3, 4)).isEqualTo(4)
+    assertThat(map.containsKey(1)).isTrue()
+    assertThat(map.containsKey(3)).isFalse()
+    assertThat(map.containsValue(1)).isTrue()
+    assertThat(map.containsValue(2)).isFalse()
+    map[2] = 2
+    assertThat(map[2]).isEqualTo(2)
+    assertThat(map.size).isEqualTo(2)
+  }
+
+  @Test
+  fun remove() {
+    val map = ConcurrentHashMap(mapOf(1 to 2, 2 to 3, 3 to 4))
+    assertThat(map.size).isEqualTo(3)
+    assertThat(map.remove(4)).isNull()
+    assertThat(map.remove(3, 5)).isFalse()
+    assertThat(map.size).isEqualTo(3)
+
+    assertThat(map.remove(3)).isEqualTo(4)
+    assertThat(map.size).isEqualTo(2)
+    assertThat(map.remove(2, 3)).isTrue()
+    assertThat(map.size).isEqualTo(1)
+    map.clear()
+    assertThat(map.isEmpty()).isTrue()
+  }
+
+  @Test
+  fun entries() {
+    val map = ConcurrentHashMap(mapOf(1 to 2, 2 to 3, 3 to 4))
+    assertThat(map.entries.map { it.key to it.value }).containsExactlyInAnyOrder(1 to 2, 2 to 3, 3 to 4)
+    assertThat(map.keys).containsExactlyInAnyOrder(1, 2, 3)
+    assertThat(map.values).containsExactlyInAnyOrder(2, 3, 4)
+  }
+
+  @Test
+  fun `put if absent`() {
+    val map = ConcurrentHashMap(mapOf(1 to 2))
+    assertThat(map.putIfAbsent(1, 3)).isEqualTo(2)
+    assertThat(map[1]).isEqualTo(2)
+    assertThat(map.putIfAbsent(2, 3)).isNull()
+    assertThat(map[2]).isEqualTo(3)
+  }
+
+  @Test
+  fun `compute if absent`() {
+    val map = ConcurrentHashMap(mapOf(1 to 2))
+    assertThat(map.computeIfAbsent(1) { 3 }).isEqualTo(2)
+    assertThat(map[1]).isEqualTo(2)
+    assertThat(map.computeIfAbsent(2) { 3 }).isEqualTo(3)
+    assertThat(map[2]).isEqualTo(3)
+  }
+
+  @Test
+  fun `compute if present`() {
+    val map = ConcurrentHashMap(mapOf(1 to 2))
+    assertThat(map.computeIfPresent(2) { _, _ -> 3 }).isNull()
+    assertThat(map[1]).isEqualTo(2)
+    assertThat(map.computeIfPresent(1) { _, _ -> 3 }).isEqualTo(3)
+    assertThat(map[1]).isEqualTo(3)
+  }
+
+  @Test
+  fun compute() {
+    val map = ConcurrentHashMap(mapOf(1 to 2))
+    assertThat(map.compute(1) { _, _ -> 3 }).isEqualTo(3)
+    assertThat(map[1]).isEqualTo(3)
+    assertThat(map.compute(2) { _, _ -> 3 }).isEqualTo(3)
+    assertThat(map[2]).isEqualTo(3)
+  }
+
+  @Test
+  fun replace() {
+    val map = ConcurrentHashMap(mapOf(1 to 2, 2 to 3))
+    assertThat(map.replace(3, 4)).isNull()
+    assertThat(map.replace(2, 4)).isEqualTo(3)
+    assertThat(map[2]).isEqualTo(4)
+    assertThat(map.replace(1, 3, 4)).isFalse()
+    assertThat(map.replace(1, 2, 3)).isTrue()
+    assertThat(map[1]).isEqualTo(3)
+  }
+
+  @Test
+  fun `replace all`() {
+    val map = ConcurrentHashMap(mapOf(1 to 2, 2 to 3))
+    map.replaceAll { k, v -> k + v}
+    assertThat(map).isEqualTo(mapOf(1 to 3, 2 to 5))
+  }
+
+  @Test
+  fun merge() {
+    val map = ConcurrentHashMap(mapOf(1 to 2, 2 to 3))
+    assertThat(map.merge(3, 4) { _, _ -> 4 }).isEqualTo(4)
+    assertThat(map[3]).isEqualTo(4)
+    assertThat(map.merge(1, 3) { a, b -> a+b }).isEqualTo(5)
+    assertThat(map[1]).isEqualTo(5)
+  }
+}
\ No newline at end of file