diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 17faed52..2edf1f2d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -8,7 +8,7 @@ on: [push]
 jobs:
   checks:
     runs-on: ubuntu-latest
-    container: eclipse-temurin:17-jdk
+    container: eclipse-temurin:21-jdk
     steps:
       - name: Checkout code
         uses: actions/checkout@v4
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ae9f5886..11b292b2 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -14,7 +14,7 @@ jobs:
   release:
 
     runs-on: ubuntu-latest
-    container: eclipse-temurin:17-jdk
+    container: eclipse-temurin:21-jdk
 
     steps:
       - uses: actions/checkout@v3
diff --git a/README.md b/README.md
index 879b388e..2897b6c5 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
  A P2P connection proxy for Supreme Commander: Forged Alliance using [ICE](https://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment).
 
 ## Building
-Build the project using gradle from the provided wrapper (Java 17 required).
+Build the project using gradle from the provided wrapper (Java 21 required).
 
 ## JSONRPC Protocol
 The `faf-ice-adapter` is controlled using a bi-directional [JSON-RPC](http://www.jsonrpc.org/specification) interface over TCP.
diff --git a/build.gradle b/build.gradle
index 4379ba42..975933d5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -8,7 +8,7 @@ plugins {
 group 'com.faforever'
 version '1.0-SNAPSHOT'
 
-sourceCompatibility = JavaVersion.VERSION_17
+sourceCompatibility = JavaVersion.VERSION_21
 
 repositories {
     mavenCentral()
diff --git a/client/build.gradle b/client/build.gradle
index 15c2d1de..be569808 100644
--- a/client/build.gradle
+++ b/client/build.gradle
@@ -5,7 +5,7 @@ apply plugin: 'com.github.johnrengelman.shadow'
 group 'com.faforever'
 version '1.0-SNAPSHOT'
 
-sourceCompatibility = JavaVersion.VERSION_17
+sourceCompatibility = JavaVersion.VERSION_21
 
 repositories {
     mavenCentral()
diff --git a/ice-adapter/build.gradle b/ice-adapter/build.gradle
index c478cae9..1a7903af 100644
--- a/ice-adapter/build.gradle
+++ b/ice-adapter/build.gradle
@@ -14,7 +14,7 @@ apply plugin: 'com.diffplug.spotless'
 group 'com.faforever'
 
 
-sourceCompatibility = JavaVersion.VERSION_17
+sourceCompatibility = JavaVersion.VERSION_21
 
 repositories {
     mavenCentral()
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/IceAdapter.java b/ice-adapter/src/main/java/com/faforever/iceadapter/IceAdapter.java
index 6cfd090a..bd6bbe3a 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/IceAdapter.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/IceAdapter.java
@@ -1,26 +1,30 @@
 package com.faforever.iceadapter;
 
-import static com.faforever.iceadapter.debug.Debug.debug;
-
 import com.faforever.iceadapter.debug.Debug;
 import com.faforever.iceadapter.gpgnet.GPGNetServer;
 import com.faforever.iceadapter.gpgnet.GameState;
 import com.faforever.iceadapter.ice.GameSession;
 import com.faforever.iceadapter.ice.PeerIceModule;
 import com.faforever.iceadapter.rpc.RPCService;
-import com.faforever.iceadapter.util.Executor;
+import com.faforever.iceadapter.util.ExecutorHolder;
+import com.faforever.iceadapter.util.LockUtil;
 import com.faforever.iceadapter.util.TrayIcon;
-import java.util.concurrent.Callable;
 import lombok.extern.slf4j.Slf4j;
 import picocli.CommandLine;
 
+import java.util.concurrent.*;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import static com.faforever.iceadapter.debug.Debug.debug;
+
 @CommandLine.Command(
         name = "faf-ice-adapter",
         mixinStandardHelpOptions = true,
         usageHelpAutoWidth = true,
         description = "An ice (RFC 5245) based network bridge between FAF client and ForgedAlliance.exe")
 @Slf4j
-public class IceAdapter implements Callable<Integer> {
+public class IceAdapter implements Callable<Integer>, AutoCloseable {
     private static IceAdapter INSTANCE;
     private static String VERSION = "SNAPSHOT";
     private static volatile GameSession GAME_SESSION;
@@ -29,6 +33,8 @@ public class IceAdapter implements Callable<Integer> {
     private IceOptions iceOptions;
 
     private volatile boolean running = true;
+    private final ExecutorService executor = ExecutorHolder.getExecutor();
+    private static final Lock lockGameSession = new ReentrantLock();
 
     public static void main(String[] args) {
         new CommandLine(new IceAdapter()).setUnmatchedArgumentsAllowed(true).execute(args);
@@ -60,6 +66,13 @@ public void start() {
         debug().startupComplete();
     }
 
+    @Override
+    public void close() {
+        executor.shutdown();
+        CompletableFuture.runAsync(executor::shutdownNow,
+                CompletableFuture.delayedExecutor(250, TimeUnit.MILLISECONDS));
+    }
+
     public static void onHostGame(String mapName) {
         log.info("onHostGame");
         createGameSession();
@@ -113,42 +126,46 @@ public static void onDisconnectFromPeer(int remotePlayerId) {
         });
     }
 
-    private static synchronized void createGameSession() {
-        if (GAME_SESSION != null) {
-            GAME_SESSION.close();
-            GAME_SESSION = null;
-        }
+    private static void createGameSession() {
+        LockUtil.executeWithLock(lockGameSession, () -> {
+            if (GAME_SESSION != null) {
+                GAME_SESSION.close();
+                GAME_SESSION = null;
+            }
 
-        GAME_SESSION = new GameSession();
+            GAME_SESSION = new GameSession();
+        });
     }
 
     /**
      * Triggered by losing gpgnet connection to FA.
      * Closes the active Game/ICE session
      */
-    public static synchronized void onFAShutdown() {
-        if (GAME_SESSION != null) {
-            log.info("FA SHUTDOWN, closing everything");
-            GAME_SESSION.close();
-            GAME_SESSION = null;
-            // Do not put code outside of this if clause, else it will be executed multiple times
-        }
+    public static void onFAShutdown() {
+        LockUtil.executeWithLock(lockGameSession, () -> {
+            if (GAME_SESSION != null) {
+                log.info("FA SHUTDOWN, closing everything");
+                GAME_SESSION.close();
+                GAME_SESSION = null;
+                // Do not put code outside of this if clause, else it will be executed multiple times
+            }
+        });
     }
 
     /**
      * Stop the ICE adapter
      */
-    public static void close() {
-        log.info("close() - stopping the adapter");
-
-        Executor.executeDelayed(500, () -> System.exit(0));
+    public static void close(int status) {
+        log.info("close() - stopping the adapter. Status: {}", status);
 
         onFAShutdown(); // will close gameSession aswell
         GPGNetServer.close();
         RPCService.close();
+        Debug.close();
         TrayIcon.close();
-
-        System.exit(0);
+        INSTANCE.close();
+        CompletableFuture.runAsync(() -> System.exit(status),
+                CompletableFuture.delayedExecutor(500, TimeUnit.MILLISECONDS));
     }
 
     public static int getId() {
@@ -183,6 +200,10 @@ public static boolean isRunning() {
         return INSTANCE.running;
     }
 
+    public static Executor getExecutor() {
+        return INSTANCE.executor;
+    }
+
     public static GameSession getGameSession() {
         return GAME_SESSION;
     }
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/debug/Debug.java b/ice-adapter/src/main/java/com/faforever/iceadapter/debug/Debug.java
index bcdbb888..8e0fc7f3 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/debug/Debug.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/debug/Debug.java
@@ -1,9 +1,11 @@
 package com.faforever.iceadapter.debug;
 
 import com.faforever.iceadapter.IceAdapter;
-import java.lang.reflect.InvocationTargetException;
 import lombok.extern.slf4j.Slf4j;
 
+import java.lang.reflect.InvocationTargetException;
+import java.util.concurrent.CompletableFuture;
+
 @Slf4j
 public class Debug {
     // TODO
@@ -17,6 +19,8 @@ public class Debug {
 
     public static int RPC_PORT;
 
+    private static TelemetryDebugger telemetryDebugger;
+
     private static final DebugFacade debugFacade = new DebugFacade();
 
     public static void register(Debugger debugger) {
@@ -28,7 +32,8 @@ public static void remove(Debugger debugger) {
     }
 
     public static void init() {
-        new TelemetryDebugger(IceAdapter.getTelemetryServer(), IceAdapter.getGameId(), IceAdapter.getId());
+        telemetryDebugger = new TelemetryDebugger(
+                IceAdapter.getTelemetryServer(), IceAdapter.getGameId(), IceAdapter.getId());
 
         // Debugger window is started and set to debugFuture when either window is requested as the info window can be
         // used to open the debug window
@@ -38,25 +43,33 @@ public static void init() {
         }
 
         if (isJavaFxSupported()) {
-            new Thread(() -> {
+            CompletableFuture.runAsync(
+                    () -> {
                         try {
                             Class.forName("com.faforever.iceadapter.debug.DebugWindow")
                                     .getMethod("launchApplication")
                                     .invoke(null);
+                        } catch (InvocationTargetException e) {
+                            log.info("DebugWindows stopped");
                         } catch (IllegalAccessException
                                 | ClassNotFoundException
-                                | NoSuchMethodException
-                                | InvocationTargetException e) {
-                            e.printStackTrace();
-                            log.error("Could not create DebugWindow. Running without debug window.");
+                                | NoSuchMethodException e) {
+                            log.error("Could not create DebugWindow. Running without debug window.", e);
                         }
-                    })
-                    .start(); // Completes future once application started
+                    },
+                    IceAdapter.getExecutor());
         } else {
             log.info("No JavaFX support detected. Running without debug window.");
         }
     }
 
+    public static void close() {
+        if (telemetryDebugger != null) {
+            telemetryDebugger.close();
+            telemetryDebugger = null;
+        }
+    }
+
     public static Debugger debug() {
         return debugFacade;
     }
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/debug/DebugWindow.java b/ice-adapter/src/main/java/com/faforever/iceadapter/debug/DebugWindow.java
index 418848ed..b10508cd 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/debug/DebugWindow.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/debug/DebugWindow.java
@@ -5,11 +5,7 @@
 import com.faforever.iceadapter.gpgnet.GameState;
 import com.faforever.iceadapter.ice.Peer;
 import com.faforever.iceadapter.ice.PeerConnectivityCheckerModule;
-import com.faforever.iceadapter.util.Executor;
 import com.nbarraille.jjsonrpc.JJsonPeer;
-import java.io.IOException;
-import java.util.Optional;
-import java.util.concurrent.CompletableFuture;
 import javafx.application.Application;
 import javafx.application.Platform;
 import javafx.beans.property.SimpleBooleanProperty;
@@ -29,6 +25,13 @@
 import org.ice4j.ice.CandidatePair;
 import org.ice4j.ice.CandidateType;
 import org.ice4j.ice.Component;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.util.Comparator;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
 
 @Slf4j
 public class DebugWindow extends Application implements Debugger {
@@ -69,25 +72,30 @@ public void start(Stage stage) {
 
         stage.setScene(scene);
         stage.setTitle("FAF ICE adapter - Debugger - Build: %s".formatted(IceAdapter.getVersion()));
-        //		stage.setOnCloseRequest(Event::consume);
-        //		stage.show();
 
         if (Debug.ENABLE_DEBUG_WINDOW) {
-            Executor.executeDelayed(Debug.DELAY_UI_MS, () -> runOnUIThread(stage::show));
+            CompletableFuture.runAsync(
+                    () -> runOnUIThread(stage::show),
+                    CompletableFuture.delayedExecutor(
+                            Debug.DELAY_UI_MS, TimeUnit.MILLISECONDS, IceAdapter.getExecutor()));
         }
 
-        //		new Thread(() -> Debug.debug.complete(this)).start();
         log.info("Created debug window.");
 
         if (Debug.ENABLE_INFO_WINDOW) {
-            Executor.executeDelayed(Debug.DELAY_UI_MS, () -> runOnUIThread(() -> new InfoWindow().init()));
+            CompletableFuture.runAsync(
+                    () -> runOnUIThread(() -> new InfoWindow().init()),
+                    CompletableFuture.delayedExecutor(
+                            Debug.DELAY_UI_MS, TimeUnit.MILLISECONDS, IceAdapter.getExecutor()));
         }
     }
 
     public void showWindow() {
-        runOnUIThread(() -> stage.show());
-        initStaticVariables();
-        initPeers();
+        runOnUIThread(() -> {
+            stage.show();
+            initStaticVariables();
+            initPeers();
+        });
     }
 
     @Override
@@ -96,7 +104,6 @@ public void startupComplete() {
     }
 
     public void initStaticVariables() {
-
         runOnUIThread(() -> {
             controller.versionLabel.setText("Version: %s".formatted(IceAdapter.getVersion()));
             controller.userLabel.setText("User: %s(%d)".formatted(IceAdapter.getLogin(), IceAdapter.getId()));
@@ -116,6 +123,7 @@ public void initPeers() {
                     p.connectivityUpdate(peer);
                     peers.add(p);
                 }
+                peers.sort(DebugPeer::compareTo);
             }
         });
     }
@@ -158,36 +166,33 @@ public void gameStateChanged() {
 
     @Override
     public void connectToPeer(int id, String login, boolean localOffer) {
-        new Thread(() -> {
-                    synchronized (peers) {
-                        peers.add(new DebugPeer(id, login, localOffer)); // Might callback into jfx
-                    }
-                })
-                .start();
+        runOnUIThread(() -> {
+            synchronized (peers) {
+                peers.add(new DebugPeer(id, login, localOffer)); // Might callback into jfx
+                peers.sort(DebugPeer::compareTo);
+            }
+        });
     }
 
     @Override
     public void disconnectFromPeer(int id) {
-        new Thread(() -> {
-                    synchronized (peers) {
-                        peers.removeIf(peer -> peer.id.get() == id); // Might callback into jfx
-                    }
-                })
-                .start();
+        runOnUIThread(() -> {
+            synchronized (peers) {
+                peers.removeIf(peer -> peer.id.get() == id); // Might callback into jfx
+                peers.sort(DebugPeer::compareTo);
+            }
+        });
     }
 
     @Override
     public void peerStateChanged(Peer peer) {
-        new Thread(() -> {
-                    synchronized (peers) {
-                        peers.stream()
-                                .filter(p -> p.id.get() == peer.getRemoteId())
-                                .forEach(p -> {
-                                    p.stateChangedUpdate(peer);
-                                });
-                    }
-                })
-                .start();
+        runOnUIThread(() -> {
+            synchronized (peers) {
+                peers.stream().filter(p -> p.id.get() == peer.getRemoteId()).forEach(p -> {
+                    p.stateChangedUpdate(peer);
+                });
+            }
+        });
     }
 
     @Override
@@ -217,7 +222,7 @@ public static void launchApplication() {
     @AllArgsConstructor
     // @Getter //PropertyValueFactory will attempt to access fieldNameProperty(), then getFieldName() (expecting value,
     // not property) and then isFieldName() methods
-    public static class DebugPeer {
+    public static class DebugPeer implements Comparable<DebugPeer> {
         public SimpleIntegerProperty id = new SimpleIntegerProperty(-1);
         public SimpleStringProperty login = new SimpleStringProperty("");
         public SimpleBooleanProperty localOffer = new SimpleBooleanProperty(false);
@@ -366,5 +371,10 @@ public void connectivityUpdate(Peer peer) {
                     .orElse(-1L)
                     .intValue());
         }
+
+        @Override
+        public int compareTo(@NotNull DebugPeer o) {
+            return Comparator.comparingLong(DebugPeer::getId).compare(this, o);
+        }
     }
 }
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/debug/DebugWindowController.java b/ice-adapter/src/main/java/com/faforever/iceadapter/debug/DebugWindowController.java
index 3a23aee4..178fecc1 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/debug/DebugWindowController.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/debug/DebugWindowController.java
@@ -1,7 +1,6 @@
 package com.faforever.iceadapter.debug;
 
 import com.faforever.iceadapter.IceAdapter;
-import java.util.Objects;
 import javafx.event.ActionEvent;
 import javafx.fxml.FXML;
 import javafx.scene.control.*;
@@ -11,6 +10,8 @@
 import lombok.extern.slf4j.Slf4j;
 import org.slf4j.LoggerFactory;
 
+import java.util.concurrent.CompletableFuture;
+
 @Slf4j
 public class DebugWindowController {
     public static DebugWindowController INSTANCE;
@@ -49,12 +50,13 @@ public class DebugWindowController {
     public DebugWindowController() {}
 
     public void onKillAdapterClicked(ActionEvent actionEvent) {
-        System.exit(337);
+        IceAdapter.close(337);
     }
 
     public void reconnectToPeer(DebugWindow.DebugPeer peer) {
-        if (Objects.nonNull(peer)) {
-            new Thread(() -> IceAdapter.getGameSession().reconnectToPeer(peer.getId())).start();
+        if (peer != null) {
+            CompletableFuture.runAsync(
+                    () -> IceAdapter.getGameSession().reconnectToPeer(peer.getId()), IceAdapter.getExecutor());
         }
     }
 
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/debug/InfoWindowController.java b/ice-adapter/src/main/java/com/faforever/iceadapter/debug/InfoWindowController.java
index 516d1c59..05608228 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/debug/InfoWindowController.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/debug/InfoWindowController.java
@@ -2,14 +2,16 @@
 
 import com.faforever.iceadapter.IceAdapter;
 import com.faforever.iceadapter.util.TrayIcon;
-import java.awt.*;
-import java.io.IOException;
-import java.net.URI;
 import javafx.event.ActionEvent;
 import javafx.fxml.FXML;
 import javafx.scene.control.Button;
 import lombok.SneakyThrows;
 
+import java.awt.*;
+import java.io.IOException;
+import java.net.URI;
+import java.util.concurrent.CompletableFuture;
+
 public class InfoWindowController {
     public Button killAdapterButton;
     public Button showDebugWindowButton;
@@ -25,12 +27,12 @@ public void onKillAdapterClicked(ActionEvent actionEvent) {
             killAdapterButton.setText(
                     "This will disconnect you from the game! Click " + (3 - killRequests) + " more times.");
         } else {
-            System.exit(337);
+            IceAdapter.close(337);
         }
     }
 
     public void onShowDebugWindowClicked(ActionEvent actionEvent) {
-        DebugWindow.INSTANCE.thenAcceptAsync(DebugWindow::showWindow);
+        DebugWindow.INSTANCE.thenAcceptAsync(DebugWindow::showWindow, IceAdapter.getExecutor());
     }
 
     @SneakyThrows
@@ -41,14 +43,15 @@ public void onTelemetryWebUiClicked(ActionEvent actionEvent) {
                         IceAdapter.getGameId(),
                         IceAdapter.getId());
 
-        new Thread(() -> {
+        CompletableFuture.runAsync(
+                () -> {
                     try {
                         Desktop.getDesktop().browse(URI.create(url));
                     } catch (IOException e) {
                         throw new RuntimeException(e);
                     }
-                })
-                .start();
+                },
+                IceAdapter.getExecutor());
     }
 
     public void onMinimizeToTrayClicked(ActionEvent actionEvent) {
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/debug/TelemetryDebugger.java b/ice-adapter/src/main/java/com/faforever/iceadapter/debug/TelemetryDebugger.java
index 2b4325d0..b7f00289 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/debug/TelemetryDebugger.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/debug/TelemetryDebugger.java
@@ -4,20 +4,19 @@
 import com.faforever.iceadapter.gpgnet.GPGNetServer;
 import com.faforever.iceadapter.ice.Peer;
 import com.faforever.iceadapter.ice.PeerConnectivityCheckerModule;
-import com.faforever.iceadapter.telemetry.ConnectToPeer;
-import com.faforever.iceadapter.telemetry.CoturnServer;
-import com.faforever.iceadapter.telemetry.DisconnectFromPeer;
-import com.faforever.iceadapter.telemetry.OutgoingMessageV1;
-import com.faforever.iceadapter.telemetry.RegisterAsPeer;
-import com.faforever.iceadapter.telemetry.UpdateCoturnList;
-import com.faforever.iceadapter.telemetry.UpdateGameState;
-import com.faforever.iceadapter.telemetry.UpdateGpgnetState;
-import com.faforever.iceadapter.telemetry.UpdatePeerConnectivity;
-import com.faforever.iceadapter.telemetry.UpdatePeerState;
+import com.faforever.iceadapter.telemetry.*;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import com.google.common.util.concurrent.RateLimiter;
 import com.nbarraille.jjsonrpc.JJsonPeer;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.ice4j.ice.Candidate;
+import org.ice4j.ice.CandidatePair;
+import org.ice4j.ice.Component;
+import org.java_websocket.client.WebSocketClient;
+import org.java_websocket.handshake.ServerHandshake;
+
 import java.net.ConnectException;
 import java.net.URI;
 import java.time.Instant;
@@ -29,16 +28,9 @@
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.LinkedBlockingQueue;
-import lombok.SneakyThrows;
-import lombok.extern.slf4j.Slf4j;
-import org.ice4j.ice.Candidate;
-import org.ice4j.ice.CandidatePair;
-import org.ice4j.ice.Component;
-import org.java_websocket.client.WebSocketClient;
-import org.java_websocket.handshake.ServerHandshake;
 
 @Slf4j
-public class TelemetryDebugger implements Debugger {
+public class TelemetryDebugger implements Debugger, AutoCloseable {
     private final WebSocketClient websocketClient;
     private final ObjectMapper objectMapper;
 
@@ -87,10 +79,9 @@ public void onError(Exception ex) {
         objectMapper = new ObjectMapper();
         objectMapper.registerModule(new JavaTimeModule());
 
-        sendingLoopThread = new Thread(this::sendingLoop, "sendingLoop");
-        sendingLoopThread.setUncaughtExceptionHandler(
-                (t, e) -> log.error("Thread sendingLoop crashed unexpectedly", e));
-        sendingLoopThread.start();
+        sendingLoopThread = Thread.ofVirtual()
+                .name("sendingLoop")
+                .start(this::sendingLoop);
     }
 
     private void sendMessage(OutgoingMessageV1 message) {
@@ -103,7 +94,7 @@ private void sendMessage(OutgoingMessageV1 message) {
 
     @SneakyThrows
     private void sendingLoop() {
-        while (true) {
+        while (!Thread.currentThread().isInterrupted()) {
             var message = messageQueue.take();
             try {
                 String json = objectMapper.writeValueAsString(message);
@@ -116,6 +107,9 @@ private void sendingLoop() {
 
                 log.trace("Sending telemetry message: {}", json);
                 websocketClient.send(json);
+            } catch (InterruptedException e) {
+                log.info("Sending loop interrupted");
+                return;
             } catch (Exception e) {
                 log.error("Error on sending message object: {}", message, e);
             }
@@ -226,4 +220,9 @@ public void updateCoturnList(Collection<CoturnServer> servers) {
                 servers.stream().map(CoturnServer::host).findFirst().orElse(null),
                 servers));
     }
+
+    @Override
+    public void close() {
+        sendingLoopThread.interrupt();
+    }
 }
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/gpgnet/FaDataOutputStream.java b/ice-adapter/src/main/java/com/faforever/iceadapter/gpgnet/FaDataOutputStream.java
index 02bb91a8..08a3442b 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/gpgnet/FaDataOutputStream.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/gpgnet/FaDataOutputStream.java
@@ -8,6 +8,8 @@
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * Writes data to Forged Alliance (the forgedalliance, not the lobby).
@@ -20,6 +22,7 @@ public class FaDataOutputStream extends OutputStream {
     public static final char DELIMITER = '\b';
     private final LittleEndianDataOutputStream outputStream;
     private final Charset charset = StandardCharsets.UTF_8;
+    private final Lock writer = new ReentrantLock();
 
     public FaDataOutputStream(OutputStream outputStream) {
         this.outputStream = new LittleEndianDataOutputStream(new BufferedOutputStream(outputStream));
@@ -27,15 +30,46 @@ public FaDataOutputStream(OutputStream outputStream) {
 
     @Override
     public void write(int b) throws IOException {
-        outputStream.write(b);
+        writer.lock();
+        try {
+            outputStream.write(b);
+        } finally {
+            writer.unlock();
+        }
+    }
+
+    public void writeMessage(String header, Object... args) throws IOException {
+        writer.lock();
+        try {
+            writeString(header);
+            writeArgs(Arrays.asList(args));
+            outputStream.flush();
+        } finally {
+            writer.unlock();
+        }
     }
 
     @Override
     public void flush() throws IOException {
-        outputStream.flush();
+        writer.lock();
+        try {
+            outputStream.flush();
+        } finally {
+            writer.unlock();
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        writer.lock();
+        try {
+            outputStream.close();
+        } finally {
+            writer.unlock();
+        }
     }
 
-    public synchronized void writeArgs(List<Object> args) throws IOException {
+    private void writeArgs(List<Object> args) throws IOException {
         writeInt(args.size());
 
         for (Object arg : args) {
@@ -52,27 +86,16 @@ public synchronized void writeArgs(List<Object> args) throws IOException {
         }
     }
 
-    public void writeInt(int value) throws IOException {
+    private void writeInt(int value) throws IOException {
         outputStream.writeInt(value);
     }
 
-    public synchronized void writeByte(int b) throws IOException {
+    private void writeByte(int b) throws IOException {
         outputStream.writeByte(b);
     }
 
-    public synchronized void writeString(String string) throws IOException {
+    private void writeString(String string) throws IOException {
         outputStream.writeInt(string.length());
         outputStream.write(string.getBytes(charset));
     }
-
-    public synchronized void writeMessage(String header, Object... args) throws IOException {
-        writeString(header);
-        writeArgs(Arrays.asList(args));
-        outputStream.flush();
-    }
-
-    @Override
-    public void close() throws IOException {
-        outputStream.close();
-    }
 }
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/gpgnet/GPGNetServer.java b/ice-adapter/src/main/java/com/faforever/iceadapter/gpgnet/GPGNetServer.java
index 6395389b..269110f8 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/gpgnet/GPGNetServer.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/gpgnet/GPGNetServer.java
@@ -1,10 +1,12 @@
 package com.faforever.iceadapter.gpgnet;
 
-import static com.faforever.iceadapter.debug.Debug.debug;
-
 import com.faforever.iceadapter.IceAdapter;
 import com.faforever.iceadapter.rpc.RPCService;
+import com.faforever.iceadapter.util.LockUtil;
 import com.faforever.iceadapter.util.NetworkToolbox;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
 import java.io.IOException;
 import java.net.ServerSocket;
 import java.net.Socket;
@@ -13,14 +15,17 @@
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 import java.util.stream.Collectors;
-import lombok.Getter;
-import lombok.extern.slf4j.Slf4j;
+
+import static com.faforever.iceadapter.debug.Debug.debug;
 
 @Slf4j
 public class GPGNetServer {
     private static int GPGNET_PORT;
     private static int LOBBY_PORT;
+    private static final Lock lockSocket = new ReentrantLock();
     private static ServerSocket serverSocket;
     private static volatile GPGNetClient currentClient;
 
@@ -50,9 +55,10 @@ public static void init(int gpgnetPort, int lobbyPort) {
             serverSocket = new ServerSocket(GPGNetServer.getGpgnetPort());
         } catch (IOException e) {
             log.error("Couldn't start GPGNetServer", e);
-            System.exit(-1);
+            IceAdapter.close(-1);
         }
-        new Thread(GPGNetServer::acceptThread).start();
+
+        CompletableFuture.runAsync(GPGNetServer::acceptThread, IceAdapter.getExecutor());
         log.info("GPGNetServer started");
     }
 
@@ -67,6 +73,7 @@ public static class GPGNetClient {
         private final Thread listenerThread;
         private volatile boolean stopping = false;
         private FaDataOutputStream gpgnetOut;
+        private final Lock lockStream = new ReentrantLock();
         private final CompletableFuture<GPGNetClient> lobbyFuture = new CompletableFuture<>();
 
         private GPGNetClient(Socket socket) {
@@ -77,9 +84,7 @@ private GPGNetClient(Socket socket) {
             } catch (IOException e) {
                 log.error("Could not create GPGNet output steam to FA", e);
             }
-
-            listenerThread = new Thread(this::listenerThread);
-            listenerThread.start();
+            listenerThread = Thread.startVirtualThread(this::listenerThread);
 
             RPCService.onConnectionStateChanged("Connected");
             log.info("GPGNetClient has connected");
@@ -129,17 +134,20 @@ private void processGpgnetMessage(String command, List<Object> args) {
         /**
          * Send a message to this FA instance via GPGNet
          */
-        public synchronized void sendGpgnetMessage(String command, Object... args) {
-            try {
-                gpgnetOut.writeMessage(command, args);
-                log.info(
-                        "Sent GPGNet message: {} {}",
-                        command,
-                        Arrays.stream(args).map(Object::toString).collect(Collectors.joining(" ")));
-            } catch (IOException e) {
-                log.error("Error while communicating with FA (output), assuming shutdown", e);
-                GPGNetServer.onGpgnetConnectionLost();
-            }
+        public void sendGpgnetMessage(String command, Object... args) {
+            LockUtil.executeWithLock(lockStream, () -> {
+                try {
+
+                    gpgnetOut.writeMessage(command, args);
+                    log.info(
+                            "Sent GPGNet message: {} {}",
+                            command,
+                            Arrays.stream(args).map(Object::toString).collect(Collectors.joining(" ")));
+                } catch (IOException e) {
+                    log.error("Error while communicating with FA (output), assuming shutdown", e);
+                    GPGNetServer.onGpgnetConnectionLost();
+                }
+            });
         }
 
         /**
@@ -152,7 +160,9 @@ private void listenerThread() {
             // and is now going to set GPGNetServer.currentClient
             try (var inputStream = socket.getInputStream();
                     var gpgnetIn = new FaDataInputStream(inputStream)) {
-                while ((!triggerActive || GPGNetServer.currentClient == this) && !stopping) {
+                while (!Thread.currentThread().isInterrupted()
+                        && (!triggerActive || GPGNetServer.currentClient == this)
+                        && !stopping) {
                     String command = gpgnetIn.readString();
                     List<Object> args = gpgnetIn.readChunks();
 
@@ -191,7 +201,7 @@ public void close() {
      */
     private static void onGpgnetConnectionLost() {
         log.info("GPGNet connection lost");
-        synchronized (serverSocket) {
+        LockUtil.executeWithLock(lockSocket, () -> {
             if (currentClient != null) {
                 currentClient.close();
                 currentClient = null;
@@ -204,7 +214,7 @@ private static void onGpgnetConnectionLost() {
 
                 IceAdapter.onFAShutdown();
             }
-        }
+        });
         debug().gpgnetConnectedDisconnected();
     }
 
@@ -212,10 +222,10 @@ private static void onGpgnetConnectionLost() {
      * Listens for incoming connections from a game instance
      */
     private static void acceptThread() {
-        while (IceAdapter.isRunning()) {
+        while (!Thread.currentThread().isInterrupted() && IceAdapter.isRunning()) {
             try {
                 Socket socket = serverSocket.accept();
-                synchronized (serverSocket) {
+                LockUtil.executeWithLock(lockSocket, () -> {
                     if (currentClient != null) {
                         onGpgnetConnectionLost();
                     }
@@ -224,7 +234,7 @@ private static void acceptThread() {
                     clientFuture.complete(currentClient);
 
                     debug().gpgnetConnectedDisconnected();
-                }
+                });
             } catch (SocketException e) {
                 return;
             } catch (IOException e) {
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/ice/GameSession.java b/ice-adapter/src/main/java/com/faforever/iceadapter/ice/GameSession.java
index 95a5c447..c43db400 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/ice/GameSession.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/ice/GameSession.java
@@ -55,7 +55,7 @@ public int connectToPeer(String remotePlayerLogin, int remotePlayerId, boolean o
         Peer peer = new Peer(this, remotePlayerId, remotePlayerLogin, offer, preferredPort);
         peers.put(remotePlayerId, peer);
         debug().connectToPeer(remotePlayerId, remotePlayerLogin, offer);
-        return peer.getFaSocket().getLocalPort();
+        return peer.getLocalPort();
     }
 
     /**
@@ -80,7 +80,7 @@ public void reconnectToPeer(Integer remotePlayerId) {
         if (Objects.nonNull(reconnectPeer)) {
             String remotePlayerLogin = reconnectPeer.getRemoteLogin();
             boolean offer = reconnectPeer.isLocalOffer();
-            int port = reconnectPeer.getFaSocket().getLocalPort();
+            int port = reconnectPeer.getLocalPort();
 
             disconnectFromPeer(remotePlayerId);
             connectToPeer(remotePlayerLogin, remotePlayerId, offer, port);
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/ice/Peer.java b/ice-adapter/src/main/java/com/faforever/iceadapter/ice/Peer.java
index b5893fdd..29dceccd 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/ice/Peer.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/ice/Peer.java
@@ -2,18 +2,23 @@
 
 import com.faforever.iceadapter.IceAdapter;
 import com.faforever.iceadapter.gpgnet.GPGNetServer;
-import java.io.IOException;
-import java.net.*;
+import com.faforever.iceadapter.util.LockUtil;
 import lombok.Getter;
+import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 
+import java.io.IOException;
+import java.net.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
 /**
  * Represents a peer in the current game session which we are connected to
  */
 @Getter
 @Slf4j
 public class Peer {
-
     private final GameSession gameSession;
 
     private final int remoteId;
@@ -24,7 +29,8 @@ public class Peer {
     public volatile boolean closing = false;
 
     private final PeerIceModule ice = new PeerIceModule(this);
-    private DatagramSocket faSocket; // Socket on which we are listening for FA / sending data to FA
+    private final DatagramSocket faSocket; // Socket on which we are listening for FA / sending data to FA
+    private final Lock lockSocketSend = new ReentrantLock();
 
     public Peer(GameSession gameSession, int remoteId, String remoteLogin, boolean localOffer, int preferredPort) {
         this.gameSession = gameSession;
@@ -36,26 +42,32 @@ public Peer(GameSession gameSession, int remoteId, String remoteLogin, boolean l
         log.debug(
                 "Peer created: {}, localOffer: {}, preferredPort: {}", getPeerIdentifier(), localOffer, preferredPort);
 
-        initForwarding(preferredPort);
+        faSocket = initForwarding(preferredPort);
+
+        CompletableFuture.runAsync(this::faListener, IceAdapter.getExecutor());
 
         if (localOffer) {
-            new Thread(ice::initiateIce).start();
+            CompletableFuture.runAsync(ice::initiateIce, IceAdapter.getExecutor());
         }
     }
 
+    public int getLocalPort() {
+        return faSocket.getLocalPort();
+    }
+
     /**
      * Starts waiting for data from FA
      */
-    private void initForwarding(int port) {
+    @SneakyThrows(SocketException.class)
+    private DatagramSocket initForwarding(int port) {
         try {
-            faSocket = new DatagramSocket(port);
+            DatagramSocket socket = new DatagramSocket(port);
+            log.debug("Now forwarding data to peer {}", getPeerIdentifier());
+            return socket;
         } catch (SocketException e) {
             log.error("Could not create socket for peer: {}", getPeerIdentifier(), e);
+            throw e;
         }
-
-        new Thread(this::faListener).start();
-
-        log.debug("Now forwarding data to peer {}", getPeerIdentifier());
     }
 
     /**
@@ -64,31 +76,37 @@ private void initForwarding(int port) {
      * @param offset
      * @param length
      */
-    synchronized void onIceDataReceived(byte data[], int offset, int length) {
-        try {
-            DatagramPacket packet = new DatagramPacket(
-                    data, offset, length, InetAddress.getByName("127.0.0.1"), GPGNetServer.getLobbyPort());
-            faSocket.send(packet);
-        } catch (UnknownHostException e) {
-        } catch (IOException e) {
-            if (closing) {
-                log.debug("Ignoring error the send packet because the connection was closed {}", getPeerIdentifier());
-            } else {
-                log.error(
-                        "Error while writing to local FA as peer (probably disconnecting from peer) {}",
-                        getPeerIdentifier(),
-                        e);
+    void onIceDataReceived(byte[] data, int offset, int length) {
+        LockUtil.executeWithLock(lockSocketSend, () -> {
+            try {
+                DatagramPacket packet = new DatagramPacket(
+                        data, offset, length, InetAddress.getByName("127.0.0.1"), GPGNetServer.getLobbyPort());
+                faSocket.send(packet);
+            } catch (UnknownHostException e) {
+            } catch (IOException e) {
+                if (closing) {
+                    log.debug(
+                            "Ignoring error the send packet because the connection was closed {}", getPeerIdentifier());
+                } else {
+                    log.error(
+                            "Error while writing to local FA as peer (probably disconnecting from peer) {}",
+                            getPeerIdentifier(),
+                            e);
+                }
             }
-        }
+        });
     }
 
     /**
      * This method get's invoked by the thread listening for data from FA
      */
     private void faListener() {
-        byte data[] = new byte
+        byte[] data = new byte
                 [65536]; // 64KiB = UDP MTU, in practice due to ethernet frames being <= 1500 B, this is often not used
-        while (IceAdapter.isRunning() && IceAdapter.getGameSession() == gameSession) {
+        while (!Thread.currentThread().isInterrupted()
+                && IceAdapter.isRunning()
+                && IceAdapter.getGameSession() == gameSession
+                && !closing) {
             try {
                 DatagramPacket packet = new DatagramPacket(data, data.length);
                 faSocket.receive(packet);
@@ -118,9 +136,7 @@ public void close() {
         log.info("Closing peer for player {}", getPeerIdentifier());
 
         closing = true;
-        if (faSocket != null) {
-            faSocket.close();
-        }
+        faSocket.close();
 
         ice.close();
     }
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/ice/PeerConnectivityCheckerModule.java b/ice-adapter/src/main/java/com/faforever/iceadapter/ice/PeerConnectivityCheckerModule.java
index d32a4770..1dcee461 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/ice/PeerConnectivityCheckerModule.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/ice/PeerConnectivityCheckerModule.java
@@ -1,22 +1,29 @@
 package com.faforever.iceadapter.ice;
 
-import static com.faforever.iceadapter.debug.Debug.debug;
-
+import com.faforever.iceadapter.IceAdapter;
+import com.faforever.iceadapter.util.LockUtil;
 import com.google.common.primitives.Longs;
-import java.util.Arrays;
 import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
 
-@Slf4j
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import static com.faforever.iceadapter.debug.Debug.debug;
+
 /**
  * Periodically sends echo requests via the ICE data channel and initiates a reconnect after timeout
  * ONLY THE OFFERING ADAPTER of a connection will send echos and reoffer.
  */
+@Slf4j
 public class PeerConnectivityCheckerModule {
 
     private static final int ECHO_INTERVAL = 1000;
 
     private final PeerIceModule ice;
+    private final Lock lockIce = new ReentrantLock();
     private volatile boolean running = false;
     private volatile Thread checkerThread;
 
@@ -36,38 +43,42 @@ public PeerConnectivityCheckerModule(PeerIceModule ice) {
         this.ice = ice;
     }
 
-    synchronized void start() {
-        if (running) {
-            return;
-        }
+    void start() {
+        LockUtil.executeWithLock(lockIce, () -> {
+            if (running) {
+                return;
+            }
 
-        running = true;
-        log.debug("Starting connectivity checker for peer {}", ice.getPeer().getRemoteId());
+            running = true;
+            log.debug("Starting connectivity checker for peer {}", ice.getPeer().getRemoteId());
 
-        averageRTT = 0.0f;
-        lastPacketReceived = System.currentTimeMillis();
+            averageRTT = 0.0f;
+            lastPacketReceived = System.currentTimeMillis();
 
-        checkerThread = new Thread(this::checkerThread, getThreadName());
-        checkerThread.setUncaughtExceptionHandler(
-                (t, e) -> log.error("Thread {} crashed unexpectedly", t.getName(), e));
-        checkerThread.start();
+            checkerThread = Thread.ofVirtual()
+                    .name(getThreadName())
+                    .uncaughtExceptionHandler((t, e) -> log.error("Thread {} crashed unexpectedly", t.getName(), e))
+                    .start(this::checkerThread);
+        });
     }
 
     private String getThreadName() {
         return "connectivityChecker-" + ice.getPeer().getRemoteId();
     }
 
-    synchronized void stop() {
-        if (!running) {
-            return;
-        }
+    void stop() {
+        LockUtil.executeWithLock(lockIce, () -> {
+            if (!running) {
+                return;
+            }
 
-        running = false;
+            running = false;
 
-        if (checkerThread != null) {
-            checkerThread.interrupt();
-            checkerThread = null;
-        }
+            if (checkerThread != null) {
+                checkerThread.interrupt();
+                checkerThread = null;
+            }
+        });
     }
 
     /**
@@ -100,9 +111,10 @@ void echoReceived(byte[] data, int offset, int length) {
     }
 
     private void checkerThread() {
-        while (!Thread.currentThread().isInterrupted()) {
+        while (!Thread.currentThread().isInterrupted() && running) {
             log.trace("Running connectivity checker");
 
+            Peer peer = ice.getPeer();
             byte[] data = new byte[9];
             data[0] = 'e';
 
@@ -111,7 +123,7 @@ private void checkerThread() {
 
             ice.sendViaIce(data, 0, data.length);
 
-            debug().peerConnectivityUpdate(ice.getPeer());
+            debug().peerConnectivityUpdate(peer);
 
             try {
                 Thread.sleep(ECHO_INTERVAL);
@@ -119,15 +131,14 @@ private void checkerThread() {
                 log.warn(
                         "{} (sleeping checkerThread) was interrupted",
                         Thread.currentThread().getName());
-                Thread.currentThread().interrupt();
                 return;
             }
 
             if (System.currentTimeMillis() - lastPacketReceived > 10000) {
                 log.warn(
                         "Didn't receive any answer to echo requests for the past 10 seconds from {}, aborting connection",
-                        ice.getPeer().getRemoteLogin());
-                new Thread(ice::onConnectionLost).start();
+                        peer.getRemoteLogin());
+                CompletableFuture.runAsync(ice::onConnectionLost, IceAdapter.getExecutor());
                 return;
             }
         }
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/ice/PeerIceModule.java b/ice-adapter/src/main/java/com/faforever/iceadapter/ice/PeerIceModule.java
index 67843191..1879cc7f 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/ice/PeerIceModule.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/ice/PeerIceModule.java
@@ -1,23 +1,10 @@
 package com.faforever.iceadapter.ice;
 
-import static com.faforever.iceadapter.debug.Debug.debug;
-import static com.faforever.iceadapter.ice.IceState.*;
-
 import com.faforever.iceadapter.IceAdapter;
 import com.faforever.iceadapter.rpc.RPCService;
 import com.faforever.iceadapter.util.CandidateUtil;
-import com.faforever.iceadapter.util.Executor;
+import com.faforever.iceadapter.util.LockUtil;
 import com.faforever.iceadapter.util.TrayIcon;
-import java.io.IOException;
-import java.net.DatagramPacket;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionException;
-import java.util.concurrent.ThreadLocalRandom;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Collectors;
 import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
 import org.ice4j.TransportAddress;
@@ -26,6 +13,19 @@
 import org.ice4j.ice.harvest.TurnCandidateHarvester;
 import org.ice4j.security.LongTermCredential;
 
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Collectors;
+
+import static com.faforever.iceadapter.debug.Debug.debug;
+import static com.faforever.iceadapter.ice.IceState.*;
+
 @Getter
 @Slf4j
 public class PeerIceModule {
@@ -72,6 +72,10 @@ public static void setForceRelay(boolean forceRelay) {
     // How often have we been waiting for a response to local candidates/offer
     private final AtomicInteger awaitingCandidatesEventId = new AtomicInteger(0);
 
+    private final Lock lockInit = new ReentrantLock();
+    private final Lock lockLostConnection = new ReentrantLock();
+    private final Lock lockMessageReceived = new ReentrantLock();
+
     public PeerIceModule(Peer peer) {
         this.peer = peer;
     }
@@ -89,19 +93,27 @@ private void setState(IceState newState) {
     /**
      * Will start the ICE Process
      */
-    synchronized void initiateIce() {
-        if (iceState != NEW && iceState != DISCONNECTED) {
-            log.warn(
-                    getLogPrefix() + "ICE already in progress, aborting re initiation. current state: {}",
-                    iceState.getMessage());
-            return;
-        }
+    void initiateIce() {
+        LockUtil.executeWithLock(lockInit, () -> {
+            if (peer.isClosing()) {
+                log.warn("{} Peer not connected anymore, aborting reinitiation of ICE", getLogPrefix());
+                return;
+            }
+
+            if (iceState != NEW && iceState != DISCONNECTED) {
+                log.warn(
+                        "{} ICE already in progress, aborting re initiation. current state: {}",
+                        getLogPrefix(),
+                        iceState.getMessage());
+                return;
+            }
 
-        setState(GATHERING);
-        log.info(getLogPrefix() + "Initiating ICE for peer");
+            setState(GATHERING);
+            log.info("{} Initiating ICE for peer", getLogPrefix());
 
-        createAgent();
-        gatherCandidates();
+            createAgent();
+            gatherCandidates();
+        });
     }
 
     /**
@@ -122,7 +134,7 @@ private void createAgent() {
      * Gathers all local candidates, packs them into a message and sends them to the other peer via RPC
      */
     private void gatherCandidates() {
-        log.info(getLogPrefix() + "Gathering ice candidates");
+        log.info("{} Gathering ice candidates", getLogPrefix());
 
         List<IceServer> iceServers = getViableIceServers();
 
@@ -135,39 +147,44 @@ private void gatherCandidates() {
                         a, new LongTermCredential(iceServer.getTurnUsername(), iceServer.getTurnCredential())))
                 .forEach(agent::addCandidateHarvester));
 
-        CompletableFuture<Void> gatheringFuture = CompletableFuture.runAsync(() -> {
-            try {
-                component = agent.createComponent(
-                        mediaStream,
-                        MINIMUM_PORT + (int) (ThreadLocalRandom.current().nextDouble() * 999.0),
-                        MINIMUM_PORT,
-                        MINIMUM_PORT + 1000);
-            } catch (IOException e) {
-                throw new RuntimeException(e);
-            }
-        });
+        CompletableFuture<Void> gatheringFuture = CompletableFuture.runAsync(
+                () -> {
+                    try {
+                        component = agent.createComponent(
+                                mediaStream,
+                                MINIMUM_PORT
+                                        + (int) (ThreadLocalRandom.current().nextDouble() * 999.0),
+                                MINIMUM_PORT,
+                                MINIMUM_PORT + 1000);
+                    } catch (IOException e) {
+                        throw new RuntimeException(e);
+                    }
+                },
+                IceAdapter.getExecutor());
 
-        Executor.executeDelayed(5000, () -> {
-            if (!gatheringFuture.isDone()) {
-                gatheringFuture.cancel(true);
-            }
-        });
+        CompletableFuture.runAsync(
+                () -> {
+                    if (!gatheringFuture.isDone()) {
+                        gatheringFuture.cancel(true);
+                    }
+                },
+                CompletableFuture.delayedExecutor(5000, TimeUnit.MILLISECONDS, IceAdapter.getExecutor()));
 
         try {
             gatheringFuture.join();
         } catch (CompletionException e) {
             // Completed exceptionally
-            log.error(getLogPrefix() + "Error while creating stream component/gathering candidates", e);
-            new Thread(this::onConnectionLost).start();
+            log.error("{} Error while creating stream component/gathering candidates", getLogPrefix(), e);
+            CompletableFuture.runAsync(this::onConnectionLost, IceAdapter.getExecutor());
             return;
         } catch (CancellationException e) {
             // was cancelled due to timeout
-            log.error(getLogPrefix() + "Gathering candidates timed out", e);
-            new Thread(this::onConnectionLost).start();
+            log.error("{} Gathering candidates timed out", getLogPrefix(), e);
+            CompletableFuture.runAsync(this::onConnectionLost, IceAdapter.getExecutor());
             return;
         }
 
-        int previousConnectivityAttempts = getConnectivityAttempsInThePast(FORCE_SRFLX_RELAY_INTERVAL);
+        long previousConnectivityAttempts = getConnectivityAttempsInThePast(FORCE_SRFLX_RELAY_INTERVAL);
         CandidatesMessage localCandidatesMessage = CandidateUtil.packCandidates(
                 IceAdapter.getId(),
                 peer.getRemoteId(),
@@ -177,7 +194,8 @@ a, new LongTermCredential(iceServer.getTurnUsername(), iceServer.getTurnCredenti
                 previousConnectivityAttempts < FORCE_RELAY_COUNT && ALLOW_REFLEXIVE,
                 ALLOW_RELAY);
         log.debug(
-                getLogPrefix() + "Sending own candidates to {}, offered candidates: {}",
+                "{} Sending own candidates to {}, offered candidates: {}",
+                getLogPrefix(),
                 peer.getRemoteId(),
                 localCandidatesMessage.candidates().stream()
                         .map(it -> it.type().toString() + "(" + it.protocol() + ")")
@@ -188,18 +206,21 @@ a, new LongTermCredential(iceServer.getTurnUsername(), iceServer.getTurnCredenti
         // Make sure to abort the connection process and reinitiate when we haven't received an answer to our offer in 6
         // seconds, candidate packet was probably lost
         final int currentAwaitingCandidatesEventId = awaitingCandidatesEventId.incrementAndGet();
-        Executor.executeDelayed(6000, () -> {
-            if (peer.isClosing()) {
-                log.warn(
-                        getLogPrefix() + "Peer {} not connected anymore, aborting reinitiation of ICE",
-                        peer.getRemoteId());
-                return;
-            }
-            if (iceState == AWAITING_CANDIDATES
-                    && currentAwaitingCandidatesEventId == awaitingCandidatesEventId.get()) {
-                onConnectionLost();
-            }
-        });
+        CompletableFuture.runAsync(
+                () -> {
+                    if (peer.isClosing()) {
+                        log.warn(
+                                "{} Peer {} not connected anymore, aborting reinitiation of ICE",
+                                getLogPrefix(),
+                                peer.getRemoteId());
+                        return;
+                    }
+                    if (iceState == AWAITING_CANDIDATES
+                            && currentAwaitingCandidatesEventId == awaitingCandidatesEventId.get()) {
+                        onConnectionLost();
+                    }
+                },
+                CompletableFuture.delayedExecutor(6000, TimeUnit.MILLISECONDS, IceAdapter.getExecutor()));
     }
 
     private List<IceServer> getViableIceServers() {
@@ -240,54 +261,59 @@ private List<IceServer> getViableIceServers() {
      * Starts harvesting local candidates if in answer mode, then initiates the actual ICE process
      * @param remoteCandidatesMessage
      */
-    public synchronized void onIceMessageReceived(CandidatesMessage remoteCandidatesMessage) {
-        if (peer.isClosing()) {
-            log.warn(getLogPrefix() + "Peer not connected anymore, discarding ice message");
-            return;
-        }
-
-        // Start ICE async as it's blocking and this is the RPC thread
-        new Thread(() -> {
-                    log.debug(
-                            getLogPrefix() + "Got IceMsg for peer, offered candidates: {}",
-                            remoteCandidatesMessage.candidates().stream()
-                                    .map(it -> it.type().toString() + "(" + it.protocol() + ")")
-                                    .collect(Collectors.joining(", ")));
-
-                    if (peer.isLocalOffer()) {
-                        if (iceState != AWAITING_CANDIDATES) {
-                            log.warn(
-                                    getLogPrefix() + "Received candidates unexpectedly, current state: {}",
-                                    iceState.getMessage());
-                            return;
-                        }
+    public void onIceMessageReceived(CandidatesMessage remoteCandidatesMessage) {
+        LockUtil.executeWithLock(lockMessageReceived, () -> {
+            if (peer.isClosing()) {
+                log.warn("{} Peer not connected anymore, discarding ice message", getLogPrefix());
+                return;
+            }
 
-                    } else {
-                        // Check if we are already processing an ICE offer and if so stop it
-                        if (iceState != NEW && iceState != DISCONNECTED) {
-                            log.info(getLogPrefix() + "Received new candidates/offer, stopping...");
-                            onConnectionLost();
+            // Start ICE async as it's blocking and this is the RPC thread
+            CompletableFuture.runAsync(
+                    () -> {
+                        log.debug(
+                                "{} Got IceMsg for peer, offered candidates: {}",
+                                getLogPrefix(),
+                                remoteCandidatesMessage.candidates().stream()
+                                        .map(it -> it.type().toString() + "(" + it.protocol() + ")")
+                                        .collect(Collectors.joining(", ")));
+
+                        if (peer.isLocalOffer()) {
+                            if (iceState != AWAITING_CANDIDATES) {
+                                log.warn(
+                                        "{} Received candidates unexpectedly, current state: {}",
+                                        getLogPrefix(),
+                                        iceState.getMessage());
+                                return;
+                            }
+
+                        } else {
+                            // Check if we are already processing an ICE offer and if so stop it
+                            if (iceState != NEW && iceState != DISCONNECTED) {
+                                log.info("{} Received new candidates/offer, stopping...", getLogPrefix());
+                                onConnectionLost();
+                            }
+
+                            // Answer mode, initialize agent and gather candidates
+                            initiateIce();
                         }
 
-                        // Answer mode, initialize agent and gather candidates
-                        initiateIce();
-                    }
-
-                    setState(CHECKING);
-
-                    int previousConnectivityAttempts = getConnectivityAttempsInThePast(FORCE_SRFLX_RELAY_INTERVAL);
-                    CandidateUtil.unpackCandidates(
-                            remoteCandidatesMessage,
-                            agent,
-                            component,
-                            mediaStream,
-                            previousConnectivityAttempts < FORCE_SRFLX_COUNT && ALLOW_HOST,
-                            previousConnectivityAttempts < FORCE_RELAY_COUNT && ALLOW_REFLEXIVE,
-                            ALLOW_RELAY);
-
-                    startIce();
-                })
-                .start();
+                        setState(CHECKING);
+
+                        long previousConnectivityAttempts = getConnectivityAttempsInThePast(FORCE_SRFLX_RELAY_INTERVAL);
+                        CandidateUtil.unpackCandidates(
+                                remoteCandidatesMessage,
+                                agent,
+                                component,
+                                mediaStream,
+                                previousConnectivityAttempts < FORCE_SRFLX_COUNT && ALLOW_HOST,
+                                previousConnectivityAttempts < FORCE_RELAY_COUNT && ALLOW_REFLEXIVE,
+                                ALLOW_RELAY);
+
+                        startIce();
+                    },
+                    IceAdapter.getExecutor());
+        });
     }
 
     /**
@@ -296,17 +322,21 @@ public synchronized void onIceMessageReceived(CandidatesMessage remoteCandidates
     private void startIce() {
         connectivityAttemptTimes.add(0, System.currentTimeMillis());
 
-        log.debug(getLogPrefix() + "Starting ICE for peer {}", peer.getRemoteId());
+        log.debug("{} Starting ICE for peer {}", getLogPrefix(), peer.getRemoteId());
         agent.startConnectivityEstablishment();
 
         // Wait for termination/completion of the agent
         long iceStartTime = System.currentTimeMillis();
-        while (agent.getState()
-                != IceProcessingState.COMPLETED) { // TODO include more?, maybe stop on COMPLETED, is that to early?
+        while (!Thread.currentThread().isInterrupted()
+                && agent.getState()
+                        != IceProcessingState
+                                .COMPLETED) { // TODO include more?, maybe stop on COMPLETED, is that to early?
             try {
                 Thread.sleep(20);
             } catch (InterruptedException e) {
-                log.error(getLogPrefix() + "Interrupted while waiting for ICE", e);
+                log.error("{} Interrupted while waiting for ICE", getLogPrefix(), e);
+                onConnectionLost();
+                return;
             }
 
             if (agent.getState() == IceProcessingState.FAILED) { // TODO null pointer due to no agent?
@@ -315,15 +345,17 @@ private void startIce() {
             }
 
             if (System.currentTimeMillis() - iceStartTime > 15_000) {
-                log.error(getLogPrefix() + "ABORTING ICE DUE TO TIMEOUT");
+                log.error("{} ABORTING ICE DUE TO TIMEOUT", getLogPrefix());
                 onConnectionLost();
                 return;
             }
         }
 
-        log.debug(getLogPrefix() + "ICE terminated, connected, selected candidate pair: "
-                + component.getSelectedPair().getLocalCandidate().getType().toString() + " <-> "
-                + component.getSelectedPair().getRemoteCandidate().getType().toString());
+        log.debug(
+                "{} ICE terminated, connected, selected candidate pair: {} <-> {}",
+                getLogPrefix(),
+                component.getSelectedPair().getLocalCandidate().getType().toString(),
+                component.getSelectedPair().getRemoteCandidate().getType().toString());
 
         // We are connected
         connected = true;
@@ -339,8 +371,7 @@ private void startIce() {
             connectivityChecker.start();
         }
 
-        listenerThread = new Thread(this::listener);
-        listenerThread.start();
+        listenerThread = Thread.startVirtualThread(this::listener);
     }
 
     /**
@@ -348,72 +379,70 @@ private void startIce() {
      * Will close agent, stop listener and connectivity checker thread and change state to disconnected
      * Will then reinitiate ICE
      */
-    public synchronized void onConnectionLost() {
-        if (iceState == DISCONNECTED) {
-            log.warn(getLogPrefix() + "Lost connection, albeit already in ice state disconnected");
-            return; // TODO: will this kill the life cycle?
-        }
-
-        IceState previousState = getIceState();
+    public void onConnectionLost() {
+        LockUtil.executeWithLock(lockLostConnection, () -> {
+            if (iceState == DISCONNECTED) {
+                log.warn("{} Lost connection, albeit already in ice state disconnected", getLogPrefix());
+                return; // TODO: will this kill the life cycle?
+            }
 
-        if (listenerThread != null) {
-            listenerThread.interrupt();
-            listenerThread = null;
-        }
+            IceState previousState = getIceState();
 
-        if (turnRefreshModule != null) {
-            turnRefreshModule.close();
-            turnRefreshModule = null;
-        }
+            if (listenerThread != null) {
+                listenerThread.interrupt();
+                listenerThread = null;
+            }
 
-        connectivityChecker.stop();
+            if (turnRefreshModule != null) {
+                turnRefreshModule.close();
+                turnRefreshModule = null;
+            }
 
-        if (connected) {
-            connected = false;
-            log.warn(getLogPrefix() + "ICE connection has been lost for peer");
-            RPCService.onConnected(IceAdapter.getId(), peer.getRemoteId(), false);
-        }
+            connectivityChecker.stop();
 
-        setState(DISCONNECTED);
+            if (connected) {
+                connected = false;
+                log.warn("{} ICE connection has been lost for peer", getLogPrefix());
+                RPCService.onConnected(IceAdapter.getId(), peer.getRemoteId(), false);
+            }
 
-        if (agent != null) {
-            agent.free();
-            agent = null;
-            mediaStream = null;
-            component = null;
-        }
+            setState(DISCONNECTED);
 
-        debug().peerStateChanged(this.peer);
+            if (agent != null) {
+                agent.free();
+                agent = null;
+                mediaStream = null;
+                component = null;
+            }
 
-        if (peer.isClosing()) {
-            log.warn(getLogPrefix() + "Peer not connected anymore, aborting onConnectionLost of ICE");
-            return;
-        }
+            debug().peerStateChanged(this.peer);
 
-        if (peer.getGameSession().isGameEnded()) {
-            log.warn(getLogPrefix() + "GAME ENDED, ABORTING onConnectionLost of ICE for peer ");
-            return;
-        }
+            if (peer.isClosing()) {
+                log.warn("{} Peer not connected anymore, aborting onConnectionLost of ICE", getLogPrefix());
+                return;
+            }
 
-        if (previousState == CONNECTED) {
-            TrayIcon.showMessage("Reconnecting to " + this.peer.getRemoteLogin() + " (connection lost)");
-        }
+            if (peer.getGameSession().isGameEnded()) {
+                log.warn("{} GAME ENDED, ABORTING onConnectionLost of ICE for peer ", getLogPrefix());
+                return;
+            }
 
-        if (previousState == CONNECTED && peer.isLocalOffer()) {
-            // We were connected before, retry immediately
-            Executor.executeDelayed(0, this::reinitIce);
-        } else if (peer.isLocalOffer()) {
-            // Last ice attempt didn't succeed, so wait a bit
-            Executor.executeDelayed(5000, this::reinitIce);
-        }
-    }
+            if (previousState == CONNECTED) {
+                TrayIcon.showMessage("Reconnecting to %s (connection lost)".formatted(this.peer.getRemoteLogin()));
+            }
 
-    private synchronized void reinitIce() {
-        if (peer.isClosing()) {
-            log.warn(getLogPrefix() + "Peer not connected anymore, aborting reinitiation of ICE");
-            return;
-        }
-        initiateIce();
+            if (previousState == CONNECTED && peer.isLocalOffer()) {
+                // We were connected before, retry immediately
+                CompletableFuture.runAsync(
+                        this::initiateIce,
+                        CompletableFuture.delayedExecutor(0, TimeUnit.MILLISECONDS, IceAdapter.getExecutor()));
+            } else if (peer.isLocalOffer()) {
+                // Last ice attempt didn't succeed, so wait a bit
+                CompletableFuture.runAsync(
+                        this::initiateIce,
+                        CompletableFuture.delayedExecutor(5000, TimeUnit.MILLISECONDS, IceAdapter.getExecutor()));
+            }
+        });
     }
 
     /**
@@ -455,7 +484,7 @@ void sendViaIce(byte[] data, int offset, int length) {
                                         .getTransportAddress()
                                         .getPort()));
             } catch (IOException e) {
-                log.warn(getLogPrefix() + "Failed to send data via ICE", e);
+                log.warn("{} Failed to send data via ICE", getLogPrefix(), e);
                 onConnectionLost();
             } catch (NullPointerException e) {
                 log.error("Component is null", e);
@@ -467,12 +496,14 @@ void sendViaIce(byte[] data, int offset, int length) {
      * Listens for data incoming via ice socket
      */
     public void listener() {
-        log.debug(getLogPrefix() + "Now forwarding data from ICE to FA for peer");
+        log.debug("{} Now forwarding data from ICE to FA for peer", getLogPrefix());
         Component localComponent = component;
 
         byte[] data = new byte
                 [65536]; // 64KiB = UDP MTU, in practice due to ethernet frames being <= 1500 B, this is often not used
-        while (IceAdapter.isRunning() && IceAdapter.getGameSession() == peer.getGameSession()) {
+        while (!Thread.currentThread().isInterrupted()
+                && IceAdapter.isRunning()
+                && IceAdapter.getGameSession() == peer.getGameSession()) {
             try {
                 DatagramPacket packet = new DatagramPacket(data, data.length);
                 localComponent
@@ -497,13 +528,14 @@ public void listener() {
                     }
                 } else {
                     log.warn(
-                            getLogPrefix() + "Received invalid packet, first byte: 0x{}, length: {}",
+                            "{} Received invalid packet, first byte: 0x{}, length: {}",
+                            getLogPrefix(),
                             data[0],
                             packet.getLength());
                 }
 
             } catch (IOException e) { // TODO: nullpointer from localComponent.xxxx????
-                log.warn(getLogPrefix() + "Error while reading from ICE adapter", e);
+                log.warn("{} Error while reading from ICE adapter", getLogPrefix(), e);
                 if (component == localComponent) {
                     onConnectionLost();
                 }
@@ -511,10 +543,14 @@ public void listener() {
             }
         }
 
-        log.debug(getLogPrefix() + "No longer listening for messages from ICE");
+        log.debug("{} No longer listening for messages from ICE", getLogPrefix());
     }
 
     void close() {
+        if (listenerThread != null) {
+            listenerThread.interrupt();
+            listenerThread = null;
+        }
         if (turnRefreshModule != null) {
             turnRefreshModule.close();
         }
@@ -524,15 +560,15 @@ void close() {
         connectivityChecker.stop();
     }
 
-    public int getConnectivityAttempsInThePast(final long millis) {
+    public long getConnectivityAttempsInThePast(final long millis) {
         // copy list to avoid concurrency issues
-        return (int) new ArrayList<Long>(connectivityAttemptTimes)
+        return new ArrayList<>(connectivityAttemptTimes)
                 .stream()
                         .filter(time -> time > (System.currentTimeMillis() - millis))
                         .count();
     }
 
     public String getLogPrefix() {
-        return "ICE %s: ".formatted(peer.getPeerIdentifier());
+        return "ICE %s:".formatted(peer.getPeerIdentifier());
     }
 }
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/ice/PeerTurnRefreshModule.java b/ice-adapter/src/main/java/com/faforever/iceadapter/ice/PeerTurnRefreshModule.java
index 0e8c1b32..44da49cb 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/ice/PeerTurnRefreshModule.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/ice/PeerTurnRefreshModule.java
@@ -1,9 +1,5 @@
 package com.faforever.iceadapter.ice;
 
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.time.Duration;
 import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
 import org.ice4j.ice.RelayedCandidate;
@@ -13,6 +9,11 @@
 import org.ice4j.message.Request;
 import org.ice4j.stack.TransactionID;
 
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.time.Duration;
+
 /**
  * Sends continuous refresh requests to the turn server
  */
@@ -58,15 +59,14 @@ public PeerTurnRefreshModule(PeerIceModule ice, RelayedCandidate candidate) {
         }
 
         if (harvest != null) {
-            refreshThread = new Thread(this::refreshThread);
-            refreshThread.start();
+            refreshThread = Thread.startVirtualThread(this::refreshThread);
 
             log.info("Started turn refresh module for peer {}", ice.getPeer().getRemoteLogin());
         }
     }
 
     private void refreshThread() {
-        while (running) {
+        while (!Thread.currentThread().isInterrupted() && running) {
 
             Request refreshRequest = MessageFactory.createRefreshRequest(
                     600); // Maximum lifetime of turn is 600 seconds (10 minutes), server may limit this even further
@@ -77,19 +77,23 @@ private void refreshThread() {
 
                 log.info("Sent turn refresh request.");
             } catch (IllegalAccessException | InvocationTargetException e) {
-                log.error("Could not send turn refresh request!.", e);
+                log.error("Could not send turn refresh request!", e);
             }
 
             try {
                 Thread.sleep(REFRESH_INTERVAL);
             } catch (InterruptedException e) {
                 log.warn("Sleeping refreshThread was interrupted");
+                return;
             }
         }
     }
 
     public void close() {
         running = false;
-        refreshThread.interrupt();
+        if (refreshThread != null) {
+            refreshThread.interrupt();
+            refreshThread = null;
+        }
     }
 }
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/rpc/RPCHandler.java b/ice-adapter/src/main/java/com/faforever/iceadapter/rpc/RPCHandler.java
index a712c984..591d9d32 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/rpc/RPCHandler.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/rpc/RPCHandler.java
@@ -9,11 +9,6 @@
 import com.faforever.iceadapter.ice.Peer;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
 import lombok.RequiredArgsConstructor;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
@@ -23,6 +18,14 @@
 import org.ice4j.ice.CandidateType;
 import org.ice4j.ice.Component;
 
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
 /**
  * Handles calls from JsonRPC (the client)
  */
@@ -31,6 +34,7 @@
 public class RPCHandler {
 
     private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
+    private final Lock lockStatus = new ReentrantLock();
     private final int rpcPort;
 
     public void hostGame(String mapName) {
@@ -99,7 +103,8 @@ public String status() {
         List<IceStatus.IceRelay> relays = new ArrayList<>();
         GameSession gameSession = IceAdapter.getGameSession();
         if (gameSession != null) {
-            synchronized (gameSession.getPeers()) {
+            lockStatus.lock();
+            try {
                 gameSession.getPeers().values().stream()
                         .map(peer -> {
                             IceStatus.IceRelay.IceRelayICEState iceRelayICEState =
@@ -142,6 +147,8 @@ public String status() {
                                     iceRelayICEState);
                         })
                         .forEach(relays::add);
+            } finally {
+                lockStatus.unlock();
             }
         }
 
@@ -163,6 +170,6 @@ public String status() {
 
     public void quit() {
         log.warn("Close requested, stopping...");
-        IceAdapter.close();
+        IceAdapter.close(0);
     }
 }
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/rpc/RPCService.java b/ice-adapter/src/main/java/com/faforever/iceadapter/rpc/RPCService.java
index c9200e9f..3c38f262 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/rpc/RPCService.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/rpc/RPCService.java
@@ -1,7 +1,5 @@
 package com.faforever.iceadapter.rpc;
 
-import static com.faforever.iceadapter.debug.Debug.debug;
-
 import com.faforever.iceadapter.IceAdapter;
 import com.faforever.iceadapter.debug.Debug;
 import com.faforever.iceadapter.debug.InfoWindow;
@@ -13,10 +11,13 @@
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import com.nbarraille.jjsonrpc.JJsonPeer;
 import com.nbarraille.jjsonrpc.TcpServer;
+import lombok.extern.slf4j.Slf4j;
+
 import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
-import lombok.extern.slf4j.Slf4j;
+
+import static com.faforever.iceadapter.debug.Debug.debug;
 
 /**
  * Handles communication between client and adapter, opens a server for the client to connect to
@@ -52,7 +53,7 @@ public static void init(int port) {
                     log.info(
                             "Lost connection to first RPC Peer. GameState: {}, Stopping adapter...",
                             gameState.getName());
-                    IceAdapter.close();
+                    IceAdapter.close(0);
                 }
             });
         });
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/util/Executor.java b/ice-adapter/src/main/java/com/faforever/iceadapter/util/Executor.java
deleted file mode 100644
index f814d2de..00000000
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/util/Executor.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.faforever.iceadapter.util;
-
-public class Executor {
-
-    /**
-     * Creates a that sleeps for a give time and then calls the provided runnable
-     * @param timeMs The time to wait in ms
-     * @param runnable The runnable
-     */
-    public static void executeDelayed(int timeMs, Runnable runnable) {
-        new Thread(() -> {
-                    try {
-                        Thread.sleep(timeMs);
-                    } catch (InterruptedException e) {
-                    }
-                    runnable.run();
-                })
-                .start();
-    }
-}
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/util/ExecutorHolder.java b/ice-adapter/src/main/java/com/faforever/iceadapter/util/ExecutorHolder.java
new file mode 100644
index 00000000..61553a26
--- /dev/null
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/util/ExecutorHolder.java
@@ -0,0 +1,13 @@
+package com.faforever.iceadapter.util;
+
+import lombok.experimental.UtilityClass;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+@UtilityClass
+public class ExecutorHolder {
+    public ExecutorService getExecutor() {
+        return Executors.newVirtualThreadPerTaskExecutor();
+    }
+}
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/util/LockUtil.java b/ice-adapter/src/main/java/com/faforever/iceadapter/util/LockUtil.java
new file mode 100644
index 00000000..6161b9aa
--- /dev/null
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/util/LockUtil.java
@@ -0,0 +1,17 @@
+package com.faforever.iceadapter.util;
+
+import lombok.experimental.UtilityClass;
+
+import java.util.concurrent.locks.Lock;
+
+@UtilityClass
+public class LockUtil {
+    public void executeWithLock(Lock lock, Runnable task) {
+        lock.lock();
+        try {
+            task.run();
+        } finally {
+            lock.unlock();
+        }
+    }
+}
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/util/NetworkToolbox.java b/ice-adapter/src/main/java/com/faforever/iceadapter/util/NetworkToolbox.java
index 5f739bdb..0bdb7a6c 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/util/NetworkToolbox.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/util/NetworkToolbox.java
@@ -1,11 +1,13 @@
 package com.faforever.iceadapter.util;
 
+import com.faforever.iceadapter.IceAdapter;
+import lombok.extern.slf4j.Slf4j;
+
 import java.io.IOException;
 import java.net.DatagramSocket;
 import java.net.ServerSocket;
 import java.net.SocketException;
 import java.util.Random;
-import lombok.extern.slf4j.Slf4j;
 
 @Slf4j
 public class NetworkToolbox {
@@ -32,7 +34,7 @@ public static int findFreeTCPPort(int min, int max) {
         }
 
         log.error("Could not find a free tcp port");
-        System.exit(-1);
+        IceAdapter.close(-1);
         return -1;
     }
 
@@ -56,7 +58,7 @@ public static int findFreeUDPPort(int min, int max) {
         }
 
         log.error("Could not find a free tcp port");
-        System.exit(-1);
+        IceAdapter.close(-1);
         return -1;
     }
 }
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/util/PingWrapper.java b/ice-adapter/src/main/java/com/faforever/iceadapter/util/PingWrapper.java
index 0e040133..4ff5a93e 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/util/PingWrapper.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/util/PingWrapper.java
@@ -1,13 +1,15 @@
 package com.faforever.iceadapter.util;
 
+import com.faforever.iceadapter.IceAdapter;
 import com.google.common.io.CharStreams;
+import lombok.extern.slf4j.Slf4j;
+
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionException;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import lombok.extern.slf4j.Slf4j;
 
 /*
  * A wrapper around calling the system `ping` executable to query the latency of a host.
@@ -34,27 +36,29 @@ public static CompletableFuture<Double> getLatency(String address, Integer count
                 output_pattern = GNU_OUTPUT_PATTERN;
             }
 
-            return CompletableFuture.supplyAsync(() -> {
-                try {
-                    process.waitFor();
-                    InputStreamReader reader = new InputStreamReader(process.getInputStream());
-                    String output = CharStreams.toString(reader);
-                    reader.close();
+            return CompletableFuture.supplyAsync(
+                    () -> {
+                        try {
+                            process.waitFor();
+                            InputStreamReader reader = new InputStreamReader(process.getInputStream());
+                            String output = CharStreams.toString(reader);
+                            reader.close();
 
-                    Matcher m = output_pattern.matcher(output);
+                            Matcher m = output_pattern.matcher(output);
 
-                    if (m.find()) {
-                        double result = Double.parseDouble(m.group(1));
-                        log.debug("Pinged {} with an RTT of {}", address, result);
-                        return result;
-                    } else {
-                        log.warn("Failed to ping {}", address);
-                        throw new RuntimeException("Failed to contact the host");
-                    }
-                } catch (InterruptedException | IOException | RuntimeException e) {
-                    throw new CompletionException(e);
-                }
-            });
+                            if (m.find()) {
+                                double result = Double.parseDouble(m.group(1));
+                                log.debug("Pinged {} with an RTT of {}", address, result);
+                                return result;
+                            } else {
+                                log.warn("Failed to ping {}", address);
+                                throw new RuntimeException("Failed to contact the host");
+                            }
+                        } catch (InterruptedException | IOException | RuntimeException e) {
+                            throw new CompletionException(e);
+                        }
+                    },
+                    IceAdapter.getExecutor());
         } catch (IOException e) {
             CompletableFuture<Double> future = new CompletableFuture<>();
             future.completeExceptionally(e);
diff --git a/ice-adapter/src/main/java/com/faforever/iceadapter/util/TrayIcon.java b/ice-adapter/src/main/java/com/faforever/iceadapter/util/TrayIcon.java
index a95a744a..7939891c 100644
--- a/ice-adapter/src/main/java/com/faforever/iceadapter/util/TrayIcon.java
+++ b/ice-adapter/src/main/java/com/faforever/iceadapter/util/TrayIcon.java
@@ -1,16 +1,19 @@
 package com.faforever.iceadapter.util;
 
+import com.faforever.iceadapter.IceAdapter;
 import com.faforever.iceadapter.debug.Debug;
 import com.faforever.iceadapter.debug.DebugWindow;
 import com.faforever.iceadapter.debug.InfoWindow;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.imageio.ImageIO;
+import javax.swing.*;
 import java.awt.*;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
 import java.io.IOException;
 import java.net.URL;
-import javax.imageio.ImageIO;
-import javax.swing.*;
-import lombok.extern.slf4j.Slf4j;
+import java.util.concurrent.CompletableFuture;
 
 @Slf4j
 public class TrayIcon {
@@ -45,7 +48,8 @@ public void mouseClicked(MouseEvent mouseEvent) {}
 
             @Override
             public void mousePressed(MouseEvent mouseEvent) {
-                new Thread(() -> {
+                CompletableFuture.runAsync(
+                        () -> {
                             if (InfoWindow.INSTANCE == null) {
                                 log.info("Launching ICE adapter debug window");
                                 Debug.ENABLE_INFO_WINDOW = true;
@@ -53,8 +57,8 @@ public void mousePressed(MouseEvent mouseEvent) {
                             } else {
                                 InfoWindow.INSTANCE.show();
                             }
-                        })
-                        .start();
+                        },
+                        IceAdapter.getExecutor());
             }
 
             @Override
diff --git a/server/build.gradle b/server/build.gradle
index 7ad82821..3d46c03c 100644
--- a/server/build.gradle
+++ b/server/build.gradle
@@ -5,7 +5,7 @@ apply plugin: 'com.github.johnrengelman.shadow'
 group 'com.faforever'
 version '1.0-SNAPSHOT'
 
-sourceCompatibility = JavaVersion.VERSION_17
+sourceCompatibility = JavaVersion.VERSION_21
 
 repositories {
     mavenCentral()
diff --git a/shared/build.gradle b/shared/build.gradle
index d8cf6312..95aa06ba 100644
--- a/shared/build.gradle
+++ b/shared/build.gradle
@@ -4,7 +4,7 @@ apply plugin: 'java'
 group 'com.faforever'
 version '1.0-SNAPSHOT'
 
-sourceCompatibility = JavaVersion.VERSION_17
+sourceCompatibility = JavaVersion.VERSION_21
 
 dependencies{
     annotationProcessor("org.projectlombok:lombok:$lombokVersion")
