Search code examples
javadesign-patternsobserver-patternconcurrentmodification

Java concurrency with arraylists (How to handle this?)


It is true that you get a ConcurrentModificationException error when you try and modify an arraylist when iterating through it, yes? But how does the error still persist if you first remove the element you want to modify, iterate through the list, and then insert the element back? I do not understand if the logic is like this why it throws a concurrentmodificationexception.

This whole program uses the Observer Pattern.


The purpose of the program is to have multiple text boxes, when you type in one, the others update with the same text.

Subject - Interface

public interface Subject {

public void attach(Observer o);
public void detach(Observer o);
public void notifyAllObservers();
}

SubjectImpl - implements Subject

import java.util.*;

public class SubjectImpl implements Subject {

private List <Observer> observers;

public SubjectImpl(){
    observers = new ArrayList<Observer>();
}


@Override
public void attach(Observer o) {
    observers.add(o);
}

@Override
public void detach(Observer o) {
    observers.remove(o);
}

@Override
public void notifyAllObservers() {
            //Iterating code
    for(Observer o: observers){
        o.update();
    }
}
}

Observer - interface

public interface Observer {
public void update();
}

Editor - Implements Observer

import java.awt.*
import java.io.*;
import javax.swing.*;



public class Editor extends JFrame implements DocumentListener, Observer {

private FileContentSubject reference;

    private JScrollPane textAreaScrollPane;
    private JTextArea textArea;


public Editor(FileContentSubject filecontentsubject) throws IOException {
    super("Editor");
    initComponents();

    this.reference = filecontentsubject;
    textArea.getDocument().addDocumentListener(reference);
    textArea.getDocument().putProperty("ownerEditor", this); 
}


private void initComponents(){

    textArea = new JTextArea();       

    //set location, size, visible and defaultcloseoperation here        

    getContentPane().setLayout(new BorderLayout());
    getContentPane().add(textArea, BorderLayout.CENTER);
}



@Override
public void update() {
    textArea.setText(reference.getState()); //Call to update each text box
}


@Override
public void changedUpdate(DocumentEvent e) {
}


@Override
public void insertUpdate(DocumentEvent e) {
}


@Override
public void removeUpdate(DocumentEvent e) {
}
}

File Content System - Acts as concrete subject in Observer (there only exists one)

import javax.swing.event.*;
import javax.swing.text.*;

public class FileContentSubject implements Subject, DocumentListener {

private String state; //Current, most recent copy of everything

public String getState() {
    return this.state;
}

private SubjectImpl reference;

@Override
public void attach(Observer o) {
    reference.attach(o);
}

@Override
public void detach(Observer o) {
    reference.detach(o);
}

@Override
public void notifyAllObservers() {
    reference.notifyAllObservers();
}

public FileContentSubject() {
    reference = new SubjectImpl();
}

@Override
public void changedUpdate(DocumentEvent arg0) {
}

@Override
public void insertUpdate(DocumentEvent arg0) {

    Document doc = (Document) arg0.getDocument();
    try {
        this.state = doc.getText(0, doc.getLength());
    } catch (BadLocationException e) {
        e.printStackTrace();
    }

    Editor e = (Editor) doc.getProperty("ownerEditor");
    reference.detach(e); 
    notifyAllObservers(); 
    reference.attach(e); 
}

@Override
public void removeUpdate(DocumentEvent arg0) {

   //same as insertUpdate(DocumentEvent arg0) ^^
}

}

Solution

  • I believe your ConcurrentModificationException is only thrown when 3 or more editors are used, right? Anyway, let me try to explain what is happening.

    Let's suppose you have 3 Editor instances: "Editor1", "Editor2" and "Editor3".

    This is what happens when you type a text into "Editor1" (or, how I call it, when you "begin the chain of events"):

    User types on Editor1 // chain begins
    FileContentSubject detaches editor: Editor1
    FileContentSubject begin notifyAllObservers(): [Editor2, Editor3]
        Editor2 receives update() to new state
    

    So far, normal behavior, right? The thing is when Editor2's update() is called, it changes Editor2's textArea, and FileContentSubject is a DocumentListener of that textArea as well (as it was of Editor1's textArea).

    This triggers a new edition, thus creating a new "chain of events". In practice, here's what will happen (repeating from the beginning):

    User types on Editor1 // chain begins
    FileContentSubject detaches editor: Editor1
    FileContentSubject begin notifyAllObservers(): [Editor2, Editor3] // first notification
        Editor2 receives update() to new state // another chain begins! (the 1st chain didn't end yet!)
            FileContentSubject detaches editor: Editor2
            FileContentSubject begin notifyAllObservers(): [Editor3]
                Editor3 receives update() to new state // chain THREE!!
                    FileContentSubject detaches editor: Editor3
                    FileContentSubject begin notifyAllObservers(): [] // empty list... nobody to notify
                    FileContentSubject reattaches editor: Editor3
                    FileContentSubject ended notifyAllObservers(): [Editor3] // chain 3 over
            FileContentSubject reattaches editor: Editor2
            FileContentSubject ended notifyAllObservers(): [Editor3, Editor2] // chain 2 over
    

    This is the point where ConcurrentModificationException is thrown. But where exactly is it throw, on the code?

    Actually, it's on SubjectImpl(!):

    @Override
    public void notifyAllObservers() {
        for (Observer o : observers) {
            o.update();
        }
    }
    

    What is the problem?

    The thing is:

    • Notice that in the line with the // first notification comment there's a call to notifyAllObservers()
    • At his moment the list of attached observers is (in this order) [Editor2, Editor3].
    • notifyAllObservers(), when called, then, begins iterating on that list
      • the first step of the iteration is to call Editor2.update().
        • Editor2.update() starts a whole new chain...
        • and when that chain ends, the list of attached observers is now [Editor3, Editor2] (notice: the order has changed!)
      • when notifyAllObservers() tries to continue its iteration to the next step, it notices that the list has changed and... bam! ConcurrentModificationException is thrown.

    Why? Because you cannot change a list where you have begun to iterate. (Either using an iterator, or "foreach".)

    The solution:

    There may be many, but the simplest is just to stop the "chain of events". To do that, just change update() on Editor from:

    @Override
    public void update() {
        textArea.setText(reference.getState()); // Call to update each text box
    }
    

    To:

    @Override
    public void update() {
        textArea.getDocument().removeDocumentListener(reference);
        textArea.setText(reference.getState()); // Call to update each text box
        textArea.getDocument().addDocumentListener(reference);
    }
    

    What does it mean? It means that when an Editor changes itself due to anybody else and not some typing on its textArea, he won't notify FileContentSubject (the DocumentListener) anymore, he'll just change, quietly.

    This will stop any second chain of events from beginning in the first place.

    Hope I managed to explain the problem! If I can make any point clearer, just let me know! Cheers!