2

Update

As was pointed out by Jiri Tousek, the error that was being thrown in my code has misled many amateur (and experienced) Java developers. Contrary to what the name seems to imply, ConcurrentModificationException does not have anything to do with multi-threading. Consider the following code:

import java.util.List;
import java.util.ArrayList;

class Main {
  public static void main(String[] args) {
    List<String> originalArray = new ArrayList<>();
    originalArray.add("foo");
    originalArray.add("bar");
    List<String> arraySlice = originalArray.subList(0, 1);
    originalArray.remove(0);
    System.out.println(Integer.toString(arraySlice.size()));
  }
}

This will throw a ConcurrentModificationException despite there being no threading involved.

The misleading exception name led me to think my problem was the result of how I was handling multi-threading. I've updated the title of my post with the actual issue.

Original (title: How to inform Java that you are finished modifying an ArrayList in a thread?)

I have code that looks roughly like the following:

class MessageQueue {
    private List<String> messages = new ArrayList<>();
    private List<String> messagesInFlight = new ArrayList<>();

    public void add(String message) {
        messages.add(message);
    }

    public void send() {
        if (messagesInFlight.size() > 0) {
            // Wait for previous request to finish
            return;
        }

        messagesInFlight = messages.subList(0, Math.min(messages.size, 10));
        for( int i = 0; i < messagesInFlight.size(); i++ )
        {
            messages.remove(0);
        }

        sendViaHTTP(messagesInFlight, new Callback() {
            @Override
            public void run() {
                messagesInFlight.clear();
            }
        });
    }
}

This is utilized in my code for analytics purposes. Every 10 seconds I call messageQueue.send() from a timer, and whenever an event of interest occurs I call messageQueue.add(). This class works *for the most part* -- I can add messages and they get sent via HTTP, and when the HTTP request completes the callback is run

The issue lies on the second tick of the timer. When I hit the line if (messagesInFlight.size() > 0) {, I get the following error:

java.util.ConcurrentModificationException
        at java.util.ArrayList$SubList.size(ArrayList.java:1057)

It seems like I can't read the .size() of the array in one thread (the second timer's callback) because it thinks the array is still being modified by the other thread (the first timer's callback). However I would expect the first timer's thread was destroyed and cleaned up after my call to sendViaHTTP, since there was no additional code for it to execute. Furthermore, the HTTP request is completing within 500 milliseconds, so a full 9.5 seconds passes without anything touching the empty messagesInFlight array

Is there a way to say say "hey, I'm done modifying this array, people can safely read it now"? Or perhaps a better way to organize my code?

Jiri Tousek
  • 12,211
  • 5
  • 29
  • 43
stevendesu
  • 15,753
  • 22
  • 105
  • 182
  • 1
    I'd suggest using a `CopyOnWriteArrayList` or another suitable concurrent collection. Using plain `ArrayList` on multiple threads is asking for trouble. – Mark Rotteveel May 22 '19 at 15:13
  • surround the places where you are modifying the array or calling size with a synchronized block: `synchronized(array) {` – ControlAltDel May 22 '19 at 15:14
  • @ControlAltDel That is a brittle solution, and only really usable when you're using the result of `Collections.synchronizedList`. – Mark Rotteveel May 22 '19 at 15:15
  • 2
    You need a tutorial in multi-threading and concurrency. – Raedwald May 22 '19 at 15:16
  • @MarkRotteveel I replaced `messages` with a `CopyOnWriteArrayList` and got the same error but this time in `at java.util.concurrent.CopyOnWriteArrayList$COWSubList.checkForComodification(CopyOnWriteArrayList.java:1207)` – stevendesu May 22 '19 at 15:17
  • @MarkRotteveel while I agree that splattering 'synchronized' keywords around the implementation code is somewhat brittle, why would using synchronized blocks only work with a synchronized list? – Frank Hopkins May 22 '19 at 15:18
  • @Raedwald Accurate, but not helpful. If there's a key concept I'm missing could you at least name it so I can look it up? – stevendesu May 22 '19 at 15:18
  • May I ask how someone voted "too broad" on this? I literally posted the exact error being thrown, the exact code throwing it, and described exactly what I want to accomplish. There's nothing broad about this. I didn't say "what is multi-threading and how do I do it?" I said "how do I make this code do this without this error?" – stevendesu May 22 '19 at 15:19
  • 1
    @stevendesu the 'too broad' might come from the feeling that the best solution would be a different design of the whole thing plus an introduction course to concurrency handling... – Frank Hopkins May 22 '19 at 15:20
  • @FrankHopkins When using `synchronizedList`, the atomic (single method) operations are already synchronized, so you will only need to add `synchronized`-blocks for non-atomic operations (things that require multiple method invocations on the list). This makes your code less brittle, because having to explicitly synchronize for the atomic operations is pretty easy to forget and it gives less clutter in your code. – Mark Rotteveel May 22 '19 at 15:23
  • @FrankHopkins I'm pretty sure that answer is more broad than the question. If there's a specific design principle that's being abused or ignored, it would be helpful to name it. As for the general design of the code, I'm honestly taking C# code that our company already had (using Xamarin) and re-writing it in Java. This is the exact layout and structure of our existing codebase, the C# array simply doesn't care and lets you perform dangerous concurrent acrobatics under the assumption you know what you're doing. – stevendesu May 22 '19 at 15:23
  • "If there's a key concept I'm missing could you at least name it so I can look it up": no. The point is, if you need a tutorial, looking up[ a few "key concepts" will not help. – Raedwald May 22 '19 at 15:24
  • @stevendesu I'm not saying that this is the case or completely my opinion, just what I guess the reasoning behind those "too broad" votes is. – Frank Hopkins May 22 '19 at 15:31
  • @Raedwald I disagree 100%. That's literally what an introductory tutorial _*is*_. They teach key concepts. – stevendesu May 22 '19 at 15:31
  • 1
    @MarkRotteveel IMO there's value in encouraging the uninitiated (as the writer of this question is) to attempt with the "brittle" solution first; this helps the learner to learn why and where other data structures or programming paradigms can lead to better solutions. It definitely helped me when I was learning OOP: First we coded hashtables and lists using procedural paradigm... ack what a mess! – ControlAltDel May 22 '19 at 15:32
  • @MarkRotteveel but then it's not "only working with synchronizedList, just less boiler plate synchronisation when you combine both. – Frank Hopkins May 22 '19 at 15:33
  • @FrankHopkins I did not say "only working with synchronizedList", I said "only really usable". I did not claim it wouldn't work, but I was talking about a usability aspect (having to add `synchronized` blocks left and right) and the ease to forget them which could introduce subtle bugs. – Mark Rotteveel May 22 '19 at 15:35
  • @MarkRotteveel ah, sorry for the misquote didn't find your comment and thought it was gone (but yes, still there). And well, I would counter argue that it might well make sense to rather use more synchronized blocks rather than for some cases to rely on the used structure and not for others which can make it harder to get things right, i.e. you need to keep two places that synchronize in mind. Plus to me that sounded like "will not work". – Frank Hopkins May 22 '19 at 15:38

2 Answers2

5

The most glaring issue you have there is that you're using ArrayList.subList() while you don't seem to understand what it really does:

Returns a view of the portion of this list ... The returned list is backed by this list.

What you're storing in messagesInFlight is a view, not a copy. When you delete the first messages from messages, you're in fact deleting the very messages you had in your messagesInFlight right after subList() call. So after the for loop, there will be completely different messages, and the first n messages will be completely lost.


As to why you are getting the error you see - subList() specifically allows non-structural changes to both sub-list and the original list (non-structural means replacing the elements, not adding or removing them), and the example in the documentation also showcases how the original list may be modified by modifying the sub-list. However, modifying the original list and then accessing it through the sub-list is not allowed and may result in ConcurrentModifcationException, similarly to what happens when you change a list you're iterating through with an iterator.

Jiri Tousek
  • 12,211
  • 5
  • 29
  • 43
  • Updating my code to eliminate `subList()` (which I was originally only using to mimic the format of our old C# code, and didn't realize it didn't perform a copy) actually eliminated the concurrency issue, as well. I'm no longer getting an error, even if I return to using `ArrayList` instead of `CopyOnWriteArrayList`! I guess somehow creating the view was tripping up the concurrency checker (regardless, it was introducing a bug so it's good that it was caught) – stevendesu May 22 '19 at 15:28
  • @stevendesu The `ConcurrentModificationException` doesn't really have to do with threading, but with modifying the list and performing an operation that expected the list not to have been modified. You'd get the same exception if you had called `size()` on the sub list in the original thread. – Mark Rotteveel May 22 '19 at 15:29
  • 1
    See my edit. This exception in fact doesn't even relate to multi-threading, it's a protection against you modifying the underlying structure in a way that the sub-list has no way to detect and act upon. – Jiri Tousek May 22 '19 at 15:30
  • Ha nicely spotted! And here all "us experts" were misled by the concurrency exception and jumping on the "learn about concurrency before you use it" train when there's none. (Though, one might argue, it looks like a scenario where it typically could make sense to go parallel^^) - and there is obviously some risk by the callback – Frank Hopkins May 22 '19 at 15:40
  • 1
    @FrankHopkins I've seen that exception's name misleading many skilled Java programmers the exactly same way in my career :) After a quick look through the code, I spotted nothing wrong from the multi-threading point of view (after fixing the `subList()`), so I find the flame under the post mildly amusing. – Jiri Tousek May 22 '19 at 15:44
0

If you need to be sure method send will not be called if previous call was not finished you can drop messages in fly collection and just use flag variable:

private AtomicBoolean inProgress = new AtomicBoolean(false);

public void send() {

    if(inProgress.getAndSet(true)) {
        return;
    }

    // Your logic here ...  

    sendViaHTTP(messagesInFlight, new Callback() {
        @Override
        public void run() {

            inProgress.set(false);
        }
    });

}
alexey28
  • 5,170
  • 1
  • 20
  • 25
  • I think this (waiting until the previous send call is finished) is invalidated by the check for messages in flight. The `send` method is a no-op if the previous send hasn't finished. – stevendesu May 22 '19 at 15:29
  • @stevendesu thanks, I update my answer according to your comment – alexey28 May 22 '19 at 15:34
  • @alexey28 Still not equivalent to what the original code does - what if a third `send()` call comes in while the initial request is still not finished? – Jiri Tousek May 22 '19 at 15:38