I'm building a WebSocket server that handles drawing of objects. Here is how the server's class looks like:
import fmi.whiteboard.models.paths.*;
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@ServerEndpoint(value = "/whiteboard",
encoders = {DrawingEncoder.class, ShapeEncoder.class, PathEncoder.class},
decoders = {DrawingDecoder.class, ShapeDecoder.class, PathDecoder.class})
public class WhiteboardServer {
private static Set<Session> peers = Collections.synchronizedSet(new HashSet<Session>());
@OnOpen
public void onOpen(Session peer) {
peers.add(peer);
}
@OnClose
public void onClose(Session peer) {
peers.remove(peer);
}
@OnMessage
public void broadcastShape(Drawing drawing, Session session) throws IOException, EncodeException {
for (Session peer : peers) {
if (!peer.equals(session)) {
peer.getAsyncRemote().sendObject(drawing);
}
}
}
}
And here is how the ReactJS component that handles drawing looks like:
export default function Whiteboard() {
const canvas = useRef(null);
const ws = useMemo(() => {
const socket = new WebSocket("ws://localhost:8080/backend_war/whiteboard");
socket.binaryType = "arraybuffer";
return socket;
}, []);
useEffect(() => {
ws.onmessage = (evt: MessageEvent) => {
const data: SocketResponse = JSON.parse(evt.data);
if (data && data.shapes) {
canvas?.current?.loadPaths(data.shapes);
}
};
ws.onerror= (err) => console.log(err);
}, [ws]);
const onDraw = (message: CanvasPath[]) => {
ws.send(JSON.stringify({ shapes: message }));
};
return (
<>
<ReactSketchCanvas
ref={canvas}
style={styles}
onUpdate={(paths) => setPaths(paths)}
strokeWidth={4}
strokeColor="red"
/>
</>
);
}
The app works fine for 2-3 seconds but as soon I start drawing a lot more shapes, the socket server crashes with ConcurrentModificationException
on the line with for (Session peer : peers)
.
If it is of any help, I'm using Java 11 and Tomcat 10 as the server.
CodePudding user response:
Collections.synchronizedSet creates a Set where single item access are synchronized. This concerns methods such as get, add, remove, etc. However, the iterator isn't synchronized. You obtain ConcurrentModificationException when another thread modifies the collection while iteration is in progress.
You have two solutions. Either protect the look with a synchronized block like this:
synchronized(peers) {
for (Session peer: peers) {
...
}
}
Or, better, switch to ConcurrentHashSet, which guarantees optimized access by multiple threads without never throwing ConcurrentModificationException.
CodePudding user response:
When you synchronize and existing collection with Collections.synchronizedSet
it only synchronizes the methods to access the data such as get()
, set()
... and not the iterator. Therefore you need to synchronize the iterator as well like this for example
synchronized(peers){
for (Session peer : peers) {
if (!peer.equals(session)) {
peer.getAsyncRemote().sendObject(drawing);
}
}
}
Here is the documentation