Swing – JavaFX 互操作性并发修改异常错误
Swing–JavaFX Interoperability ConcurrentModificationException bug
我花了过去 3 天(20 多个小时)试图解决这个 ConcurrentModificationException 错误。我正在尝试将我相当大的 Java 项目迁移到 JavaFX,但我在管理 Swing–JavaFX 互操作性和线程方面遇到了麻烦,因为我希望我的程序在其中是混合的尊重
错误
当我足够快地点击 Add Particle
按钮时就会出现错误(但也来自我的主项目中的多个其他来源,其他按钮也会产生相同的错误)。我得到以下堆栈跟踪
Exception in thread "AWT-EventQueue-0" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1043)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:997)
at MainPanel.actionPerformed(MainPanel.java:40)
at java.desktop/javax.swing.Timer.fireActionPerformed(Timer.java:317)
at java.desktop/javax.swing.Timer$DoPostEvent.run(Timer.java:249)
at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:313)
at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:770)
at java.desktop/java.awt.EventQueue.run(EventQueue.java:721)
at java.desktop/java.awt.EventQueue.run(EventQueue.java:715)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:740)
at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)
下面是我可以提供的我的项目的最小可复制示例 (MRE),带有 Github link here :
testProjectFx.fxml
(FXML 文件)
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="SampleController">
<children>
<BorderPane fx:id="mainPanel" prefHeight="318.0" prefWidth="535.0" />
<Button fx:id="addParticle" layoutX="26.0" layoutY="347.0" mnemonicParsing="false" text="Add Particle" />
</children>
</AnchorPane>
SampleController.java
(FXML 控制器文件)
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
public class SampleController {
@FXML
private BorderPane mainPanel;
@FXML
private Button addParticle;
public void initializeButtons(MainPanel mainPanel) {
addParticle.addEventHandler(ActionEvent.ACTION, event -> mainPanel.addParticle(
100, 5
));
}
}
App.java
(扩展应用程序的主要 class).
import javafx.application.Application;
import javafx.embed.swing.SwingNode;
import javafx.fxml.FXMLLoader;
import java.awt.Color;
import javax.swing.SwingUtilities;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class App extends Application{
public static void main(String[] args) {
launch(args);
}
public static SampleController controller;
@Override
public void start(Stage stage) throws Exception {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("testProjectFx.fxml"));
Parent root = loader.load();
controller = (SampleController) loader.getController();
Scene scene = new Scene(root, 600 , 400);
BorderPane scenePanel = (BorderPane) scene.lookup("#mainPanel");
SwingNode swingNode = new SwingNode();
MainPanel mainPanel = new MainPanel();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
mainPanel.setSize(535, 318);
mainPanel.setBackground(Color.black);
mainPanel.physicsTimer.start();
mainPanel.fpsTimer.start();
swingNode.setContent(mainPanel);
}
});
scenePanel.setCenter(swingNode);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
mainPanel.initializeParticles(15, 100 ,5);
controller.initializeButtons(mainPanel);
}
});
stage.setTitle("FXML Welcome");
stage.setScene(scene);
stage.show();
} catch(Exception e){
e.printStackTrace();
}
}
}
MainPanel.java
(J绘画发生的面板)
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Arc2D;
import java.util.ArrayList;
import java.util.Random;
import javax.swing.JPanel;
import javax.swing.Timer;
public class MainPanel extends JPanel{
static Random rand = new Random();
static ArrayList<Particle> particleList = new ArrayList<Particle>();
static int timeTick = 1000/60; //fps normal
/*
* Timer that updates the screen every at 60 FPS.
*/
Timer fpsTimer = new Timer(timeTick, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
for(Particle p : particleList) {
p.x += p.vx;
p.y += p.vy;
}
repaint();
}
});
/*
* Timer that updates the physics every 1 millisecond.
*/
Timer physicsTimer = new Timer(1 , new ActionListener() {
@Override
public void actionPerformed(ActionEvent ae) {
for(Particle p1 : particleList) {
for(Particle p2 : particleList) {
if(p1 == p2) continue;
// p1.edgeCollision(p2);
// applyForces(p1, p2);
// applyCollision(p1, p2);
}
p1.x += p1.vx;
p1.y += p1.vy;
}
}
});
/*
* Function that initializes some particles on the canvas at the beggining.
*/
public void initializeParticles(int numberOfParticles, int mass, int charge) {
for(int i = 0 ; i < numberOfParticles ; i++) {
int xPos;
int yPos;
do {
xPos = rand.nextInt(100)+25;
yPos = rand.nextInt(100)+25;
Particle p = new Particle(xPos, yPos, 0, 0,mass, charge); //mass charge at end
particleList.add(p);
}while(!particleAlreadyExists(xPos, yPos));
}
}
/*
* Draw the particles on the canvas.
*/
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
for(Particle particle : particleList) {
g2d.setColor(Color.WHITE);
Shape circle = new Arc2D.Double(particle.x, particle.y, particle.width, particle.height,
0 , 360, Arc2D.CHORD);
g2d.fill(circle);
}
}
/*
* Checks if a particle already exists at the generated position.
*/
public boolean particleAlreadyExists(double x, double y) {
boolean particleExistsAlready = false;
for(Particle particle : particleList) {
if(x >= particle.x-particle.width && x <= particle.x+ 2*particle.width &&
y >= particle.y-particle.height && y <= particle.y + 2*particle.height) {
particleExistsAlready=true;
break;
}
}
return particleExistsAlready;
}
public void addParticle(int mass, int charge) {
initializeParticles(1, mass, charge);
}
}
Particle.java
(粒子对象class)
import java.awt.Color;
import java.util.ArrayList;
import java.util.Random;
public class Particle{
double x, y;
double width, height;
double mass;
double vx, vy;
int charge;
public double radius = 0;
public double centerX = 0;
public double centerY = 0;
static Random rand = new Random();
public Particle(double x, double y, double velX, double velY,double mass, int charge) {
this.x = x;
this.y=y;
this.width=mass/10;
this.height=mass/10;
this.vx=velX;
this.vy = velY;
this.mass=mass;
this.charge = charge;
this.radius = this.width/2;
this.centerX = this.x + (this.width/2);
this.centerY = this.y + (this.height/2);
}
public static double velInit() {
double val;
if(rand.nextBoolean()) {
val = rand.nextDouble()*0.5;
} else {
val = -rand.nextDouble()*0.5;
}
return val;
}
public void reinitializeVel(ArrayList<Particle> particles) {
for(Particle p : particles) {
p.vx = velInit();
p.vy = velInit();
}
}
}
我尝试过的事情
我尝试用
包装部分代码
Platform.runLater(new Runnable() { });
和
SwingUtilities.invokeLater(new Runnable() { });
如 Oracle's documentation 中所述,但没有成功。有很多可能的组合,每一种组合似乎都会使我的代码表现不同(而且是错误的)。
我已经尝试了很多组合,在这一点上我认为用这两个函数包装甚至可能不是解决方案。
有谁知道我做错了什么吗?
您正在从 swing 调用 JavaFX 函数,反之亦然。如果您需要 javafx/swing 通信,使用 Platfrom.runlater 和 SwingUtilities.invokeLater 包装代码是正确的方法,但您需要以正确的方式进行;)。
让我们从 App.java 开始
SwingNode 和controller 不是JavaFX 组件,而是用SwingUtilities 调用的,不可能是对的。
应该是这样的:
SwingUtilities.invokeLater(() -> {
mainPanel.setSize(535, 318);
mainPanel.setBackground(Color.black);
mainPanel.physicsTimer.start();
mainPanel.fpsTimer.start();
mainPanel.initializeParticles(15, 100 ,5);
});
scenePanel.setCenter(swingNode);
swingNode.setContent(mainPanel);
controller.initializeButtons(mainPanel);
您的主要错误来自于在 javafx 线程中操作 particleList 而您的 swing 部分正在迭代绘制函数中的列表。
将您的 initializeParticle 函数包装在 SwingUtilities.invokeLater() 函数中应该可以解决这个问题。
public void initializeParticles(int numberOfParticles, int mass, int charge) {
SwingUtilities.invokeLater(() -> {
for (int i = 0; i < numberOfParticles; i++) {
int xPos;
int yPos;
do {
xPos = rand.nextInt(100) + 25;
yPos = rand.nextInt(100) + 25;
Particle p = new Particle(xPos, yPos, 0, 0, mass, charge); //mass charge at end
particleList.add(p);
} while (!particleAlreadyExists(xPos, yPos));
}
});
}
然而,这是非常难看的代码,您不应该那样混合使用这两个框架。
虽然它们可能是代码的其他问题,但 ConcurrentModificationException
的 根本原因 是尝试遍历集合(在本例中是 ArrayList
) 同时更改同一个集合。
这是不允许的 here:
it is not generally permissible for one thread to modify a Collection while
another thread is iterating over it.
在发布的代码中 initializeParticles
通过向其添加元素来修改 particleList
:particleList.add(p);
而 paintComponent
以及 physicsTimer
和 fpsTimer
都使用 for(Particle particle : particleList)
遍历同一个集合
虽然有 several ways to overcome this issue 最简单的方法是简单地将 all 迭代 particleList
更改为以下形式:
for(int i=0; i< particleList.size(); i++) {
Particle p = particleList.get(i);
.........
}
我花了过去 3 天(20 多个小时)试图解决这个 ConcurrentModificationException 错误。我正在尝试将我相当大的 Java 项目迁移到 JavaFX,但我在管理 Swing–JavaFX 互操作性和线程方面遇到了麻烦,因为我希望我的程序在其中是混合的尊重
错误
当我足够快地点击 Add Particle
按钮时就会出现错误(但也来自我的主项目中的多个其他来源,其他按钮也会产生相同的错误)。我得到以下堆栈跟踪
Exception in thread "AWT-EventQueue-0" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1043)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:997)
at MainPanel.actionPerformed(MainPanel.java:40)
at java.desktop/javax.swing.Timer.fireActionPerformed(Timer.java:317)
at java.desktop/javax.swing.Timer$DoPostEvent.run(Timer.java:249)
at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:313)
at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:770)
at java.desktop/java.awt.EventQueue.run(EventQueue.java:721)
at java.desktop/java.awt.EventQueue.run(EventQueue.java:715)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:740)
at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)
下面是我可以提供的我的项目的最小可复制示例 (MRE),带有 Github link here :
testProjectFx.fxml
(FXML 文件)
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="SampleController">
<children>
<BorderPane fx:id="mainPanel" prefHeight="318.0" prefWidth="535.0" />
<Button fx:id="addParticle" layoutX="26.0" layoutY="347.0" mnemonicParsing="false" text="Add Particle" />
</children>
</AnchorPane>
SampleController.java
(FXML 控制器文件)
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
public class SampleController {
@FXML
private BorderPane mainPanel;
@FXML
private Button addParticle;
public void initializeButtons(MainPanel mainPanel) {
addParticle.addEventHandler(ActionEvent.ACTION, event -> mainPanel.addParticle(
100, 5
));
}
}
App.java
(扩展应用程序的主要 class).
import javafx.application.Application;
import javafx.embed.swing.SwingNode;
import javafx.fxml.FXMLLoader;
import java.awt.Color;
import javax.swing.SwingUtilities;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class App extends Application{
public static void main(String[] args) {
launch(args);
}
public static SampleController controller;
@Override
public void start(Stage stage) throws Exception {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("testProjectFx.fxml"));
Parent root = loader.load();
controller = (SampleController) loader.getController();
Scene scene = new Scene(root, 600 , 400);
BorderPane scenePanel = (BorderPane) scene.lookup("#mainPanel");
SwingNode swingNode = new SwingNode();
MainPanel mainPanel = new MainPanel();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
mainPanel.setSize(535, 318);
mainPanel.setBackground(Color.black);
mainPanel.physicsTimer.start();
mainPanel.fpsTimer.start();
swingNode.setContent(mainPanel);
}
});
scenePanel.setCenter(swingNode);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
mainPanel.initializeParticles(15, 100 ,5);
controller.initializeButtons(mainPanel);
}
});
stage.setTitle("FXML Welcome");
stage.setScene(scene);
stage.show();
} catch(Exception e){
e.printStackTrace();
}
}
}
MainPanel.java
(J绘画发生的面板)
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Arc2D;
import java.util.ArrayList;
import java.util.Random;
import javax.swing.JPanel;
import javax.swing.Timer;
public class MainPanel extends JPanel{
static Random rand = new Random();
static ArrayList<Particle> particleList = new ArrayList<Particle>();
static int timeTick = 1000/60; //fps normal
/*
* Timer that updates the screen every at 60 FPS.
*/
Timer fpsTimer = new Timer(timeTick, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
for(Particle p : particleList) {
p.x += p.vx;
p.y += p.vy;
}
repaint();
}
});
/*
* Timer that updates the physics every 1 millisecond.
*/
Timer physicsTimer = new Timer(1 , new ActionListener() {
@Override
public void actionPerformed(ActionEvent ae) {
for(Particle p1 : particleList) {
for(Particle p2 : particleList) {
if(p1 == p2) continue;
// p1.edgeCollision(p2);
// applyForces(p1, p2);
// applyCollision(p1, p2);
}
p1.x += p1.vx;
p1.y += p1.vy;
}
}
});
/*
* Function that initializes some particles on the canvas at the beggining.
*/
public void initializeParticles(int numberOfParticles, int mass, int charge) {
for(int i = 0 ; i < numberOfParticles ; i++) {
int xPos;
int yPos;
do {
xPos = rand.nextInt(100)+25;
yPos = rand.nextInt(100)+25;
Particle p = new Particle(xPos, yPos, 0, 0,mass, charge); //mass charge at end
particleList.add(p);
}while(!particleAlreadyExists(xPos, yPos));
}
}
/*
* Draw the particles on the canvas.
*/
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
for(Particle particle : particleList) {
g2d.setColor(Color.WHITE);
Shape circle = new Arc2D.Double(particle.x, particle.y, particle.width, particle.height,
0 , 360, Arc2D.CHORD);
g2d.fill(circle);
}
}
/*
* Checks if a particle already exists at the generated position.
*/
public boolean particleAlreadyExists(double x, double y) {
boolean particleExistsAlready = false;
for(Particle particle : particleList) {
if(x >= particle.x-particle.width && x <= particle.x+ 2*particle.width &&
y >= particle.y-particle.height && y <= particle.y + 2*particle.height) {
particleExistsAlready=true;
break;
}
}
return particleExistsAlready;
}
public void addParticle(int mass, int charge) {
initializeParticles(1, mass, charge);
}
}
Particle.java
(粒子对象class)
import java.awt.Color;
import java.util.ArrayList;
import java.util.Random;
public class Particle{
double x, y;
double width, height;
double mass;
double vx, vy;
int charge;
public double radius = 0;
public double centerX = 0;
public double centerY = 0;
static Random rand = new Random();
public Particle(double x, double y, double velX, double velY,double mass, int charge) {
this.x = x;
this.y=y;
this.width=mass/10;
this.height=mass/10;
this.vx=velX;
this.vy = velY;
this.mass=mass;
this.charge = charge;
this.radius = this.width/2;
this.centerX = this.x + (this.width/2);
this.centerY = this.y + (this.height/2);
}
public static double velInit() {
double val;
if(rand.nextBoolean()) {
val = rand.nextDouble()*0.5;
} else {
val = -rand.nextDouble()*0.5;
}
return val;
}
public void reinitializeVel(ArrayList<Particle> particles) {
for(Particle p : particles) {
p.vx = velInit();
p.vy = velInit();
}
}
}
我尝试过的事情
我尝试用
包装部分代码Platform.runLater(new Runnable() { });
和
SwingUtilities.invokeLater(new Runnable() { });
如 Oracle's documentation 中所述,但没有成功。有很多可能的组合,每一种组合似乎都会使我的代码表现不同(而且是错误的)。
我已经尝试了很多组合,在这一点上我认为用这两个函数包装甚至可能不是解决方案。
有谁知道我做错了什么吗?
您正在从 swing 调用 JavaFX 函数,反之亦然。如果您需要 javafx/swing 通信,使用 Platfrom.runlater 和 SwingUtilities.invokeLater 包装代码是正确的方法,但您需要以正确的方式进行;)。
让我们从 App.java 开始 SwingNode 和controller 不是JavaFX 组件,而是用SwingUtilities 调用的,不可能是对的。
应该是这样的:
SwingUtilities.invokeLater(() -> {
mainPanel.setSize(535, 318);
mainPanel.setBackground(Color.black);
mainPanel.physicsTimer.start();
mainPanel.fpsTimer.start();
mainPanel.initializeParticles(15, 100 ,5);
});
scenePanel.setCenter(swingNode);
swingNode.setContent(mainPanel);
controller.initializeButtons(mainPanel);
您的主要错误来自于在 javafx 线程中操作 particleList 而您的 swing 部分正在迭代绘制函数中的列表。
将您的 initializeParticle 函数包装在 SwingUtilities.invokeLater() 函数中应该可以解决这个问题。
public void initializeParticles(int numberOfParticles, int mass, int charge) {
SwingUtilities.invokeLater(() -> {
for (int i = 0; i < numberOfParticles; i++) {
int xPos;
int yPos;
do {
xPos = rand.nextInt(100) + 25;
yPos = rand.nextInt(100) + 25;
Particle p = new Particle(xPos, yPos, 0, 0, mass, charge); //mass charge at end
particleList.add(p);
} while (!particleAlreadyExists(xPos, yPos));
}
});
}
然而,这是非常难看的代码,您不应该那样混合使用这两个框架。
虽然它们可能是代码的其他问题,但 ConcurrentModificationException
的 根本原因 是尝试遍历集合(在本例中是 ArrayList
) 同时更改同一个集合。
这是不允许的 here:
it is not generally permissible for one thread to modify a Collection while
another thread is iterating over it.
在发布的代码中 initializeParticles
通过向其添加元素来修改 particleList
:particleList.add(p);
而 paintComponent
以及 physicsTimer
和 fpsTimer
都使用 for(Particle particle : particleList)
虽然有 several ways to overcome this issue 最简单的方法是简单地将 all 迭代 particleList
更改为以下形式:
for(int i=0; i< particleList.size(); i++) {
Particle p = particleList.get(i);
.........
}