Como resolver este erro: java.util.ConcurrentModificationException?

Tenho uma Thread fazendo uma verificação constante no meu ArrayList, mas sempre que esse ArrayList sofre alguma alteração a Thread de verificação para e o Netbeans printa esse erro. Alguém pode me ajudar?

Explique melhor o que você está tentando fazer. Você tem um Arraylist num lugar e está acessando-o constantemente a partir de outra thread? Por que exatamente você precisa disso?

Manipular um objeto em mais de uma thread diferente está sujeito a erros como esse, então talvez seja o caso de repensar bem sua arquitetura.

Abraço.

Em geral, quando você tem um objeto em um thread e o altera em outra, recebe esta exceção.
Uma possível solução

Eu tenho as seguintes informações no banco de dados de um hotel: Hora de entrada e tempo que ira permanecer. Assim que eu abro o sistema um método preenche esta lista com todas estas informações e assim que o faz é chamado uma Thread que vai ficar percorrendo esta lista e verificando se já deu o tempo de estadia total. Digamos que eu entrei 2 da tarde e irei permanecer 1 hora no local, então essa thread vai ficar verificando, se for 3 da tarde ele me avisa que aquele cliente excedeu o limite de hospedagem e dai acrescenta mais determinado tempo no banco de dados, logo após isso ele chama o método que preenche a lista de dados (É ai que está o problema), quando o método dá um “lista.clear()” para setar as novas informações o java apresenta este erro e acusa a linha do “FOR” que percorre a lista. Logo após isso a Thread para.

Este é o método que preenche a lista:

private void setDados() {
        lista.clear();
        //
        conexao.conectar2();
        //
        try {
            //
            conexao.executarSQL2("SELECT s.suite, s.codigo, s.periodo, s.tolerancia_periodo, s.pernoite, s.tolerancia_pernoite, e.hospedagem, e.data_entrada, e.hora_entrada, e.horaExtra, e.tolerancia FROM suites AS s INNER JOIN entrada_suite AS e ON s.codigo = e.codigo_suite");
            //
            if (conexao.rs2.first()) {
                //
                do {
                    //
                    mapa.put("suite", conexao.rs2.getString("s.suite")); 
                    mapa.put("codigo", conexao.rs2.getString("s.codigo"));
                    mapa.put("hospedagem", conexao.rs2.getString("e.hospedagem"));
                    mapa.put("status_tolerancia", conexao.rs2.getString("e.tolerancia"));
                    mapa.put("periodo", String.valueOf(conexao.rs2.getTime("s.periodo")));
                    mapa.put("tolerancia_periodo", String.valueOf(conexao.rs2.getTime("s.tolerancia_periodo")));
                    mapa.put("pernoite", String.valueOf(conexao.rs2.getTime("s.pernoite")));
                    mapa.put("tolerancia_pernoite", String.valueOf(conexao.rs2.getTime("s.tolerancia_pernoite")));
                    mapa.put("data_entrada", String.valueOf(conexao.rs2.getDate("e.data_entrada")));
                    mapa.put("hora_entrada", String.valueOf(conexao.rs2.getTime("e.hora_entrada")));
                    mapa.put("hora_extra", String.valueOf(conexao.rs2.getTime("e.horaExtra")));
                    lista.add(mapa);
                    //
                } while (conexao.rs2.next());
                //
            }
            //
        } catch(SQLException ex) {
            JOptionPane.showMessageDialog(null, "Erro ao setDados.\nERRO: " + ex);
        }
        //
        conexao.desconectar2();
    }


private void calcularTempo() {
        //
        new Thread() {
            @Override
            public void run() {
                //
                do {
                    //
                    for (HashMap<String, String> m : lista) {  //Este for é acusado como o problema depois
                        //
                        if (m.get("hospedagem").equals("Período")) {
                            //
                            DateTime entrada = DateTime.parse(m.get("data_entrada") + "T" + m.get("hora_entrada") + ".266-03:00");
                            DateTime tempo = DateTime.parse("T" + m.get("periodo") + ".266-03:00");
                            DateTime extra = DateTime.parse("T" + m.get("hora_extra") + ".266-03:00");
                            //
                            entrada = entrada.plusHours(Integer.parseInt(tempo.toString("HH"))).plusMinutes(Integer.parseInt(tempo.toString("mm"))).plusSeconds(Integer.parseInt(tempo.toString("ss")));
                            entrada = entrada.plusHours(Integer.parseInt(extra.toString("HH"))).plusMinutes(Integer.parseInt(extra.toString("mm"))).plusSeconds(Integer.parseInt(extra.toString("ss")));
                            //
                            DateTime hAtual = new DateTime(DateTimeZone.forID("America/Fortaleza"));
                            //
                            if (m.get("status_tolerancia").equals("S")) {
                                //
                                DateTime tolerancia = DateTime.parse("T" + m.get("tolerancia_periodo") + ".266-03:00");
                                entrada = entrada.plusHours(Integer.parseInt(tolerancia.toString("HH"))).plusMinutes(Integer.parseInt(tolerancia.toString("mm"))).plusSeconds(Integer.parseInt(tolerancia.toString("ss")));
                                //
                                if (entrada.equals(hAtual) || entrada.isBefore(hAtual)) {
                                    //
                                    controleG.setHoraExtra(m.get("codigo"));
                                    //
                                    if (m.get("codigo").equals(codSuit)) {
                                        //
                                        while (tblConsumo.getModel().getRowCount() > 0) {  
                                            ((DefaultTableModel) tblConsumo.getModel()).removeRow(0);  
                                        }
                                        //
                                        preencheOcupado(codSuit);
                                        //
                                    }
                                    //
                                    if (nTE.equals("S")) {
                                        //
                                        new Thread() {
                                            @Override
                                            public void run() {
                                                //
                                                try {
                                                    JOptionPane.showMessageDialog(null, "A suíte " + m.get("suite") + " já passou do limite de tolerância.\nA partir de agora se iniciará uma nova hora de estadia.");
                                                } catch (HeadlessException ex) {
                                                    JOptionPane.showMessageDialog(null, "Erro na Thread JOptioPane calculo estadia tolerancia periodo.\nERRO: " + ex);
                                                }
                                            }
                                        }.start();
                                    }
                                    //
                                    setDados();
                                }
                            } else {
                                if (entrada.equals(hAtual) || entrada.isBefore(hAtual)) {
                                    //
                                    controleG.setTolerancia(m.get("codigo"));
                                    //
                                    if (m.get("codigo").equals(codSuit)) {
                                        //
                                        while (tblConsumo.getModel().getRowCount() > 0) {  
                                            ((DefaultTableModel) tblConsumo.getModel()).removeRow(0);  
                                        }
                                        //
                                        preencheOcupado(codSuit);
                                    }
                                    //
                                    if (nTE.equals("S")) {
                                        //
                                        new Thread() {
                                            @Override
                                            public void run() {
                                                //
                                                try {
                                                    JOptionPane.showMessageDialog(null, "A suíte " + m.get("suite") + " já atingiu o tempo máximo de hospedagem.\nA partir de agora se iniciará a tolerância para desocupação da suíte.");
                                                } catch (HeadlessException ex) {
                                                    JOptionPane.showMessageDialog(null, "Erro na Thread joptionpane calculo estadia sem tolerancia periodo.\nERRO: " + ex);
                                                }
                                            }
                                        }.start();
                                    }
                                    //
                                    setDados();
                                    //
                                }
                            }
                        } else {
                            //
                            DateTime entrada = DateTime.parse(m.get("data_entrada") + "T" + m.get("hora_entrada") + ".266-03:00");
                            DateTime estadia = DateTime.parse("T" + m.get("pernoite") + ".266-03:00");
                            DateTime ex = DateTime.parse("T" + m.get("hora_extra") + ".266-03:00");
                            DateTime hAtual = new DateTime(DateTimeZone.forID("America/Fortaleza"));
                            //
                            entrada = entrada.plusHours(Integer.parseInt(estadia.toString("HH"))).plusMinutes(Integer.parseInt(estadia.toString("mm"))).plusSeconds(Integer.parseInt(estadia.toString("ss")));
                            entrada = entrada.plusHours(Integer.parseInt(ex.toString("HH"))).plusMinutes(Integer.parseInt(ex.toString("mm"))).plusSeconds(Integer.parseInt(ex.toString("ss")));
                            //
                             if (m.get("status_tolerancia").equals("S")) {
                                 //
                                DateTime tolerancia = DateTime.parse("T" + m.get("tolerancia_pernoite") + ".266-03:00");
                                entrada = entrada.plusHours(Integer.parseInt(tolerancia.toString("HH"))).plusMinutes(Integer.parseInt(tolerancia.toString("mm"))).plusSeconds(Integer.parseInt(tolerancia.toString("ss")));
                                //
                                if (entrada.equals(hAtual) || entrada.isBefore(hAtual)) {
                                    //
                                    controleG.setHoraExtra(m.get("codigo"));
                                    //
                                    setDados();
                                    //
                                    if (m.get("codigo").equals(codSuit)) {
                                        //
                                        while (tblConsumo.getModel().getRowCount() > 0) {  
                                            ((DefaultTableModel) tblConsumo.getModel()).removeRow(0);  
                                        }
                                        //
                                        preencheOcupado(codSuit);
                                    }
                                    //
                                    if (nTE.equals("S")) {
                                        new Thread() {
                                            @Override
                                            public void run() {
                                                try {
                                                    JOptionPane.showMessageDialog(null, "A suíte: " + m.get("suite") + " já passou do limite de tolerância.\nA partir de agora se iniciará uma nova hora de estadia.");
                                                } catch (HeadlessException ex) {
                                                    JOptionPane.showMessageDialog(null, "Erro na Thread joptionpane calculo estadia tolerancia periodo.\nERRO: " + ex);
                                                }
                                            }
                                        }.start();
                                    }
                                    //
                                }
                                //
                            } else {
                                if (entrada.equals(hAtual) || entrada.isBefore(hAtual)) {
                                    //
                                    controleG.setTolerancia(m.get("codigo"));
                                    //
                                    setDados();
                                    //
                                    if (m.get("codigo").equals(codSuit)) {
                                        //
                                        while (tblConsumo.getModel().getRowCount() > 0) {  
                                            ((DefaultTableModel) tblConsumo.getModel()).removeRow(0);  
                                        }
                                        //
                                        preencheOcupado(codSuit);
                                    }
                                    //
                                    if (nTE.equals("S")) {
                                        new Thread() {
                                            @Override
                                            public void run() {
                                                try {
                                                    JOptionPane.showMessageDialog(null, "A suíte: " + m.get("suite") + " já atingiu o tempo máximo de hospedagem.\nA partir de agora se iniciará a tolerância para desocupação da suíte.");
                                                } catch (HeadlessException ex) {
                                                    JOptionPane.showMessageDialog(null, "Erro na Thread joptionpane calculo estadia sem tolerancia periodo.\nERRO: " + ex);
                                                }
                                            }
                                        }.start();
                                    }
                                    //
                                }
                            }
                        }
                        //
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException ex) {
                            JOptionPane.showMessageDialog(null, "Erro ao dormir thread calcula tempo");
                        }
                        //
                    }
                } while (1 == 1);
                //
            }
        }.start();
    }

Já tentei incrementar está solução, mas não obtive sucesso.
Eu uso um lista.clear(); e tentei usar um
while (iterator.hasNext) {
iterator.remove();
}
Mas dá um erro.

Isso é um exercício ou uma aplicação real que você está tentando ajustar?

Acho que seu problema é de arquitetura, não com só threads. O cenário que você descreveu é até simples, mas você escolheu um jeito desnecessariamente complexo pra atendê-lo.

Por que você não cria uma consulta apenas para te trazer as hospedagens atrasadas, baseado no tempo atual? Essa consulta pode ser disparada por uma Thread (ou um SwingWorker, se estiver usando Swing) que executa a cada 10 minutos (um intervalo razoável, na minha opinião). Depois, sob essa lista retornada, você dispara a alteração de tempo extra para cada registro, mas fazendo uma nova consulta para trazer um registro completo.

Ou seja, a primeira consulta seria algo como:

select s.codigo from suites s, entrada_suite e where s.codigo = e.codigo_suite and e.data_hora_saida_programada < current_date_and_time();

Que retornaria a lista de estadias que já deveriam ter sido encerradas. Com base nessa lista, você pode disparar um processo que altera as estadias atrasadas, adicionando o tempo extra. Mas esse processo precisa carregar cada registros a ser processado individualmente.

Obs: note que não usei os mesmos campos da sua tabela. Estou supondo que você pode ajustar a estrutura caso necessário. current_date_and_time() é qualquer função do banco que você está usando que retorna a data e hora atuais.

Por que você precisa de tantas threads? Dá pra fazer isso com uma thread só (a principal da aplicação), se você quiser. Ou fez tudo isso para a a aplicação não travar enquanto faz a consulta?

Abraço.

Eu estou fazendo um sistema completo para hotel, até agora está indo tudo ótimo, mas eu realmente preciso mudar a forma como o sistema pega as informações de hospedagem. Atualmente a Thread de verificação pega as informações de horas direto no banco a cada loop. O que eu quero mudar é que ele pegue essas informações do ArrayList para evitar conexões excessivas no banco.
Está tudo ótimo, ele calcula perfeitamente em tempo real todas as informações, o que está atrasando minha vida é na hora de acrescentar novas informações de hospedagem a este ArrayList, pois sempre que eu acrescentar algo ou remover uma informação vai disparar o erro: java.util.ConcurrentModificationException e a Thread que fica verificando simplesmente para.
Eu realmente não sei o que fazer.

Por que quer reduzir as conexões ao banco? Está tendo problemas com desempenho?

Não sei se você entendeu o que significa essa exceção, mas ficar tentando contorná-la não vai dar o resultado que você espera.

ConcurrentModificationException ocorre quando você tenta alterar um objeto ao mesmo tempo em que outro processamento (ou thread) está operando sobre ele. Geralmente, não é um problema que você resolve com 1 ou 2 linhas de código, mas sim repensando a arquitetura como um todo.

Não li seu código todo, mas pelo que entendi você tem várias threads disparadas que atuam sobre uma mesma lista, alterando-a por motivos distintos. É isso que causa sua exceção. O que você esperava obter usando tantas threads?

Recomendo que você repense esse código. Faça-o funcionar de uma forma mais linear (sem threads) e veja como ele se comporta. Só aí, somente se for necessário, você adiciona threads.

Abraço.

Eu não sei que tipo de hotel você está tentando atender.
Em geral, os hotéis possuem sistemas de horários fixos, por exemplo, check-in a partir das 14 e check-out a partir das 10 horas da manhã.
Se este é o caso, você pode definir uma rotina que execute às 10 da manhã, todos os dias.
Caso seja um motel, em que você precisa validar a cada período (2, 4, 6, 8, 12, etc), você pode procurar por schedulers ou timers, onde você cria instâncias isoladas que irão executar a cada x tempo. Ou mesmo, cada nova hospedagem disparar um thread com sleep deste x tempo.

Não sei se você entendeu o que significa essa exceção, mas ficar tentando contorná-la não vai dar o resultado que você espera.

Você tinha toda razão. Ao invés de tentar achar uma solução para o problema de modificar o ArrayList eu simplesmente procurei outra solução para o problema original. Eu estava tão fixado na ideai que tive de usar o ArrayList que não estava percebendo outras possibilidades mais simples.
Valeu mesmo, depois de ler seu conselho eu reli todo o código e vi que eu não precisava daquilo para realizar meu objetivo e acabei por resolver o problema, agora tudo está uma maravilha.

Ótima ideia, cara. Obrigado!