Repository: netty/netty (Java)

Issue:
`FastThreadLocal.set(V)` method throws `NegativeArraySizeException` after called more than (1 << 30) times of `new FastThreadLocal()`
### Expected behavior

The `FastThreadLocal.set(V)` method should still work after called more than (1 << 30) and less than (Integer.MAX_VALUE) times of `new FastThreadLocal()`.

### Actual behavior

After called more than (1 << 30) times of `new FastThreadLocal()`, the newly created `FastThreadLocal` instance can not set value anymore.

### Minimal yet complete reproducer code (or URL to code)
```
import io.netty.util.concurrent.FastThreadLocal;
import io.netty.util.internal.InternalThreadLocalMap;

public static void main(String[] args) {
        int expand_threshold = 1 << 30;
        while (true) {
            FastThreadLocal<Object> fst = new FastThreadLocal<>();
            if (InternalThreadLocalMap.lastVariableIndex() < expand_threshold) {
                continue;
            }
            fst.set(new Object()); // throws NegativeArraySizeException
            return;
        }
    }
```
Run the code above, the console prints Exception:

> Exception in thread "main" java.lang.NegativeArraySizeException
> 	at java.util.Arrays.copyOf(Arrays.java:3210)
> 	at java.util.Arrays.copyOf(Arrays.java:3181)
> 	at io.netty.util.internal.InternalThreadLocalMap.expandIndexedVariableTableAndSet(InternalThreadLocalMap.java:342)
> 	at io.netty.util.internal.InternalThreadLocalMap.setIndexedVariable(InternalThreadLocalMap.java:326)
> 	at io.netty.util.concurrent.FastThreadLocal.setKnownNotUnset(FastThreadLocal.java:214)
> 	at io.netty.util.concurrent.FastThreadLocal.set(FastThreadLocal.java:193)
> 	at XXX.main(XXX.java:15)

### Netty version
4.1.72.Final.
### JVM version (e.g. `java -version`)
JDK-8.
### OS version (e.g. `uname -a`)
MacOS.


Hints:

Patch:
diff --git a/common/src/main/java/io/netty/util/internal/InternalThreadLocalMap.java b/common/src/main/java/io/netty/util/internal/InternalThreadLocalMap.java
--- a/common/src/main/java/io/netty/util/internal/InternalThreadLocalMap.java
+++ b/common/src/main/java/io/netty/util/internal/InternalThreadLocalMap.java
@@ -45,6 +45,9 @@ public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap
     private static final AtomicInteger nextIndex = new AtomicInteger();
 
     private static final int DEFAULT_ARRAY_LIST_INITIAL_CAPACITY = 8;
+    private static final int ARRAY_LIST_CAPACITY_EXPAND_THRESHOLD = 1 << 30;
+    // Reference: https://hg.openjdk.java.net/jdk8/jdk8/jdk/file/tip/src/share/classes/java/util/ArrayList.java#l229
+    private static final int ARRAY_LIST_CAPACITY_MAX_SIZE = Integer.MAX_VALUE - 8;
     private static final int STRING_BUILDER_INITIAL_SIZE;
     private static final int STRING_BUILDER_MAX_SIZE;
     private static final int HANDLER_SHARABLE_CACHE_INITIAL_CAPACITY = 4;
@@ -135,8 +138,8 @@ public static void destroy() {
 
     public static int nextVariableIndex() {
         int index = nextIndex.getAndIncrement();
-        if (index < 0) {
-            nextIndex.decrementAndGet();
+        if (index >= ARRAY_LIST_CAPACITY_MAX_SIZE || index < 0) {
+            nextIndex.set(ARRAY_LIST_CAPACITY_MAX_SIZE);
             throw new IllegalStateException("too many thread-local indexed variables");
         }
         return index;
@@ -331,13 +334,18 @@ public boolean setIndexedVariable(int index, Object value) {
     private void expandIndexedVariableTableAndSet(int index, Object value) {
         Object[] oldArray = indexedVariables;
         final int oldCapacity = oldArray.length;
-        int newCapacity = index;
-        newCapacity |= newCapacity >>>  1;
-        newCapacity |= newCapacity >>>  2;
-        newCapacity |= newCapacity >>>  4;
-        newCapacity |= newCapacity >>>  8;
-        newCapacity |= newCapacity >>> 16;
-        newCapacity ++;
+        int newCapacity;
+        if (index < ARRAY_LIST_CAPACITY_EXPAND_THRESHOLD) {
+            newCapacity = index;
+            newCapacity |= newCapacity >>>  1;
+            newCapacity |= newCapacity >>>  2;
+            newCapacity |= newCapacity >>>  4;
+            newCapacity |= newCapacity >>>  8;
+            newCapacity |= newCapacity >>> 16;
+            newCapacity ++;
+        } else {
+            newCapacity = ARRAY_LIST_CAPACITY_MAX_SIZE;
+        }
 
         Object[] newArray = Arrays.copyOf(oldArray, newCapacity);
         Arrays.fill(newArray, oldCapacity, newArray.length, UNSET);


Test patch:
diff --git a/common/src/test/java/io/netty/util/concurrent/FastThreadLocalTest.java b/common/src/test/java/io/netty/util/concurrent/FastThreadLocalTest.java
--- a/common/src/test/java/io/netty/util/concurrent/FastThreadLocalTest.java
+++ b/common/src/test/java/io/netty/util/concurrent/FastThreadLocalTest.java
@@ -16,17 +16,22 @@
 
 package io.netty.util.concurrent;
 
+import io.netty.util.internal.InternalThreadLocalMap;
 import io.netty.util.internal.ObjectCleaner;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Timeout;
 
+import java.lang.reflect.Field;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
+import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.nullValue;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -238,4 +243,52 @@ protected void onRemoval(String value) throws Exception {
             onRemovalCalled.set(value);
         }
     }
+
+    @Test
+    public void testConstructionWithIndex() throws Exception {
+        int ARRAY_LIST_CAPACITY_MAX_SIZE = Integer.MAX_VALUE - 8;
+        Field nextIndexField =
+                InternalThreadLocalMap.class.getDeclaredField("nextIndex");
+        nextIndexField.setAccessible(true);
+        AtomicInteger nextIndex = (AtomicInteger) nextIndexField.get(AtomicInteger.class);
+        int nextIndex_before = nextIndex.get();
+        try {
+            while (nextIndex.get() < ARRAY_LIST_CAPACITY_MAX_SIZE) {
+                new FastThreadLocal<Boolean>();
+            }
+            assertEquals(ARRAY_LIST_CAPACITY_MAX_SIZE - 1, InternalThreadLocalMap.lastVariableIndex());
+            try {
+                new FastThreadLocal<Boolean>();
+            } catch (Throwable t) {
+                // assert the max index cannot greater than (ARRAY_LIST_CAPACITY_MAX_SIZE - 1)
+                assertThat(t, is(instanceOf(IllegalStateException.class)));
+            }
+            // assert the index was reset to ARRAY_LIST_CAPACITY_MAX_SIZE after it reaches ARRAY_LIST_CAPACITY_MAX_SIZE
+            assertEquals(ARRAY_LIST_CAPACITY_MAX_SIZE - 1, InternalThreadLocalMap.lastVariableIndex());
+        } finally {
+            // restore the index
+            nextIndex.set(nextIndex_before);
+        }
+    }
+
+    @Test
+    public void testInternalThreadLocalMapExpand() throws Exception {
+        final AtomicReference<Throwable> throwable = new AtomicReference<Throwable>();
+        Runnable runnable = new Runnable() {
+            @Override
+            public void run() {
+                int expand_threshold = 1 << 30;
+                try {
+                    InternalThreadLocalMap.get().setIndexedVariable(expand_threshold, null);
+                } catch (Throwable t) {
+                    throwable.set(t);
+                }
+            }
+        };
+        FastThreadLocalThread fastThreadLocalThread = new FastThreadLocalThread(runnable);
+        fastThreadLocalThread.start();
+        fastThreadLocalThread.join();
+        // assert the expanded size is not overflowed to negative value
+        assertThat(throwable.get(), is(not(instanceOf(NegativeArraySizeException.class))));
+    }
 }