diff --git a/tests/tcg/multiarch/Makefile.target b/tests/tcg/multiarch/Makefile.target
index 5e3391ec9d2..0a9cd037094 100644
--- a/tests/tcg/multiarch/Makefile.target
+++ b/tests/tcg/multiarch/Makefile.target
@@ -42,6 +42,9 @@ munmap-pthread: LDFLAGS+=-pthread
 vma-pthread: CFLAGS+=-pthread
 vma-pthread: LDFLAGS+=-pthread
 
+thread-breakpoint-stress: CFLAGS+=-pthread
+thread-breakpoint-stress: LDFLAGS+=-pthread
+
 # The vma-pthread seems very sensitive on gitlab and we currently
 # don't know if its exposing a real bug or the test is flaky.
 ifneq ($(GITLAB_CI),)
@@ -127,6 +130,13 @@ run-gdbstub-follow-fork-mode-parent: follow-fork-mode
 		--bin $< --test $(MULTIARCH_SRC)/gdbstub/follow-fork-mode-parent.py, \
 	following parents on fork)
 
+run-gdbstub-thread-breakpoint-stress: thread-breakpoint-stress
+	$(call run-test, $@, $(GDB_SCRIPT) \
+		--gdb $(GDB) \
+		--qemu $(QEMU) --qargs "$(QEMU_OPTS)" \
+		--bin $< --test $(MULTIARCH_SRC)/gdbstub/test-thread-breakpoint-stress.py, \
+	hitting many breakpoints on different threads)
+
 else
 run-gdbstub-%:
 	$(call skip-test, "gdbstub test $*", "need working gdb with $(patsubst -%,,$(TARGET_NAME)) support")
@@ -136,7 +146,8 @@ EXTRA_RUNS += run-gdbstub-sha1 run-gdbstub-qxfer-auxv-read \
 	      run-gdbstub-registers run-gdbstub-prot-none \
 	      run-gdbstub-catch-syscalls run-gdbstub-follow-fork-mode-child \
 	      run-gdbstub-follow-fork-mode-parent \
-	      run-gdbstub-qxfer-siginfo-read
+	      run-gdbstub-qxfer-siginfo-read \
+	      run-gdbstub-thread-breakpoint-stress
 
 # ARM Compatible Semi Hosting Tests
 #
diff --git a/tests/tcg/multiarch/gdbstub/test-thread-breakpoint-stress.py b/tests/tcg/multiarch/gdbstub/test-thread-breakpoint-stress.py
new file mode 100644
index 00000000000..489d238f02d
--- /dev/null
+++ b/tests/tcg/multiarch/gdbstub/test-thread-breakpoint-stress.py
@@ -0,0 +1,28 @@
+"""Test multiple threads hitting breakpoints.
+
+SPDX-License-Identifier: GPL-2.0-or-later
+"""
+from test_gdbstub import main, report
+
+
+N_BREAK_THREADS = 2
+N_BREAKS = 100
+
+
+def run_test():
+    """Run through the tests one by one"""
+    if gdb.selected_inferior().architecture().name() == "MicroBlaze":
+        print("SKIP: Atomics are broken on MicroBlaze")
+        exit(0)
+    gdb.execute("break break_here")
+    gdb.execute("continue")
+    for _ in range(N_BREAK_THREADS * N_BREAKS):
+        counter1 = int(gdb.parse_and_eval("s->counter"))
+        counter2 = int(gdb.parse_and_eval("s->counter"))
+        report(counter1 == counter2, "{} == {}".format(counter1, counter2))
+        gdb.execute("continue")
+    exitcode = int(gdb.parse_and_eval("$_exitcode"))
+    report(exitcode == 0, "{} == 0".format(exitcode))
+
+
+main(run_test)
diff --git a/tests/tcg/multiarch/thread-breakpoint-stress.c b/tests/tcg/multiarch/thread-breakpoint-stress.c
new file mode 100644
index 00000000000..1feed8577aa
--- /dev/null
+++ b/tests/tcg/multiarch/thread-breakpoint-stress.c
@@ -0,0 +1,92 @@
+/*
+ * Test multiple threads hitting breakpoints.
+ *
+ * The main thread performs a lengthy syscall. The test verifies that this
+ * does not interfere with the ability to stop threads.
+ *
+ * The counter thread constantly increments a value by 1. The test verifies
+ * that it is stopped when another thread hits a breakpoint.
+ *
+ * The break threads constantly and simultaneously hit the same breakpoint.
+ * The test verifies that GDB and gdbstub do not lose any hits and do not
+ * deadlock.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+#include <assert.h>
+#include <pthread.h>
+#include <stdlib.h>
+
+struct state {
+    int counter;
+    int done;
+    pthread_barrier_t barrier;
+    int break_counter;
+};
+
+static void *counter_loop(void *arg)
+{
+    struct state *s = arg;
+
+    while (!__atomic_load_n(&s->done, __ATOMIC_SEQ_CST)) {
+        __atomic_add_fetch(&s->counter, 1, __ATOMIC_SEQ_CST);
+    }
+
+    return NULL;
+}
+
+#define N_BREAK_THREADS 2
+#define N_BREAKS 100
+
+/* Non-static to avoid inlining. */
+void break_here(struct state *s)
+{
+    __atomic_add_fetch(&s->break_counter, 1, __ATOMIC_SEQ_CST);
+}
+
+static void *break_loop(void *arg)
+{
+    struct state *s = arg;
+    int i;
+
+    pthread_barrier_wait(&s->barrier);
+    for (i = 0; i < N_BREAKS; i++) {
+        break_here(s);
+    }
+
+    return NULL;
+}
+
+int main(void)
+{
+    pthread_t break_threads[N_BREAK_THREADS], counter_thread;
+    struct state s = {};
+    int i, ret;
+
+#ifdef __MICROBLAZE__
+    /*
+     * Microblaze has broken atomics.
+     * See https://github.com/Xilinx/meta-xilinx/blob/xlnx-rel-v2024.1/meta-microblaze/recipes-devtools/gcc/gcc-12/0009-Patch-microblaze-Fix-atomic-boolean-return-value.patch
+     */
+    return EXIT_SUCCESS;
+#endif
+
+    ret = pthread_barrier_init(&s.barrier, NULL, N_BREAK_THREADS);
+    assert(ret == 0);
+    ret = pthread_create(&counter_thread, NULL, counter_loop, &s);
+    assert(ret == 0);
+    for (i = 0; i < N_BREAK_THREADS; i++) {
+        ret = pthread_create(&break_threads[i], NULL, break_loop, &s);
+        assert(ret == 0);
+    }
+    for (i = 0; i < N_BREAK_THREADS; i++) {
+        ret = pthread_join(break_threads[i], NULL);
+        assert(ret == 0);
+    }
+    __atomic_store_n(&s.done, 1, __ATOMIC_SEQ_CST);
+    ret = pthread_join(counter_thread, NULL);
+    assert(ret == 0);
+    assert(s.break_counter == N_BREAK_THREADS * N_BREAKS);
+
+    return EXIT_SUCCESS;
+}
