Repository: mockito/mockito
Issue: Parallel use of mocks with deep stubbing may lead to ConcurrentModificationException

Java8 / JUnit5
Mockito version: 3.11.2

Test class:
```
import org.junit.jupiter.api.RepeatedTest;
import org.mockito.Answers;

import java.util.List;
import java.util.stream.IntStream;

import static org.mockito.Mockito.mock;

class ConcurrencyTest {

    @RepeatedTest(value = 1000)
    void test() {
        final Service mock = mock(Service.class, Answers.RETURNS_DEEP_STUBS);
        IntStream.range(1, 100).parallel().forEach(i -> mock.doSomething());
    }

    interface Service {
        List<String> doSomething();
    }
}

```

Error: 30/5000 iterations fail with `java.util.ConcurrentModificationException`

Stacktrace:
```
Caused by: java.util.ConcurrentModificationException
	at java.util.LinkedList$ListItr.checkForComodification(LinkedList.java:966)
	at java.util.LinkedList$ListItr.next(LinkedList.java:888)
	at org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs.deepStub(ReturnsDeepStubs.java:85)
	at org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs.answer(ReturnsDeepStubs.java:75)
	at org.mockito.Answers.answer(Answers.java:99)
	at org.mockito.internal.handler.MockHandlerImpl.handle(MockHandlerImpl.java:110)
	at org.mockito.internal.handler.NullResultGuardian.handle(NullResultGuardian.java:29)
	at org.mockito.internal.handler.InvocationNotifierHandler.handle(InvocationNotifierHandler.java:34)
	at org.mockito.internal.creation.bytebuddy.MockMethodInterceptor.doIntercept(MockMethodInterceptor.java:82)
	at org.mockito.internal.creation.bytebuddy.MockMethodInterceptor.doIntercept(MockMethodInterceptor.java:56)
	at org.mockito.internal.creation.bytebuddy.MockMethodInterceptor$DispatcherDefaultingToRealMethod.interceptAbstract(MockMethodInterceptor.java:161)
	at ConcurrencyTest$Service$MockitoMock$351714249.doSomething(Unknown Source)
	at ConcurrencyTest.lambda$test$0(ConcurrencyTest.java:14)
	at java.util.stream.ForEachOps$ForEachOp$OfInt.accept(ForEachOps.java:204)
	at java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:110)
	at java.util.Spliterator$OfInt.forEachRemaining(Spliterator.java:693)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482)
	at java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:290)
	at java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:731)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:175)
```

P.S. If the `Answers.RETURNS_DEEP_STUBS` is removed, no error occurred.


Issue comments:

Pull request review comments:

Patch that resolved the issue:
diff --git a/src/main/java/org/mockito/internal/stubbing/InvocationContainerImpl.java b/src/main/java/org/mockito/internal/stubbing/InvocationContainerImpl.java
index 849300e61e..3091670fb1 100644
--- a/src/main/java/org/mockito/internal/stubbing/InvocationContainerImpl.java
+++ b/src/main/java/org/mockito/internal/stubbing/InvocationContainerImpl.java
@@ -163,13 +163,20 @@ public Object invokedMock() {
         return invocationForStubbing.getInvocation().getMock();
     }
 
-    public MatchableInvocation getInvocationForStubbing() {
-        return invocationForStubbing;
-    }
-
     private RegisteredInvocations createRegisteredInvocations(MockCreationSettings mockSettings) {
         return mockSettings.isStubOnly()
                 ? new SingleRegisteredInvocation()
                 : new DefaultRegisteredInvocations();
     }
+
+    public Answer findStubbedAnswer() {
+        synchronized (stubbed) {
+            for (StubbedInvocationMatcher s : stubbed) {
+                if (invocationForStubbing.matches(s.getInvocation())) {
+                    return s;
+                }
+            }
+        }
+        return null;
+    }
 }
diff --git a/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsDeepStubs.java b/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsDeepStubs.java
index 0b5a0637a2..097b11b6e2 100644
--- a/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsDeepStubs.java
+++ b/src/main/java/org/mockito/internal/stubbing/defaultanswers/ReturnsDeepStubs.java
@@ -20,7 +20,6 @@
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.mock.MockCreationSettings;
 import org.mockito.stubbing.Answer;
-import org.mockito.stubbing.Stubbing;
 
 /**
  * Returning deep stub implementation.
@@ -80,12 +79,9 @@ private Object deepStub(
             throws Throwable {
         InvocationContainerImpl container = MockUtil.getInvocationContainer(invocation.getMock());
 
-        // matches invocation for verification
-        // TODO why don't we do container.findAnswer here?
-        for (Stubbing stubbing : container.getStubbingsDescending()) {
-            if (container.getInvocationForStubbing().matches(stubbing.getInvocation())) {
-                return stubbing.answer(invocation);
-            }
+        Answer existingAnswer = container.findStubbedAnswer();
+        if (existingAnswer != null) {
+            return existingAnswer.answer(invocation);
         }
 
         // record deep stub answer


Test patch:
diff --git a/src/test/java/org/mockito/internal/stubbing/defaultanswers/ReturnsDeepStubsConcurrentTest.java b/src/test/java/org/mockito/internal/stubbing/defaultanswers/ReturnsDeepStubsConcurrentTest.java
new file mode 100644
index 0000000000..b3658342ed
--- /dev/null
+++ b/src/test/java/org/mockito/internal/stubbing/defaultanswers/ReturnsDeepStubsConcurrentTest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2021 Mockito contributors
+ * This program is made available under the terms of the MIT License.
+ */
+package org.mockito.internal.stubbing.defaultanswers;
+
+import static org.mockito.Mockito.mock;
+
+import java.util.List;
+import java.util.stream.IntStream;
+
+import org.junit.Test;
+import org.mockito.Answers;
+
+public class ReturnsDeepStubsConcurrentTest {
+
+    @Test
+    public void
+            given_mock_with_returns_deep_stubs__when_called_concurrently__then_does_not_throw_concurrent_modification_exception() {
+        for (int i = 0; i < 1000; i++) {
+            Service mock = mock(Service.class, Answers.RETURNS_DEEP_STUBS);
+            IntStream.range(1, 100).parallel().forEach(index -> mock.doSomething());
+        }
+    }
+
+    interface Service {
+        List<String> doSomething();
+    }
+}