diff a/.jcheck/conf b/.jcheck/conf
--- a/.jcheck/conf
+++ b/.jcheck/conf
@@ -1,36 +1,16 @@
 [general]
-project=jdk
+project=loom
+repository=loom
 jbs=JDK
 version=24
 
 [checks]
-error=author,committer,reviewers,merge,issues,executable,symlink,message,hg-tag,whitespace,problemlists
-warning=issuestitle,binary
-
-[repository]
-tags=(?:jdk-(?:[1-9]([0-9]*)(?:\.(?:0|[1-9][0-9]*)){0,4})(?:\+(?:(?:[0-9]+))|(?:-ga)))|(?:jdk[4-9](?:u\d{1,3})?-(?:(?:b\d{2,3})|(?:ga)))|(?:hs\d\d(?:\.\d{1,2})?-b\d\d)
-branches=.*
+error=author,committer,whitespace
 
 [census]
 version=0
 domain=openjdk.org
 
 [checks "whitespace"]
 files=.*\.cpp|.*\.hpp|.*\.c|.*\.h|.*\.java|.*\.cc|.*\.hh|.*\.m|.*\.mm|.*\.S|.*\.md|.*\.properties|.*\.gmk|.*\.m4|.*\.ac|Makefile
 ignore-tabs=.*\.gmk|Makefile
-
-[checks "merge"]
-message=Merge
-
-[checks "reviewers"]
-reviewers=1
-ignore=duke
-
-[checks "committer"]
-role=committer
-
-[checks "issues"]
-pattern=^([124-8][0-9]{6}): (\S.*)$
-
-[checks "problemlists"]
-dirs=test/jdk|test/langtools|test/lib-test|test/hotspot/jtreg|test/jaxp
diff a/src/hotspot/cpu/aarch64/c1_MacroAssembler_aarch64.cpp b/src/hotspot/cpu/aarch64/c1_MacroAssembler_aarch64.cpp
--- a/src/hotspot/cpu/aarch64/c1_MacroAssembler_aarch64.cpp
+++ b/src/hotspot/cpu/aarch64/c1_MacroAssembler_aarch64.cpp
@@ -117,12 +117,12 @@
     str(hdr, Address(disp_hdr, 0));
     // otherwise we don't care about the result and handle locking via runtime call
     cbnz(hdr, slow_case);
     // done
     bind(done);
+    inc_held_monitor_count();
   }
-  increment(Address(rthread, JavaThread::held_monitor_count_offset()));
   return null_check_offset;
 }
 
 
 void C1_MacroAssembler::unlock_object(Register hdr, Register obj, Register disp_hdr, Register temp, Label& slow_case) {
@@ -157,12 +157,12 @@
     } else {
       cmpxchgptr(disp_hdr, hdr, obj, rscratch2, done, &slow_case);
     }
     // done
     bind(done);
+    dec_held_monitor_count();
   }
-  decrement(Address(rthread, JavaThread::held_monitor_count_offset()));
 }
 
 
 // Defines obj, preserves var_size_in_bytes
 void C1_MacroAssembler::try_allocate(Register obj, Register var_size_in_bytes, int con_size_in_bytes, Register t1, Register t2, Label& slow_case) {
diff a/src/hotspot/cpu/aarch64/c1_Runtime1_aarch64.cpp b/src/hotspot/cpu/aarch64/c1_Runtime1_aarch64.cpp
--- a/src/hotspot/cpu/aarch64/c1_Runtime1_aarch64.cpp
+++ b/src/hotspot/cpu/aarch64/c1_Runtime1_aarch64.cpp
@@ -168,48 +168,69 @@
 
 class StubFrame: public StackObj {
  private:
   StubAssembler* _sasm;
   bool _return_state;
+  bool _use_pop_on_epilogue;
 
- public:
-  StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments, return_state_t return_state=requires_return);
-  void load_argument(int offset_in_words, Register reg);
+  StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments,
+            return_state_t return_state, bool use_pop_on_epilogue);
 
+ public:
+  StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments, bool use_pop_on_epilogue);
+  StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments, return_state_t return_state);
+  StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments);
   ~StubFrame();
+
+  void load_argument(int offset_in_words, Register reg);
 };;
 
 void StubAssembler::prologue(const char* name, bool must_gc_arguments) {
   set_info(name, must_gc_arguments);
   enter();
 }
 
-void StubAssembler::epilogue() {
-  leave();
+void StubAssembler::epilogue(bool use_pop) {
+  // Avoid using a leave instruction when this frame may
+  // have been frozen, since the current value of rfp
+  // restored from the stub would be invalid. We still
+  // must restore the rfp value saved on enter though.
+  if (use_pop) {
+    ldp(rfp, lr, Address(post(sp, 2 * wordSize)));
+  } else {
+    leave();
+  }
   ret(lr);
 }
 
 #define __ _sasm->
 
-StubFrame::StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments, return_state_t return_state) {
-  _sasm = sasm;
-  _return_state = return_state;
+StubFrame::StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments,
+                     return_state_t return_state, bool use_pop_on_epilogue)
+  : _sasm(sasm), _return_state(return_state), _use_pop_on_epilogue(use_pop_on_epilogue) {
   __ prologue(name, must_gc_arguments);
 }
 
+StubFrame::StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments,
+                     bool use_pop_on_epilogue) :
+  StubFrame(sasm, name, must_gc_arguments, requires_return, use_pop_on_epilogue) {}
+
+StubFrame::StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments,
+                     return_state_t return_state) :
+  StubFrame(sasm, name, must_gc_arguments, return_state, /*use_pop_on_epilogue*/false) {}
+
+StubFrame::StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments) :
+  StubFrame(sasm, name, must_gc_arguments, requires_return, /*use_pop_on_epilogue*/false) {}
+
 // load parameters that were stored with LIR_Assembler::store_parameter
 // Note: offsets for store_parameter and load_argument must match
 void StubFrame::load_argument(int offset_in_words, Register reg) {
   __ load_parameter(offset_in_words, reg);
 }
 
 StubFrame::~StubFrame() {
-  if (_return_state == requires_return) {
-    __ epilogue();
-  } else {
-    __ should_not_reach_here();
-  }
+  __ epilogue(_use_pop_on_epilogue);
 }
 
 #undef __
 
 
@@ -250,11 +271,11 @@
   int frame_size_in_slots = frame_size_in_bytes / sizeof(jint);
   OopMap* oop_map = new OopMap(frame_size_in_slots, 0);
 
   for (int i = 0; i < FrameMap::nof_cpu_regs; i++) {
     Register r = as_Register(i);
-    if (i <= 18 && i != rscratch1->encoding() && i != rscratch2->encoding()) {
+    if (r == rthread || (i <= 18 && i != rscratch1->encoding() && i != rscratch2->encoding())) {
       int sp_offset = cpu_reg_save_offsets[i];
       oop_map->set_callee_saved(VMRegImpl::stack2reg(sp_offset),
                                 r->as_VMReg());
     }
   }
@@ -335,10 +356,19 @@
     cpu_reg_save_offsets[i] = sp_offset;
     sp_offset += 2;   // SP offsets are in halfwords
   }
 }
 
+// return: offset in 64-bit words.
+uint Runtime1::runtime_blob_current_thread_offset(frame f) {
+  CodeBlob* cb = f.cb();
+  assert(cb == Runtime1::blob_for(C1StubId::monitorenter_id) ||
+         cb == Runtime1::blob_for(C1StubId::monitorenter_nofpu_id), "must be");
+  assert(cb != nullptr && cb->is_runtime_stub(), "invalid frame");
+  int offset = cpu_reg_save_offsets[rthread->encoding()];
+  return offset / 2;   // SP offsets are in halfwords
+}
 
 // target: the entry point of the method that creates and posts the exception oop
 // has_argument: true if the exception needs arguments (passed in rscratch1 and rscratch2)
 
 OopMapSet* Runtime1::generate_exception_throw(StubAssembler* sasm, address target, bool has_argument) {
@@ -860,11 +890,11 @@
     case C1StubId::monitorenter_nofpu_id:
       save_fpu_registers = false;
       // fall through
     case C1StubId::monitorenter_id:
       {
-        StubFrame f(sasm, "monitorenter", dont_gc_arguments);
+        StubFrame f(sasm, "monitorenter", dont_gc_arguments, /*use_pop_on_epilogue*/true);
         OopMap* map = save_live_registers(sasm, save_fpu_registers);
 
         // Called with store_parameter and not C abi
 
         f.load_argument(1, r0); // r0,: object
diff a/src/hotspot/cpu/aarch64/c2_MacroAssembler_aarch64.cpp b/src/hotspot/cpu/aarch64/c2_MacroAssembler_aarch64.cpp
--- a/src/hotspot/cpu/aarch64/c2_MacroAssembler_aarch64.cpp
+++ b/src/hotspot/cpu/aarch64/c2_MacroAssembler_aarch64.cpp
@@ -55,11 +55,11 @@
   Label cont;
   Label object_has_monitor;
   Label count, no_count;
 
   assert(LockingMode != LM_LIGHTWEIGHT, "lightweight locking should use fast_lock_lightweight");
-  assert_different_registers(oop, box, tmp, disp_hdr);
+  assert_different_registers(oop, box, tmp, disp_hdr, rscratch2);
 
   // Load markWord from object into displaced_header.
   ldr(disp_hdr, Address(oop, oopDesc::mark_offset_in_bytes()));
 
   if (DiagnoseSyncOnValueBasedClasses != 0) {
@@ -109,15 +109,17 @@
 
   // Handle existing monitor.
   bind(object_has_monitor);
 
   // The object's monitor m is unlocked iff m->owner == nullptr,
-  // otherwise m->owner may contain a thread or a stack address.
+  // otherwise m->owner may contain a thread id, a stack address for LM_LEGACY,
+  // or the ANONYMOUS_OWNER constant for LM_LIGHTWEIGHT.
   //
   // Try to CAS m->owner from null to current thread.
+  ldr(rscratch2, Address(rthread, JavaThread::lock_id_offset()));
   add(tmp, disp_hdr, (in_bytes(ObjectMonitor::owner_offset())-markWord::monitor_value));
-  cmpxchg(tmp, zr, rthread, Assembler::xword, /*acquire*/ true,
+  cmpxchg(tmp, zr, rscratch2, Assembler::xword, /*acquire*/ true,
           /*release*/ true, /*weak*/ false, tmp3Reg); // Sets flags for result
 
   // Store a non-null value into the box to avoid looking like a re-entrant
   // lock. The fast-path monitor unlock code checks for
   // markWord::monitor_value so use markWord::unused_mark which has the
@@ -125,11 +127,11 @@
   mov(tmp, (address)markWord::unused_mark().value());
   str(tmp, Address(box, BasicLock::displaced_header_offset_in_bytes()));
 
   br(Assembler::EQ, cont); // CAS success means locking succeeded
 
-  cmp(tmp3Reg, rthread);
+  cmp(tmp3Reg, rscratch2);
   br(Assembler::NE, cont); // Check for recursive locking
 
   // Recursive lock case
   increment(Address(disp_hdr, in_bytes(ObjectMonitor::recursions_offset()) - markWord::monitor_value), 1);
   // flag == EQ still from the cmp above, checking if this is a reentrant lock
@@ -138,11 +140,13 @@
   // flag == EQ indicates success
   // flag == NE indicates failure
   br(Assembler::NE, no_count);
 
   bind(count);
-  increment(Address(rthread, JavaThread::held_monitor_count_offset()));
+  if (LockingMode == LM_LEGACY) {
+    inc_held_monitor_count();
+  }
 
   bind(no_count);
 }
 
 void C2_MacroAssembler::fast_unlock(Register objectReg, Register boxReg, Register tmpReg,
@@ -245,19 +249,21 @@
   // flag == EQ indicates success
   // flag == NE indicates failure
   br(Assembler::NE, no_count);
 
   bind(count);
-  decrement(Address(rthread, JavaThread::held_monitor_count_offset()));
+  if (LockingMode == LM_LEGACY) {
+    dec_held_monitor_count();
+  }
 
   bind(no_count);
 }
 
 void C2_MacroAssembler::fast_lock_lightweight(Register obj, Register box, Register t1,
                                               Register t2, Register t3) {
   assert(LockingMode == LM_LIGHTWEIGHT, "must be");
-  assert_different_registers(obj, box, t1, t2, t3);
+  assert_different_registers(obj, box, t1, t2, t3, rscratch2);
 
   // Handle inflated monitor.
   Label inflated;
   // Finish fast lock successfully. MUST branch to with flag == EQ
   Label locked;
@@ -369,17 +375,18 @@
     Label monitor_locked;
 
     // Compute owner address.
     lea(t2_owner_addr, owner_address);
 
-    // CAS owner (null => current thread).
-    cmpxchg(t2_owner_addr, zr, rthread, Assembler::xword, /*acquire*/ true,
+    // CAS owner (null => current thread id).
+    ldr(rscratch2, Address(rthread, JavaThread::lock_id_offset()));
+    cmpxchg(t2_owner_addr, zr, rscratch2, Assembler::xword, /*acquire*/ true,
             /*release*/ false, /*weak*/ false, t3_owner);
     br(Assembler::EQ, monitor_locked);
 
     // Check if recursive.
-    cmp(t3_owner, rthread);
+    cmp(t3_owner, rscratch2);
     br(Assembler::NE, slow_path);
 
     // Recursive.
     increment(recursions_address, 1);
 
@@ -388,11 +395,10 @@
       str(t1_monitor, Address(box, BasicLock::object_monitor_cache_offset_in_bytes()));
     }
   }
 
   bind(locked);
-  increment(Address(rthread, JavaThread::held_monitor_count_offset()));
 
 #ifdef ASSERT
   // Check that locked label is reached with Flags == EQ.
   Label flag_correct;
   br(Assembler::EQ, flag_correct);
@@ -557,11 +563,10 @@
     cmp(zr, rthread); // Set Flag to NE => slow path
     b(slow_path);
   }
 
   bind(unlocked);
-  decrement(Address(rthread, JavaThread::held_monitor_count_offset()));
   cmp(zr, zr); // Set Flags to EQ => fast path
 
 #ifdef ASSERT
   // Check that unlocked label is reached with Flags == EQ.
   Label flag_correct;
diff a/src/hotspot/cpu/aarch64/continuationFreezeThaw_aarch64.inline.hpp b/src/hotspot/cpu/aarch64/continuationFreezeThaw_aarch64.inline.hpp
--- a/src/hotspot/cpu/aarch64/continuationFreezeThaw_aarch64.inline.hpp
+++ b/src/hotspot/cpu/aarch64/continuationFreezeThaw_aarch64.inline.hpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -127,10 +127,16 @@
   if (real_unextended_sp != nullptr) {
     f.set_unextended_sp(real_unextended_sp); // can be null at a safepoint
   }
 }
 
+inline void FreezeBase::prepare_freeze_interpreted_top_frame(const frame& f) {
+  assert(*f.addr_at(frame::interpreter_frame_last_sp_offset) == 0, "should be null for top frame");
+  intptr_t* lspp = f.addr_at(frame::interpreter_frame_last_sp_offset);
+  *lspp = f.unextended_sp() - f.fp();
+}
+
 inline void FreezeBase::relativize_interpreted_frame_metadata(const frame& f, const frame& hf) {
   assert(hf.fp() == hf.unextended_sp() + (f.fp() - f.unextended_sp()), "");
   assert((f.at(frame::interpreter_frame_last_sp_offset) != 0)
     || (f.unextended_sp() == f.sp()), "");
   assert(f.fp() > (intptr_t*)f.at_relative(frame::interpreter_frame_initial_sp_offset), "");
@@ -147,14 +153,20 @@
   assert(hf.at_absolute(frame::interpreter_frame_monitor_block_top_offset) <= frame::interpreter_frame_initial_sp_offset, "");
 
   // extended_sp is already relativized by TemplateInterpreterGenerator::generate_normal_entry or
   // AbstractInterpreter::layout_activation
 
+  // The interpreter native wrapper code adds space in the stack equal to size_of_parameters()
+  // after the fixed part of the frame. For wait0 this is equal to 3 words (this + long parameter).
+  // We adjust by this size since otherwise the saved last sp will be less than the extended_sp.
+  DEBUG_ONLY(Method* m = hf.interpreter_frame_method();)
+  DEBUG_ONLY(int extra_space = m->is_object_wait0() ? m->size_of_parameters() : 0;)
+
   assert((hf.fp() - hf.unextended_sp()) == (f.fp() - f.unextended_sp()), "");
   assert(hf.unextended_sp() == (intptr_t*)hf.at(frame::interpreter_frame_last_sp_offset), "");
   assert(hf.unextended_sp() <= (intptr_t*)hf.at(frame::interpreter_frame_initial_sp_offset), "");
-  assert(hf.unextended_sp() >  (intptr_t*)hf.at(frame::interpreter_frame_extended_sp_offset), "");
+  assert(hf.unextended_sp() + extra_space >  (intptr_t*)hf.at(frame::interpreter_frame_extended_sp_offset), "");
   assert(hf.fp()            >  (intptr_t*)hf.at(frame::interpreter_frame_initial_sp_offset), "");
   assert(hf.fp()            <= (intptr_t*)hf.at(frame::interpreter_frame_locals_offset), "");
 }
 
 inline void FreezeBase::set_top_frame_metadata_pd(const frame& hf) {
@@ -211,11 +223,10 @@
   if (FKind::interpreted) {
     intptr_t* heap_sp = hf.unextended_sp();
     // If caller is interpreted it already made room for the callee arguments
     int overlap = caller.is_interpreted_frame() ? ContinuationHelper::InterpretedFrame::stack_argsize(hf) : 0;
     const int fsize = (int)(ContinuationHelper::InterpretedFrame::frame_bottom(hf) - hf.unextended_sp() - overlap);
-    const int locals = hf.interpreter_frame_method()->max_locals();
     intptr_t* frame_sp = caller.unextended_sp() - fsize;
     intptr_t* fp = frame_sp + (hf.fp() - heap_sp);
     if ((intptr_t)fp % frame::frame_alignment != 0) {
       fp--;
       frame_sp--;
@@ -233,11 +244,11 @@
     return f;
   } else {
     int fsize = FKind::size(hf);
     intptr_t* frame_sp = caller.unextended_sp() - fsize;
     if (bottom || caller.is_interpreted_frame()) {
-      int argsize = hf.compiled_frame_stack_argsize();
+      int argsize = FKind::stack_argsize(hf);
 
       fsize += argsize;
       frame_sp   -= argsize;
       caller.set_sp(caller.sp() - argsize);
       assert(caller.sp() == frame_sp + (fsize-argsize), "");
@@ -250,12 +261,12 @@
     intptr_t* fp;
     if (PreserveFramePointer) {
       // we need to recreate a "real" frame pointer, pointing into the stack
       fp = frame_sp + FKind::size(hf) - frame::sender_sp_offset;
     } else {
-      fp = FKind::stub
-        ? frame_sp + fsize - frame::sender_sp_offset // on AArch64, this value is used for the safepoint stub
+      fp = FKind::stub || FKind::native
+        ? frame_sp + fsize - frame::sender_sp_offset // fp always points to the address below the pushed return pc. We need correct address.
         : *(intptr_t**)(hf.sp() - frame::sender_sp_offset); // we need to re-read fp because it may be an oop and we might have fixed the frame.
     }
     return frame(frame_sp, frame_sp, fp, hf.pc(), hf.cb(), hf.oop_map(), false); // TODO PERF : this computes deopt state; is it necessary?
   }
 }
@@ -275,17 +286,57 @@
 
 inline void ThawBase::patch_pd(frame& f, const frame& caller) {
   patch_callee_link(caller, caller.fp());
 }
 
+inline void ThawBase::patch_pd(frame& f, intptr_t* caller_sp) {
+  intptr_t* fp = caller_sp - frame::sender_sp_offset;
+  patch_callee_link(f, fp);
+}
+
+inline intptr_t* ThawBase::possibly_adjust_frame(frame& top) {
+  intptr_t* sp = top.sp();
+  CodeBlob* cb = top.cb();
+
+  if (cb->frame_size() == 2) {
+    // C2 runtime stub case. For aarch64 the real size of the c2 runtime stub is 2 words bigger
+    // than what we think, i.e. size is 4. This is because the _last_Java_sp is not set to the
+    // sp right before making the call to the VM, but rather it is artificially set 2 words above
+    // this real sp so that we can store the return address at last_Java_sp[-1], and keep this
+    // property where we can retrieve the last_Java_pc from the last_Java_sp. But that means that
+    // once we return to the runtime stub, the code will adjust sp according to this real size.
+    // So we must adjust the frame size back here and we copy lr/rfp again.
+    sp -= 2;
+    sp[-2] = sp[0];
+    sp[-1] = sp[1];
+
+    log_develop_trace(continuations, preempt)("adjusted sp for c2 runtime stub, initial sp: " INTPTR_FORMAT " final sp: " INTPTR_FORMAT
+                                              " fp: " INTPTR_FORMAT, p2i(sp + frame::metadata_words), p2i(sp), sp[-2]);
+  }
+  return sp;
+}
+
+inline intptr_t* ThawBase::push_cleanup_continuation() {
+  frame enterSpecial = new_entry_frame();
+  intptr_t* sp = enterSpecial.sp();
+
+  sp[-1] = (intptr_t)ContinuationEntry::cleanup_pc();
+  sp[-2] = (intptr_t)enterSpecial.fp();
+
+  log_develop_trace(continuations, preempt)("push_cleanup_continuation initial sp: " INTPTR_FORMAT " final sp: " INTPTR_FORMAT, p2i(sp + 2 * frame::metadata_words), p2i(sp));
+  return sp;
+}
+
 inline void ThawBase::derelativize_interpreted_frame_metadata(const frame& hf, const frame& f) {
   // Make sure that last_sp is kept relativized.
   assert((intptr_t*)f.at_relative(frame::interpreter_frame_last_sp_offset) == f.unextended_sp(), "");
 
   // Make sure that monitor_block_top is still relativized.
   assert(f.at_absolute(frame::interpreter_frame_monitor_block_top_offset) <= frame::interpreter_frame_initial_sp_offset, "");
 
   // Make sure that extended_sp is kept relativized.
-  assert((intptr_t*)f.at_relative(frame::interpreter_frame_extended_sp_offset) < f.unextended_sp(), "");
+  DEBUG_ONLY(Method* m = hf.interpreter_frame_method();)
+  DEBUG_ONLY(int extra_space = m->is_object_wait0() ? m->size_of_parameters() : 0;) // see comment in relativize_interpreted_frame_metadata()
+  assert((intptr_t*)f.at_relative(frame::interpreter_frame_extended_sp_offset) < f.unextended_sp() + extra_space, "");
 }
 
 #endif // CPU_AARCH64_CONTINUATIONFREEZETHAW_AARCH64_INLINE_HPP
diff a/src/hotspot/cpu/aarch64/continuationHelper_aarch64.inline.hpp b/src/hotspot/cpu/aarch64/continuationHelper_aarch64.inline.hpp
--- a/src/hotspot/cpu/aarch64/continuationHelper_aarch64.inline.hpp
+++ b/src/hotspot/cpu/aarch64/continuationHelper_aarch64.inline.hpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -38,10 +38,26 @@
   return FKind::interpreted
             ? (intptr_t**)(f.fp() + frame::link_offset)
             : (intptr_t**)(f.unextended_sp() + f.cb()->frame_size() - frame::sender_sp_offset);
 }
 
+static inline void patch_return_pc_with_preempt_stub(frame& f) {
+  if (f.is_runtime_frame()) {
+    // Unlike x86 we don't know where in the callee frame the return pc is
+    // saved so we can't patch the return from the VM call back to Java.
+    // Instead, we will patch the return from the runtime stub back to the
+    // compiled method so that the target returns to the preempt cleanup stub.
+    intptr_t* caller_sp = f.sp() + f.cb()->frame_size();
+    caller_sp[-1] = (intptr_t)StubRoutines::cont_preempt_stub();
+  } else {
+    // The target will check for preemption once it returns to the interpreter
+    // or the native wrapper code and will manually jump to the preempt stub.
+    JavaThread *thread = JavaThread::current();
+    thread->set_preempt_alternate_return(StubRoutines::cont_preempt_stub());
+  }
+}
+
 inline int ContinuationHelper::frame_align_words(int size) {
 #ifdef _LP64
   return size & 1;
 #else
   return 0;
@@ -81,16 +97,16 @@
 
 inline void ContinuationHelper::set_anchor_to_entry_pd(JavaFrameAnchor* anchor, ContinuationEntry* entry) {
   anchor->set_last_Java_fp(entry->entry_fp());
 }
 
-#ifdef ASSERT
 inline void ContinuationHelper::set_anchor_pd(JavaFrameAnchor* anchor, intptr_t* sp) {
   intptr_t* fp = *(intptr_t**)(sp - frame::sender_sp_offset);
   anchor->set_last_Java_fp(fp);
 }
 
+#ifdef ASSERT
 inline bool ContinuationHelper::Frame::assert_frame_laid_out(frame f) {
   intptr_t* sp = f.sp();
   address pc = ContinuationHelper::return_address_at(
                  sp - frame::sender_sp_ret_address_offset());
   intptr_t* fp = *(intptr_t**)(sp - frame::sender_sp_offset);
diff a/src/hotspot/cpu/aarch64/frame_aarch64.hpp b/src/hotspot/cpu/aarch64/frame_aarch64.hpp
--- a/src/hotspot/cpu/aarch64/frame_aarch64.hpp
+++ b/src/hotspot/cpu/aarch64/frame_aarch64.hpp
@@ -71,11 +71,12 @@
     link_offset                                      =  0,
     return_addr_offset                               =  1,
     sender_sp_offset                                 =  2,
 
     // Interpreter frames
-    interpreter_frame_oop_temp_offset                =  3, // for native calls only
+    interpreter_frame_result_handler_offset          =  3, // for native calls only
+    interpreter_frame_oop_temp_offset                =  2, // for native calls only
 
     interpreter_frame_sender_sp_offset               = -1,
     // outgoing sp before a call to an invoked method
     interpreter_frame_last_sp_offset                 = interpreter_frame_sender_sp_offset - 1,
     interpreter_frame_method_offset                  = interpreter_frame_last_sp_offset - 1,
diff a/src/hotspot/cpu/aarch64/interp_masm_aarch64.cpp b/src/hotspot/cpu/aarch64/interp_masm_aarch64.cpp
--- a/src/hotspot/cpu/aarch64/interp_masm_aarch64.cpp
+++ b/src/hotspot/cpu/aarch64/interp_masm_aarch64.cpp
@@ -664,11 +664,11 @@
 //      rscratch1, rscratch2 (scratch regs)
 void InterpreterMacroAssembler::lock_object(Register lock_reg)
 {
   assert(lock_reg == c_rarg1, "The argument is only for looks. It must be c_rarg1");
   if (LockingMode == LM_MONITOR) {
-    call_VM(noreg,
+    call_VM_preemptable(noreg,
             CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
             lock_reg);
   } else {
     Label count, done;
 
@@ -695,11 +695,11 @@
       br(Assembler::NE, slow_case);
     }
 
     if (LockingMode == LM_LIGHTWEIGHT) {
       lightweight_lock(lock_reg, obj_reg, tmp, tmp2, tmp3, slow_case);
-      b(count);
+      b(done);
     } else if (LockingMode == LM_LEGACY) {
       // Load (object->mark() | 1) into swap_reg
       ldr(rscratch1, Address(obj_reg, oopDesc::mark_offset_in_bytes()));
       orr(swap_reg, rscratch1, 1);
 
@@ -745,22 +745,22 @@
       sub(swap_reg, swap_reg, rscratch1);
       ands(swap_reg, swap_reg, (uint64_t)(7 - (int)os::vm_page_size()));
 
       // Save the test result, for recursive case, the result is zero
       str(swap_reg, Address(lock_reg, mark_offset));
-      br(Assembler::EQ, count);
+      br(Assembler::NE, slow_case);
+
+      bind(count);
+      inc_held_monitor_count();
+      b(done);
     }
     bind(slow_case);
 
     // Call the runtime routine for slow case
-    call_VM(noreg,
+    call_VM_preemptable(noreg,
             CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
             lock_reg);
-    b(done);
-
-    bind(count);
-    increment(Address(rthread, JavaThread::held_monitor_count_offset()));
 
     bind(done);
   }
 }
 
@@ -802,34 +802,34 @@
     ldr(obj_reg, Address(lock_reg, BasicObjectLock::obj_offset()));
 
     // Free entry
     str(zr, Address(lock_reg, BasicObjectLock::obj_offset()));
 
+    Label slow_case;
     if (LockingMode == LM_LIGHTWEIGHT) {
-      Label slow_case;
       lightweight_unlock(obj_reg, header_reg, swap_reg, tmp_reg, slow_case);
-      b(count);
-      bind(slow_case);
+      b(done);
     } else if (LockingMode == LM_LEGACY) {
       // Load the old header from BasicLock structure
       ldr(header_reg, Address(swap_reg,
                               BasicLock::displaced_header_offset_in_bytes()));
 
       // Test for recursion
       cbz(header_reg, count);
 
       // Atomic swap back the old header
-      cmpxchg_obj_header(swap_reg, header_reg, obj_reg, rscratch1, count, /*fallthrough*/nullptr);
+      cmpxchg_obj_header(swap_reg, header_reg, obj_reg, rscratch1, count, &slow_case);
+
+      bind(count);
+      dec_held_monitor_count();
+      b(done);
     }
+
+    bind(slow_case);
     // Call the runtime routine for slow case.
     str(obj_reg, Address(lock_reg, BasicObjectLock::obj_offset())); // restore obj
     call_VM_leaf(CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorexit), lock_reg);
-    b(done);
-
-    bind(count);
-    decrement(Address(rthread, JavaThread::held_monitor_count_offset()));
-
     bind(done);
     restore_bcp();
   }
 }
 
@@ -1529,10 +1529,58 @@
 // interpreter specific
   restore_bcp();
   restore_locals();
 }
 
+void InterpreterMacroAssembler::call_VM_preemptable(Register oop_result,
+                                                    address entry_point,
+                                                    Register arg_1) {
+  assert(arg_1 == c_rarg1, "");
+  Label resume_pc, not_preempted;
+
+#ifdef ASSERT
+  {
+    Label L;
+    ldr(rscratch1, Address(rthread, JavaThread::preempt_alternate_return_offset()));
+    cbz(rscratch1, L);
+    stop("Should not have alternate return address set");
+    bind(L);
+  }
+#endif /* ASSERT */
+
+  push_cont_fastpath();
+
+  // Make VM call. In case of preemption set last_pc to the one we want to resume to.
+  adr(rscratch1, resume_pc);
+  str(rscratch1, Address(rthread, JavaThread::last_Java_pc_offset()));
+  call_VM_base(oop_result, noreg, noreg, entry_point, 1, false /*check_exceptions*/);
+
+  pop_cont_fastpath();
+
+  // Check if preempted.
+  ldr(rscratch1, Address(rthread, JavaThread::preempt_alternate_return_offset()));
+  cbz(rscratch1, not_preempted);
+  str(zr, Address(rthread, JavaThread::preempt_alternate_return_offset()));
+  br(rscratch1);
+
+  // In case of preemption, this is where we will resume once we finally acquire the monitor.
+  bind(resume_pc);
+  restore_after_resume(false /* is_native */);
+
+  bind(not_preempted);
+}
+
+void InterpreterMacroAssembler::restore_after_resume(bool is_native) {
+  lea(rscratch1, ExternalAddress(Interpreter::cont_resume_interpreter_adapter()));
+  blr(rscratch1);
+  if (is_native) {
+    // On resume we need to set up stack as expected
+    push(dtos);
+    push(ltos);
+  }
+}
+
 void InterpreterMacroAssembler::profile_obj_type(Register obj, const Address& mdo_addr) {
   assert_different_registers(obj, rscratch1, mdo_addr.base(), mdo_addr.index());
   Label update, next, none;
 
   verify_oop(obj);
diff a/src/hotspot/cpu/aarch64/interp_masm_aarch64.hpp b/src/hotspot/cpu/aarch64/interp_masm_aarch64.hpp
--- a/src/hotspot/cpu/aarch64/interp_masm_aarch64.hpp
+++ b/src/hotspot/cpu/aarch64/interp_masm_aarch64.hpp
@@ -56,10 +56,15 @@
  public:
   InterpreterMacroAssembler(CodeBuffer* code) : MacroAssembler(code) {}
 
   void load_earlyret_value(TosState state);
 
+  void call_VM_preemptable(Register oop_result,
+                           address entry_point,
+                           Register arg_1);
+  void restore_after_resume(bool is_native);
+
   void jump_to_entry(address entry);
 
   virtual void check_and_handle_popframe(Register java_thread);
   virtual void check_and_handle_earlyret(Register java_thread);
 
diff a/src/hotspot/cpu/aarch64/macroAssembler_aarch64.cpp b/src/hotspot/cpu/aarch64/macroAssembler_aarch64.cpp
--- a/src/hotspot/cpu/aarch64/macroAssembler_aarch64.cpp
+++ b/src/hotspot/cpu/aarch64/macroAssembler_aarch64.cpp
@@ -37,10 +37,11 @@
 #include "gc/shared/cardTable.hpp"
 #include "gc/shared/collectedHeap.hpp"
 #include "gc/shared/tlab_globals.hpp"
 #include "interpreter/bytecodeHistogram.hpp"
 #include "interpreter/interpreter.hpp"
+#include "interpreter/interpreterRuntime.hpp"
 #include "jvm.h"
 #include "memory/resourceArea.hpp"
 #include "memory/universe.hpp"
 #include "nativeInst_aarch64.hpp"
 #include "oops/accessDecorators.hpp"
@@ -773,10 +774,14 @@
   if (c_rarg3 != arg ) {
     masm->mov(c_rarg3, arg);
   }
 }
 
+static bool is_preemptable(address entry_point) {
+  return entry_point == CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter);
+}
+
 void MacroAssembler::call_VM_base(Register oop_result,
                                   Register java_thread,
                                   Register last_java_sp,
                                   address  entry_point,
                                   int      number_of_arguments,
@@ -808,11 +813,16 @@
 
   // set last Java frame before call
   assert(last_java_sp != rfp, "can't use rfp");
 
   Label l;
-  set_last_Java_frame(last_java_sp, rfp, l, rscratch1);
+  if (is_preemptable(entry_point)) {
+    // skip setting last_pc since we already set it to desired value.
+    set_last_Java_frame(last_java_sp, rfp, noreg, rscratch1);
+  } else {
+    set_last_Java_frame(last_java_sp, rfp, l, rscratch1);
+  }
 
   // do the call, remove parameters
   MacroAssembler::call_VM_leaf_base(entry_point, number_of_arguments, &l);
 
   // lr could be poisoned with PAC signature during throw_pending_exception
@@ -5325,10 +5335,42 @@
                                    Label& slow_case) {
   BarrierSetAssembler *bs = BarrierSet::barrier_set()->barrier_set_assembler();
   bs->tlab_allocate(this, obj, var_size_in_bytes, con_size_in_bytes, t1, t2, slow_case);
 }
 
+void MacroAssembler::inc_held_monitor_count() {
+  Address dst = Address(rthread, JavaThread::held_monitor_count_offset());
+#ifdef ASSERT
+  ldr(rscratch2, dst);
+  increment(rscratch2);
+  str(rscratch2, dst);
+  Label ok;
+  tbz(rscratch2, 63, ok);
+  STOP("assert(held monitor count underflow)");
+  should_not_reach_here();
+  bind(ok);
+#else
+  increment(dst);
+#endif
+}
+
+void MacroAssembler::dec_held_monitor_count() {
+  Address dst = Address(rthread, JavaThread::held_monitor_count_offset());
+#ifdef ASSERT
+  ldr(rscratch2, dst);
+  decrement(rscratch2);
+  str(rscratch2, dst);
+  Label ok;
+  tbz(rscratch2, 63, ok);
+  STOP("assert(held monitor count underflow)");
+  should_not_reach_here();
+  bind(ok);
+#else
+  decrement(dst);
+#endif
+}
+
 void MacroAssembler::verify_tlab() {
 #ifdef ASSERT
   if (UseTLAB && VerifyOops) {
     Label next, ok;
 
diff a/src/hotspot/cpu/aarch64/macroAssembler_aarch64.hpp b/src/hotspot/cpu/aarch64/macroAssembler_aarch64.hpp
--- a/src/hotspot/cpu/aarch64/macroAssembler_aarch64.hpp
+++ b/src/hotspot/cpu/aarch64/macroAssembler_aarch64.hpp
@@ -936,12 +936,15 @@
   void push_CPU_state(bool save_vectors = false, bool use_sve = false,
                       int sve_vector_size_in_bytes = 0, int total_predicate_in_bytes = 0);
   void pop_CPU_state(bool restore_vectors = false, bool use_sve = false,
                      int sve_vector_size_in_bytes = 0, int total_predicate_in_bytes = 0);
 
-  void push_cont_fastpath(Register java_thread);
-  void pop_cont_fastpath(Register java_thread);
+  void push_cont_fastpath(Register java_thread = rthread);
+  void pop_cont_fastpath(Register java_thread = rthread);
+
+  void inc_held_monitor_count();
+  void dec_held_monitor_count();
 
   // Round up to a power of two
   void round_to(Register reg, int modulus);
 
   // java.lang.Math::round intrinsics
diff a/src/hotspot/cpu/aarch64/sharedRuntime_aarch64.cpp b/src/hotspot/cpu/aarch64/sharedRuntime_aarch64.cpp
--- a/src/hotspot/cpu/aarch64/sharedRuntime_aarch64.cpp
+++ b/src/hotspot/cpu/aarch64/sharedRuntime_aarch64.cpp
@@ -1164,16 +1164,18 @@
 
   __ b(exit);
 
   __ bind(call_thaw);
 
+  ContinuationEntry::_thaw_call_pc_offset = __ pc() - start;
   __ rt_call(CAST_FROM_FN_PTR(address, StubRoutines::cont_thaw()));
   oop_maps->add_gc_map(__ pc() - start, map->deep_copy());
   ContinuationEntry::_return_pc_offset = __ pc() - start;
   __ post_call_nop();
 
   __ bind(exit);
+  ContinuationEntry::_cleanup_offset = __ pc() - start;
   continuation_enter_cleanup(masm);
   __ leave();
   __ ret(lr);
 
   /// exception handling
@@ -1266,10 +1268,14 @@
 
     OopMap* map = new OopMap(framesize, 1);
     oop_maps->add_gc_map(the_pc - start, map);
 }
 
+void SharedRuntime::continuation_enter_cleanup(MacroAssembler* masm) {
+  ::continuation_enter_cleanup(masm);
+}
+
 static void gen_special_dispatch(MacroAssembler* masm,
                                  const methodHandle& method,
                                  const BasicType* sig_bt,
                                  const VMRegPair* regs) {
   verify_oop_args(masm, method, sig_bt, regs);
@@ -1732,15 +1738,24 @@
     // and protect the arg if we must spill
     c_arg--;
   }
 
   // Change state to native (we save the return address in the thread, since it might not
-  // be pushed on the stack when we do a stack traversal).
-  // We use the same pc/oopMap repeatedly when we call out
+  // be pushed on the stack when we do a stack traversal). It is enough that the pc()
+  // points into the right code segment. It does not have to be the correct return pc.
+  // We use the same pc/oopMap repeatedly when we call out.
 
   Label native_return;
-  __ set_last_Java_frame(sp, noreg, native_return, rscratch1);
+  if (LockingMode != LM_LEGACY && method->is_object_wait0()) {
+    // For convenience we use the pc we want to resume to in case of preemption on Object.wait.
+    __ set_last_Java_frame(sp, noreg, native_return, rscratch1);
+  } else {
+    intptr_t the_pc = (intptr_t) __ pc();
+    oop_maps->add_gc_map(the_pc - start, map);
+
+    __ set_last_Java_frame(sp, noreg, __ pc(), rscratch1);
+  }
 
   Label dtrace_method_entry, dtrace_method_entry_done;
   if (DTraceMethodProbes) {
     __ b(dtrace_method_entry);
     __ bind(dtrace_method_entry_done);
@@ -1814,16 +1829,17 @@
       __ ands(swap_reg, swap_reg, 3 - (int)os::vm_page_size());
 
       // Save the test result, for recursive case, the result is zero
       __ str(swap_reg, Address(lock_reg, mark_word_offset));
       __ br(Assembler::NE, slow_path_lock);
+
+      __ bind(count);
+      __ inc_held_monitor_count();
     } else {
       assert(LockingMode == LM_LIGHTWEIGHT, "must be");
       __ lightweight_lock(lock_reg, obj_reg, swap_reg, tmp, lock_tmp, slow_path_lock);
     }
-    __ bind(count);
-    __ increment(Address(rthread, JavaThread::held_monitor_count_offset()));
 
     // Slow path will re-enter here
     __ bind(lock_done);
   }
 
@@ -1838,15 +1854,10 @@
   __ lea(rscratch2, Address(rthread, JavaThread::thread_state_offset()));
   __ stlrw(rscratch1, rscratch2);
 
   __ rt_call(native_func);
 
-  __ bind(native_return);
-
-  intptr_t return_pc = (intptr_t) __ pc();
-  oop_maps->add_gc_map(return_pc - start, map);
-
   // Verify or restore cpu control state after JNI call
   __ restore_cpu_control_state_after_jni(rscratch1, rscratch2);
 
   // Unpack native results.
   switch (ret_type) {
@@ -1901,10 +1912,22 @@
   __ mov(rscratch1, _thread_in_Java);
   __ lea(rscratch2, Address(rthread, JavaThread::thread_state_offset()));
   __ stlrw(rscratch1, rscratch2);
   __ bind(after_transition);
 
+  if (LockingMode != LM_LEGACY && method->is_object_wait0()) {
+    // Check preemption for Object.wait()
+    __ ldr(rscratch1, Address(rthread, JavaThread::preempt_alternate_return_offset()));
+    __ cbz(rscratch1, native_return);
+    __ str(zr, Address(rthread, JavaThread::preempt_alternate_return_offset()));
+    __ br(rscratch1);
+    __ bind(native_return);
+
+    intptr_t the_pc = (intptr_t) __ pc();
+    oop_maps->add_gc_map(the_pc - start, map);
+  }
+
   Label reguard;
   Label reguard_done;
   __ ldrb(rscratch1, Address(rthread, JavaThread::stack_guard_state_offset()));
   __ cmpw(rscratch1, StackOverflow::stack_guard_yellow_reserved_disabled);
   __ br(Assembler::EQ, reguard);
@@ -1924,11 +1947,11 @@
 
     if (LockingMode == LM_LEGACY) {
       // Simple recursive lock?
       __ ldr(rscratch1, Address(sp, lock_slot_offset * VMRegImpl::stack_slot_size));
       __ cbnz(rscratch1, not_recursive);
-      __ decrement(Address(rthread, JavaThread::held_monitor_count_offset()));
+      __ dec_held_monitor_count();
       __ b(done);
     }
 
     __ bind(not_recursive);
 
@@ -1947,15 +1970,14 @@
 
       // Atomic swap old header if oop still contains the stack lock
       Label count;
       __ cmpxchg_obj_header(r0, old_hdr, obj_reg, rscratch1, count, &slow_path_unlock);
       __ bind(count);
-      __ decrement(Address(rthread, JavaThread::held_monitor_count_offset()));
+      __ dec_held_monitor_count();
     } else {
       assert(LockingMode == LM_LIGHTWEIGHT, "");
       __ lightweight_unlock(obj_reg, old_hdr, swap_reg, lock_tmp, slow_path_unlock);
-      __ decrement(Address(rthread, JavaThread::held_monitor_count_offset()));
     }
 
     // slow path re-enters here
     __ bind(unlock_done);
     if (ret_type != T_FLOAT && ret_type != T_DOUBLE && ret_type != T_VOID) {
@@ -2019,11 +2041,15 @@
     __ mov(c_rarg0, obj_reg);
     __ mov(c_rarg1, lock_reg);
     __ mov(c_rarg2, rthread);
 
     // Not a leaf but we have last_Java_frame setup as we want
+    // Force freeze slow path in case we try to preempt. We will pin the
+    // vthread to the carrier (see FreezeBase::recurse_freeze_native_frame()).
+    __ push_cont_fastpath();
     __ call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::complete_monitor_locking_C), 3);
+    __ pop_cont_fastpath();
     restore_args(masm, total_c_args, c_arg, out_regs);
 
 #ifdef ASSERT
     { Label L;
       __ ldr(rscratch1, Address(rthread, in_bytes(Thread::pending_exception_offset())));
@@ -2560,10 +2586,14 @@
 uint SharedRuntime::out_preserve_stack_slots() {
   return 0;
 }
 
 
+VMReg SharedRuntime::thread_register() {
+  return rthread->as_VMReg();
+}
+
 //------------------------------generate_handler_blob------
 //
 // Generate a special Compile2Runtime blob that saves all registers,
 // and setup oopmap.
 //
diff a/src/hotspot/cpu/aarch64/stackChunkFrameStream_aarch64.inline.hpp b/src/hotspot/cpu/aarch64/stackChunkFrameStream_aarch64.inline.hpp
--- a/src/hotspot/cpu/aarch64/stackChunkFrameStream_aarch64.inline.hpp
+++ b/src/hotspot/cpu/aarch64/stackChunkFrameStream_aarch64.inline.hpp
@@ -114,10 +114,11 @@
   InterpreterOopMap mask;
   frame f = to_frame();
   f.interpreted_frame_oop_map(&mask);
   return  mask.num_oops()
         + 1 // for the mirror oop
+        + (f.interpreter_frame_method()->is_native() ? 1 : 0) // temp oop slot
         + pointer_delta_as_int((intptr_t*)f.interpreter_frame_monitor_begin(),
               (intptr_t*)f.interpreter_frame_monitor_end())/BasicObjectLock::size();
 }
 
 template<>
diff a/src/hotspot/cpu/aarch64/stubGenerator_aarch64.cpp b/src/hotspot/cpu/aarch64/stubGenerator_aarch64.cpp
--- a/src/hotspot/cpu/aarch64/stubGenerator_aarch64.cpp
+++ b/src/hotspot/cpu/aarch64/stubGenerator_aarch64.cpp
@@ -7138,10 +7138,41 @@
     generate_cont_thaw(Continuation::thaw_return_barrier_exception);
 
     return start;
   }
 
+  address generate_cont_preempt_stub() {
+    if (!Continuations::enabled()) return nullptr;
+    StubCodeMark mark(this, "StubRoutines","Continuation preempt stub");
+    address start = __ pc();
+
+    __ reset_last_Java_frame(true);
+
+    // Set sp to enterSpecial frame, i.e. remove all frames copied into the heap.
+    __ ldr(rscratch2, Address(rthread, JavaThread::cont_entry_offset()));
+    __ mov(sp, rscratch2);
+
+    Label preemption_cancelled;
+    __ ldrb(rscratch1, Address(rthread, JavaThread::preemption_cancelled_offset()));
+    __ cbnz(rscratch1, preemption_cancelled);
+
+    // Remove enterSpecial frame from the stack and return to Continuation.run() to unmount.
+    SharedRuntime::continuation_enter_cleanup(_masm);
+    __ leave();
+    __ ret(lr);
+
+    // We acquired the monitor after freezing the frames so call thaw to continue execution.
+    __ bind(preemption_cancelled);
+    __ strb(zr, Address(rthread, JavaThread::preemption_cancelled_offset()));
+    __ lea(rfp, Address(sp, checked_cast<int32_t>(ContinuationEntry::size())));
+    __ lea(rscratch1, ExternalAddress(ContinuationEntry::thaw_call_pc_address()));
+    __ ldr(rscratch1, Address(rscratch1));
+    __ br(rscratch1);
+
+    return start;
+  }
+
   // In sun.security.util.math.intpoly.IntegerPolynomial1305, integers
   // are represented as long[5], with BITS_PER_LIMB = 26.
   // Pack five 26-bit limbs into three 64-bit registers.
   void pack_26(Register dest0, Register dest1, Register dest2, Register src) {
     __ ldp(dest0, rscratch1, Address(src, 0));     // 26 bits
@@ -8220,10 +8251,11 @@
   void generate_continuation_stubs() {
     // Continuation stubs:
     StubRoutines::_cont_thaw          = generate_cont_thaw();
     StubRoutines::_cont_returnBarrier = generate_cont_returnBarrier();
     StubRoutines::_cont_returnBarrierExc = generate_cont_returnBarrier_exception();
+    StubRoutines::_cont_preempt_stub = generate_cont_preempt_stub();
   }
 
   void generate_final_stubs() {
     // support for verify_oop (must happen after universe_init)
     if (VerifyOops) {
diff a/src/hotspot/cpu/aarch64/templateInterpreterGenerator_aarch64.cpp b/src/hotspot/cpu/aarch64/templateInterpreterGenerator_aarch64.cpp
--- a/src/hotspot/cpu/aarch64/templateInterpreterGenerator_aarch64.cpp
+++ b/src/hotspot/cpu/aarch64/templateInterpreterGenerator_aarch64.cpp
@@ -605,10 +605,44 @@
   __ membar(Assembler::AnyAny);
   __ dispatch_via(vtos, Interpreter::_normal_table.table_for(vtos));
   return entry;
 }
 
+address TemplateInterpreterGenerator::generate_cont_resume_interpreter_adapter() {
+  if (!Continuations::enabled()) return nullptr;
+  address start = __ pc();
+
+  __ restore_bcp();
+  __ restore_locals();
+
+  // Restore constant pool cache
+  __ ldr(rcpool, Address(rfp, frame::interpreter_frame_cache_offset * wordSize));
+
+  // Restore Java expression stack pointer
+  __ ldr(rscratch1, Address(rfp, frame::interpreter_frame_last_sp_offset * wordSize));
+  __ lea(esp, Address(rfp, rscratch1, Address::lsl(Interpreter::logStackElementSize)));
+  // and NULL it as marker that esp is now tos until next java call
+  __ str(zr, Address(rfp, frame::interpreter_frame_last_sp_offset * wordSize));
+
+  // Restore machine SP
+  __ ldr(rscratch1, Address(rfp, frame::interpreter_frame_extended_sp_offset * wordSize));
+  __ lea(sp, Address(rfp, rscratch1, Address::lsl(LogBytesPerWord)));
+
+  // Restore method
+  __ ldr(rmethod, Address(rfp, frame::interpreter_frame_method_offset * wordSize));
+
+  // Restore dispatch
+  uint64_t offset;
+  __ adrp(rdispatch, ExternalAddress((address)Interpreter::dispatch_table()), offset);
+  __ add(rdispatch, rdispatch, offset);
+
+  __ ret(lr);
+
+  return start;
+}
+
+
 // Helpers for commoning out cases in the various type of method entries.
 //
 
 
 // increment invocation count & check for overflow
@@ -1312,10 +1346,12 @@
 
 
   // result handler is in r0
   // set result handler
   __ mov(result_handler, r0);
+  __ str(r0, Address(rfp, frame::interpreter_frame_result_handler_offset * wordSize));
+
   // pass mirror handle if static call
   {
     Label L;
     __ ldrw(t, Address(rmethod, Method::access_flags_offset()));
     __ tbz(t, exact_log2(JVM_ACC_STATIC), L);
@@ -1347,13 +1383,14 @@
   }
 
   // pass JNIEnv
   __ add(c_rarg0, rthread, in_bytes(JavaThread::jni_environment_offset()));
 
-  // Set the last Java PC in the frame anchor to be the return address from
-  // the call to the native method: this will allow the debugger to
-  // generate an accurate stack trace.
+  // It is enough that the pc() points into the right code
+  // segment. It does not have to be the correct return pc.
+  // For convenience we use the pc we want to resume to in
+  // case of preemption on Object.wait.
   Label native_return;
   __ set_last_Java_frame(esp, rfp, native_return, rscratch1);
 
   // change thread state
 #ifdef ASSERT
@@ -1370,13 +1407,17 @@
   // Change state to native
   __ mov(rscratch1, _thread_in_native);
   __ lea(rscratch2, Address(rthread, JavaThread::thread_state_offset()));
   __ stlrw(rscratch1, rscratch2);
 
+  __ push_cont_fastpath();
+
   // Call the native method.
   __ blr(r10);
-  __ bind(native_return);
+
+  __ pop_cont_fastpath();
+
   __ get_method(rmethod);
   // result potentially in r0 or v0
 
   // Restore cpu control state after JNI call
   __ restore_cpu_control_state_after_jni(rscratch1, rscratch2);
@@ -1430,10 +1471,25 @@
   // change thread state
   __ mov(rscratch1, _thread_in_Java);
   __ lea(rscratch2, Address(rthread, JavaThread::thread_state_offset()));
   __ stlrw(rscratch1, rscratch2);
 
+  if (LockingMode != LM_LEGACY) {
+    // Check preemption for Object.wait()
+    Label not_preempted;
+    __ ldr(rscratch1, Address(rthread, JavaThread::preempt_alternate_return_offset()));
+    __ cbz(rscratch1, not_preempted);
+    __ str(zr, Address(rthread, JavaThread::preempt_alternate_return_offset()));
+    __ br(rscratch1);
+    __ bind(native_return);
+    __ restore_after_resume(true /* is_native */);
+    __ bind(not_preempted);
+  } else {
+    // any pc will do so just use this one for LM_LEGACY to keep code together.
+    __ bind(native_return);
+  }
+
   // reset_last_Java_frame
   __ reset_last_Java_frame(true);
 
   if (CheckJNICalls) {
     // clear_pending_jni_exception_check
@@ -1448,10 +1504,11 @@
   // and result handler will pick it up
 
   {
     Label no_oop;
     __ adr(t, ExternalAddress(AbstractInterpreter::result_handler(T_OBJECT)));
+    __ ldr(result_handler, Address(rfp, frame::interpreter_frame_result_handler_offset*wordSize));
     __ cmp(t, result_handler);
     __ br(Assembler::NE, no_oop);
     // Unbox oop result, e.g. JNIHandles::resolve result.
     __ pop(ltos);
     __ resolve_jobject(r0, t, rscratch2);
diff a/src/hotspot/cpu/arm/c1_Runtime1_arm.cpp b/src/hotspot/cpu/arm/c1_Runtime1_arm.cpp
--- a/src/hotspot/cpu/arm/c1_Runtime1_arm.cpp
+++ b/src/hotspot/cpu/arm/c1_Runtime1_arm.cpp
@@ -249,10 +249,14 @@
 }
 
 void Runtime1::initialize_pd() {
 }
 
+uint Runtime1::runtime_blob_current_thread_offset(frame f) {
+  Unimplemented();
+  return 0;
+}
 
 OopMapSet* Runtime1::generate_exception_throw(StubAssembler* sasm, address target, bool has_argument) {
   OopMap* oop_map = save_live_registers(sasm);
 
   int call_offset;
diff a/src/hotspot/cpu/arm/continuationFreezeThaw_arm.inline.hpp b/src/hotspot/cpu/arm/continuationFreezeThaw_arm.inline.hpp
--- a/src/hotspot/cpu/arm/continuationFreezeThaw_arm.inline.hpp
+++ b/src/hotspot/cpu/arm/continuationFreezeThaw_arm.inline.hpp
@@ -46,10 +46,14 @@
 
 void FreezeBase::adjust_interpreted_frame_unextended_sp(frame& f) {
   Unimplemented();
 }
 
+inline void FreezeBase::prepare_freeze_interpreted_top_frame(const frame& f) {
+  Unimplemented();
+}
+
 inline void FreezeBase::relativize_interpreted_frame_metadata(const frame& f, const frame& hf) {
   Unimplemented();
 }
 
 inline void FreezeBase::patch_pd(frame& hf, const frame& caller) {
@@ -81,10 +85,24 @@
 
 inline void ThawBase::patch_pd(frame& f, const frame& caller) {
   Unimplemented();
 }
 
+inline void ThawBase::patch_pd(frame& f, intptr_t* caller_sp) {
+  Unimplemented();
+}
+
+inline intptr_t* ThawBase::possibly_adjust_frame(frame& top) {
+  Unimplemented();
+  return nullptr;
+}
+
+inline intptr_t* ThawBase::push_cleanup_continuation() {
+  Unimplemented();
+  return nullptr;
+}
+
 template <typename ConfigT>
 inline void Thaw<ConfigT>::patch_caller_links(intptr_t* sp, intptr_t* bottom) {
   Unimplemented();
 }
 
diff a/src/hotspot/cpu/arm/continuationHelper_arm.inline.hpp b/src/hotspot/cpu/arm/continuationHelper_arm.inline.hpp
--- a/src/hotspot/cpu/arm/continuationHelper_arm.inline.hpp
+++ b/src/hotspot/cpu/arm/continuationHelper_arm.inline.hpp
@@ -33,10 +33,14 @@
 static inline intptr_t** link_address(const frame& f) {
   Unimplemented();
   return nullptr;
 }
 
+static inline void patch_return_pc_with_preempt_stub(frame& f) {
+  Unimplemented();
+}
+
 inline int ContinuationHelper::frame_align_words(int size) {
   Unimplemented();
   return 0;
 }
 
@@ -60,15 +64,15 @@
 
 inline void ContinuationHelper::set_anchor_to_entry_pd(JavaFrameAnchor* anchor, ContinuationEntry* cont) {
   Unimplemented();
 }
 
-#ifdef ASSERT
 inline void ContinuationHelper::set_anchor_pd(JavaFrameAnchor* anchor, intptr_t* sp) {
   Unimplemented();
 }
 
+#ifdef ASSERT
 inline bool ContinuationHelper::Frame::assert_frame_laid_out(frame f) {
   Unimplemented();
   return false;
 }
 #endif
diff a/src/hotspot/cpu/arm/sharedRuntime_arm.cpp b/src/hotspot/cpu/arm/sharedRuntime_arm.cpp
--- a/src/hotspot/cpu/arm/sharedRuntime_arm.cpp
+++ b/src/hotspot/cpu/arm/sharedRuntime_arm.cpp
@@ -1356,10 +1356,15 @@
 
 uint SharedRuntime::out_preserve_stack_slots() {
   return 0;
 }
 
+VMReg SharedRuntime::thread_register() {
+  Unimplemented();
+  return nullptr;
+}
+
 //------------------------------generate_deopt_blob----------------------------
 void SharedRuntime::generate_deopt_blob() {
   ResourceMark rm;
   const char* name = SharedRuntime::stub_name(SharedStubId::deopt_id);
   CodeBuffer buffer(name, 1024, 1024);
diff a/src/hotspot/cpu/arm/templateInterpreterGenerator_arm.cpp b/src/hotspot/cpu/arm/templateInterpreterGenerator_arm.cpp
--- a/src/hotspot/cpu/arm/templateInterpreterGenerator_arm.cpp
+++ b/src/hotspot/cpu/arm/templateInterpreterGenerator_arm.cpp
@@ -457,10 +457,14 @@
   __ ldrb(R3_bytecode, Address(Rbcp));
   __ dispatch_only_normal(vtos);
   return entry;
 }
 
+address TemplateInterpreterGenerator::generate_cont_resume_interpreter_adapter() {
+  return nullptr;
+}
+
 
 // Helpers for commoning out cases in the various type of method entries.
 //
 
 // increment invocation count & check for overflow
diff a/src/hotspot/cpu/ppc/assembler_ppc.hpp b/src/hotspot/cpu/ppc/assembler_ppc.hpp
--- a/src/hotspot/cpu/ppc/assembler_ppc.hpp
+++ b/src/hotspot/cpu/ppc/assembler_ppc.hpp
@@ -1814,10 +1814,11 @@
                          relocInfo::relocType rt = relocInfo::none);
   inline void bcctrl(int boint, int biint, int bhint = bhintbhBCLRisReturn,
                          relocInfo::relocType rt = relocInfo::none);
 
   // helper function for b, bcxx
+  inline bool is_branch(address a);
   inline bool is_within_range_of_b(address a, address pc);
   inline bool is_within_range_of_bcxx(address a, address pc);
 
   // get the destination of a bxx branch (b, bl, ba, bla)
   static inline address  bxx_destination(address baddr);
diff a/src/hotspot/cpu/ppc/assembler_ppc.inline.hpp b/src/hotspot/cpu/ppc/assembler_ppc.inline.hpp
--- a/src/hotspot/cpu/ppc/assembler_ppc.inline.hpp
+++ b/src/hotspot/cpu/ppc/assembler_ppc.inline.hpp
@@ -452,10 +452,16 @@
 inline void Assembler::bclr(  int boint, int biint, int bhint, relocInfo::relocType rt) { emit_data(BCLR_OPCODE | bo(boint) | bi(biint) | bh(bhint) | aa(0) | lk(0), rt); }
 inline void Assembler::bclrl( int boint, int biint, int bhint, relocInfo::relocType rt) { emit_data(BCLR_OPCODE | bo(boint) | bi(biint) | bh(bhint) | aa(0) | lk(1), rt); }
 inline void Assembler::bcctr( int boint, int biint, int bhint, relocInfo::relocType rt) { emit_data(BCCTR_OPCODE| bo(boint) | bi(biint) | bh(bhint) | aa(0) | lk(0), rt); }
 inline void Assembler::bcctrl(int boint, int biint, int bhint, relocInfo::relocType rt) { emit_data(BCCTR_OPCODE| bo(boint) | bi(biint) | bh(bhint) | aa(0) | lk(1), rt); }
 
+inline bool Assembler::is_branch(address a) {
+  int32_t instr = *(int32_t*) a;
+  int op = inv_op_ppc(instr);
+  return op == b_op || op == bc_op;
+}
+
 // helper function for b
 inline bool Assembler::is_within_range_of_b(address a, address pc) {
   // Guard against illegal branch targets, e.g. -1 (see CompiledDirectCall and ad-file).
   if ((((uint64_t)a) & 0x3) != 0) return false;
 
diff a/src/hotspot/cpu/ppc/c1_MacroAssembler_ppc.cpp b/src/hotspot/cpu/ppc/c1_MacroAssembler_ppc.cpp
--- a/src/hotspot/cpu/ppc/c1_MacroAssembler_ppc.cpp
+++ b/src/hotspot/cpu/ppc/c1_MacroAssembler_ppc.cpp
@@ -131,11 +131,13 @@
     std(R0/*==0, perhaps*/, BasicLock::displaced_header_offset_in_bytes(), Rbox);
     bne(CCR0, slow_int);
   }
 
   bind(done);
-  inc_held_monitor_count(Rmark /*tmp*/);
+  if (LockingMode == LM_LEGACY) {
+    inc_held_monitor_count(Rmark /*tmp*/);
+  }
 }
 
 
 void C1_MacroAssembler::unlock_object(Register Rmark, Register Roop, Register Rbox, Label& slow_case) {
   assert_different_registers(Rmark, Roop, Rbox);
@@ -177,11 +179,13 @@
   bind(slow_int);
   b(slow_case); // far
 
   // Done
   bind(done);
-  dec_held_monitor_count(Rmark /*tmp*/);
+  if (LockingMode == LM_LEGACY) {
+    dec_held_monitor_count(Rmark /*tmp*/);
+  }
 }
 
 
 void C1_MacroAssembler::try_allocate(
   Register obj,                        // result: pointer to object after successful allocation
diff a/src/hotspot/cpu/ppc/c1_Runtime1_ppc.cpp b/src/hotspot/cpu/ppc/c1_Runtime1_ppc.cpp
--- a/src/hotspot/cpu/ppc/c1_Runtime1_ppc.cpp
+++ b/src/hotspot/cpu/ppc/c1_Runtime1_ppc.cpp
@@ -62,11 +62,12 @@
   // ARG1 must hold thread address.
   mr(R3_ARG1, R16_thread);
 
   address return_pc = call_c(entry_point);
 
-  reset_last_Java_frame();
+  // Last java sp can be null when the RT call was preempted
+  reset_last_Java_frame(false /* check_last_java_sp */);
 
   // Check for pending exceptions.
   {
     ld(R0, in_bytes(Thread::pending_exception_offset()), R16_thread);
     cmpdi(CCR0, R0, 0);
@@ -255,10 +256,15 @@
     sp_offset += BytesPerWord;
   }
   frame_size_in_bytes = align_up(sp_offset, frame::alignment_in_bytes);
 }
 
+uint Runtime1::runtime_blob_current_thread_offset(frame f) {
+  // On PPC virtual threads don't save the JavaThread* in their context (e.g. C1 stub frames).
+  ShouldNotCallThis();
+  return 0;
+}
 
 OopMapSet* Runtime1::generate_exception_throw(StubAssembler* sasm, address target, bool has_argument) {
   // Make a frame and preserve the caller's caller-save registers.
   OopMap* oop_map = save_live_registers(sasm);
 
diff a/src/hotspot/cpu/ppc/continuationFreezeThaw_ppc.inline.hpp b/src/hotspot/cpu/ppc/continuationFreezeThaw_ppc.inline.hpp
--- a/src/hotspot/cpu/ppc/continuationFreezeThaw_ppc.inline.hpp
+++ b/src/hotspot/cpu/ppc/continuationFreezeThaw_ppc.inline.hpp
@@ -69,10 +69,16 @@
 
 void FreezeBase::adjust_interpreted_frame_unextended_sp(frame& f) {
   // nothing to do
 }
 
+inline void FreezeBase::prepare_freeze_interpreted_top_frame(const frame& f) {
+  // nothing to do
+  DEBUG_ONLY( intptr_t* lspp = (intptr_t*) &(f.get_ijava_state()->top_frame_sp); )
+  assert(*lspp == f.unextended_sp() - f.fp(), "should be " INTPTR_FORMAT " usp:" INTPTR_FORMAT " fp:" INTPTR_FORMAT, *lspp, p2i(f.unextended_sp()), p2i(f.fp()));
+}
+
 inline void FreezeBase::relativize_interpreted_frame_metadata(const frame& f, const frame& hf) {
   intptr_t* vfp = f.fp();
   intptr_t* hfp = hf.fp();
   assert(f.fp() > (intptr_t*)f.interpreter_frame_esp(), "");
 
@@ -348,10 +354,11 @@
     // see ThawBase::patch_return() which gets called just before
     bool is_entry_frame = pc == StubRoutines::cont_returnBarrier() || pc == _cont.entryPC();
     if (is_entry_frame) {
       callers_sp = _cont.entryFP();
     } else {
+      assert(!Interpreter::contains(pc), "sp:" PTR_FORMAT " pc:" PTR_FORMAT, p2i(sp), p2i(pc));
       CodeBlob* cb = CodeCache::find_blob_fast(pc);
       callers_sp = sp + cb->frame_size();
     }
     // set the back link
     ((frame::java_abi*) sp)->callers_sp = (intptr_t) callers_sp;
@@ -478,12 +485,12 @@
 //  if there are no stackargs. This is to comply with shared code (see e.g. StackChunkFrameStream::frame_size())
 //
 template<typename FKind> frame ThawBase::new_stack_frame(const frame& hf, frame& caller, bool bottom) {
   assert(FKind::is_instance(hf), "");
 
-  assert(is_aligned(caller.fp(), frame::frame_alignment), "");
-  assert(is_aligned(caller.sp(), frame::frame_alignment), "");
+  assert(is_aligned(caller.fp(), frame::frame_alignment), PTR_FORMAT, p2i(caller.fp()));
+  // caller.sp() can be unaligned. This is fixed below.
   if (FKind::interpreted) {
     // Note: we have to overlap with the caller, at least if it is interpreted, to match the
     // max_thawing_size calculation during freeze. See also comment above.
     intptr_t* heap_sp = hf.unextended_sp();
     const int fsize = ContinuationHelper::InterpretedFrame::frame_bottom(hf) - hf.unextended_sp();
@@ -508,11 +515,11 @@
     *f.addr_at(ijava_idx(locals)) = *hf.addr_at(ijava_idx(locals));
 
     return f;
   } else {
     int fsize = FKind::size(hf);
-    int argsize = hf.compiled_frame_stack_argsize();
+    int argsize = FKind::stack_argsize(hf);
     intptr_t* frame_sp = caller.sp() - fsize;
 
     if ((bottom && argsize > 0) || caller.is_interpreted_frame()) {
       frame_sp -= argsize + frame::metadata_words_at_top;
       frame_sp = align_down(frame_sp, frame::alignment_in_bytes);
@@ -541,16 +548,38 @@
   assert(f.at_absolute(ijava_idx(esp)) <= f.at_absolute(ijava_idx(monitors)), "");
 
   // Keep top_frame_sp relativized.
 }
 
+inline intptr_t* ThawBase::possibly_adjust_frame(frame& top) {
+  // Nothing to do
+  return top.sp();
+}
+
+inline intptr_t* ThawBase::push_cleanup_continuation() {
+  frame enterSpecial = new_entry_frame();
+  frame::common_abi* enterSpecial_abi = (frame::common_abi*)enterSpecial.sp();
+
+  enterSpecial_abi->lr = (intptr_t)ContinuationEntry::cleanup_pc();
+
+  log_develop_trace(continuations, preempt)("push_cleanup_continuation enterSpecial sp: " INTPTR_FORMAT " cleanup pc: " INTPTR_FORMAT,
+                                            p2i(enterSpecial_abi),
+                                            p2i(ContinuationEntry::cleanup_pc()));
+
+  return enterSpecial.sp();
+}
+
 inline void ThawBase::patch_pd(frame& f, const frame& caller) {
   patch_callee_link(caller, caller.fp());
   // Prevent assertion if f gets deoptimized right away before it's fully initialized
   f.mark_not_fully_initialized();
 }
 
+inline void ThawBase::patch_pd(frame& f, intptr_t* caller_sp) {
+  assert(f.own_abi()->callers_sp == (uint64_t)caller_sp, "should have been fixed by patch_caller_links");
+}
+
 //
 // Interpreter Calling Procedure on PPC
 //
 // Caller                                   Resized Caller before the Call                New Callee Frame
 //
diff a/src/hotspot/cpu/ppc/continuationHelper_ppc.inline.hpp b/src/hotspot/cpu/ppc/continuationHelper_ppc.inline.hpp
--- a/src/hotspot/cpu/ppc/continuationHelper_ppc.inline.hpp
+++ b/src/hotspot/cpu/ppc/continuationHelper_ppc.inline.hpp
@@ -25,14 +25,22 @@
 #ifndef CPU_PPC_CONTINUATIONHELPER_PPC_INLINE_HPP
 #define CPU_PPC_CONTINUATIONHELPER_PPC_INLINE_HPP
 
 #include "runtime/continuationHelper.hpp"
 
-template<typename FKind>
-static inline intptr_t** link_address(const frame& f) {
-  Unimplemented();
-  return nullptr;
+static inline void patch_return_pc_with_preempt_stub(frame& f) {
+  if (f.is_runtime_frame()) {
+    // Patch the pc of the now old last Java frame (we already set the anchor to enterSpecial)
+    // so that when target goes back to Java it will actually return to the preempt cleanup stub.
+    frame::common_abi* abi = (frame::common_abi*)f.sp();
+    abi->lr = (uint64_t)StubRoutines::cont_preempt_stub();
+  } else {
+    // The target will check for preemption once it returns to the interpreter
+    // or the native wrapper code and will manually jump to the preempt stub.
+    JavaThread *thread = JavaThread::current();
+    thread->set_preempt_alternate_return(StubRoutines::cont_preempt_stub());
+  }
 }
 
 inline int ContinuationHelper::frame_align_words(int size) {
   return size & 1;
 }
@@ -57,15 +65,15 @@
 
 inline void ContinuationHelper::set_anchor_to_entry_pd(JavaFrameAnchor* anchor, ContinuationEntry* cont) {
   // nothing to do
 }
 
-#ifdef ASSERT
 inline void ContinuationHelper::set_anchor_pd(JavaFrameAnchor* anchor, intptr_t* sp) {
   // nothing to do
 }
 
+#ifdef ASSERT
 inline bool ContinuationHelper::Frame::assert_frame_laid_out(frame f) {
   intptr_t* sp = f.sp();
   address pc = *(address*)(sp - frame::sender_sp_ret_address_offset());
   intptr_t* fp = (intptr_t*)f.own_abi()->callers_sp;
   assert(f.raw_pc() == pc, "f.ra_pc: " INTPTR_FORMAT " actual: " INTPTR_FORMAT, p2i(f.raw_pc()), p2i(pc));
diff a/src/hotspot/cpu/ppc/frame_ppc.inline.hpp b/src/hotspot/cpu/ppc/frame_ppc.inline.hpp
--- a/src/hotspot/cpu/ppc/frame_ppc.inline.hpp
+++ b/src/hotspot/cpu/ppc/frame_ppc.inline.hpp
@@ -51,11 +51,14 @@
 
   if (_fp == nullptr) {
     // The back link for compiled frames on the heap is not valid
     if (is_heap_frame()) {
       // fp for interpreted frames should have been derelativized and passed to the constructor
-      assert(is_compiled_frame(), "");
+      assert(is_compiled_frame()
+             || is_native_frame()   // native wrapper (nmethod) for j.l.Object::wait0
+             || is_runtime_frame(), // e.g. Runtime1::monitorenter, SharedRuntime::complete_monitor_locking_C
+             "sp:" PTR_FORMAT " fp:" PTR_FORMAT " name:%s", p2i(_sp), p2i(_unextended_sp + _cb->frame_size()), _cb->name());
       // The back link for compiled frames on the heap is invalid.
       _fp = _unextended_sp + _cb->frame_size();
     } else {
       _fp = (intptr_t*)own_abi()->callers_sp;
     }
diff a/src/hotspot/cpu/ppc/interp_masm_ppc.hpp b/src/hotspot/cpu/ppc/interp_masm_ppc.hpp
--- a/src/hotspot/cpu/ppc/interp_masm_ppc.hpp
+++ b/src/hotspot/cpu/ppc/interp_masm_ppc.hpp
@@ -47,10 +47,18 @@
 #define method_(field_name) in_bytes(Method::field_name ## _offset()), R19_method
 
   virtual void check_and_handle_popframe(Register scratch_reg);
   virtual void check_and_handle_earlyret(Register scratch_reg);
 
+  void call_VM_preemptable(Register oop_result, address entry_point, Register arg_1, bool check_exceptions = true);
+  void restore_after_resume(Register fp);
+  // R22 and R31 are preserved when a vthread gets preempted in the interpreter.
+  // The interpreter already assumes that these registers are nonvolatile across native calls.
+  bool nonvolatile_accross_vthread_preemtion(Register r) const {
+    return r->is_nonvolatile() && ((r == R22) || (r == R31));
+  }
+
   // Base routine for all dispatches.
   void dispatch_base(TosState state, address* table);
 
   void load_earlyret_value(TosState state, Register Rscratch1);
 
@@ -180,11 +188,11 @@
 
   // Call VM for std frames
   // Special call VM versions that check for exceptions and forward exception
   // via short cut (not via expensive forward exception stub).
   void check_and_forward_exception(Register Rscratch1, Register Rscratch2);
-  void call_VM(Register oop_result, address entry_point, bool check_exceptions = true);
+  void call_VM(Register oop_result, address entry_point, bool check_exceptions = true, Label* last_java_pc = nullptr);
   void call_VM(Register oop_result, address entry_point, Register arg_1, bool check_exceptions = true);
   void call_VM(Register oop_result, address entry_point, Register arg_1, Register arg_2, bool check_exceptions = true);
   void call_VM(Register oop_result, address entry_point, Register arg_1, Register arg_2, Register arg_3, bool check_exceptions = true);
   // Should not be used:
   void call_VM(Register oop_result, Register last_java_sp, address entry_point, bool check_exceptions = true) {ShouldNotReachHere();}
diff a/src/hotspot/cpu/ppc/interp_masm_ppc_64.cpp b/src/hotspot/cpu/ppc/interp_masm_ppc_64.cpp
--- a/src/hotspot/cpu/ppc/interp_masm_ppc_64.cpp
+++ b/src/hotspot/cpu/ppc/interp_masm_ppc_64.cpp
@@ -930,11 +930,11 @@
 //             which must be initialized with the object to lock.
 //   object  - Address of the object to be locked.
 //
 void InterpreterMacroAssembler::lock_object(Register monitor, Register object) {
   if (LockingMode == LM_MONITOR) {
-    call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter), monitor);
+    call_VM_preemptable(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter), monitor);
   } else {
     // template code (for LM_LEGACY):
     //
     // markWord displaced_header = obj->mark().set_unlocked();
     // monitor->lock()->set_displaced_header(displaced_header);
@@ -951,12 +951,11 @@
     const Register header           = R7_ARG5;
     const Register object_mark_addr = R8_ARG6;
     const Register current_header   = R9_ARG7;
     const Register tmp              = R10_ARG8;
 
-    Label count_locking, done;
-    Label cas_failed, slow_case;
+    Label count_locking, done, slow_case, cas_failed;
 
     assert_different_registers(header, object_mark_addr, current_header, tmp);
 
     // markWord displaced_header = obj->mark().set_unlocked();
 
@@ -967,11 +966,11 @@
       bne(CCR0, slow_case);
     }
 
     if (LockingMode == LM_LIGHTWEIGHT) {
       lightweight_lock(monitor, object, header, tmp, slow_case);
-      b(count_locking);
+      b(done);
     } else if (LockingMode == LM_LEGACY) {
       // Load markWord from object into header.
       ld(header, oopDesc::mark_offset_in_bytes(), object);
 
       // Set displaced_header to be (markWord of object | UNLOCK_VALUE).
@@ -1033,16 +1032,19 @@
     //   InterpreterRuntime::monitorenter(THREAD, monitor);
 
     // None of the above fast optimizations worked so we have to get into the
     // slow case of monitor enter.
     bind(slow_case);
-    call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter), monitor);
-    b(done);
+    call_VM_preemptable(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter), monitor);
     // }
-    align(32, 12);
-    bind(count_locking);
-    inc_held_monitor_count(current_header /*tmp*/);
+
+    if (LockingMode == LM_LEGACY) {
+      b(done);
+      align(32, 12);
+      bind(count_locking);
+      inc_held_monitor_count(current_header /*tmp*/);
+    }
     bind(done);
   }
 }
 
 // Unlocks an object. Used in monitorexit bytecode and remove_activation.
@@ -1135,11 +1137,13 @@
     // Exchange worked, do monitor->set_obj(nullptr);
     align(32, 12);
     bind(free_slot);
     li(R0, 0);
     std(R0, in_bytes(BasicObjectLock::obj_offset()), monitor);
-    dec_held_monitor_count(current_header /*tmp*/);
+    if (LockingMode == LM_LEGACY) {
+      dec_held_monitor_count(current_header /*tmp*/);
+    }
     bind(done);
   }
 }
 
 // Load compiled (i2c) or interpreter entry when calling from interpreted and
@@ -2131,14 +2135,14 @@
 
   align(32, 12);
   bind(Ldone);
 }
 
-void InterpreterMacroAssembler::call_VM(Register oop_result, address entry_point, bool check_exceptions) {
+void InterpreterMacroAssembler::call_VM(Register oop_result, address entry_point, bool check_exceptions, Label* last_java_pc) {
   save_interpreter_state(R11_scratch1);
 
-  MacroAssembler::call_VM(oop_result, entry_point, false);
+  MacroAssembler::call_VM(oop_result, entry_point, false /*check_exceptions*/, last_java_pc);
 
   restore_interpreter_state(R11_scratch1, /*bcp_and_mdx_only*/ true);
 
   check_and_handle_popframe(R11_scratch1);
   check_and_handle_earlyret(R11_scratch1);
@@ -2153,10 +2157,78 @@
   // ARG1 is reserved for the thread.
   mr_if_needed(R4_ARG2, arg_1);
   call_VM(oop_result, entry_point, check_exceptions);
 }
 
+void InterpreterMacroAssembler::call_VM_preemptable(Register oop_result, address entry_point,
+                                        Register arg_1, bool check_exceptions) {
+  if (!Continuations::enabled()) {
+    call_VM(oop_result, entry_point, arg_1, check_exceptions);
+    return;
+  }
+
+  Label resume_pc, not_preempted;
+
+  DEBUG_ONLY(ld(R0, in_bytes(JavaThread::preempt_alternate_return_offset()), R16_thread));
+  DEBUG_ONLY(cmpdi(CCR0, R0, 0));
+  asm_assert_eq("Should not have alternate return address set");
+
+  // Preserve 2 registers
+  assert(nonvolatile_accross_vthread_preemtion(R31) && nonvolatile_accross_vthread_preemtion(R22), "");
+  ld(R3_ARG1, _abi0(callers_sp), R1_SP); // load FP
+  std(R31, _ijava_state_neg(lresult), R3_ARG1);
+  std(R22, _ijava_state_neg(fresult), R3_ARG1);
+
+  // We set resume_pc as last java pc. It will be saved if the vthread gets preempted.
+  // Later execution will continue right there.
+  mr_if_needed(R4_ARG2, arg_1);
+  push_cont_fastpath();
+  call_VM(oop_result, entry_point, false /*check_exceptions*/, &resume_pc /* last_java_pc */);
+  pop_cont_fastpath();
+
+  // Jump to handler if the call was preempted
+  ld(R0, in_bytes(JavaThread::preempt_alternate_return_offset()), R16_thread);
+  cmpdi(CCR0, R0, 0);
+  beq(CCR0, not_preempted);
+  mtlr(R0);
+  li(R0, 0);
+  std(R0, in_bytes(JavaThread::preempt_alternate_return_offset()), R16_thread);
+  blr();
+
+  bind(resume_pc); // Location to resume execution
+  restore_after_resume(noreg /* fp */);
+  bind(not_preempted);
+}
+
+void InterpreterMacroAssembler::restore_after_resume(Register fp) {
+  if (!Continuations::enabled()) return;
+
+  const address resume_adapter = TemplateInterpreter::cont_resume_interpreter_adapter();
+  add_const_optimized(R31, R29_TOC, MacroAssembler::offset_to_global_toc(resume_adapter));
+  mtctr(R31);
+  bctrl();
+  // Restore registers that are preserved across vthread preemption
+  assert(nonvolatile_accross_vthread_preemtion(R31) && nonvolatile_accross_vthread_preemtion(R22), "");
+  ld(R3_ARG1, _abi0(callers_sp), R1_SP); // load FP
+  ld(R31, _ijava_state_neg(lresult), R3_ARG1);
+  ld(R22, _ijava_state_neg(fresult), R3_ARG1);
+#ifdef ASSERT
+  // Assert FP is in R11_scratch1 (see generate_cont_resume_interpreter_adapter())
+  {
+    Label ok;
+    ld(R12_scratch2, 0, R1_SP);  // load fp
+    cmpd(CCR0, R12_scratch2, R11_scratch1);
+    beq(CCR0, ok);
+    stop(FILE_AND_LINE ": FP is expected in R11_scratch1");
+    bind(ok);
+  }
+#endif
+  if (fp != noreg && fp != R11_scratch1) {
+    mr(fp, R11_scratch1);
+  }
+}
+
 void InterpreterMacroAssembler::call_VM(Register oop_result, address entry_point,
                                         Register arg_1, Register arg_2,
                                         bool check_exceptions) {
   // ARG1 is reserved for the thread.
   mr_if_needed(R4_ARG2, arg_1);
diff a/src/hotspot/cpu/ppc/macroAssembler_ppc.cpp b/src/hotspot/cpu/ppc/macroAssembler_ppc.cpp
--- a/src/hotspot/cpu/ppc/macroAssembler_ppc.cpp
+++ b/src/hotspot/cpu/ppc/macroAssembler_ppc.cpp
@@ -29,10 +29,11 @@
 #include "compiler/disassembler.hpp"
 #include "gc/shared/collectedHeap.inline.hpp"
 #include "gc/shared/barrierSet.hpp"
 #include "gc/shared/barrierSetAssembler.hpp"
 #include "interpreter/interpreter.hpp"
+#include "interpreter/interpreterRuntime.hpp"
 #include "memory/resourceArea.hpp"
 #include "nativeInst_ppc.hpp"
 #include "oops/compressedKlass.inline.hpp"
 #include "oops/compressedOops.inline.hpp"
 #include "oops/klass.inline.hpp"
@@ -113,11 +114,12 @@
   if (is_aligned(offset() + BytesPerInstWord, 64)) { nop(); }
 }
 
 // Issue instructions that calculate given TOC from global TOC.
 void MacroAssembler::calculate_address_from_global_toc(Register dst, address addr, bool hi16, bool lo16,
-                                                       bool add_relocation, bool emit_dummy_addr) {
+                                                       bool add_relocation, bool emit_dummy_addr,
+                                                       bool add_addr_to_reloc) {
   int offset = -1;
   if (emit_dummy_addr) {
     offset = -128; // dummy address
   } else if (addr != (address)(intptr_t)-1) {
     offset = MacroAssembler::offset_to_global_toc(addr);
@@ -127,11 +129,14 @@
     addis(dst, R29_TOC, MacroAssembler::largeoffset_si16_si16_hi(offset));
   }
   if (lo16) {
     if (add_relocation) {
       // Relocate at the addi to avoid confusion with a load from the method's TOC.
-      relocate(internal_word_Relocation::spec(addr));
+      RelocationHolder rh = add_addr_to_reloc ?
+          internal_word_Relocation::spec(addr) :
+          internal_word_Relocation::spec_for_immediate();
+      relocate(rh);
     }
     addi(dst, dst, MacroAssembler::largeoffset_si16_si16_lo(offset));
   }
 }
 
@@ -712,10 +717,11 @@
     ShouldNotReachHere();
     return nullptr;
   }
 }
 
+#ifdef ASSERT
 void MacroAssembler::clobber_volatile_gprs(Register excluded_register) {
   const int magic_number = 0x42;
 
   // Preserve stack pointer register (R1_SP) and system thread id register (R13);
   // although they're technically volatile
@@ -727,10 +733,41 @@
 
     li(reg, magic_number);
   }
 }
 
+void MacroAssembler::clobber_nonvolatile_registers() {
+  BLOCK_COMMENT("clobber nonvolatile registers {");
+  Register regs[] = {
+      R14,
+      R15,
+      // don't zap R16_thread
+      R17,
+      R18,
+      R19,
+      R20,
+      R21,
+      R22,
+      R23,
+      R24,
+      R25,
+      R26,
+      R27,
+      R28,
+      // don't zap R29_TOC
+      R30,
+      R31
+  };
+  Register bad = regs[0];
+  load_const_optimized(bad, 0xbad0101babe11111);
+  for (uint32_t i = 1; i < (sizeof(regs) / sizeof(Register)); i++) {
+    mr(regs[i], bad);
+  }
+  BLOCK_COMMENT("} clobber nonvolatile registers");
+}
+#endif // ASSERT
+
 void MacroAssembler::clobber_carg_stack_slots(Register tmp) {
   const int magic_number = 0x43;
 
   li(tmp, magic_number);
   for (int m = 0; m <= 7; m++) {
@@ -1281,17 +1318,18 @@
 }
 
 void MacroAssembler::call_VM_base(Register oop_result,
                                   Register last_java_sp,
                                   address  entry_point,
-                                  bool     check_exceptions) {
+                                  bool     check_exceptions,
+                                  Label*   last_java_pc) {
   BLOCK_COMMENT("call_VM {");
   // Determine last_java_sp register.
   if (!last_java_sp->is_valid()) {
     last_java_sp = R1_SP;
   }
-  set_top_ijava_frame_at_SP_as_last_Java_frame(last_java_sp, R11_scratch1);
+  set_top_ijava_frame_at_SP_as_last_Java_frame(last_java_sp, R11_scratch1, last_java_pc);
 
   // ARG1 must hold thread address.
   mr(R3_ARG1, R16_thread);
   address return_pc = call_c(entry_point, relocInfo::none);
 
@@ -1316,12 +1354,12 @@
   BLOCK_COMMENT("call_VM_leaf {");
   call_c(entry_point);
   BLOCK_COMMENT("} call_VM_leaf");
 }
 
-void MacroAssembler::call_VM(Register oop_result, address entry_point, bool check_exceptions) {
-  call_VM_base(oop_result, noreg, entry_point, check_exceptions);
+void MacroAssembler::call_VM(Register oop_result, address entry_point, bool check_exceptions, Label* last_java_pc) {
+  call_VM_base(oop_result, noreg, entry_point, check_exceptions, last_java_pc);
 }
 
 void MacroAssembler::call_VM(Register oop_result, address entry_point, Register arg_1,
                              bool check_exceptions) {
   // R3_ARG1 is reserved for the thread.
@@ -2623,36 +2661,40 @@
   // The object's monitor m is unlocked iff m->owner is null,
   // otherwise m->owner may contain a thread or a stack address.
 
   // Try to CAS m->owner from null to current thread.
   addi(temp, displaced_header, in_bytes(ObjectMonitor::owner_offset()) - markWord::monitor_value);
+  Register thread_id = displaced_header;
+  ld(thread_id, in_bytes(JavaThread::lock_id_offset()), R16_thread);
   cmpxchgd(/*flag=*/flag,
            /*current_value=*/current_header,
            /*compare_value=*/(intptr_t)0,
-           /*exchange_value=*/R16_thread,
+           /*exchange_value=*/thread_id,
            /*where=*/temp,
            MacroAssembler::MemBarRel | MacroAssembler::MemBarAcq,
            MacroAssembler::cmpxchgx_hint_acquire_lock());
 
   // Store a non-null value into the box.
   std(box, BasicLock::displaced_header_offset_in_bytes(), box);
   beq(flag, success);
 
   // Check for recursive locking.
-  cmpd(flag, current_header, R16_thread);
+  cmpd(flag, current_header, thread_id);
   bne(flag, failure);
 
   // Current thread already owns the lock. Just increment recursions.
   Register recursions = displaced_header;
   ld(recursions, in_bytes(ObjectMonitor::recursions_offset() - ObjectMonitor::owner_offset()), temp);
   addi(recursions, recursions, 1);
   std(recursions, in_bytes(ObjectMonitor::recursions_offset() - ObjectMonitor::owner_offset()), temp);
 
-  // flag == EQ indicates success, increment held monitor count
+  // flag == EQ indicates success, increment held monitor count if LM_LEGACY is enabled
   // flag == NE indicates failure
   bind(success);
-  inc_held_monitor_count(temp);
+  if (LockingMode == LM_LEGACY) {
+    inc_held_monitor_count(temp);
+  }
 #ifdef ASSERT
   // Check that unlocked label is reached with flag == EQ.
   Label flag_correct;
   beq(flag, flag_correct);
   stop("compiler_fast_lock_object: Flag != EQ");
@@ -2668,11 +2710,11 @@
 
 void MacroAssembler::compiler_fast_unlock_object(ConditionRegister flag, Register oop, Register box,
                                                  Register temp, Register displaced_header, Register current_header) {
   assert(LockingMode != LM_LIGHTWEIGHT, "uses fast_unlock_lightweight");
   assert_different_registers(oop, box, temp, displaced_header, current_header);
-  Label success, failure, object_has_monitor, notRecursive;
+  Label success, failure, object_has_monitor, not_recursive;
 
   if (LockingMode == LM_LEGACY) {
     // Find the lock address and load the displaced header from the stack.
     ld(displaced_header, BasicLock::displaced_header_offset_in_bytes(), box);
 
@@ -2714,20 +2756,20 @@
   STATIC_ASSERT(markWord::monitor_value <= INT_MAX);
   addi(current_header, current_header, -(int)markWord::monitor_value); // monitor
 
   ld(displaced_header, in_bytes(ObjectMonitor::recursions_offset()), current_header);
   addic_(displaced_header, displaced_header, -1);
-  blt(CCR0, notRecursive); // Not recursive if negative after decrement.
+  blt(CCR0, not_recursive); // Not recursive if negative after decrement.
 
   // Recursive unlock
   std(displaced_header, in_bytes(ObjectMonitor::recursions_offset()), current_header);
   if (flag == CCR0) { // Otherwise, flag is already EQ, here.
     crorc(CCR0, Assembler::equal, CCR0, Assembler::equal); // Set CCR0 EQ
   }
   b(success);
 
-  bind(notRecursive);
+  bind(not_recursive);
 
   // Set owner to null.
   // Release to satisfy the JMM
   release();
   li(temp, 0);
@@ -2753,14 +2795,16 @@
   // Save the monitor pointer in the current thread, so we can try
   // to reacquire the lock in SharedRuntime::monitor_exit_helper().
   std(current_header, in_bytes(JavaThread::unlocked_inflated_monitor_offset()), R16_thread);
   b(failure); // flag == NE
 
-  // flag == EQ indicates success, decrement held monitor count
+  // flag == EQ indicates success, decrement held monitor count if LM_LEGACY is enabled
   // flag == NE indicates failure
   bind(success);
-  dec_held_monitor_count(temp);
+  if (LockingMode == LM_LEGACY) {
+    dec_held_monitor_count(temp);
+  }
 #ifdef ASSERT
   // Check that unlocked label is reached with flag == EQ.
   Label flag_correct;
   beq(flag, flag_correct);
   stop("compiler_fast_unlock_object: Flag != EQ");
@@ -2775,10 +2819,11 @@
 }
 
 void MacroAssembler::compiler_fast_lock_lightweight_object(ConditionRegister flag, Register obj, Register box,
                                                            Register tmp1, Register tmp2, Register tmp3) {
   assert_different_registers(obj, box, tmp1, tmp2, tmp3);
+  assert(UseObjectMonitorTable || tmp3 == noreg, "tmp3 not needed");
   assert(flag == CCR0, "bad condition register");
 
   // Handle inflated monitor.
   Label inflated;
   // Finish fast lock successfully. MUST reach to with flag == NE
@@ -2797,12 +2842,11 @@
     lbz(tmp1, in_bytes(Klass::misc_flags_offset()), tmp1);
     testbitdi(CCR0, R0, tmp1, exact_log2(KlassFlags::_misc_is_value_based_class));
     bne(CCR0, slow_path);
   }
 
-  const Register mark = tmp1;
-  const Register t = tmp3; // Usage of R0 allowed!
+  Register mark = tmp1;
 
   { // Lightweight locking
 
     // Push lock to the lock stack and finish successfully. MUST reach to with flag == EQ
     Label push;
@@ -2816,19 +2860,19 @@
 
     // The underflow check is elided. The recursive check will always fail
     // when the lock stack is empty because of the _bad_oop_sentinel field.
 
     // Check if recursive.
-    subi(t, top, oopSize);
-    ldx(t, R16_thread, t);
-    cmpd(CCR0, obj, t);
+    subi(R0, top, oopSize);
+    ldx(R0, R16_thread, R0);
+    cmpd(CCR0, obj, R0);
     beq(CCR0, push);
 
     // Check for monitor (0b10) or locked (0b00).
     ld(mark, oopDesc::mark_offset_in_bytes(), obj);
-    andi_(t, mark, markWord::lock_mask_in_place);
-    cmpldi(CCR0, t, markWord::unlocked_value);
+    andi_(R0, mark, markWord::lock_mask_in_place);
+    cmpldi(CCR0, R0, markWord::unlocked_value);
     bgt(CCR0, inflated);
     bne(CCR0, slow_path);
 
     // Not inflated.
 
@@ -2847,45 +2891,47 @@
   { // Handle inflated monitor.
     bind(inflated);
 
     // mark contains the tagged ObjectMonitor*.
     const uintptr_t monitor_tag = markWord::monitor_value;
-    const Register monitor = mark;
+    const Register monitor    = UseObjectMonitorTable ? tmp1 : noreg;
     const Register owner_addr = tmp2;
+    const Register thread_id  = UseObjectMonitorTable ? tmp3 : tmp1;
     Label monitor_locked;
 
     if (!UseObjectMonitorTable) {
       // Compute owner address.
       addi(owner_addr, mark, in_bytes(ObjectMonitor::owner_offset()) - monitor_tag);
+      mark = noreg;
     } else {
       Label monitor_found;
       Register cache_addr = tmp2;
 
       // Load cache address
       addi(cache_addr, R16_thread, in_bytes(JavaThread::om_cache_oops_offset()));
 
       const int num_unrolled = 2;
       for (int i = 0; i < num_unrolled; i++) {
-        ld(tmp3, 0, cache_addr);
-        cmpd(CCR0, tmp3, obj);
+        ld(R0, 0, cache_addr);
+        cmpd(CCR0, R0, obj);
         beq(CCR0, monitor_found);
         addi(cache_addr, cache_addr, in_bytes(OMCache::oop_to_oop_difference()));
       }
 
       Label loop;
 
       // Search for obj in cache.
       bind(loop);
 
       // Check for match.
-      ld(tmp3, 0, cache_addr);
-      cmpd(CCR0, tmp3, obj);
+      ld(R0, 0, cache_addr);
+      cmpd(CCR0, R0, obj);
       beq(CCR0, monitor_found);
 
       // Search until null encountered, guaranteed _null_sentinel at end.
       addi(cache_addr, cache_addr, in_bytes(OMCache::oop_to_oop_difference()));
-      cmpdi(CCR1, tmp3, 0);
+      cmpdi(CCR1, R0, 0);
       bne(CCR1, loop);
       // Cache Miss, CCR0.NE set from cmp above
       b(slow_path);
 
       bind(monitor_found);
@@ -2893,22 +2939,24 @@
 
       // Compute owner address.
       addi(owner_addr, monitor, in_bytes(ObjectMonitor::owner_offset()));
     }
 
-    // CAS owner (null => current thread).
+    // CAS owner (null => current thread id).
+    assert_different_registers(thread_id, monitor, owner_addr, box, R0);
+    ld(thread_id, in_bytes(JavaThread::lock_id_offset()), R16_thread);
     cmpxchgd(/*flag=*/CCR0,
-            /*current_value=*/t,
+            /*current_value=*/R0,
             /*compare_value=*/(intptr_t)0,
-            /*exchange_value=*/R16_thread,
+            /*exchange_value=*/thread_id,
             /*where=*/owner_addr,
             MacroAssembler::MemBarRel | MacroAssembler::MemBarAcq,
             MacroAssembler::cmpxchgx_hint_acquire_lock());
     beq(CCR0, monitor_locked);
 
     // Check if recursive.
-    cmpd(CCR0, t, R16_thread);
+    cmpd(CCR0, R0, thread_id);
     bne(CCR0, slow_path);
 
     // Recursive.
     if (!UseObjectMonitorTable) {
       assert_different_registers(tmp1, owner_addr);
@@ -2927,11 +2975,10 @@
       std(monitor, BasicLock::object_monitor_cache_offset_in_bytes(), box);
     }
   }
 
   bind(locked);
-  inc_held_monitor_count(tmp1);
 
 #ifdef ASSERT
   // Check that locked label is reached with flag == EQ.
   Label flag_correct;
   beq(CCR0, flag_correct);
@@ -3102,11 +3149,10 @@
     std(monitor, in_bytes(JavaThread::unlocked_inflated_monitor_offset()), R16_thread);
     b(slow_path); // flag == NE
   }
 
   bind(unlocked);
-  dec_held_monitor_count(t);
 
 #ifdef ASSERT
   // Check that unlocked label is reached with flag == EQ.
   Label flag_correct;
   beq(CCR0, flag_correct);
@@ -3184,13 +3230,15 @@
 
   // Set last_Java_sp last.
   std(last_Java_sp, in_bytes(JavaThread::last_Java_sp_offset()), R16_thread);
 }
 
-void MacroAssembler::reset_last_Java_frame(void) {
-  asm_assert_mem8_isnot_zero(in_bytes(JavaThread::last_Java_sp_offset()),
-                             R16_thread, "SP was not set, still zero");
+void MacroAssembler::reset_last_Java_frame(bool check_last_java_sp) {
+  if (check_last_java_sp) {
+    asm_assert_mem8_isnot_zero(in_bytes(JavaThread::last_Java_sp_offset()),
+                               R16_thread, "SP was not set, still zero");
+  }
 
   BLOCK_COMMENT("reset_last_Java_frame {");
   li(R0, 0);
 
   // _last_Java_sp = 0
@@ -3199,18 +3247,18 @@
   // _last_Java_pc = 0
   std(R0, in_bytes(JavaThread::last_Java_pc_offset()), R16_thread);
   BLOCK_COMMENT("} reset_last_Java_frame");
 }
 
-void MacroAssembler::set_top_ijava_frame_at_SP_as_last_Java_frame(Register sp, Register tmp1) {
+void MacroAssembler::set_top_ijava_frame_at_SP_as_last_Java_frame(Register sp, Register tmp1, Label* jpc) {
   assert_different_registers(sp, tmp1);
 
-  // sp points to a TOP_IJAVA_FRAME, retrieve frame's PC via
-  // TOP_IJAVA_FRAME_ABI.
-  // FIXME: assert that we really have a TOP_IJAVA_FRAME here!
-  address entry = pc();
-  load_const_optimized(tmp1, entry);
+  if (jpc == nullptr || jpc->is_bound()) {
+    load_const_optimized(tmp1, jpc == nullptr ? pc() : target(*jpc));
+  } else {
+    load_const(tmp1, *jpc, R12_scratch2);
+  }
 
   set_last_Java_frame(/*sp=*/sp, /*pc=*/tmp1);
 }
 
 void MacroAssembler::get_vm_result(Register oop_result) {
@@ -4476,13 +4524,13 @@
   stop(msg);
   bind(ok);
 #endif
 }
 
+#ifdef ASSERT
 void MacroAssembler::asm_assert_mems_zero(bool check_equal, int size, int mem_offset,
                                           Register mem_base, const char* msg) {
-#ifdef ASSERT
   switch (size) {
     case 4:
       lwz(R0, mem_offset, mem_base);
       cmpwi(CCR0, R0, 0);
       break;
@@ -4492,12 +4540,12 @@
       break;
     default:
       ShouldNotReachHere();
   }
   asm_assert(check_equal, msg);
-#endif // ASSERT
 }
+#endif // ASSERT
 
 void MacroAssembler::verify_coop(Register coop, const char* msg) {
   if (!VerifyOops) { return; }
   if (UseCompressedOops) { decode_heap_oop(coop); }
   verify_oop(coop, msg);
@@ -4654,19 +4702,23 @@
     fence();
   }
 }
 
 void MacroAssembler::push_cont_fastpath() {
+  if (!Continuations::enabled()) return;
+
   Label done;
   ld_ptr(R0, JavaThread::cont_fastpath_offset(), R16_thread);
   cmpld(CCR0, R1_SP, R0);
   ble(CCR0, done);
   st_ptr(R1_SP, JavaThread::cont_fastpath_offset(), R16_thread);
   bind(done);
 }
 
 void MacroAssembler::pop_cont_fastpath() {
+  if (!Continuations::enabled()) return;
+
   Label done;
   ld_ptr(R0, JavaThread::cont_fastpath_offset(), R16_thread);
   cmpld(CCR0, R1_SP, R0);
   ble(CCR0, done);
   li(R0, 0);
@@ -4674,10 +4726,11 @@
   bind(done);
 }
 
 // Note: Must preserve CCR0 EQ (invariant).
 void MacroAssembler::inc_held_monitor_count(Register tmp) {
+  assert(LockingMode == LM_LEGACY, "");
   ld(tmp, in_bytes(JavaThread::held_monitor_count_offset()), R16_thread);
 #ifdef ASSERT
   Label ok;
   cmpdi(CCR0, tmp, 0);
   bge_predict_taken(CCR0, ok);
@@ -4689,10 +4742,11 @@
   std(tmp, in_bytes(JavaThread::held_monitor_count_offset()), R16_thread);
 }
 
 // Note: Must preserve CCR0 EQ (invariant).
 void MacroAssembler::dec_held_monitor_count(Register tmp) {
+  assert(LockingMode == LM_LEGACY, "");
   ld(tmp, in_bytes(JavaThread::held_monitor_count_offset()), R16_thread);
 #ifdef ASSERT
   Label ok;
   cmpdi(CCR0, tmp, 0);
   bgt_predict_taken(CCR0, ok);
diff a/src/hotspot/cpu/ppc/macroAssembler_ppc.hpp b/src/hotspot/cpu/ppc/macroAssembler_ppc.hpp
--- a/src/hotspot/cpu/ppc/macroAssembler_ppc.hpp
+++ b/src/hotspot/cpu/ppc/macroAssembler_ppc.hpp
@@ -113,11 +113,17 @@
   inline int offset_to_method_toc(const address addr);
 
   // Global TOC.
   void calculate_address_from_global_toc(Register dst, address addr,
                                          bool hi16 = true, bool lo16 = true,
-                                         bool add_relocation = true, bool emit_dummy_addr = false);
+                                         bool add_relocation = true, bool emit_dummy_addr = false,
+                                         bool add_addr_to_reloc = true);
+  void calculate_address_from_global_toc(Register dst, Label& addr,
+                                         bool hi16 = true, bool lo16 = true,
+                                         bool add_relocation = true, bool emit_dummy_addr = false) {
+    calculate_address_from_global_toc(dst, target(addr), hi16, lo16, add_relocation, emit_dummy_addr, false);
+  }
   inline void calculate_address_from_global_toc_hi16only(Register dst, address addr) {
     calculate_address_from_global_toc(dst, addr, true, false);
   };
   inline void calculate_address_from_global_toc_lo16only(Register dst, address addr) {
     calculate_address_from_global_toc(dst, addr, false, true);
@@ -282,11 +288,14 @@
   // some ABI-related functions
 
   // Clobbers all volatile, (non-floating-point) general-purpose registers for debugging purposes.
   // This is especially useful for making calls to the JRT in places in which this hasn't been done before;
   // e.g. with the introduction of LRBs (load reference barriers) for concurrent garbage collection.
-  void clobber_volatile_gprs(Register excluded_register = noreg);
+  void clobber_volatile_gprs(Register excluded_register = noreg) NOT_DEBUG_RETURN;
+  // Load bad values into registers that are nonvolatile according to the ABI except R16_thread and R29_TOC.
+  // This is done after vthread preemption and before vthread resume.
+  void clobber_nonvolatile_registers() NOT_DEBUG_RETURN;
   void clobber_carg_stack_slots(Register tmp);
 
   void save_nonvolatile_gprs(   Register dst_base, int offset);
   void restore_nonvolatile_gprs(Register src_base, int offset);
 
@@ -396,11 +405,12 @@
     // to set up last_Java_frame in stubs; use noreg otherwise
     Register        last_java_sp,
     // the entry point
     address         entry_point,
     // flag which indicates if exception should be checked
-    bool            check_exception = true
+    bool            check_exception = true,
+    Label* last_java_pc = nullptr
   );
 
   // Support for VM calls. This is the base routine called by the
   // different versions of call_VM_leaf. The interpreter may customize
   // this version by overriding it for its purposes (e.g., to
@@ -409,11 +419,11 @@
 
  public:
   // Call into the VM.
   // Passes the thread pointer (in R3_ARG1) as a prepended argument.
   // Makes sure oop return values are visible to the GC.
-  void call_VM(Register oop_result, address entry_point, bool check_exceptions = true);
+  void call_VM(Register oop_result, address entry_point, bool check_exceptions = true, Label* last_java_pc = nullptr);
   void call_VM(Register oop_result, address entry_point, Register arg_1, bool check_exceptions = true);
   void call_VM(Register oop_result, address entry_point, Register arg_1, Register arg_2, bool check_exceptions = true);
   void call_VM(Register oop_result, address entry_point, Register arg_1, Register arg_2, Register arg3, bool check_exceptions = true);
   void call_VM_leaf(address entry_point);
   void call_VM_leaf(address entry_point, Register arg_1);
@@ -693,12 +703,12 @@
   // thread-local information).
 
   // Support for last Java frame (but use call_VM instead where possible):
   // access R16_thread->last_Java_sp.
   void set_last_Java_frame(Register last_java_sp, Register last_Java_pc);
-  void reset_last_Java_frame(void);
-  void set_top_ijava_frame_at_SP_as_last_Java_frame(Register sp, Register tmp1);
+  void reset_last_Java_frame(bool check_last_java_sp = true);
+  void set_top_ijava_frame_at_SP_as_last_Java_frame(Register sp, Register tmp1, Label* jpc = nullptr);
 
   // Read vm result from thread: oop_result = R16_thread->result;
   void get_vm_result  (Register oop_result);
   void get_vm_result_2(Register metadata_result);
 
@@ -907,11 +917,11 @@
   void asm_assert_eq(const char* msg) { asm_assert(true, msg); }
   void asm_assert_ne(const char* msg) { asm_assert(false, msg); }
 
  private:
   void asm_assert_mems_zero(bool check_equal, int size, int mem_offset, Register mem_base,
-                            const char* msg);
+                            const char* msg) NOT_DEBUG_RETURN;
 
  public:
 
   void asm_assert_mem8_is_zero(int mem_offset, Register mem_base, const char* msg) {
     asm_assert_mems_zero(true,  8, mem_offset, mem_base, msg);
diff a/src/hotspot/cpu/ppc/macroAssembler_ppc.inline.hpp b/src/hotspot/cpu/ppc/macroAssembler_ppc.inline.hpp
--- a/src/hotspot/cpu/ppc/macroAssembler_ppc.inline.hpp
+++ b/src/hotspot/cpu/ppc/macroAssembler_ppc.inline.hpp
@@ -189,12 +189,22 @@
   assert(obj_addr.rspec().type() == relocInfo::oop_type, "must be an oop reloc");
   load_const(d, obj_addr);
 }
 
 inline void MacroAssembler::pd_patch_instruction(address branch, address target, const char* file, int line) {
-  jint& stub_inst = *(jint*) branch;
-  stub_inst = patched_branch(target - branch, stub_inst, 0);
+  if (is_branch(branch)) {
+    jint& stub_inst = *(jint*) branch;
+    stub_inst = patched_branch(target - branch, stub_inst, 0);
+  } else if (is_calculate_address_from_global_toc_at(branch + BytesPerInstWord, branch)) {
+    const address inst1_addr = branch;
+    const address inst2_addr = branch + BytesPerInstWord;
+    patch_calculate_address_from_global_toc_at(inst2_addr, inst1_addr, target);
+  } else if (is_load_const_at(branch)) {
+    patch_const(branch, (long)target);
+  } else {
+    assert(false, "instruction at " PTR_FORMAT " not recognized", p2i(branch));
+  }
 }
 
 // Relocation of conditional far branches.
 inline bool MacroAssembler::is_bc_far_variant1_at(address instruction_addr) {
   // Variant 1, the 1st instruction contains the destination address:
diff a/src/hotspot/cpu/ppc/nativeInst_ppc.cpp b/src/hotspot/cpu/ppc/nativeInst_ppc.cpp
--- a/src/hotspot/cpu/ppc/nativeInst_ppc.cpp
+++ b/src/hotspot/cpu/ppc/nativeInst_ppc.cpp
@@ -203,16 +203,18 @@
     // We can reach here during GC with 'no' pointing to new object location
     // while 'heap()->is_in' still reports false (e.g. with SerialGC).
     // Therefore we use raw decoding.
     if (CompressedOops::is_null(no)) return 0;
     return cast_from_oop<intptr_t>(CompressedOops::decode_raw(no));
-  } else {
-    assert(MacroAssembler::is_load_const_from_method_toc_at(addr), "must be load_const_from_pool");
-
+  } else if (MacroAssembler::is_load_const_from_method_toc_at(addr)) {
     address ctable = cb->content_begin();
     int offset = MacroAssembler::get_offset_of_load_const_from_method_toc_at(addr);
     return *(intptr_t *)(ctable + offset);
+  } else {
+    assert(MacroAssembler::is_calculate_address_from_global_toc_at(addr, addr - BytesPerInstWord),
+           "must be calculate_address_from_global_toc");
+    return (intptr_t) MacroAssembler::get_address_of_calculate_address_from_global_toc_at(addr, addr - BytesPerInstWord);
   }
 }
 
 address NativeMovConstReg::set_data_plain(intptr_t data, CodeBlob *cb) {
   address addr         = instruction_address();
diff a/src/hotspot/cpu/ppc/ppc.ad b/src/hotspot/cpu/ppc/ppc.ad
--- a/src/hotspot/cpu/ppc/ppc.ad
+++ b/src/hotspot/cpu/ppc/ppc.ad
@@ -12091,19 +12091,35 @@
     // _complete_monitor_unlocking_Java for the case where crx is 'NE'.
   %}
   ins_pipe(pipe_class_compare);
 %}
 
-instruct cmpFastLockLightweight(flagsRegCR0 crx, iRegPdst oop, iRegPdst box, iRegPdst tmp1, iRegPdst tmp2, flagsRegCR1 cr1) %{
-  predicate(LockingMode == LM_LIGHTWEIGHT);
+instruct cmpFastLockLightweight(flagsRegCR0 crx, iRegPdst oop, iRegPdst box, iRegPdst tmp1, iRegPdst tmp2) %{
+  predicate(LockingMode == LM_LIGHTWEIGHT && !UseObjectMonitorTable);
   match(Set crx (FastLock oop box));
-  effect(TEMP tmp1, TEMP tmp2, KILL cr1);
+  effect(TEMP tmp1, TEMP tmp2);
 
   format %{ "FASTLOCK  $oop, $box, $tmp1, $tmp2" %}
   ins_encode %{
     __ fast_lock_lightweight($crx$$CondRegister, $oop$$Register, $box$$Register,
-                             $tmp1$$Register, $tmp2$$Register, /*tmp3*/ R0);
+                             $tmp1$$Register, $tmp2$$Register, noreg /*tmp3*/);
+    // If locking was successful, crx should indicate 'EQ'.
+    // The compiler generates a branch to the runtime call to
+    // _complete_monitor_locking_Java for the case where crx is 'NE'.
+  %}
+  ins_pipe(pipe_class_compare);
+%}
+
+instruct cmpFastLockMonitorTable(flagsRegCR0 crx, iRegPdst oop, iRegPdst box, iRegPdst tmp1, iRegPdst tmp2, iRegPdst tmp3, flagsRegCR1 cr1) %{
+  predicate(LockingMode == LM_LIGHTWEIGHT && UseObjectMonitorTable);
+  match(Set crx (FastLock oop box));
+  effect(TEMP tmp1, TEMP tmp2, TEMP tmp3, KILL cr1);
+
+  format %{ "FASTLOCK  $oop, $box, $tmp1, $tmp2, $tmp3" %}
+  ins_encode %{
+    __ fast_lock_lightweight($crx$$CondRegister, $oop$$Register, $box$$Register,
+                             $tmp1$$Register, $tmp2$$Register, $tmp3$$Register);
     // If locking was successful, crx should indicate 'EQ'.
     // The compiler generates a branch to the runtime call to
     // _complete_monitor_locking_Java for the case where crx is 'NE'.
   %}
   ins_pipe(pipe_class_compare);
diff a/src/hotspot/cpu/ppc/sharedRuntime_ppc.cpp b/src/hotspot/cpu/ppc/sharedRuntime_ppc.cpp
--- a/src/hotspot/cpu/ppc/sharedRuntime_ppc.cpp
+++ b/src/hotspot/cpu/ppc/sharedRuntime_ppc.cpp
@@ -1600,10 +1600,11 @@
 
   DEBUG_ONLY(__ block_comment("fill {"));
 #ifdef ASSERT
   __ load_const_optimized(tmp2, ContinuationEntry::cookie_value());
   __ stw(tmp2, in_bytes(ContinuationEntry::cookie_offset()), R1_SP);
+  __ std(tmp2, _abi0(cr), R1_SP);
 #endif //ASSERT
 
   __ li(zero, 0);
   __ st_ptr(reg_cont_obj, ContinuationEntry::cont_offset(), R1_SP);
   __ stw(reg_flags, in_bytes(ContinuationEntry::flags_offset()), R1_SP);
@@ -1643,10 +1644,14 @@
 #ifdef ASSERT
   __ block_comment("clean {");
   __ ld_ptr(tmp1, JavaThread::cont_entry_offset(), R16_thread);
   __ cmpd(CCR0, R1_SP, tmp1);
   __ asm_assert_eq(FILE_AND_LINE ": incorrect R1_SP");
+  __ load_const_optimized(tmp1, ContinuationEntry::cookie_value());
+  __ ld(tmp2, _abi0(cr), R1_SP);
+  __ cmpd(CCR0, tmp1, tmp2);
+  __ asm_assert_eq(FILE_AND_LINE ": cookie not found");
 #endif
 
   __ ld_ptr(tmp1, ContinuationEntry::parent_cont_fastpath_offset(), R1_SP);
   __ st_ptr(tmp1, JavaThread::cont_fastpath_offset(), R16_thread);
 
@@ -1851,20 +1856,22 @@
   __ b(L_exit);
 
   // --- Thawing path
 
   __ bind(L_thaw);
+  ContinuationEntry::_thaw_call_pc_offset = __ pc() - start;
   __ add_const_optimized(R0, R29_TOC, MacroAssembler::offset_to_global_toc(StubRoutines::cont_thaw()));
   __ mtctr(R0);
   __ bctrl();
   oop_maps->add_gc_map(__ pc() - start, map->deep_copy());
   ContinuationEntry::_return_pc_offset = __ pc() - start;
   __ post_call_nop();
 
   // --- Normal exit (resolve/thawing)
 
   __ bind(L_exit);
+  ContinuationEntry::_cleanup_offset = __ pc() - start;
   continuation_enter_cleanup(masm);
 
   // Pop frame and return
   DEBUG_ONLY(__ ld_ptr(R0, 0, R1_SP));
   __ addi(R1_SP, R1_SP, framesize_words*wordSize);
@@ -1968,10 +1975,14 @@
   __ load_const_optimized(tmp, StubRoutines::forward_exception_entry(), R0);
   __ mtctr(tmp);
   __ bctr();
 }
 
+void SharedRuntime::continuation_enter_cleanup(MacroAssembler* masm) {
+  ::continuation_enter_cleanup(masm);
+}
+
 // ---------------------------------------------------------------------------
 // Generate a native wrapper for a given method. The method takes arguments
 // in the Java compiled code convention, marshals them to the native
 // convention (handlizes oops, etc), transitions to native, makes the call,
 // returns to java state (possibly blocking), unhandlizes any result and
@@ -2188,22 +2199,22 @@
   // --------------------------------------------------------------------------
 
   intptr_t start_pc = (intptr_t)__ pc();
   intptr_t vep_start_pc;
   intptr_t frame_done_pc;
-  intptr_t oopmap_pc;
 
   Label    handle_pending_exception;
+  Label    last_java_pc;
 
   Register r_callers_sp = R21;
   Register r_temp_1     = R22;
   Register r_temp_2     = R23;
   Register r_temp_3     = R24;
   Register r_temp_4     = R25;
   Register r_temp_5     = R26;
   Register r_temp_6     = R27;
-  Register r_return_pc  = R28;
+  Register r_last_java_pc = R28;
 
   Register r_carg1_jnienv        = noreg;
   Register r_carg2_classorobject = noreg;
   r_carg1_jnienv        = out_regs[0].first()->as_Register();
   r_carg2_classorobject = out_regs[1].first()->as_Register();
@@ -2361,19 +2372,13 @@
   //
   // We have all of the arguments setup at this point.
   // We MUST NOT touch any outgoing regs from this point on.
   // So if we must call out we must push a new frame.
 
-  // Get current pc for oopmap, and load it patchable relative to global toc.
-  oopmap_pc = (intptr_t) __ pc();
-  __ calculate_address_from_global_toc(r_return_pc, (address)oopmap_pc, true, true, true, true);
-
-  // We use the same pc/oopMap repeatedly when we call out.
-  oop_maps->add_gc_map(oopmap_pc - start_pc, oop_map);
-
-  // r_return_pc now has the pc loaded that we will use when we finally call
-  // to native.
+  // The last java pc will also be used as resume pc if this is the wrapper for wait0.
+  // For this purpose the precise location matters but not for oopmap lookup.
+  __ calculate_address_from_global_toc(r_last_java_pc, last_java_pc, true, true, true, true);
 
   // Make sure that thread is non-volatile; it crosses a bunch of VM calls below.
   assert(R16_thread->is_nonvolatile(), "thread must be in non-volatile register");
 
 # if 0
@@ -2397,11 +2402,12 @@
     __ addi(r_box, R1_SP, lock_offset);
 
     // Try fastpath for locking.
     if (LockingMode == LM_LIGHTWEIGHT) {
       // fast_lock kills r_temp_1, r_temp_2, r_temp_3.
-      __ compiler_fast_lock_lightweight_object(CCR0, r_oop, r_box, r_temp_1, r_temp_2, r_temp_3);
+      Register r_temp_3_or_noreg = UseObjectMonitorTable ? r_temp_3 : noreg;
+      __ compiler_fast_lock_lightweight_object(CCR0, r_oop, r_box, r_temp_1, r_temp_2, r_temp_3_or_noreg);
     } else {
       // fast_lock kills r_temp_1, r_temp_2, r_temp_3.
       __ compiler_fast_lock_object(CCR0, r_oop, r_box, r_temp_1, r_temp_2, r_temp_3);
     }
     __ beq(CCR0, locked);
@@ -2414,25 +2420,29 @@
     int frame_size = frame::native_abi_reg_args_size + align_up(total_c_args * wordSize, frame::alignment_in_bytes);
     __ mr(R11_scratch1, R1_SP);
     RegisterSaver::push_frame_and_save_argument_registers(masm, R12_scratch2, frame_size, total_c_args, out_regs);
 
     // Do the call.
-    __ set_last_Java_frame(R11_scratch1, r_return_pc);
-    assert(r_return_pc->is_nonvolatile(), "expecting return pc to be in non-volatile register");
+    __ set_last_Java_frame(R11_scratch1, r_last_java_pc);
+    assert(r_last_java_pc->is_nonvolatile(), "r_last_java_pc needs to be preserved accross complete_monitor_locking_C call");
+    // The following call will not be preempted.
+    // push_cont_fastpath forces freeze slow path in case we try to preempt where we will pin the
+    // vthread to the carrier (see FreezeBase::recurse_freeze_native_frame()).
+    __ push_cont_fastpath();
     __ call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::complete_monitor_locking_C), r_oop, r_box, R16_thread);
+    __ pop_cont_fastpath();
     __ reset_last_Java_frame();
 
     RegisterSaver::restore_argument_registers_and_pop_frame(masm, frame_size, total_c_args, out_regs);
 
     __ asm_assert_mem8_is_zero(thread_(pending_exception),
        "no pending exception allowed on exit from SharedRuntime::complete_monitor_locking_C");
 
     __ bind(locked);
   }
 
-  // Use that pc we placed in r_return_pc a while back as the current frame anchor.
-  __ set_last_Java_frame(R1_SP, r_return_pc);
+  __ set_last_Java_frame(R1_SP, r_last_java_pc);
 
   // Publish thread state
   // --------------------------------------------------------------------------
 
   // Transition from _thread_in_Java to _thread_in_native.
@@ -2488,12 +2498,10 @@
     default:
       ShouldNotReachHere();
       break;
   }
 
-  Label after_transition;
-
   // Publish thread state
   // --------------------------------------------------------------------------
 
   // Switch thread to "native transition" state before reading the
   // synchronization state. This additional state is necessary because reading
@@ -2564,11 +2572,27 @@
     // Transition from _thread_in_native_trans to _thread_in_Java.
     __ li(R0, _thread_in_Java);
     __ lwsync(); // Acquire safepoint and suspend state, release thread state.
     // TODO: PPC port assert(4 == JavaThread::sz_thread_state(), "unexpected field size");
     __ stw(R0, thread_(thread_state));
-    __ bind(after_transition);
+
+    // Check preemption for Object.wait()
+    if (LockingMode != LM_LEGACY && method->is_object_wait0()) {
+      Label not_preempted;
+      __ ld(R0, in_bytes(JavaThread::preempt_alternate_return_offset()), R16_thread);
+      __ cmpdi(CCR0, R0, 0);
+      __ beq(CCR0, not_preempted);
+      __ mtlr(R0);
+      __ li(R0, 0);
+      __ std(R0, in_bytes(JavaThread::preempt_alternate_return_offset()), R16_thread);
+      __ blr();
+      __ bind(not_preempted);
+    }
+    __ bind(last_java_pc);
+    // We use the same pc/oopMap repeatedly when we call out above.
+    intptr_t oopmap_pc = (intptr_t) __ pc();
+    oop_maps->add_gc_map(oopmap_pc - start_pc, oop_map);
   }
 
   // Reguard any pages if necessary.
   // --------------------------------------------------------------------------
 
@@ -2646,11 +2670,13 @@
 # endif
 
   // Clear "last Java frame" SP and PC.
   // --------------------------------------------------------------------------
 
-  __ reset_last_Java_frame();
+  // Last java frame won't be set if we're resuming after preemption
+  bool maybe_preempted = LockingMode != LM_LEGACY && method->is_object_wait0();
+  __ reset_last_Java_frame(!maybe_preempted /* check_last_java_sp */);
 
   // Unbox oop result, e.g. JNIHandles::resolve value.
   // --------------------------------------------------------------------------
 
   if (is_reference_type(ret_type)) {
@@ -2731,10 +2757,16 @@
 #else
   return 0;
 #endif
 }
 
+VMReg SharedRuntime::thread_register() {
+  // On PPC virtual threads don't save the JavaThread* in their context (e.g. C1 stub frames).
+  ShouldNotCallThis();
+  return nullptr;
+}
+
 #if defined(COMPILER1) || defined(COMPILER2)
 // Frame generation for deopt and uncommon trap blobs.
 static void push_skeleton_frame(MacroAssembler* masm, bool deopt,
                                 /* Read */
                                 Register unroll_block_reg,
diff a/src/hotspot/cpu/ppc/stackChunkFrameStream_ppc.inline.hpp b/src/hotspot/cpu/ppc/stackChunkFrameStream_ppc.inline.hpp
--- a/src/hotspot/cpu/ppc/stackChunkFrameStream_ppc.inline.hpp
+++ b/src/hotspot/cpu/ppc/stackChunkFrameStream_ppc.inline.hpp
@@ -182,12 +182,13 @@
   InterpreterOopMap mask;
   frame f = to_frame();
   f.interpreted_frame_oop_map(&mask);
   return  mask.num_oops()
           + 1 // for the mirror oop
-          + ((intptr_t*)f.interpreter_frame_monitor_begin()
-             - (intptr_t*)f.interpreter_frame_monitor_end())/BasicObjectLock::size();
+          + (f.interpreter_frame_method()->is_native() ? 1 : 0) // temp oop slot
+          + pointer_delta_as_int((intptr_t*)f.interpreter_frame_monitor_begin(),
+                                 (intptr_t*)f.interpreter_frame_monitor_end())/BasicObjectLock::size();
 }
 
 template<>
 template<>
 inline void StackChunkFrameStream<ChunkFrames::Mixed>::update_reg_map_pd(RegisterMap* map) {
diff a/src/hotspot/cpu/ppc/stubGenerator_ppc.cpp b/src/hotspot/cpu/ppc/stubGenerator_ppc.cpp
--- a/src/hotspot/cpu/ppc/stubGenerator_ppc.cpp
+++ b/src/hotspot/cpu/ppc/stubGenerator_ppc.cpp
@@ -4482,10 +4482,14 @@
     Register nvtmp = R15_esp;   // nonvolatile tmp register
     FloatRegister nvftmp = F20; // nonvolatile fp tmp register
 
     address start = __ pc();
 
+    if (kind == Continuation::thaw_top) {
+      __ clobber_nonvolatile_registers(); // Except R16_thread and R29_TOC
+    }
+
     if (return_barrier) {
       __ mr(nvtmp, R3_RET); __ fmr(nvftmp, F1_RET); // preserve possible return value from a method returning to the return barrier
       DEBUG_ONLY(__ ld_ptr(tmp1, _abi0(callers_sp), R1_SP);)
       __ ld_ptr(R1_SP, JavaThread::cont_entry_offset(), R16_thread);
 #ifdef ASSERT
@@ -4570,10 +4574,45 @@
 
   address generate_cont_returnBarrier_exception() {
     return generate_cont_thaw("Cont thaw return barrier exception", Continuation::thaw_return_barrier_exception);
   }
 
+  address generate_cont_preempt_stub() {
+    if (!Continuations::enabled()) return nullptr;
+    StubCodeMark mark(this, "StubRoutines","Continuation preempt stub");
+    address start = __ pc();
+
+    __ clobber_nonvolatile_registers(); // Except R16_thread and R29_TOC
+
+    __ reset_last_Java_frame(false /*check_last_java_sp*/);
+
+    // Set sp to enterSpecial frame, i.e. remove all frames copied into the heap.
+    __ ld_ptr(R1_SP, JavaThread::cont_entry_offset(), R16_thread);
+
+    Label preemption_cancelled;
+    __ lbz(R11_scratch1, in_bytes(JavaThread::preemption_cancelled_offset()), R16_thread);
+    __ cmpwi(CCR0, R11_scratch1, 0);
+    __ bne(CCR0, preemption_cancelled);
+
+    // Remove enterSpecial frame from the stack and return to Continuation.run() to unmount.
+    SharedRuntime::continuation_enter_cleanup(_masm);
+    __ pop_frame();
+    __ restore_LR(R11_scratch1);
+    __ blr();
+
+    // We acquired the monitor after freezing the frames so call thaw to continue execution.
+    __ bind(preemption_cancelled);
+    __ li(R11_scratch1, 0); // false
+    __ stb(R11_scratch1, in_bytes(JavaThread::preemption_cancelled_offset()), R16_thread);
+    int simm16_offs = __ load_const_optimized(R11_scratch1, ContinuationEntry::thaw_call_pc_address(), R0, true);
+    __ ld(R11_scratch1, simm16_offs, R11_scratch1);
+    __ mtctr(R11_scratch1);
+    __ bctr();
+
+    return start;
+  }
+
   // exception handler for upcall stubs
   address generate_upcall_stub_exception_handler() {
     StubCodeMark mark(this, "StubRoutines", "upcall stub exception handler");
     address start = __ pc();
 
@@ -4645,10 +4684,11 @@
   void generate_continuation_stubs() {
     // Continuation stubs:
     StubRoutines::_cont_thaw          = generate_cont_thaw();
     StubRoutines::_cont_returnBarrier = generate_cont_returnBarrier();
     StubRoutines::_cont_returnBarrierExc = generate_cont_returnBarrier_exception();
+    StubRoutines::_cont_preempt_stub  = generate_cont_preempt_stub();
   }
 
   void generate_final_stubs() {
     // Generates all stubs and initializes the entry points
 
diff a/src/hotspot/cpu/ppc/templateInterpreterGenerator_ppc.cpp b/src/hotspot/cpu/ppc/templateInterpreterGenerator_ppc.cpp
--- a/src/hotspot/cpu/ppc/templateInterpreterGenerator_ppc.cpp
+++ b/src/hotspot/cpu/ppc/templateInterpreterGenerator_ppc.cpp
@@ -694,10 +694,21 @@
   __ dispatch_via(vtos, Interpreter::_normal_table.table_for(vtos));
 
   return entry;
 }
 
+address TemplateInterpreterGenerator::generate_cont_resume_interpreter_adapter() {
+  if (!Continuations::enabled()) return nullptr;
+  address start = __ pc();
+
+  __ load_const_optimized(R25_templateTableBase, (address)Interpreter::dispatch_table((TosState)0), R12_scratch2);
+  __ restore_interpreter_state(R11_scratch1, false, true /*restore_top_frame_sp*/);
+  __ blr();
+
+  return start;
+}
+
 // Helpers for commoning out cases in the various type of method entries.
 
 // Increment invocation count & check for overflow.
 //
 // Note: checking for negative value instead of overflow
@@ -1195,11 +1206,11 @@
   //        ...
 
   const Register signature_handler_fd = R11_scratch1;
   const Register pending_exception    = R0;
   const Register result_handler_addr  = R31;
-  const Register native_method_fd     = R11_scratch1;
+  const Register native_method_fd     = R12_scratch2; // preferred in MacroAssembler::branch_to
   const Register access_flags         = R22_tmp2;
   const Register active_handles       = R11_scratch1; // R26_monitor saved to state.
   const Register sync_state           = R12_scratch2;
   const Register sync_state_addr      = sync_state;   // Address is dead after use.
   const Register suspend_flags        = R11_scratch1;
@@ -1209,14 +1220,10 @@
 
   Label exception_return;
   Label exception_return_sync_check;
   Label stack_overflow_return;
 
-  // Generate new interpreter state and jump to stack_overflow_return in case of
-  // a stack overflow.
-  //generate_compute_interpreter_state(stack_overflow_return);
-
   Register size_of_parameters = R22_tmp2;
 
   generate_fixed_frame(true, size_of_parameters, noreg /* unused */);
 
   //=============================================================================
@@ -1251,12 +1258,12 @@
     }
   }
 
   // access_flags = method->access_flags();
   // Load access flags.
-  assert(access_flags->is_nonvolatile(),
-         "access_flags must be in a non-volatile register");
+  assert(__ nonvolatile_accross_vthread_preemtion(access_flags),
+         "access_flags not preserved");
   // Type check.
   assert(4 == sizeof(AccessFlags), "unexpected field size");
   __ lwz(access_flags, method_(access_flags));
 
   // We don't want to reload R19_method and access_flags after calls
@@ -1313,12 +1320,16 @@
 
   // First set the frame anchor while the SP/FP registers are
   // convenient and the slow signature handler can use this same frame
   // anchor.
 
+  bool support_vthread_preemption = Continuations::enabled() && LockingMode != LM_LEGACY;
+
   // We have a TOP_IJAVA_FRAME here, which belongs to us.
-  __ set_top_ijava_frame_at_SP_as_last_Java_frame(R1_SP, R12_scratch2/*tmp*/);
+  Label last_java_pc;
+  Label *resume_pc = support_vthread_preemption ? &last_java_pc : nullptr;
+  __ set_top_ijava_frame_at_SP_as_last_Java_frame(R1_SP, R3_ARG1/*tmp*/, resume_pc);
 
   // Now the interpreter frame (and its call chain) have been
   // invalidated and flushed. We are now protected against eager
   // being enabled in native code. Even if it goes eager the
   // registers will be reloaded as clean and we will invalidate after
@@ -1333,20 +1344,15 @@
   __ ld(signature_handler_fd, 0, signature_handler_fd);
 #endif
 
   __ call_stub(signature_handler_fd);
 
-  // Remove the register parameter varargs slots we allocated in
-  // compute_interpreter_state. SP+16 ends up pointing to the ABI
-  // outgoing argument area.
-  //
-  // Not needed on PPC64.
-  //__ add(SP, SP, Argument::n_int_register_parameters_c*BytesPerWord);
-
-  assert(result_handler_addr->is_nonvolatile(), "result_handler_addr must be in a non-volatile register");
+  assert(__ nonvolatile_accross_vthread_preemtion(result_handler_addr),
+         "result_handler_addr not preserved");
   // Save across call to native method.
   __ mr(result_handler_addr, R3_RET);
+  __ ld(R11_scratch1, _abi0(callers_sp), R1_SP); // load FP
 
   __ isync(); // Acquire signature handler before trying to fetch the native entry point and klass mirror.
 
   // Set up fixed parameters and call the native method.
   // If the method is static, get mirror into R4_ARG2.
@@ -1356,16 +1362,15 @@
 
     // Restore access flags.
     __ testbitdi(CCR0, R0, access_flags, JVM_ACC_STATIC_BIT);
     __ bfalse(CCR0, method_is_not_static);
 
-    __ ld(R11_scratch1, _abi0(callers_sp), R1_SP);
-    // Load mirror from interpreter frame.
-    __ ld(R12_scratch2, _ijava_state_neg(mirror), R11_scratch1);
+    // Load mirror from interpreter frame (FP in R11_scratch1)
+    __ ld(R21_tmp1, _ijava_state_neg(mirror), R11_scratch1);
     // R4_ARG2 = &state->_oop_temp;
     __ addi(R4_ARG2, R11_scratch1, _ijava_state_neg(oop_tmp));
-    __ std(R12_scratch2/*mirror*/, _ijava_state_neg(oop_tmp), R11_scratch1);
+    __ std(R21_tmp1/*mirror*/, _ijava_state_neg(oop_tmp), R11_scratch1);
     BIND(method_is_not_static);
   }
 
   // At this point, arguments have been copied off the stack into
   // their JNI positions. Oops are boxed in-place on the stack, with
@@ -1395,11 +1400,22 @@
 
   //=============================================================================
   // Call the native method. Argument registers must not have been
   // overwritten since "__ call_stub(signature_handler);" (except for
   // ARG1 and ARG2 for static methods).
+
+  if (support_vthread_preemption) {
+    // result_handler_addr is a nonvolatile register. Its value will be preserved across
+    // the native call but only if the call isn't preempted. To preserve its value even
+    // in the case of preemption we save it in the lresult slot. It is restored at
+    // resume_pc if, and only if the call was preempted. This works because only
+    // j.l.Object::wait calls are preempted which don't return a result.
+    __ std(result_handler_addr, _ijava_state_neg(lresult), R11_scratch1);
+  }
+  __ push_cont_fastpath();
   __ call_c(native_method_fd);
+  __ pop_cont_fastpath();
 
   __ li(R0, 0);
   __ ld(R11_scratch1, 0, R1_SP);
   __ std(R3_RET, _ijava_state_neg(lresult), R11_scratch1);
   __ stfd(F1_RET, _ijava_state_neg(fresult), R11_scratch1);
@@ -1493,10 +1509,39 @@
   // accesses (including the new thread state) are visible to other threads.
   __ li(R0/*thread_state*/, _thread_in_Java);
   __ lwsync(); // Acquire safepoint and suspend state, release thread state.
   __ stw(R0/*thread_state*/, thread_(thread_state));
 
+  if (support_vthread_preemption) {
+    // Check preemption for Object.wait()
+    Label not_preempted;
+    __ ld(R0, in_bytes(JavaThread::preempt_alternate_return_offset()), R16_thread);
+    __ cmpdi(CCR0, R0, 0);
+    __ beq(CCR0, not_preempted);
+    __ mtlr(R0);
+    __ li(R0, 0);
+    __ std(R0, in_bytes(JavaThread::preempt_alternate_return_offset()), R16_thread);
+    __ blr();
+
+    // Execution will be resumed here when the vthread becomes runnable again.
+    __ bind(*resume_pc);
+    __ restore_after_resume(R11_scratch1 /* fp */);
+    // We saved the result handler before the call
+    __ ld(result_handler_addr, _ijava_state_neg(lresult), R11_scratch1);
+#ifdef ASSERT
+    // Clobber result slots. Only native methods returning void can be preemted currently.
+    __ load_const(R3_RET, UCONST64(0xbad01001));
+    __ std(R3_RET, _ijava_state_neg(lresult), R11_scratch1);
+    __ std(R3_RET, _ijava_state_neg(fresult), R11_scratch1);
+    // reset_last_Java_frame() below asserts that a last java sp is set
+    __ asm_assert_mem8_is_zero(in_bytes(JavaThread::last_Java_sp_offset()),
+        R16_thread, FILE_AND_LINE ": Last java sp should not be set when resuming");
+    __ std(R3_RET, in_bytes(JavaThread::last_Java_sp_offset()), R16_thread);
+#endif
+    __ bind(not_preempted);
+  }
+
   if (CheckJNICalls) {
     // clear_pending_jni_exception_check
     __ load_const_optimized(R0, 0L);
     __ st_ptr(R0, JavaThread::pending_jni_exception_check_fn_offset(), R16_thread);
   }
diff a/src/hotspot/cpu/riscv/c1_MacroAssembler_riscv.cpp b/src/hotspot/cpu/riscv/c1_MacroAssembler_riscv.cpp
--- a/src/hotspot/cpu/riscv/c1_MacroAssembler_riscv.cpp
+++ b/src/hotspot/cpu/riscv/c1_MacroAssembler_riscv.cpp
@@ -81,12 +81,12 @@
     sd(hdr, Address(disp_hdr, 0));
     // test if object header is still the same (i.e. unlocked), and if so, store the
     // displaced header address in the object header - if it is not the same, get the
     // object header instead
     la(temp, Address(obj, hdr_offset));
-    cmpxchgptr(hdr, disp_hdr, temp, t1, done, /*fallthough*/nullptr);
     // if the object header was the same, we're done
+    cmpxchgptr(hdr, disp_hdr, temp, t1, done, /*fallthough*/nullptr);
     // if the object header was not the same, it is now in the hdr register
     // => test if it is a stack pointer into the same stack (recursive locking), i.e.:
     //
     // 1) (hdr & aligned_mask) == 0
     // 2) sp <= hdr
@@ -104,15 +104,16 @@
     // for recursive locking, the result is zero => save it in the displaced header
     // location (null in the displaced hdr location indicates recursive locking)
     sd(hdr, Address(disp_hdr, 0));
     // otherwise we don't care about the result and handle locking via runtime call
     bnez(hdr, slow_case, /* is_far */ true);
+
     // done
     bind(done);
+    inc_held_monitor_count();
   }
 
-  increment(Address(xthread, JavaThread::held_monitor_count_offset()));
   return null_check_offset;
 }
 
 void C1_MacroAssembler::unlock_object(Register hdr, Register obj, Register disp_hdr, Register temp, Label& slow_case) {
   const int aligned_mask = BytesPerWord - 1;
@@ -144,15 +145,15 @@
       la(temp, Address(obj, hdr_offset));
       cmpxchgptr(disp_hdr, hdr, temp, t1, done, &slow_case);
     } else {
       cmpxchgptr(disp_hdr, hdr, obj, t1, done, &slow_case);
     }
+
     // done
     bind(done);
+    dec_held_monitor_count();
   }
-
-  decrement(Address(xthread, JavaThread::held_monitor_count_offset()));
 }
 
 // Defines obj, preserves var_size_in_bytes
 void C1_MacroAssembler::try_allocate(Register obj, Register var_size_in_bytes, int con_size_in_bytes, Register tmp1, Register tmp2, Label& slow_case) {
   if (UseTLAB) {
diff a/src/hotspot/cpu/riscv/c1_Runtime1_riscv.cpp b/src/hotspot/cpu/riscv/c1_Runtime1_riscv.cpp
--- a/src/hotspot/cpu/riscv/c1_Runtime1_riscv.cpp
+++ b/src/hotspot/cpu/riscv/c1_Runtime1_riscv.cpp
@@ -172,50 +172,73 @@
 
 class StubFrame: public StackObj {
  private:
   StubAssembler* _sasm;
   bool _return_state;
+  bool _use_pop_on_epilogue;
 
  public:
-  StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments, return_state_t return_state=requires_return);
-  void load_argument(int offset_in_words, Register reg);
+  StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments,
+            return_state_t return_state, bool use_pop_on_epilogue);
+
+ public:
+  StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments, bool use_pop_on_epilogue);
+  StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments, return_state_t return_state);
+  StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments);
 
   ~StubFrame();
+
+  void load_argument(int offset_in_words, Register reg);
 };;
 
 void StubAssembler::prologue(const char* name, bool must_gc_arguments) {
   set_info(name, must_gc_arguments);
   enter();
 }
 
-void StubAssembler::epilogue() {
-  leave();
+void StubAssembler::epilogue(bool use_pop) {
+  // Avoid using a leave instruction when this frame may
+  // have been frozen, since the current value of fp
+  // restored from the stub would be invalid. We still
+  // must restore the fp value saved on enter though.
+  if (use_pop) {
+    ld(fp, Address(sp));
+    ld(ra, Address(sp, wordSize));
+    addi(sp, sp, 2 * wordSize);
+  } else {
+    leave();
+  }
   ret();
 }
 
 #define __ _sasm->
 
-StubFrame::StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments, return_state_t return_state) {
-  _sasm = sasm;
-  _return_state = return_state;
+StubFrame::StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments,
+                     return_state_t return_state, bool use_pop_on_epilogue)
+  : _sasm(sasm), _return_state(return_state), _use_pop_on_epilogue(use_pop_on_epilogue) {
   __ prologue(name, must_gc_arguments);
 }
 
+StubFrame::StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments,
+                     bool use_pop_on_epilogue) :
+  StubFrame(sasm, name, must_gc_arguments, requires_return, use_pop_on_epilogue) {}
+
+StubFrame::StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments,
+                     return_state_t return_state) :
+  StubFrame(sasm, name, must_gc_arguments, return_state, /*use_pop_on_epilogue*/false) {}
+
+StubFrame::StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments) :
+  StubFrame(sasm, name, must_gc_arguments, requires_return, /*use_pop_on_epilogue*/false) {}
+
 // load parameters that were stored with LIR_Assembler::store_parameter
 // Note: offsets for store_parameter and load_argument must match
 void StubFrame::load_argument(int offset_in_words, Register reg) {
   __ load_parameter(offset_in_words, reg);
 }
 
-
 StubFrame::~StubFrame() {
-  if (_return_state == requires_return) {
-    __ epilogue();
-  } else {
-    __ should_not_reach_here();
-  }
-  _sasm = nullptr;
+  __ epilogue(_use_pop_on_epilogue);
 }
 
 #undef __
 
 
@@ -264,10 +287,14 @@
     int sp_offset = cpu_reg_save_offsets[r->encoding()];
     oop_map->set_callee_saved(VMRegImpl::stack2reg(sp_offset),
                               r->as_VMReg());
   }
 
+  int sp_offset = cpu_reg_save_offsets[xthread->encoding()];
+  oop_map->set_callee_saved(VMRegImpl::stack2reg(sp_offset),
+                            xthread->as_VMReg());
+
   // fpu_regs
   if (save_fpu_registers) {
     for (int i = 0; i < FrameMap::nof_fpu_regs; i++) {
       FloatRegister r = as_FloatRegister(i);
       int sp_offset = fpu_reg_save_offsets[i];
@@ -352,10 +379,20 @@
     cpu_reg_save_offsets[i] = sp_offset;
     sp_offset += step;
   }
 }
 
+// return: offset in 64-bit words.
+uint Runtime1::runtime_blob_current_thread_offset(frame f) {
+  CodeBlob* cb = f.cb();
+  assert(cb == Runtime1::blob_for(C1StubId::monitorenter_id) ||
+         cb == Runtime1::blob_for(C1StubId::monitorenter_nofpu_id), "must be");
+  assert(cb != nullptr && cb->is_runtime_stub(), "invalid frame");
+  int offset = cpu_reg_save_offsets[xthread->encoding()];
+  return offset / 2;   // SP offsets are in halfwords
+}
+
 // target: the entry point of the method that creates and posts the exception oop
 // has_argument: true if the exception needs arguments (passed in t0 and t1)
 
 OopMapSet* Runtime1::generate_exception_throw(StubAssembler* sasm, address target, bool has_argument) {
   // make a frame and preserve the caller's caller-save registers
@@ -877,11 +914,11 @@
     case C1StubId::monitorenter_nofpu_id:
       save_fpu_registers = false;
       // fall through
     case C1StubId::monitorenter_id:
       {
-        StubFrame f(sasm, "monitorenter", dont_gc_arguments);
+        StubFrame f(sasm, "monitorenter", dont_gc_arguments, /*use_pop_on_epilogue*/true);
         OopMap* map = save_live_registers(sasm, save_fpu_registers);
         assert_cond(map != nullptr);
 
         // Called with store_parameter and not C abi
         f.load_argument(1, x10); // x10: object
diff a/src/hotspot/cpu/riscv/c2_CodeStubs_riscv.cpp b/src/hotspot/cpu/riscv/c2_CodeStubs_riscv.cpp
--- a/src/hotspot/cpu/riscv/c2_CodeStubs_riscv.cpp
+++ b/src/hotspot/cpu/riscv/c2_CodeStubs_riscv.cpp
@@ -68,7 +68,5 @@
   __ align(4);
   __ bind(guard());
   __ relocate(entry_guard_Relocation::spec());
   __ emit_int32(0);  // nmethod guard value
 }
-
-#undef __
diff a/src/hotspot/cpu/riscv/c2_MacroAssembler_riscv.cpp b/src/hotspot/cpu/riscv/c2_MacroAssembler_riscv.cpp
--- a/src/hotspot/cpu/riscv/c2_MacroAssembler_riscv.cpp
+++ b/src/hotspot/cpu/riscv/c2_MacroAssembler_riscv.cpp
@@ -43,11 +43,11 @@
 #endif
 
 #define BIND(label) bind(label); BLOCK_COMMENT(#label ":")
 
 void C2_MacroAssembler::fast_lock(Register objectReg, Register boxReg,
-                                  Register tmp1Reg, Register tmp2Reg, Register tmp3Reg) {
+                                  Register tmp1Reg, Register tmp2Reg, Register tmp3Reg, Register tmp4Reg) {
   // Use cr register to indicate the fast_lock result: zero for success; non-zero for failure.
   Register flag = t1;
   Register oop = objectReg;
   Register box = boxReg;
   Register disp_hdr = tmp1Reg;
@@ -102,27 +102,31 @@
 
     // Check if the owner is self by comparing the value in the
     // markWord of object (disp_hdr) with the stack pointer.
     sub(disp_hdr, disp_hdr, sp);
     mv(tmp, (intptr_t) (~(os::vm_page_size()-1) | (uintptr_t)markWord::lock_mask_in_place));
-    // If (mark & lock_mask) == 0 and mark - sp < page_size, we are stack-locking and goto label locked,
-    // hence we can store 0 as the displaced header in the box, which indicates that it is a
-    // recursive lock.
+    // If (mark & lock_mask) == 0 and mark - sp < page_size, we are stack-locking and goto label
+    // locked, hence we can store 0 as the displaced header in the box, which indicates that it
+    // is a recursive lock.
     andr(tmp/*==0?*/, disp_hdr, tmp);
     sd(tmp/*==0, perhaps*/, Address(box, BasicLock::displaced_header_offset_in_bytes()));
     beqz(tmp, locked);
     j(slow_path);
   }
 
   // Handle existing monitor.
   bind(object_has_monitor);
+
   // The object's monitor m is unlocked iff m->owner == nullptr,
-  // otherwise m->owner may contain a thread or a stack address.
+  // otherwise m->owner may contain a thread id, a stack address for LM_LEGACY,
+  // the ANONYMOUS_OWNER constant for LM_LIGHTWEIGHT.
   //
-  // Try to CAS m->owner from null to current thread.
+  // Try to CAS m->owner from null to current thread id.
   add(tmp, disp_hdr, (in_bytes(ObjectMonitor::owner_offset()) - markWord::monitor_value));
-  cmpxchg(/*memory address*/tmp, /*expected value*/zr, /*new value*/xthread, Assembler::int64,
+  Register tid = tmp4Reg;
+  ld(tid, Address(xthread, JavaThread::lock_id_offset()));
+  cmpxchg(/*memory address*/tmp, /*expected value*/zr, /*new value*/tid, Assembler::int64,
           Assembler::aq, Assembler::rl, /*result*/tmp3Reg); // cas succeeds if tmp3Reg == zr(expected)
 
   // Store a non-null value into the box to avoid looking like a re-entrant
   // lock. The fast-path monitor unlock code checks for
   // markWord::monitor_value so use markWord::unused_mark which has the
@@ -130,18 +134,20 @@
   mv(tmp, (address)markWord::unused_mark().value());
   sd(tmp, Address(box, BasicLock::displaced_header_offset_in_bytes()));
 
   beqz(tmp3Reg, locked); // CAS success means locking succeeded
 
-  bne(tmp3Reg, xthread, slow_path); // Check for recursive locking
+  bne(tmp3Reg, tid, slow_path); // Check for recursive locking
 
   // Recursive lock case
   increment(Address(disp_hdr, in_bytes(ObjectMonitor::recursions_offset()) - markWord::monitor_value), 1, tmp2Reg, tmp3Reg);
 
   bind(locked);
   mv(flag, zr);
-  increment(Address(xthread, JavaThread::held_monitor_count_offset()), 1, tmp2Reg, tmp3Reg);
+  if (LockingMode == LM_LEGACY) {
+    inc_held_monitor_count();
+  }
 
 #ifdef ASSERT
   // Check that locked label is reached with flag == 0.
   Label flag_correct;
   beqz(flag, flag_correct);
@@ -251,11 +257,13 @@
   mv(flag, 1);
   j(slow_path);
 
   bind(unlocked);
   mv(flag, zr);
-  decrement(Address(xthread, JavaThread::held_monitor_count_offset()), 1, tmp1Reg, tmp2Reg);
+  if (LockingMode == LM_LEGACY) {
+    dec_held_monitor_count();
+  }
 
 #ifdef ASSERT
   // Check that unlocked label is reached with flag == 0.
   Label flag_correct;
   beqz(flag, flag_correct);
@@ -271,16 +279,16 @@
 #endif
   // C2 uses the value of flag (0 vs !0) to determine the continuation.
 }
 
 void C2_MacroAssembler::fast_lock_lightweight(Register obj, Register box,
-                                              Register tmp1, Register tmp2, Register tmp3) {
+                                              Register tmp1, Register tmp2, Register tmp3, Register tmp4) {
   // Flag register, zero for success; non-zero for failure.
   Register flag = t1;
 
   assert(LockingMode == LM_LIGHTWEIGHT, "must be");
-  assert_different_registers(obj, box, tmp1, tmp2, tmp3, flag, t0);
+  assert_different_registers(obj, box, tmp1, tmp2, tmp3, tmp4, flag, t0);
 
   mv(flag, 1);
 
   // Handle inflated monitor.
   Label inflated;
@@ -347,10 +355,11 @@
 
   { // Handle inflated monitor.
     bind(inflated);
 
     const Register tmp1_monitor = tmp1;
+
     if (!UseObjectMonitorTable) {
       assert(tmp1_monitor == tmp1_mark, "should be the same here");
     } else {
       Label monitor_found;
 
@@ -393,17 +402,19 @@
     Label monitor_locked;
 
     // Compute owner address.
     la(tmp2_owner_addr, owner_address);
 
-    // CAS owner (null => current thread).
-    cmpxchg(/*addr*/ tmp2_owner_addr, /*expected*/ zr, /*new*/ xthread, Assembler::int64,
+    // CAS owner (null => current thread id).
+    Register tid = tmp4;
+    ld(tid, Address(xthread, JavaThread::lock_id_offset()));
+    cmpxchg(/*addr*/ tmp2_owner_addr, /*expected*/ zr, /*new*/ tid, Assembler::int64,
             /*acquire*/ Assembler::aq, /*release*/ Assembler::relaxed, /*result*/ tmp3_owner);
     beqz(tmp3_owner, monitor_locked);
 
     // Check if recursive.
-    bne(tmp3_owner, xthread, slow_path);
+    bne(tmp3_owner, tid, slow_path);
 
     // Recursive.
     increment(recursions_address, 1, tmp2, tmp3);
 
     bind(monitor_locked);
@@ -412,11 +423,10 @@
     }
   }
 
   bind(locked);
   mv(flag, zr);
-  increment(Address(xthread, JavaThread::held_monitor_count_offset()), 1, tmp2, tmp3);
 
 #ifdef ASSERT
   // Check that locked label is reached with flag == 0.
   Label flag_correct;
   beqz(flag, flag_correct);
@@ -584,11 +594,10 @@
     j(slow_path);
   }
 
   bind(unlocked);
   mv(flag, zr);
-  decrement(Address(xthread, JavaThread::held_monitor_count_offset()), 1, tmp2, tmp3);
 
 #ifdef ASSERT
   // Check that unlocked label is reached with flag == 0.
   Label flag_correct;
   beqz(flag, flag_correct);
diff a/src/hotspot/cpu/riscv/c2_MacroAssembler_riscv.hpp b/src/hotspot/cpu/riscv/c2_MacroAssembler_riscv.hpp
--- a/src/hotspot/cpu/riscv/c2_MacroAssembler_riscv.hpp
+++ b/src/hotspot/cpu/riscv/c2_MacroAssembler_riscv.hpp
@@ -42,15 +42,19 @@
   void compress_bits_v(Register dst, Register src, Register mask, bool is_long);
   void expand_bits_v(Register dst, Register src, Register mask, bool is_long);
 
  public:
   // Code used by cmpFastLock and cmpFastUnlock mach instructions in .ad file.
-  void fast_lock(Register object, Register box, Register tmp1, Register tmp2, Register tmp3);
+  void fast_lock(Register object, Register box,
+                 Register tmp1, Register tmp2, Register tmp3, Register tmp4);
   void fast_unlock(Register object, Register box, Register tmp1, Register tmp2);
+
   // Code used by cmpFastLockLightweight and cmpFastUnlockLightweight mach instructions in .ad file.
-  void fast_lock_lightweight(Register object, Register box, Register tmp1, Register tmp2, Register tmp3);
-  void fast_unlock_lightweight(Register object, Register box, Register tmp1, Register tmp2, Register tmp3);
+  void fast_lock_lightweight(Register object, Register box,
+                             Register tmp1, Register tmp2, Register tmp3, Register tmp4);
+  void fast_unlock_lightweight(Register object, Register box,
+                               Register tmp1, Register tmp2, Register tmp3);
 
   void string_compare(Register str1, Register str2,
                       Register cnt1, Register cnt2, Register result,
                       Register tmp1, Register tmp2, Register tmp3,
                       int ae);
diff a/src/hotspot/cpu/riscv/continuationFreezeThaw_riscv.inline.hpp b/src/hotspot/cpu/riscv/continuationFreezeThaw_riscv.inline.hpp
--- a/src/hotspot/cpu/riscv/continuationFreezeThaw_riscv.inline.hpp
+++ b/src/hotspot/cpu/riscv/continuationFreezeThaw_riscv.inline.hpp
@@ -125,10 +125,16 @@
   if (real_unextended_sp != nullptr) {
     f.set_unextended_sp(real_unextended_sp); // can be null at a safepoint
   }
 }
 
+inline void FreezeBase::prepare_freeze_interpreted_top_frame(const frame& f) {
+  assert(*f.addr_at(frame::interpreter_frame_last_sp_offset) == 0, "should be null for top frame");
+  intptr_t* lspp = f.addr_at(frame::interpreter_frame_last_sp_offset);
+  *lspp = f.unextended_sp() - f.fp();
+}
+
 inline void FreezeBase::relativize_interpreted_frame_metadata(const frame& f, const frame& hf) {
   assert(hf.fp() == hf.unextended_sp() + (f.fp() - f.unextended_sp()), "");
   assert((f.at(frame::interpreter_frame_last_sp_offset) != 0)
     || (f.unextended_sp() == f.sp()), "");
   assert(f.fp() > (intptr_t*)f.at_relative(frame::interpreter_frame_initial_sp_offset), "");
@@ -145,14 +151,20 @@
   assert(hf.at_absolute(frame::interpreter_frame_monitor_block_top_offset) <= frame::interpreter_frame_initial_sp_offset, "");
 
   // extended_sp is already relativized by TemplateInterpreterGenerator::generate_normal_entry or
   // AbstractInterpreter::layout_activation
 
+  // The interpreter native wrapper code adds space in the stack equal to size_of_parameters()
+  // after the fixed part of the frame. For wait0 this is equal to 3 words (this + long parameter).
+  // We adjust by this size since otherwise the saved last sp will be less than the extended_sp.
+  DEBUG_ONLY(Method* m = hf.interpreter_frame_method();)
+  DEBUG_ONLY(int extra_space = m->is_object_wait0() ? m->size_of_parameters() : 0;)
+
   assert((hf.fp() - hf.unextended_sp()) == (f.fp() - f.unextended_sp()), "");
   assert(hf.unextended_sp() == (intptr_t*)hf.at(frame::interpreter_frame_last_sp_offset), "");
   assert(hf.unextended_sp() <= (intptr_t*)hf.at(frame::interpreter_frame_initial_sp_offset), "");
-  assert(hf.unextended_sp() >  (intptr_t*)hf.at(frame::interpreter_frame_extended_sp_offset), "");
+  assert(hf.unextended_sp() + extra_space >  (intptr_t*)hf.at(frame::interpreter_frame_extended_sp_offset), "");
   assert(hf.fp()            >  (intptr_t*)hf.at(frame::interpreter_frame_initial_sp_offset), "");
 #ifdef ASSERT
   if (f.interpreter_frame_method()->max_locals() > 0) {
     assert(hf.fp()          <= (intptr_t*)hf.at(frame::interpreter_frame_locals_offset), "");
   }
@@ -201,11 +213,12 @@
 
 // Slow path
 
 inline frame ThawBase::new_entry_frame() {
   intptr_t* sp = _cont.entrySP();
-  return frame(sp, sp, _cont.entryFP(), _cont.entryPC()); // TODO PERF: This finds code blob and computes deopt state
+  // TODO PERF: This finds code blob and computes deopt state
+  return frame(sp, sp, _cont.entryFP(), _cont.entryPC());
 }
 
 template<typename FKind> frame ThawBase::new_stack_frame(const frame& hf, frame& caller, bool bottom) {
   assert(FKind::is_instance(hf), "");
   // The values in the returned frame object will be written into the callee's stack in patch.
@@ -213,11 +226,10 @@
   if (FKind::interpreted) {
     intptr_t* heap_sp = hf.unextended_sp();
     // If caller is interpreted it already made room for the callee arguments
     int overlap = caller.is_interpreted_frame() ? ContinuationHelper::InterpretedFrame::stack_argsize(hf) : 0;
     const int fsize = (int)(ContinuationHelper::InterpretedFrame::frame_bottom(hf) - hf.unextended_sp() - overlap);
-    const int locals = hf.interpreter_frame_method()->max_locals();
     intptr_t* frame_sp = caller.unextended_sp() - fsize;
     intptr_t* fp = frame_sp + (hf.fp() - heap_sp);
     if ((intptr_t)fp % frame::frame_alignment != 0) {
       fp--;
       frame_sp--;
@@ -235,11 +247,11 @@
     return f;
   } else {
     int fsize = FKind::size(hf);
     intptr_t* frame_sp = caller.unextended_sp() - fsize;
     if (bottom || caller.is_interpreted_frame()) {
-      int argsize = hf.compiled_frame_stack_argsize();
+      int argsize = FKind::stack_argsize(hf);
 
       fsize += argsize;
       frame_sp -= argsize;
       caller.set_sp(caller.sp() - argsize);
       assert(caller.sp() == frame_sp + (fsize-argsize), "");
@@ -250,17 +262,20 @@
     assert(hf.cb() != nullptr, "");
     assert(hf.oop_map() != nullptr, "");
     intptr_t* fp;
     if (PreserveFramePointer) {
       // we need to recreate a "real" frame pointer, pointing into the stack
-      fp = frame_sp + FKind::size(hf) - 2;
+      fp = frame_sp + FKind::size(hf) - frame::sender_sp_offset;
     } else {
-      fp = FKind::stub
-        ? frame_sp + fsize - 2 // On RISCV, this value is used for the safepoint stub
-        : *(intptr_t**)(hf.sp() - 2); // we need to re-read fp because it may be an oop and we might have fixed the frame.
+      fp = FKind::stub || FKind::native
+        // fp always points to the address above the pushed return pc. We need correct address.
+        ? frame_sp + fsize - frame::sender_sp_offset
+        // we need to re-read fp because it may be an oop and we might have fixed the frame.
+        : *(intptr_t**)(hf.sp() - 2);
     }
-    return frame(frame_sp, frame_sp, fp, hf.pc(), hf.cb(), hf.oop_map(), false); // TODO PERF : this computes deopt state; is it necessary?
+    // TODO PERF : this computes deopt state; is it necessary?
+    return frame(frame_sp, frame_sp, fp, hf.pc(), hf.cb(), hf.oop_map(), false);
   }
 }
 
 inline intptr_t* ThawBase::align(const frame& hf, intptr_t* frame_sp, frame& caller, bool bottom) {
 #ifdef _LP64
@@ -277,17 +292,58 @@
 
 inline void ThawBase::patch_pd(frame& f, const frame& caller) {
   patch_callee_link(caller, caller.fp());
 }
 
+inline void ThawBase::patch_pd(frame& f, intptr_t* caller_sp) {
+  intptr_t* fp = caller_sp - frame::sender_sp_offset;
+  patch_callee_link(f, fp);
+}
+
+inline intptr_t* ThawBase::possibly_adjust_frame(frame& top) {
+  intptr_t* sp = top.sp();
+  CodeBlob* cb = top.cb();
+
+  if (cb->frame_size() == 2) {
+    // C2 runtime stub case. For riscv64 the real size of the c2 runtime stub is 2 words bigger
+    // than what we think, i.e. size is 4. This is because the _last_Java_sp is not set to the
+    // sp right before making the call to the VM, but rather it is artificially set 2 words above
+    // this real sp so that we can store the return address at last_Java_sp[-1], and keep this
+    // property where we can retrieve the last_Java_pc from the last_Java_sp. But that means that
+    // once we return to the runtime stub, the code will adjust sp according to this real size.
+    // So we must adjust the frame size back here and we copy ra/fp again.
+    sp -= 2;
+    sp[-2] = sp[0];
+    sp[-1] = sp[1];
+
+    log_develop_trace(continuations, preempt)("adjusted sp for c2 runtime stub, initial sp: " INTPTR_FORMAT " final sp: " INTPTR_FORMAT
+                                              " fp: " INTPTR_FORMAT, p2i(sp + frame::metadata_words), p2i(sp), sp[-2]);
+  }
+  return sp;
+}
+
+inline intptr_t* ThawBase::push_cleanup_continuation() {
+  frame enterSpecial = new_entry_frame();
+  intptr_t* sp = enterSpecial.sp();
+
+  sp[-1] = (intptr_t)ContinuationEntry::cleanup_pc();
+  sp[-2] = (intptr_t)enterSpecial.fp();
+
+  log_develop_trace(continuations, preempt)("push_cleanup_continuation initial sp: " INTPTR_FORMAT " final sp: " INTPTR_FORMAT, p2i(sp + 2 * frame::metadata_words), p2i(sp));
+  return sp;
+}
+
 inline void ThawBase::derelativize_interpreted_frame_metadata(const frame& hf, const frame& f) {
   // Make sure that last_sp is kept relativized.
   assert((intptr_t*)f.at_relative(frame::interpreter_frame_last_sp_offset) == f.unextended_sp(), "");
 
   // Make sure that monitor_block_top is still relativized.
   assert(f.at_absolute(frame::interpreter_frame_monitor_block_top_offset) <= frame::interpreter_frame_initial_sp_offset, "");
 
+  DEBUG_ONLY(Method* m = hf.interpreter_frame_method();)
+  DEBUG_ONLY(int extra_space = m->is_object_wait0() ? m->size_of_parameters() : 0;) // see comment in relativize_interpreted_frame_metadata()
+
   // Make sure that extended_sp is kept relativized.
-  assert((intptr_t*)f.at_relative(frame::interpreter_frame_extended_sp_offset) < f.unextended_sp(), "");
+  assert((intptr_t*)f.at_relative(frame::interpreter_frame_extended_sp_offset) < f.unextended_sp() + extra_space, "");
 }
 
 #endif // CPU_RISCV_CONTINUATIONFREEZETHAW_RISCV_INLINE_HPP
diff a/src/hotspot/cpu/riscv/continuationHelper_riscv.inline.hpp b/src/hotspot/cpu/riscv/continuationHelper_riscv.inline.hpp
--- a/src/hotspot/cpu/riscv/continuationHelper_riscv.inline.hpp
+++ b/src/hotspot/cpu/riscv/continuationHelper_riscv.inline.hpp
@@ -38,10 +38,26 @@
   return FKind::interpreted
             ? (intptr_t**)(f.fp() + frame::link_offset)
             : (intptr_t**)(f.unextended_sp() + f.cb()->frame_size() - 2);
 }
 
+static inline void patch_return_pc_with_preempt_stub(frame& f) {
+  if (f.is_runtime_frame()) {
+    // Unlike x86 we don't know where in the callee frame the return pc is
+    // saved so we can't patch the return from the VM call back to Java.
+    // Instead, we will patch the return from the runtime stub back to the
+    // compiled method so that the target returns to the preempt cleanup stub.
+    intptr_t* caller_sp = f.sp() + f.cb()->frame_size();
+    caller_sp[-1] = (intptr_t)StubRoutines::cont_preempt_stub();
+  } else {
+    // The target will check for preemption once it returns to the interpreter
+    // or the native wrapper code and will manually jump to the preempt stub.
+    JavaThread *thread = JavaThread::current();
+    thread->set_preempt_alternate_return(StubRoutines::cont_preempt_stub());
+  }
+}
+
 inline int ContinuationHelper::frame_align_words(int size) {
 #ifdef _LP64
   return size & 1;
 #else
   return 0;
@@ -70,16 +86,16 @@
 
 inline void ContinuationHelper::set_anchor_to_entry_pd(JavaFrameAnchor* anchor, ContinuationEntry* entry) {
   anchor->set_last_Java_fp(entry->entry_fp());
 }
 
-#ifdef ASSERT
 inline void ContinuationHelper::set_anchor_pd(JavaFrameAnchor* anchor, intptr_t* sp) {
   intptr_t* fp = *(intptr_t**)(sp - 2);
   anchor->set_last_Java_fp(fp);
 }
 
+#ifdef ASSERT
 inline bool ContinuationHelper::Frame::assert_frame_laid_out(frame f) {
   intptr_t* sp = f.sp();
   address pc = *(address*)(sp - frame::sender_sp_ret_address_offset());
   intptr_t* fp = *(intptr_t**)(sp - 2);
   assert(f.raw_pc() == pc, "f.ra_pc: " INTPTR_FORMAT " actual: " INTPTR_FORMAT, p2i(f.raw_pc()), p2i(pc));
diff a/src/hotspot/cpu/riscv/frame_riscv.hpp b/src/hotspot/cpu/riscv/frame_riscv.hpp
--- a/src/hotspot/cpu/riscv/frame_riscv.hpp
+++ b/src/hotspot/cpu/riscv/frame_riscv.hpp
@@ -109,11 +109,12 @@
     link_offset                                      = -2,
     return_addr_offset                               = -1,
     sender_sp_offset                                 =  0,
 
     // Interpreter frames
-    interpreter_frame_oop_temp_offset                =  1, // for native calls only
+    interpreter_frame_result_handler_offset          =  1, // for native calls only
+    interpreter_frame_oop_temp_offset                =  0, // for native calls only
 
     interpreter_frame_sender_sp_offset               = -3,
     // outgoing sp before a call to an invoked method
     interpreter_frame_last_sp_offset                 = interpreter_frame_sender_sp_offset - 1,
     interpreter_frame_method_offset                  = interpreter_frame_last_sp_offset - 1,
diff a/src/hotspot/cpu/riscv/interp_masm_riscv.cpp b/src/hotspot/cpu/riscv/interp_masm_riscv.cpp
--- a/src/hotspot/cpu/riscv/interp_masm_riscv.cpp
+++ b/src/hotspot/cpu/riscv/interp_masm_riscv.cpp
@@ -724,11 +724,11 @@
 //      t0, t1 (temp regs)
 void InterpreterMacroAssembler::lock_object(Register lock_reg)
 {
   assert(lock_reg == c_rarg1, "The argument is only for looks. It must be c_rarg1");
   if (LockingMode == LM_MONITOR) {
-    call_VM(noreg,
+    call_VM_preemptable(noreg,
             CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
             lock_reg);
   } else {
     Label count, done;
 
@@ -755,11 +755,11 @@
       bnez(tmp, slow_case);
     }
 
     if (LockingMode == LM_LIGHTWEIGHT) {
       lightweight_lock(lock_reg, obj_reg, tmp, tmp2, tmp3, slow_case);
-      j(count);
+      j(done);
     } else if (LockingMode == LM_LEGACY) {
       // Load (object->mark() | 1) into swap_reg
       ld(t0, Address(obj_reg, oopDesc::mark_offset_in_bytes()));
       ori(swap_reg, t0, 1);
 
@@ -784,23 +784,23 @@
       mv(t0, (int64_t)(7 - (int)os::vm_page_size()));
       andr(swap_reg, swap_reg, t0);
 
       // Save the test result, for recursive case, the result is zero
       sd(swap_reg, Address(lock_reg, mark_offset));
-      beqz(swap_reg, count);
+      bnez(swap_reg, slow_case);
+
+      bind(count);
+      inc_held_monitor_count();
+      j(done);
     }
 
     bind(slow_case);
 
     // Call the runtime routine for slow case
-    call_VM(noreg,
+    call_VM_preemptable(noreg,
             CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
             lock_reg);
-    j(done);
-
-    bind(count);
-    increment(Address(xthread, JavaThread::held_monitor_count_offset()));
 
     bind(done);
   }
 }
 
@@ -842,39 +842,36 @@
     ld(obj_reg, Address(lock_reg, BasicObjectLock::obj_offset()));
 
     // Free entry
     sd(zr, Address(lock_reg, BasicObjectLock::obj_offset()));
 
+    Label slow_case;
     if (LockingMode == LM_LIGHTWEIGHT) {
-      Label slow_case;
       lightweight_unlock(obj_reg, header_reg, swap_reg, tmp_reg, slow_case);
-      j(count);
-
-      bind(slow_case);
+      j(done);
     } else if (LockingMode == LM_LEGACY) {
       // Load the old header from BasicLock structure
       ld(header_reg, Address(swap_reg,
                              BasicLock::displaced_header_offset_in_bytes()));
 
       // Test for recursion
       beqz(header_reg, count);
 
       // Atomic swap back the old header
-      cmpxchg_obj_header(swap_reg, header_reg, obj_reg, tmp_reg, count, /*fallthrough*/nullptr);
+      cmpxchg_obj_header(swap_reg, header_reg, obj_reg, tmp_reg, count, &slow_case);
+
+      bind(count);
+      dec_held_monitor_count();
+      j(done);
     }
 
+    bind(slow_case);
     // Call the runtime routine for slow case.
     sd(obj_reg, Address(lock_reg, BasicObjectLock::obj_offset())); // restore obj
     call_VM_leaf(CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorexit), lock_reg);
 
-    j(done);
-
-    bind(count);
-    decrement(Address(xthread, JavaThread::held_monitor_count_offset()));
-
     bind(done);
-
     restore_bcp();
   }
 }
 
 
@@ -1578,10 +1575,58 @@
 // interpreter specific
   restore_bcp();
   restore_locals();
 }
 
+void InterpreterMacroAssembler::call_VM_preemptable(Register oop_result,
+                                                    address entry_point,
+                                                    Register arg_1) {
+  assert(arg_1 == c_rarg1, "");
+  Label resume_pc, not_preempted;
+
+#ifdef ASSERT
+  {
+    Label L;
+    ld(t0, Address(xthread, JavaThread::preempt_alternate_return_offset()));
+    beqz(t0, L);
+    stop("Should not have alternate return address set");
+    bind(L);
+  }
+#endif /* ASSERT */
+
+  push_cont_fastpath();
+
+  // Make VM call. In case of preemption set last_pc to the one we want to resume to.
+  la(t0, resume_pc);
+  sd(t0, Address(xthread, JavaThread::last_Java_pc_offset()));
+  call_VM_base(oop_result, noreg, noreg, entry_point, 1, false /*check_exceptions*/);
+
+  pop_cont_fastpath();
+
+  // Check if preempted.
+  ld(t0, Address(xthread, JavaThread::preempt_alternate_return_offset()));
+  beqz(t0, not_preempted);
+  sd(zr, Address(xthread, JavaThread::preempt_alternate_return_offset()));
+  jr(t0);
+
+  // In case of preemption, this is where we will resume once we finally acquire the monitor.
+  bind(resume_pc);
+  restore_after_resume(false /* is_native */);
+
+  bind(not_preempted);
+}
+
+void InterpreterMacroAssembler::restore_after_resume(bool is_native) {
+  la(t0, ExternalAddress(Interpreter::cont_resume_interpreter_adapter()));
+  jalr(t0);
+  if (is_native) {
+    // On resume we need to set up stack as expected
+    push(dtos);
+    push(ltos);
+  }
+}
+
 void InterpreterMacroAssembler::profile_obj_type(Register obj, const Address& mdo_addr, Register tmp) {
   assert_different_registers(obj, tmp, t0, mdo_addr.base());
   Label update, next, none;
 
   verify_oop(obj);
diff a/src/hotspot/cpu/riscv/interp_masm_riscv.hpp b/src/hotspot/cpu/riscv/interp_masm_riscv.hpp
--- a/src/hotspot/cpu/riscv/interp_masm_riscv.hpp
+++ b/src/hotspot/cpu/riscv/interp_masm_riscv.hpp
@@ -57,10 +57,15 @@
  public:
   InterpreterMacroAssembler(CodeBuffer* code) : MacroAssembler(code) {}
 
   void load_earlyret_value(TosState state);
 
+  void call_VM_preemptable(Register oop_result,
+                           address entry_point,
+                           Register arg_1);
+  void restore_after_resume(bool is_native);
+
   void jump_to_entry(address entry);
 
   virtual void check_and_handle_popframe(Register java_thread);
   virtual void check_and_handle_earlyret(Register java_thread);
 
diff a/src/hotspot/cpu/riscv/macroAssembler_riscv.cpp b/src/hotspot/cpu/riscv/macroAssembler_riscv.cpp
--- a/src/hotspot/cpu/riscv/macroAssembler_riscv.cpp
+++ b/src/hotspot/cpu/riscv/macroAssembler_riscv.cpp
@@ -34,10 +34,11 @@
 #include "gc/shared/cardTable.hpp"
 #include "gc/shared/cardTableBarrierSet.hpp"
 #include "gc/shared/collectedHeap.hpp"
 #include "interpreter/bytecodeHistogram.hpp"
 #include "interpreter/interpreter.hpp"
+#include "interpreter/interpreterRuntime.hpp"
 #include "memory/resourceArea.hpp"
 #include "memory/universe.hpp"
 #include "oops/accessDecorators.hpp"
 #include "oops/compressedKlass.inline.hpp"
 #include "oops/compressedOops.inline.hpp"
@@ -224,10 +225,40 @@
   bltu(sp, t0, done);
   sd(zr, Address(java_thread, JavaThread::cont_fastpath_offset()));
   bind(done);
 }
 
+void MacroAssembler::inc_held_monitor_count(Register tmp) {
+  Address dst = Address(xthread, JavaThread::held_monitor_count_offset());
+  ld(tmp, dst);
+  addi(tmp, tmp, 1);
+  sd(tmp, dst);
+#ifdef ASSERT
+  Label ok;
+  test_bit(tmp, tmp, 63);
+  beqz(tmp, ok);
+  STOP("assert(held monitor count overflow)");
+  should_not_reach_here();
+  bind(ok);
+#endif
+}
+
+void MacroAssembler::dec_held_monitor_count(Register tmp) {
+  Address dst = Address(xthread, JavaThread::held_monitor_count_offset());
+  ld(tmp, dst);
+  addi(tmp, tmp, -1);
+  sd(tmp, dst);
+#ifdef ASSERT
+  Label ok;
+  test_bit(tmp, tmp, 63);
+  beqz(tmp, ok);
+  STOP("assert(held monitor count underflow)");
+  should_not_reach_here();
+  bind(ok);
+#endif
+}
+
 int MacroAssembler::align(int modulus, int extra_offset) {
   CompressibleRegion cr(this);
   intptr_t before = offset();
   while ((offset() + extra_offset) % modulus != 0) { nop(); }
   return (int)(offset() - before);
@@ -405,10 +436,14 @@
 
   // Always clear the pc because it could have been set by make_walkable()
   sd(zr, Address(xthread, JavaThread::last_Java_pc_offset()));
 }
 
+static bool is_preemptable(address entry_point) {
+  return entry_point == CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter);
+}
+
 void MacroAssembler::call_VM_base(Register oop_result,
                                   Register java_thread,
                                   Register last_java_sp,
                                   address  entry_point,
                                   int      number_of_arguments,
@@ -434,11 +469,16 @@
 
   // set last Java frame before call
   assert(last_java_sp != fp, "can't use fp");
 
   Label l;
-  set_last_Java_frame(last_java_sp, fp, l, t0);
+  if (is_preemptable(entry_point)) {
+    // skip setting last_pc since we already set it to desired value.
+    set_last_Java_frame(last_java_sp, fp, noreg);
+  } else {
+    set_last_Java_frame(last_java_sp, fp, l, t0);
+  }
 
   // do the call, remove parameters
   MacroAssembler::call_VM_leaf_base(entry_point, number_of_arguments, &l);
 
   // reset last Java frame
diff a/src/hotspot/cpu/riscv/macroAssembler_riscv.hpp b/src/hotspot/cpu/riscv/macroAssembler_riscv.hpp
--- a/src/hotspot/cpu/riscv/macroAssembler_riscv.hpp
+++ b/src/hotspot/cpu/riscv/macroAssembler_riscv.hpp
@@ -784,12 +784,15 @@
   }
 
   void push_CPU_state(bool save_vectors = false, int vector_size_in_bytes = 0);
   void pop_CPU_state(bool restore_vectors = false, int vector_size_in_bytes = 0);
 
-  void push_cont_fastpath(Register java_thread);
-  void pop_cont_fastpath(Register java_thread);
+  void push_cont_fastpath(Register java_thread = xthread);
+  void pop_cont_fastpath(Register java_thread = xthread);
+
+  void inc_held_monitor_count(Register tmp = t0);
+  void dec_held_monitor_count(Register tmp = t0);
 
   // if heap base register is used - reinit it with the correct value
   void reinit_heapbase();
 
   void bind(Label& L) {
diff a/src/hotspot/cpu/riscv/riscv.ad b/src/hotspot/cpu/riscv/riscv.ad
--- a/src/hotspot/cpu/riscv/riscv.ad
+++ b/src/hotspot/cpu/riscv/riscv.ad
@@ -10521,21 +10521,23 @@
   ins_pipe(pipe_class_empty);
 %}
 
 // inlined locking and unlocking
 // using t1 as the 'flag' register to bridge the BoolNode producers and consumers
-instruct cmpFastLock(rFlagsReg cr, iRegP object, iRegP box, iRegPNoSp tmp1, iRegPNoSp tmp2, iRegPNoSp tmp3)
+instruct cmpFastLock(rFlagsReg cr, iRegP object, iRegP box,
+                     iRegPNoSp tmp1, iRegPNoSp tmp2, iRegPNoSp tmp3, iRegPNoSp tmp4)
 %{
   predicate(LockingMode != LM_LIGHTWEIGHT);
   match(Set cr (FastLock object box));
-  effect(TEMP tmp1, TEMP tmp2, TEMP tmp3);
+  effect(TEMP tmp1, TEMP tmp2, TEMP tmp3, TEMP tmp4);
 
   ins_cost(10 * DEFAULT_COST);
-  format %{ "fastlock $object,$box\t! kills $tmp1,$tmp2,$tmp3, #@cmpFastLock" %}
+  format %{ "fastlock $object,$box\t! kills $tmp1,$tmp2,$tmp3,$tmp4 #@cmpFastLock" %}
 
   ins_encode %{
-    __ fast_lock($object$$Register, $box$$Register, $tmp1$$Register, $tmp2$$Register, $tmp3$$Register);
+    __ fast_lock($object$$Register, $box$$Register,
+                 $tmp1$$Register, $tmp2$$Register, $tmp3$$Register, $tmp4$$Register);
   %}
 
   ins_pipe(pipe_serial);
 %}
 
@@ -10554,37 +10556,41 @@
   %}
 
   ins_pipe(pipe_serial);
 %}
 
-instruct cmpFastLockLightweight(rFlagsReg cr, iRegP object, iRegP box, iRegPNoSp tmp1, iRegPNoSp tmp2, iRegPNoSp tmp3)
+instruct cmpFastLockLightweight(rFlagsReg cr, iRegP object, iRegP box,
+                                iRegPNoSp tmp1, iRegPNoSp tmp2, iRegPNoSp tmp3, iRegPNoSp tmp4)
 %{
   predicate(LockingMode == LM_LIGHTWEIGHT);
   match(Set cr (FastLock object box));
-  effect(TEMP tmp1, TEMP tmp2, TEMP tmp3);
+  effect(TEMP tmp1, TEMP tmp2, TEMP tmp3, TEMP tmp4);
 
   ins_cost(10 * DEFAULT_COST);
-  format %{ "fastlock $object,$box\t! kills $tmp1,$tmp2,$tmp3 #@cmpFastLockLightweight" %}
+  format %{ "fastlock $object,$box\t! kills $tmp1,$tmp2,$tmp3,$tmp4 #@cmpFastLockLightweight" %}
 
   ins_encode %{
-    __ fast_lock_lightweight($object$$Register, $box$$Register, $tmp1$$Register, $tmp2$$Register, $tmp3$$Register);
+    __ fast_lock_lightweight($object$$Register, $box$$Register,
+                             $tmp1$$Register, $tmp2$$Register, $tmp3$$Register, $tmp4$$Register);
   %}
 
   ins_pipe(pipe_serial);
 %}
 
-instruct cmpFastUnlockLightweight(rFlagsReg cr, iRegP object, iRegP box, iRegPNoSp tmp1, iRegPNoSp tmp2, iRegPNoSp tmp3)
+instruct cmpFastUnlockLightweight(rFlagsReg cr, iRegP object, iRegP box,
+                                  iRegPNoSp tmp1, iRegPNoSp tmp2, iRegPNoSp tmp3)
 %{
   predicate(LockingMode == LM_LIGHTWEIGHT);
   match(Set cr (FastUnlock object box));
   effect(TEMP tmp1, TEMP tmp2, TEMP tmp3);
 
   ins_cost(10 * DEFAULT_COST);
   format %{ "fastunlock $object,$box\t! kills $tmp1,$tmp2,$tmp3 #@cmpFastUnlockLightweight" %}
 
   ins_encode %{
-    __ fast_unlock_lightweight($object$$Register, $box$$Register, $tmp1$$Register, $tmp2$$Register, $tmp3$$Register);
+    __ fast_unlock_lightweight($object$$Register, $box$$Register,
+                               $tmp1$$Register, $tmp2$$Register, $tmp3$$Register);
   %}
 
   ins_pipe(pipe_serial);
 %}
 
diff a/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp b/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp
--- a/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp
+++ b/src/hotspot/cpu/riscv/sharedRuntime_riscv.cpp
@@ -1049,16 +1049,18 @@
 
   __ j(exit);
 
   __ bind(call_thaw);
 
+  ContinuationEntry::_thaw_call_pc_offset = __ pc() - start;
   __ rt_call(CAST_FROM_FN_PTR(address, StubRoutines::cont_thaw()));
   oop_maps->add_gc_map(__ pc() - start, map->deep_copy());
   ContinuationEntry::_return_pc_offset = __ pc() - start;
   __ post_call_nop();
 
   __ bind(exit);
+  ContinuationEntry::_cleanup_offset = __ pc() - start;
   continuation_enter_cleanup(masm);
   __ leave();
   __ ret();
 
   // exception handling
@@ -1149,10 +1151,14 @@
 
   OopMap* map = new OopMap(framesize, 1);
   oop_maps->add_gc_map(the_pc - start, map);
 }
 
+void SharedRuntime::continuation_enter_cleanup(MacroAssembler* masm) {
+  ::continuation_enter_cleanup(masm);
+}
+
 static void gen_special_dispatch(MacroAssembler* masm,
                                  const methodHandle& method,
                                  const BasicType* sig_bt,
                                  const VMRegPair* regs) {
   verify_oop_args(masm, method, sig_bt, regs);
@@ -1631,15 +1637,24 @@
     // and protect the arg if we must spill
     c_arg--;
   }
 
   // Change state to native (we save the return address in the thread, since it might not
-  // be pushed on the stack when we do a stack traversal).
-  // We use the same pc/oopMap repeatedly when we call out
+  // be pushed on the stack when we do a stack traversal). It is enough that the pc()
+  // points into the right code segment. It does not have to be the correct return pc.
+  // We use the same pc/oopMap repeatedly when we call out.
 
   Label native_return;
-  __ set_last_Java_frame(sp, noreg, native_return, t0);
+  if (LockingMode != LM_LEGACY && method->is_object_wait0()) {
+    // For convenience we use the pc we want to resume to in case of preemption on Object.wait.
+    __ set_last_Java_frame(sp, noreg, native_return, t0);
+  } else {
+    intptr_t the_pc = (intptr_t) __ pc();
+    oop_maps->add_gc_map(the_pc - start, map);
+
+    __ set_last_Java_frame(sp, noreg, __ pc(), t0);
+  }
 
   Label dtrace_method_entry, dtrace_method_entry_done;
   if (DTraceMethodProbes) {
     __ j(dtrace_method_entry);
     __ bind(dtrace_method_entry_done);
@@ -1711,18 +1726,18 @@
       __ andi(swap_reg, swap_reg, 3 - (int)os::vm_page_size());
 
       // Save the test result, for recursive case, the result is zero
       __ sd(swap_reg, Address(lock_reg, mark_word_offset));
       __ bnez(swap_reg, slow_path_lock);
+
+      __ bind(count);
+      __ inc_held_monitor_count();
     } else {
       assert(LockingMode == LM_LIGHTWEIGHT, "must be");
       __ lightweight_lock(lock_reg, obj_reg, swap_reg, tmp, lock_tmp, slow_path_lock);
     }
 
-    __ bind(count);
-    __ increment(Address(xthread, JavaThread::held_monitor_count_offset()));
-
     // Slow path will re-enter here
     __ bind(lock_done);
   }
 
 
@@ -1737,15 +1752,10 @@
   __ membar(MacroAssembler::LoadStore | MacroAssembler::StoreStore);
   __ sw(t0, Address(t1));
 
   __ rt_call(native_func);
 
-  __ bind(native_return);
-
-  intptr_t return_pc = (intptr_t) __ pc();
-  oop_maps->add_gc_map(return_pc - start, map);
-
   // Verify or restore cpu control state after JNI call
   __ restore_cpu_control_state_after_jni(t0);
 
   // Unpack native results.
   if (ret_type != T_OBJECT && ret_type != T_ARRAY) {
@@ -1792,10 +1802,22 @@
   __ mv(t0, _thread_in_Java);
   __ membar(MacroAssembler::LoadStore | MacroAssembler::StoreStore);
   __ sw(t0, Address(t1));
   __ bind(after_transition);
 
+  if (LockingMode != LM_LEGACY && method->is_object_wait0()) {
+    // Check preemption for Object.wait()
+    __ ld(t0, Address(xthread, JavaThread::preempt_alternate_return_offset()));
+    __ beqz(t0, native_return);
+    __ sd(zr, Address(xthread, JavaThread::preempt_alternate_return_offset()));
+    __ jr(t0);
+    __ bind(native_return);
+
+    intptr_t the_pc = (intptr_t) __ pc();
+    oop_maps->add_gc_map(the_pc - start, map);
+  }
+
   Label reguard;
   Label reguard_done;
   __ lbu(t0, Address(xthread, JavaThread::stack_guard_state_offset()));
   __ mv(t1, StackOverflow::stack_guard_yellow_reserved_disabled);
   __ beq(t0, t1, reguard);
@@ -1815,11 +1837,11 @@
 
     if (LockingMode == LM_LEGACY) {
       // Simple recursive lock?
       __ ld(t0, Address(sp, lock_slot_offset * VMRegImpl::stack_slot_size));
       __ bnez(t0, not_recursive);
-      __ decrement(Address(xthread, JavaThread::held_monitor_count_offset()));
+      __ dec_held_monitor_count();
       __ j(done);
     }
 
     __ bind(not_recursive);
 
@@ -1838,15 +1860,14 @@
 
       // Atomic swap old header if oop still contains the stack lock
       Label count;
       __ cmpxchg_obj_header(x10, old_hdr, obj_reg, lock_tmp, count, &slow_path_unlock);
       __ bind(count);
-      __ decrement(Address(xthread, JavaThread::held_monitor_count_offset()));
+      __ dec_held_monitor_count();
     } else {
       assert(LockingMode == LM_LIGHTWEIGHT, "");
       __ lightweight_unlock(obj_reg, old_hdr, swap_reg, lock_tmp, slow_path_unlock);
-      __ decrement(Address(xthread, JavaThread::held_monitor_count_offset()));
     }
 
     // slow path re-enters here
     __ bind(unlock_done);
     if (ret_type != T_FLOAT && ret_type != T_DOUBLE && ret_type != T_VOID) {
@@ -1911,11 +1932,15 @@
     __ mv(c_rarg0, obj_reg);
     __ mv(c_rarg1, lock_reg);
     __ mv(c_rarg2, xthread);
 
     // Not a leaf but we have last_Java_frame setup as we want
+    // Force freeze slow path in case we try to preempt. We will pin the
+    // vthread to the carrier (see FreezeBase::recurse_freeze_native_frame()).
+    __ push_cont_fastpath();
     __ call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::complete_monitor_locking_C), 3);
+    __ pop_cont_fastpath();
     restore_args(masm, total_c_args, c_arg, out_regs);
 
 #ifdef ASSERT
     { Label L;
       __ ld(t0, Address(xthread, in_bytes(Thread::pending_exception_offset())));
@@ -2442,10 +2467,14 @@
 
 uint SharedRuntime::out_preserve_stack_slots() {
   return 0;
 }
 
+VMReg SharedRuntime::thread_register() {
+  return xthread->as_VMReg();
+}
+
 //------------------------------generate_handler_blob------
 //
 // Generate a special Compile2Runtime blob that saves all registers,
 // and setup oopmap.
 //
diff a/src/hotspot/cpu/riscv/stackChunkFrameStream_riscv.inline.hpp b/src/hotspot/cpu/riscv/stackChunkFrameStream_riscv.inline.hpp
--- a/src/hotspot/cpu/riscv/stackChunkFrameStream_riscv.inline.hpp
+++ b/src/hotspot/cpu/riscv/stackChunkFrameStream_riscv.inline.hpp
@@ -112,10 +112,11 @@
   InterpreterOopMap mask;
   frame f = to_frame();
   f.interpreted_frame_oop_map(&mask);
   return mask.num_oops()
         + 1 // for the mirror oop
+        + (f.interpreter_frame_method()->is_native() ? 1 : 0) // temp oop slot
         + pointer_delta_as_int((intptr_t*)f.interpreter_frame_monitor_begin(),
               (intptr_t*)f.interpreter_frame_monitor_end()) / BasicObjectLock::size();
 }
 
 template<>
diff a/src/hotspot/cpu/riscv/stubGenerator_riscv.cpp b/src/hotspot/cpu/riscv/stubGenerator_riscv.cpp
--- a/src/hotspot/cpu/riscv/stubGenerator_riscv.cpp
+++ b/src/hotspot/cpu/riscv/stubGenerator_riscv.cpp
@@ -3871,10 +3871,40 @@
     generate_cont_thaw(Continuation::thaw_return_barrier_exception);
 
     return start;
   }
 
+  address generate_cont_preempt_stub() {
+    if (!Continuations::enabled()) return nullptr;
+    StubCodeMark mark(this, "StubRoutines","Continuation preempt stub");
+    address start = __ pc();
+
+    __ reset_last_Java_frame(true);
+
+    // Set sp to enterSpecial frame, i.e. remove all frames copied into the heap.
+    __ ld(sp, Address(xthread, JavaThread::cont_entry_offset()));
+
+    Label preemption_cancelled;
+    __ lbu(t0, Address(xthread, JavaThread::preemption_cancelled_offset()));
+    __ bnez(t0, preemption_cancelled);
+
+    // Remove enterSpecial frame from the stack and return to Continuation.run() to unmount.
+    SharedRuntime::continuation_enter_cleanup(_masm);
+    __ leave();
+    __ ret();
+
+    // We acquired the monitor after freezing the frames so call thaw to continue execution.
+    __ bind(preemption_cancelled);
+    __ sb(zr, Address(xthread, JavaThread::preemption_cancelled_offset()));
+    __ la(fp, Address(sp, checked_cast<int32_t>(ContinuationEntry::size() + 2 * wordSize)));
+    __ la(t0, ExternalAddress(ContinuationEntry::thaw_call_pc_address()));
+    __ ld(t0, Address(t0));
+    __ jr(t0);
+
+    return start;
+  }
+
 #if COMPILER2_OR_JVMCI
 
 #undef __
 #define __ this->
 
@@ -6233,10 +6263,11 @@
   void generate_continuation_stubs() {
     // Continuation stubs:
     StubRoutines::_cont_thaw             = generate_cont_thaw();
     StubRoutines::_cont_returnBarrier    = generate_cont_returnBarrier();
     StubRoutines::_cont_returnBarrierExc = generate_cont_returnBarrier_exception();
+    StubRoutines::_cont_preempt_stub     = generate_cont_preempt_stub();
   }
 
   void generate_final_stubs() {
     // support for verify_oop (must happen after universe_init)
     if (VerifyOops) {
diff a/src/hotspot/cpu/riscv/templateInterpreterGenerator_riscv.cpp b/src/hotspot/cpu/riscv/templateInterpreterGenerator_riscv.cpp
--- a/src/hotspot/cpu/riscv/templateInterpreterGenerator_riscv.cpp
+++ b/src/hotspot/cpu/riscv/templateInterpreterGenerator_riscv.cpp
@@ -538,10 +538,42 @@
   __ membar(MacroAssembler::AnyAny);
   __ dispatch_via(vtos, Interpreter::_normal_table.table_for(vtos));
   return entry;
 }
 
+address TemplateInterpreterGenerator::generate_cont_resume_interpreter_adapter() {
+  if (!Continuations::enabled()) return nullptr;
+  address start = __ pc();
+
+  __ restore_bcp();
+  __ restore_locals();
+
+  // Restore constant pool cache
+  __ ld(xcpool, Address(fp, frame::interpreter_frame_cache_offset * wordSize));
+
+  // Restore Java expression stack pointer
+  __ ld(t0, Address(fp, frame::interpreter_frame_last_sp_offset * wordSize));
+  __ shadd(esp, t0, fp, t0, Interpreter::logStackElementSize);
+  // and NULL it as marker that esp is now tos until next java call
+  __ sd(zr, Address(fp, frame::interpreter_frame_last_sp_offset * wordSize));
+
+  // Restore machine SP
+  __ ld(t0, Address(fp, frame::interpreter_frame_extended_sp_offset * wordSize));
+  __ shadd(sp, t0, fp, t0, LogBytesPerWord);
+
+  // Restore method
+  __ ld(xmethod, Address(fp, frame::interpreter_frame_method_offset * wordSize));
+
+  // Restore dispatch
+  __ la(xdispatch, ExternalAddress((address)Interpreter::dispatch_table()));
+
+  __ ret();
+
+  return start;
+}
+
+
 // Helpers for commoning out cases in the various type of method entries.
 //
 
 
 // increment invocation count & check for overflow
@@ -1091,10 +1123,12 @@
 
 
   // result handler is in x10
   // set result handler
   __ mv(result_handler, x10);
+  __ sd(x10, Address(fp, frame::interpreter_frame_result_handler_offset * wordSize));
+
   // pass mirror handle if static call
   {
     Label L;
     __ lwu(t, Address(xmethod, Method::access_flags_offset()));
     __ test_bit(t0, t, exact_log2(JVM_ACC_STATIC));
@@ -1129,10 +1163,12 @@
   // pass JNIEnv
   __ add(c_rarg0, xthread, in_bytes(JavaThread::jni_environment_offset()));
 
   // It is enough that the pc() points into the right code
   // segment. It does not have to be the correct return pc.
+  // For convenience we use the pc we want to resume to in
+  // case of preemption on Object.wait.
   Label native_return;
   __ set_last_Java_frame(esp, fp, native_return, x30);
 
   // change thread state
 #ifdef ASSERT
@@ -1150,13 +1186,17 @@
   __ la(t1, Address(xthread, JavaThread::thread_state_offset()));
   __ mv(t0, _thread_in_native);
   __ membar(MacroAssembler::LoadStore | MacroAssembler::StoreStore);
   __ sw(t0, Address(t1));
 
+  __ push_cont_fastpath();
+
   // Call the native method.
   __ jalr(x28);
-  __ bind(native_return);
+
+  __ pop_cont_fastpath();
+
   __ get_method(xmethod);
   // result potentially in x10 or f10
 
   // Restore cpu control state after JNI call
   __ restore_cpu_control_state_after_jni(t0);
@@ -1218,10 +1258,25 @@
   __ membar(MacroAssembler::LoadStore | MacroAssembler::StoreStore);
 
   __ mv(t0, _thread_in_Java);
   __ sw(t0, Address(xthread, JavaThread::thread_state_offset()));
 
+  if (LockingMode != LM_LEGACY) {
+    // Check preemption for Object.wait()
+    Label not_preempted;
+    __ ld(t0, Address(xthread, JavaThread::preempt_alternate_return_offset()));
+    __ beqz(t0, not_preempted);
+    __ sd(zr, Address(xthread, JavaThread::preempt_alternate_return_offset()));
+    __ jr(t0);
+    __ bind(native_return);
+    __ restore_after_resume(true /* is_native */);
+    __ bind(not_preempted);
+  } else {
+    // any pc will do so just use this one for LM_LEGACY to keep code together.
+    __ bind(native_return);
+  }
+
   // reset_last_Java_frame
   __ reset_last_Java_frame(true);
 
   if (CheckJNICalls) {
     // clear_pending_jni_exception_check
@@ -1236,10 +1291,11 @@
   // and result handler will pick it up
 
   {
     Label no_oop;
     __ la(t, ExternalAddress(AbstractInterpreter::result_handler(T_OBJECT)));
+    __ ld(result_handler, Address(fp, frame::interpreter_frame_result_handler_offset * wordSize));
     __ bne(t, result_handler, no_oop);
     // Unbox oop result, e.g. JNIHandles::resolve result.
     __ pop(ltos);
     __ resolve_jobject(x10, t, t1);
     __ sd(x10, Address(fp, frame::interpreter_frame_oop_temp_offset * wordSize));
diff a/src/hotspot/cpu/riscv/templateTable_riscv.cpp b/src/hotspot/cpu/riscv/templateTable_riscv.cpp
--- a/src/hotspot/cpu/riscv/templateTable_riscv.cpp
+++ b/src/hotspot/cpu/riscv/templateTable_riscv.cpp
@@ -3894,10 +3894,11 @@
    // expression stack looks correct.
    __ addi(xbcp, xbcp, 1);
 
    // store object
    __ sd(x10, Address(c_rarg1, BasicObjectLock::obj_offset()));
+
    __ lock_object(c_rarg1);
 
    // check to make sure this monitor doesn't cause stack overflow after locking
    __ save_bcp();  // in case of exception
    __ generate_stack_overflow_check(0);
diff a/src/hotspot/cpu/s390/c1_Runtime1_s390.cpp b/src/hotspot/cpu/s390/c1_Runtime1_s390.cpp
--- a/src/hotspot/cpu/s390/c1_Runtime1_s390.cpp
+++ b/src/hotspot/cpu/s390/c1_Runtime1_s390.cpp
@@ -206,10 +206,15 @@
 
 void Runtime1::initialize_pd() {
   // Nothing to do.
 }
 
+uint Runtime1::runtime_blob_current_thread_offset(frame f) {
+  Unimplemented();
+  return 0;
+}
+
 OopMapSet* Runtime1::generate_exception_throw(StubAssembler* sasm, address target, bool has_argument) {
   // Make a frame and preserve the caller's caller-save registers.
   OopMap* oop_map = save_live_registers(sasm);
   int call_offset;
   if (!has_argument) {
diff a/src/hotspot/cpu/s390/continuationFreezeThaw_s390.inline.hpp b/src/hotspot/cpu/s390/continuationFreezeThaw_s390.inline.hpp
--- a/src/hotspot/cpu/s390/continuationFreezeThaw_s390.inline.hpp
+++ b/src/hotspot/cpu/s390/continuationFreezeThaw_s390.inline.hpp
@@ -46,10 +46,14 @@
 
 void FreezeBase::adjust_interpreted_frame_unextended_sp(frame& f) {
   Unimplemented();
 }
 
+inline void FreezeBase::prepare_freeze_interpreted_top_frame(const frame& f) {
+  Unimplemented();
+}
+
 inline void FreezeBase::relativize_interpreted_frame_metadata(const frame& f, const frame& hf) {
   Unimplemented();
 }
 
 inline void FreezeBase::patch_pd(frame& hf, const frame& caller) {
@@ -81,10 +85,24 @@
 
 inline void ThawBase::patch_pd(frame& f, const frame& caller) {
   Unimplemented();
 }
 
+inline void ThawBase::patch_pd(frame& f, intptr_t* caller_sp) {
+  Unimplemented();
+}
+
+inline intptr_t* ThawBase::possibly_adjust_frame(frame& top) {
+  Unimplemented();
+  return nullptr;
+}
+
+inline intptr_t* ThawBase::push_cleanup_continuation() {
+  Unimplemented();
+  return nullptr;
+}
+
 template <typename ConfigT>
 inline void Thaw<ConfigT>::patch_caller_links(intptr_t* sp, intptr_t* bottom) {
   Unimplemented();
 }
 
diff a/src/hotspot/cpu/s390/continuationHelper_s390.inline.hpp b/src/hotspot/cpu/s390/continuationHelper_s390.inline.hpp
--- a/src/hotspot/cpu/s390/continuationHelper_s390.inline.hpp
+++ b/src/hotspot/cpu/s390/continuationHelper_s390.inline.hpp
@@ -33,10 +33,14 @@
 static inline intptr_t** link_address(const frame& f) {
   Unimplemented();
   return nullptr;
 }
 
+static inline void patch_return_pc_with_preempt_stub(frame& f) {
+  Unimplemented();
+}
+
 inline int ContinuationHelper::frame_align_words(int size) {
   Unimplemented();
   return 0;
 }
 
@@ -60,15 +64,15 @@
 
 inline void ContinuationHelper::set_anchor_to_entry_pd(JavaFrameAnchor* anchor, ContinuationEntry* cont) {
   Unimplemented();
 }
 
-#ifdef ASSERT
 inline void ContinuationHelper::set_anchor_pd(JavaFrameAnchor* anchor, intptr_t* sp) {
   Unimplemented();
 }
 
+#ifdef ASSERT
 inline bool ContinuationHelper::Frame::assert_frame_laid_out(frame f) {
   Unimplemented();
   return false;
 }
 #endif
diff a/src/hotspot/cpu/s390/macroAssembler_s390.cpp b/src/hotspot/cpu/s390/macroAssembler_s390.cpp
--- a/src/hotspot/cpu/s390/macroAssembler_s390.cpp
+++ b/src/hotspot/cpu/s390/macroAssembler_s390.cpp
@@ -3562,24 +3562,25 @@
   Register zero = temp;
   Register monitor_tagged = displacedHeader; // Tagged with markWord::monitor_value.
   // The object's monitor m is unlocked iff m->owner is null,
   // otherwise m->owner may contain a thread or a stack address.
 
-  // Try to CAS m->owner from null to current thread.
-  // If m->owner is null, then csg succeeds and sets m->owner=THREAD and CR=EQ.
+  // Try to CAS m->owner from null to current thread's id.
+  // If m->owner is null, then csg succeeds and sets m->owner=THREAD_ID and CR=EQ.
   // Otherwise, register zero is filled with the current owner.
   z_lghi(zero, 0);
-  z_csg(zero, Z_thread, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner), monitor_tagged);
+  z_l(Z_R1_scratch, Address(Z_thread, JavaThread::lock_id_offset()));
+  z_csg(zero, Z_R1_scratch, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner), monitor_tagged);
 
   // Store a non-null value into the box.
   z_stg(box, BasicLock::displaced_header_offset_in_bytes(), box);
 
   z_bre(done); // acquired the lock for the first time.
 
   BLOCK_COMMENT("fast_path_recursive_lock {");
   // Check if we are already the owner (recursive lock)
-  z_cgr(Z_thread, zero); // owner is stored in zero by "z_csg" above
+  z_cgr(Z_R1_scratch, zero); // owner is stored in zero by "z_csg" above
   z_brne(done); // not a recursive lock
 
   // Current thread already owns the lock. Just increment recursion count.
   z_agsi(Address(monitor_tagged, OM_OFFSET_NO_MONITOR_VALUE_TAG(recursions)), 1ll);
   z_cgr(zero, zero); // set the CC to EQUAL
@@ -3640,11 +3641,12 @@
   // This is handled like owner thread mismatches: We take the slow path.
 
   // Handle existing monitor.
   bind(object_has_monitor);
 
-  z_cg(Z_thread, Address(currentHeader, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner)));
+  z_l(Z_R1_scratch, Address(Z_thread, JavaThread::lock_id_offset()));
+  z_cg(Z_R1_scratch, Address(currentHeader, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner)));
   z_brne(done);
 
   BLOCK_COMMENT("fast_path_recursive_unlock {");
   load_and_test_long(temp, Address(currentHeader, OM_OFFSET_NO_MONITOR_VALUE_TAG(recursions)));
   z_bre(not_recursive); // if 0 then jump, it's not recursive locking
@@ -6305,19 +6307,20 @@
     const ByteSize monitor_tag = in_ByteSize(UseObjectMonitorTable ? 0 : checked_cast<int>(markWord::monitor_value));
     const Address owner_address(tmp1_monitor, ObjectMonitor::owner_offset() - monitor_tag);
     const Address recursions_address(tmp1_monitor, ObjectMonitor::recursions_offset() - monitor_tag);
 
 
-    // Try to CAS m->owner from null to current thread.
-    // If m->owner is null, then csg succeeds and sets m->owner=THREAD and CR=EQ.
+    // Try to CAS m->owner from null to current thread's id.
+    // If m->owner is null, then csg succeeds and sets m->owner=THREAD_ID and CR=EQ.
     // Otherwise, register zero is filled with the current owner.
     z_lghi(zero, 0);
-    z_csg(zero, Z_thread, owner_address);
+    z_l(Z_R1_scratch, Address(Z_thread, JavaThread::lock_id_offset()));
+    z_csg(zero, Z_R1_scratch, owner_address);
     z_bre(monitor_locked);
 
     // Check if recursive.
-    z_cgr(Z_thread, zero); // zero contains the owner from z_csg instruction
+    z_cgr(Z_R1_scratch, zero); // zero contains the owner from z_csg instruction
     z_brne(slow_path);
 
     // Recursive
     z_agsi(recursions_address, 1ll);
 
diff a/src/hotspot/cpu/s390/sharedRuntime_s390.cpp b/src/hotspot/cpu/s390/sharedRuntime_s390.cpp
--- a/src/hotspot/cpu/s390/sharedRuntime_s390.cpp
+++ b/src/hotspot/cpu/s390/sharedRuntime_s390.cpp
@@ -2385,10 +2385,15 @@
 
 uint SharedRuntime::out_preserve_stack_slots() {
   return frame::z_jit_out_preserve_size/VMRegImpl::stack_slot_size;
 }
 
+VMReg SharedRuntime::thread_register() {
+  Unimplemented();
+  return nullptr;
+}
+
 //
 // Frame generation for deopt and uncommon trap blobs.
 //
 static void push_skeleton_frame(MacroAssembler* masm,
                           /* Unchanged */
diff a/src/hotspot/cpu/s390/templateInterpreterGenerator_s390.cpp b/src/hotspot/cpu/s390/templateInterpreterGenerator_s390.cpp
--- a/src/hotspot/cpu/s390/templateInterpreterGenerator_s390.cpp
+++ b/src/hotspot/cpu/s390/templateInterpreterGenerator_s390.cpp
@@ -719,10 +719,15 @@
   __ call_VM(noreg, runtime_entry);
   __ dispatch_via(vtos, Interpreter::_normal_table.table_for (vtos));
   return entry;
 }
 
+address TemplateInterpreterGenerator::generate_cont_resume_interpreter_adapter() {
+  return nullptr;
+}
+
+
 //
 // Helpers for commoning out cases in the various type of method entries.
 //
 
 // Increment invocation count & check for overflow.
diff a/src/hotspot/cpu/x86/assembler_x86.cpp b/src/hotspot/cpu/x86/assembler_x86.cpp
--- a/src/hotspot/cpu/x86/assembler_x86.cpp
+++ b/src/hotspot/cpu/x86/assembler_x86.cpp
@@ -2842,10 +2842,32 @@
   prefix(src, dst);
   emit_int8((unsigned char)0x8D);
   emit_operand(dst, src, 0);
 }
 
+#ifdef _LP64
+void Assembler::lea(Register dst, Label& L) {
+  assert(dst == r10, "invalid destination register");
+  if (L.is_bound()) {
+    const int inst_size = 7;
+    address entry = target(L);
+    int offs = checked_cast<int>((intptr_t)entry - (intptr_t)pc());
+    emit_int8((unsigned char)0x4C);
+    emit_int8((unsigned char)0x8D);
+    emit_int8((unsigned char)0x15);
+    emit_int32(offs - inst_size);
+  } else {
+    InstructionMark im(this);
+    L.add_patch_at(code(), locator());
+    emit_int8((unsigned char)0x4C);
+    emit_int8((unsigned char)0x8D);
+    emit_int8((unsigned char)0x15);
+    emit_int32(0);
+  }
+}
+#endif
+
 void Assembler::lfence() {
   emit_int24(0x0F, (unsigned char)0xAE, (unsigned char)0xE8);
 }
 
 void Assembler::lock() {
diff a/src/hotspot/cpu/x86/assembler_x86.hpp b/src/hotspot/cpu/x86/assembler_x86.hpp
--- a/src/hotspot/cpu/x86/assembler_x86.hpp
+++ b/src/hotspot/cpu/x86/assembler_x86.hpp
@@ -1622,10 +1622,14 @@
 
   void leal(Register dst, Address src);
 
   void leaq(Register dst, Address src);
 
+#ifdef _LP64
+  void lea(Register dst, Label& L);
+#endif
+
   void lfence();
 
   void lock();
   void size_prefix();
 
diff a/src/hotspot/cpu/x86/c1_MacroAssembler_x86.cpp b/src/hotspot/cpu/x86/c1_MacroAssembler_x86.cpp
--- a/src/hotspot/cpu/x86/c1_MacroAssembler_x86.cpp
+++ b/src/hotspot/cpu/x86/c1_MacroAssembler_x86.cpp
@@ -107,14 +107,13 @@
     movptr(Address(disp_hdr, 0), hdr);
     // otherwise we don't care about the result and handle locking via runtime call
     jcc(Assembler::notZero, slow_case);
     // done
     bind(done);
+    inc_held_monitor_count();
   }
 
-  inc_held_monitor_count();
-
   return null_check_offset;
 }
 
 void C1_MacroAssembler::unlock_object(Register hdr, Register obj, Register disp_hdr, Label& slow_case) {
   const int aligned_mask = BytesPerWord -1;
@@ -151,13 +150,13 @@
     cmpxchgptr(hdr, Address(obj, hdr_offset));
     // if the object header was not pointing to the displaced header,
     // we do unlocking via runtime call
     jcc(Assembler::notEqual, slow_case);
     // done
+    bind(done);
+    dec_held_monitor_count();
   }
-  bind(done);
-  dec_held_monitor_count();
 }
 
 
 // Defines obj, preserves var_size_in_bytes
 void C1_MacroAssembler::try_allocate(Register obj, Register var_size_in_bytes, int con_size_in_bytes, Register t1, Register t2, Label& slow_case) {
diff a/src/hotspot/cpu/x86/c1_Runtime1_x86.cpp b/src/hotspot/cpu/x86/c1_Runtime1_x86.cpp
--- a/src/hotspot/cpu/x86/c1_Runtime1_x86.cpp
+++ b/src/hotspot/cpu/x86/c1_Runtime1_x86.cpp
@@ -204,32 +204,38 @@
 // Implementation of StubFrame
 
 class StubFrame: public StackObj {
  private:
   StubAssembler* _sasm;
+  bool _use_pop_on_epilog;
 
  public:
-  StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments);
+  StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments, bool use_pop_on_epilog = false);
   void load_argument(int offset_in_words, Register reg);
 
   ~StubFrame();
 };
 
 void StubAssembler::prologue(const char* name, bool must_gc_arguments) {
   set_info(name, must_gc_arguments);
   enter();
 }
 
-void StubAssembler::epilogue() {
-  leave();
+void StubAssembler::epilogue(bool use_pop) {
+  // Avoid using a leave instruction when this frame may
+  // have been frozen, since the current value of rbp
+  // restored from the stub would be invalid. We still
+  // must restore the rbp value saved on enter though.
+  use_pop ? pop(rbp) : leave();
   ret(0);
 }
 
 #define __ _sasm->
 
-StubFrame::StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments) {
+StubFrame::StubFrame(StubAssembler* sasm, const char* name, bool must_gc_arguments, bool use_pop_on_epilog) {
   _sasm = sasm;
+  _use_pop_on_epilog = use_pop_on_epilog;
   __ prologue(name, must_gc_arguments);
 }
 
 // load parameters that were stored with LIR_Assembler::store_parameter
 // Note: offsets for store_parameter and load_argument must match
@@ -237,11 +243,11 @@
   __ load_parameter(offset_in_words, reg);
 }
 
 
 StubFrame::~StubFrame() {
-  __ epilogue();
+  __ epilogue(_use_pop_on_epilog);
 }
 
 #undef __
 
 
@@ -630,10 +636,18 @@
 
 void Runtime1::initialize_pd() {
   // nothing to do
 }
 
+uint Runtime1::runtime_blob_current_thread_offset(frame f) {
+#ifdef _LP64
+  return r15_off / 2;
+#else
+  Unimplemented();
+  return 0;
+#endif
+}
 
 // Target: the entry point of the method that creates and posts the exception oop.
 // has_argument: true if the exception needs arguments (passed on the stack because
 //               registers must be preserved).
 OopMapSet* Runtime1::generate_exception_throw(StubAssembler* sasm, address target, bool has_argument) {
@@ -1306,11 +1320,11 @@
     case C1StubId::monitorenter_nofpu_id:
       save_fpu_registers = false;
       // fall through
     case C1StubId::monitorenter_id:
       {
-        StubFrame f(sasm, "monitorenter", dont_gc_arguments);
+        StubFrame f(sasm, "monitorenter", dont_gc_arguments, true /* use_pop_on_epilog */);
         OopMap* map = save_live_registers(sasm, 3, save_fpu_registers);
 
         // Called with store_parameter and not C abi
 
         f.load_argument(1, rax); // rax,: object
diff a/src/hotspot/cpu/x86/c2_MacroAssembler_x86.cpp b/src/hotspot/cpu/x86/c2_MacroAssembler_x86.cpp
--- a/src/hotspot/cpu/x86/c2_MacroAssembler_x86.cpp
+++ b/src/hotspot/cpu/x86/c2_MacroAssembler_x86.cpp
@@ -309,65 +309,28 @@
 
   bind(IsInflated);
   // The object is inflated. tmpReg contains pointer to ObjectMonitor* + markWord::monitor_value
 
 #ifndef _LP64
-  // The object is inflated.
-
-  // boxReg refers to the on-stack BasicLock in the current frame.
-  // We'd like to write:
-  //   set box->_displaced_header = markWord::unused_mark().  Any non-0 value suffices.
-  // This is convenient but results a ST-before-CAS penalty.  The following CAS suffers
-  // additional latency as we have another ST in the store buffer that must drain.
-
-  // avoid ST-before-CAS
-  // register juggle because we need tmpReg for cmpxchgptr below
-  movptr(scrReg, boxReg);
-  movptr(boxReg, tmpReg);                   // consider: LEA box, [tmp-2]
-
-  // Optimistic form: consider XORL tmpReg,tmpReg
-  movptr(tmpReg, NULL_WORD);
-
-  // Appears unlocked - try to swing _owner from null to non-null.
-  // Ideally, I'd manifest "Self" with get_thread and then attempt
-  // to CAS the register containing Self into m->Owner.
-  // But we don't have enough registers, so instead we can either try to CAS
-  // rsp or the address of the box (in scr) into &m->owner.  If the CAS succeeds
-  // we later store "Self" into m->Owner.  Transiently storing a stack address
-  // (rsp or the address of the box) into  m->owner is harmless.
-  // Invariant: tmpReg == 0.  tmpReg is EAX which is the implicit cmpxchg comparand.
-  lock();
-  cmpxchgptr(scrReg, Address(boxReg, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner)));
-  movptr(Address(scrReg, 0), 3);          // box->_displaced_header = 3
-  // If we weren't able to swing _owner from null to the BasicLock
-  // then take the slow path.
-  jccb  (Assembler::notZero, NO_COUNT);
-  // update _owner from BasicLock to thread
-  get_thread (scrReg);                    // beware: clobbers ICCs
-  movptr(Address(boxReg, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner)), scrReg);
-  xorptr(boxReg, boxReg);                 // set icc.ZFlag = 1 to indicate success
-
-  // If the CAS fails we can either retry or pass control to the slow path.
-  // We use the latter tactic.
-  // Pass the CAS result in the icc.ZFlag into DONE_LABEL
-  // If the CAS was successful ...
-  //   Self has acquired the lock
-  //   Invariant: m->_recursions should already be 0, so we don't need to explicitly set it.
-  // Intentional fall-through into DONE_LABEL ...
-#else // _LP64
+  // Just take slow path to avoid dealing with 64 bit atomic instructions here.
+  orl(boxReg, 1);  // set ICC.ZF=0 to indicate failure
+#else
+  // Unconditionally set box->_displaced_header = markWord::unused_mark().
+  // Without cast to int32_t this style of movptr will destroy r10 which is typically obj.
+  movptr(Address(boxReg, 0), checked_cast<int32_t>(markWord::unused_mark().value()));
+
   // It's inflated and we use scrReg for ObjectMonitor* in this section.
   movq(scrReg, tmpReg);
   xorq(tmpReg, tmpReg);
+  movptr(boxReg, Address(r15_thread, JavaThread::lock_id_offset()));
   lock();
-  cmpxchgptr(thread, Address(scrReg, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner)));
-  // Unconditionally set box->_displaced_header = markWord::unused_mark().
-  // Without cast to int32_t this style of movptr will destroy r10 which is typically obj.
-  movptr(Address(boxReg, 0), checked_cast<int32_t>(markWord::unused_mark().value()));
+  cmpxchgptr(boxReg, Address(scrReg, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner)));
+
   // Propagate ICC.ZF from CAS above into DONE_LABEL.
-  jccb(Assembler::equal, COUNT);          // CAS above succeeded; propagate ZF = 1 (success)
+  jccb(Assembler::equal, COUNT);    // CAS above succeeded; propagate ZF = 1 (success)
 
-  cmpptr(thread, rax);                // Check if we are already the owner (recursive lock)
+  cmpptr(boxReg, rax);                // Check if we are already the owner (recursive lock)
   jccb(Assembler::notEqual, NO_COUNT);    // If not recursive, ZF = 0 at this point (fail)
   incq(Address(scrReg, OM_OFFSET_NO_MONITOR_VALUE_TAG(recursions)));
   xorq(rax, rax); // Set ZF = 1 (success) for recursive lock, denoting locking success
 #endif // _LP64
   bind(DONE_LABEL);
@@ -375,13 +338,16 @@
   // ZFlag == 1 count in fast path
   // ZFlag == 0 count in slow path
   jccb(Assembler::notZero, NO_COUNT); // jump if ZFlag == 0
 
   bind(COUNT);
-  // Count monitors in fast path
-  increment(Address(thread, JavaThread::held_monitor_count_offset()));
-
+  if (LockingMode == LM_LEGACY) {
+#ifdef _LP64
+    // Count monitors in fast path
+    increment(Address(thread, JavaThread::held_monitor_count_offset()));
+#endif
+  }
   xorl(tmpReg, tmpReg); // Set ZF == 1
 
   bind(NO_COUNT);
 
   // At NO_COUNT the icc ZFlag is set as follows ...
@@ -439,10 +405,15 @@
     jcc(Assembler::zero, Stacked);
   }
 
   // It's inflated.
 
+#ifndef _LP64
+  // Just take slow path to avoid dealing with 64 bit atomic instructions here.
+  orl(boxReg, 1);  // set ICC.ZF=0 to indicate failure
+  jmpb(DONE_LABEL);
+#else
   // Despite our balanced locking property we still check that m->_owner == Self
   // as java routines or native JNI code called by this thread might
   // have released the lock.
   // Refer to the comments in synchronizer.cpp for how we might encode extra
   // state in _succ so we can avoid fetching EntryList|cxq.
@@ -487,23 +458,19 @@
   jccb(Assembler::notZero, LSuccess); // If so we are done.
 
   // Save the monitor pointer in the current thread, so we can try to
   // reacquire the lock in SharedRuntime::monitor_exit_helper().
   andptr(tmpReg, ~(int32_t)markWord::monitor_value);
-#ifndef _LP64
-  get_thread(boxReg);
-  movptr(Address(boxReg, JavaThread::unlocked_inflated_monitor_offset()), tmpReg);
-#else // _LP64
   movptr(Address(r15_thread, JavaThread::unlocked_inflated_monitor_offset()), tmpReg);
-#endif
 
   orl   (boxReg, 1);                      // set ICC.ZF=0 to indicate failure
   jmpb  (DONE_LABEL);
 
   bind  (LSuccess);
   testl (boxReg, 0);                      // set ICC.ZF=1 to indicate success
   jmpb  (DONE_LABEL);
+#endif  // _LP64
 
   if (LockingMode == LM_LEGACY) {
     bind  (Stacked);
     movptr(tmpReg, Address (boxReg, 0));      // re-fetch
     lock();
@@ -516,17 +483,17 @@
   // ZFlag == 1 count in fast path
   // ZFlag == 0 count in slow path
   jccb(Assembler::notZero, NO_COUNT);
 
   bind(COUNT);
-  // Count monitors in fast path
-#ifndef _LP64
-  get_thread(tmpReg);
-  decrementl(Address(tmpReg, JavaThread::held_monitor_count_offset()));
-#else // _LP64
-  decrementq(Address(r15_thread, JavaThread::held_monitor_count_offset()));
+
+  if (LockingMode == LM_LEGACY) {
+    // Count monitors in fast path
+#ifdef _LP64
+    decrementq(Address(r15_thread, JavaThread::held_monitor_count_offset()));
 #endif
+  }
 
   xorl(tmpReg, tmpReg); // Set ZF == 1
 
   bind(NO_COUNT);
 }
@@ -600,10 +567,15 @@
   }
 
   { // Handle inflated monitor.
     bind(inflated);
 
+#ifndef _LP64
+    // Just take slow path to avoid dealing with 64 bit atomic instructions here.
+    orl(box, 1);  // set ICC.ZF=0 to indicate failure
+    jmpb(slow_path);
+#else
     const Register monitor = t;
 
     if (!UseObjectMonitorTable) {
       assert(mark == monitor, "should be the same here");
     } else {
@@ -645,31 +617,34 @@
     const Address owner_address(monitor, ObjectMonitor::owner_offset() - monitor_tag);
 
     Label monitor_locked;
     // Lock the monitor.
 
+    if (UseObjectMonitorTable) {
+      // Cache the monitor for unlock before trashing box. On failure to acquire
+      // the lock, the slow path will reset the entry accordingly (see CacheSetter).
+      movptr(Address(box, BasicLock::object_monitor_cache_offset_in_bytes()), monitor);
+    }
+
     // CAS owner (null => current thread).
     xorptr(rax_reg, rax_reg);
-    lock(); cmpxchgptr(thread, owner_address);
+    movptr(box, Address(thread, JavaThread::lock_id_offset()));
+    lock(); cmpxchgptr(box, owner_address);
     jccb(Assembler::equal, monitor_locked);
 
     // Check if recursive.
-    cmpptr(thread, rax_reg);
+    cmpptr(box, rax_reg);
     jccb(Assembler::notEqual, slow_path);
 
     // Recursive.
     increment(recursions_address);
 
     bind(monitor_locked);
-    if (UseObjectMonitorTable) {
-      // Cache the monitor for unlock
-      movptr(Address(box, BasicLock::object_monitor_cache_offset_in_bytes()), monitor);
-    }
+#endif  // _LP64
   }
 
   bind(locked);
-  increment(Address(thread, JavaThread::held_monitor_count_offset()));
   // Set ZF = 1
   xorl(rax_reg, rax_reg);
 
 #ifdef ASSERT
   // Check that locked label is reached with ZF set.
@@ -775,10 +750,15 @@
     stop("Fast Unlock not monitor");
 #endif
 
     bind(inflated);
 
+#ifndef _LP64
+    // Just take slow path to avoid dealing with 64 bit atomic instructions here.
+    orl(t, 1);  // set ICC.ZF=0 to indicate failure
+    jmpb(slow_path);
+#else
     if (!UseObjectMonitorTable) {
       assert(mark == monitor, "should be the same here");
     } else {
       // Uses ObjectMonitorTable.  Look for the monitor in our BasicLock on the stack.
       movptr(monitor, Address(box, BasicLock::object_monitor_cache_offset_in_bytes()));
@@ -826,14 +806,14 @@
     jmpb(slow_path);
 
     // Recursive unlock.
     bind(recursive);
     decrement(recursions_address);
+#endif  // _LP64
   }
 
   bind(unlocked);
-  decrement(Address(thread, JavaThread::held_monitor_count_offset()));
   xorl(t, t); // Fast Unlock ZF = 1
 
 #ifdef ASSERT
   // Check that unlocked label is reached with ZF set.
   Label zf_correct;
diff a/src/hotspot/cpu/x86/continuationFreezeThaw_x86.inline.hpp b/src/hotspot/cpu/x86/continuationFreezeThaw_x86.inline.hpp
--- a/src/hotspot/cpu/x86/continuationFreezeThaw_x86.inline.hpp
+++ b/src/hotspot/cpu/x86/continuationFreezeThaw_x86.inline.hpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -124,21 +124,29 @@
   if (real_unextended_sp != nullptr) {
     f.set_unextended_sp(real_unextended_sp); // can be null at a safepoint
   }
 }
 
+inline void FreezeBase::prepare_freeze_interpreted_top_frame(const frame& f) {
+  assert(*f.addr_at(frame::interpreter_frame_last_sp_offset) == 0, "should be null for top frame");
+  intptr_t* lspp = f.addr_at(frame::interpreter_frame_last_sp_offset);
+  *lspp = f.unextended_sp() - f.fp();
+}
+
 inline void FreezeBase::relativize_interpreted_frame_metadata(const frame& f, const frame& hf) {
   assert(hf.fp() == hf.unextended_sp() + (f.fp() - f.unextended_sp()), "");
   assert((f.at(frame::interpreter_frame_last_sp_offset) != 0)
     || (f.unextended_sp() == f.sp()), "");
   assert(f.fp() > (intptr_t*)f.at_relative(frame::interpreter_frame_initial_sp_offset), "");
 
   // Make sure that last_sp is already relativized.
   assert((intptr_t*)hf.at_relative(frame::interpreter_frame_last_sp_offset) == hf.unextended_sp(), "");
 
   // Make sure that locals is already relativized.
-  assert((*hf.addr_at(frame::interpreter_frame_locals_offset) == frame::sender_sp_offset + f.interpreter_frame_method()->max_locals() - 1), "");
+  DEBUG_ONLY(Method* m = f.interpreter_frame_method();)
+  DEBUG_ONLY(int max_locals = !m->is_native() ? m->max_locals() : m->size_of_parameters() + 2;)
+  assert((*hf.addr_at(frame::interpreter_frame_locals_offset) == frame::sender_sp_offset + max_locals - 1), "");
 
   // Make sure that monitor_block_top is already relativized.
   assert(hf.at_absolute(frame::interpreter_frame_monitor_block_top_offset) <= frame::interpreter_frame_initial_sp_offset, "");
 
   assert((hf.fp() - hf.unextended_sp()) == (f.fp() - f.unextended_sp()), "");
@@ -205,29 +213,30 @@
   if (FKind::interpreted) {
     intptr_t* heap_sp = hf.unextended_sp();
     // If caller is interpreted it already made room for the callee arguments
     int overlap = caller.is_interpreted_frame() ? ContinuationHelper::InterpretedFrame::stack_argsize(hf) : 0;
     const int fsize = (int)(ContinuationHelper::InterpretedFrame::frame_bottom(hf) - hf.unextended_sp() - overlap);
-    const int locals = hf.interpreter_frame_method()->max_locals();
     intptr_t* frame_sp = caller.unextended_sp() - fsize;
     intptr_t* fp = frame_sp + (hf.fp() - heap_sp);
     DEBUG_ONLY(intptr_t* unextended_sp = fp + *hf.addr_at(frame::interpreter_frame_last_sp_offset);)
     assert(frame_sp == unextended_sp, "");
     caller.set_sp(fp + frame::sender_sp_offset);
     frame f(frame_sp, frame_sp, fp, hf.pc());
     // we need to set the locals so that the caller of new_stack_frame() can call
     // ContinuationHelper::InterpretedFrame::frame_bottom
     intptr_t locals_offset = *hf.addr_at(frame::interpreter_frame_locals_offset);
-    assert((int)locals_offset == frame::sender_sp_offset + locals - 1, "");
+    DEBUG_ONLY(Method* m = hf.interpreter_frame_method();)
+    DEBUG_ONLY(const int max_locals = !m->is_native() ? m->max_locals() : m->size_of_parameters() + 2;)
+    assert((int)locals_offset == frame::sender_sp_offset + max_locals - 1, "");
     // copy relativized locals from the heap frame
     *f.addr_at(frame::interpreter_frame_locals_offset) = locals_offset;
     return f;
   } else {
     int fsize = FKind::size(hf);
     intptr_t* frame_sp = caller.unextended_sp() - fsize;
     if (bottom || caller.is_interpreted_frame()) {
-      int argsize = hf.compiled_frame_stack_argsize();
+      int argsize = FKind::stack_argsize(hf);
 
       fsize += argsize;
       frame_sp   -= argsize;
       caller.set_sp(caller.sp() - argsize);
       assert(caller.sp() == frame_sp + (fsize-argsize), "");
@@ -240,12 +249,13 @@
     intptr_t* fp;
     if (PreserveFramePointer) {
       // we need to recreate a "real" frame pointer, pointing into the stack
       fp = frame_sp + FKind::size(hf) - frame::sender_sp_offset;
     } else {
-       // we need to re-read fp because it may be an oop and we might have fixed the frame.
-      fp = *(intptr_t**)(hf.sp() - frame::sender_sp_offset);
+      fp = FKind::stub || FKind::native
+        ? frame_sp + fsize - frame::sender_sp_offset // fp always points to the address below the pushed return pc. We need correct address.
+        : *(intptr_t**)(hf.sp() - frame::sender_sp_offset); // we need to re-read fp because it may be an oop and we might have fixed the frame.
     }
     return frame(frame_sp, frame_sp, fp, hf.pc(), hf.cb(), hf.oop_map(), false); // TODO PERF : this computes deopt state; is it necessary?
   }
 }
 
@@ -264,10 +274,31 @@
 
 inline void ThawBase::patch_pd(frame& f, const frame& caller) {
   patch_callee_link(caller, caller.fp());
 }
 
+inline void ThawBase::patch_pd(frame& f, intptr_t* caller_sp) {
+  intptr_t* fp = caller_sp - frame::sender_sp_offset;
+  patch_callee_link(f, fp);
+}
+
+inline intptr_t* ThawBase::possibly_adjust_frame(frame& top) {
+  // Nothing to do
+  return top.sp();
+}
+
+inline intptr_t* ThawBase::push_cleanup_continuation() {
+  frame enterSpecial = new_entry_frame();
+  intptr_t* sp = enterSpecial.sp();
+
+  sp[-1] = (intptr_t)ContinuationEntry::cleanup_pc();
+  sp[-2] = (intptr_t)enterSpecial.fp();
+
+  log_develop_trace(continuations, preempt)("push_cleanup_continuation initial sp: " INTPTR_FORMAT " final sp: " INTPTR_FORMAT, p2i(sp + 2 * frame::metadata_words), p2i(sp));
+  return sp;
+}
+
 inline void ThawBase::derelativize_interpreted_frame_metadata(const frame& hf, const frame& f) {
   // Make sure that last_sp is kept relativized.
   assert((intptr_t*)f.at_relative(frame::interpreter_frame_last_sp_offset) == f.unextended_sp(), "");
 
   // Make sure that monitor_block_top is still relativized.
diff a/src/hotspot/cpu/x86/continuationHelper_x86.inline.hpp b/src/hotspot/cpu/x86/continuationHelper_x86.inline.hpp
--- a/src/hotspot/cpu/x86/continuationHelper_x86.inline.hpp
+++ b/src/hotspot/cpu/x86/continuationHelper_x86.inline.hpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -38,10 +38,24 @@
   return FKind::interpreted
     ? (intptr_t**)(f.fp() + frame::link_offset)
     : (intptr_t**)(f.unextended_sp() + f.cb()->frame_size() - frame::sender_sp_offset);
 }
 
+static inline void patch_return_pc_with_preempt_stub(frame& f) {
+  if (f.is_runtime_frame()) {
+    // Patch the pc of the now old last Java frame (we already set the anchor to enterSpecial)
+    // so that when target goes back to Java it will actually return to the preempt cleanup stub.
+    intptr_t* sp = f.sp();
+    sp[-1] = (intptr_t)StubRoutines::cont_preempt_stub();
+  } else {
+    // The target will check for preemption once it returns to the interpreter
+    // or the native wrapper code and will manually jump to the preempt stub.
+    JavaThread *thread = JavaThread::current();
+    thread->set_preempt_alternate_return(StubRoutines::cont_preempt_stub());
+  }
+}
+
 inline int ContinuationHelper::frame_align_words(int size) {
 #ifdef _LP64
   return size & 1;
 #else
   return 0;
@@ -70,16 +84,16 @@
 
 inline void ContinuationHelper::set_anchor_to_entry_pd(JavaFrameAnchor* anchor, ContinuationEntry* entry) {
   anchor->set_last_Java_fp(entry->entry_fp());
 }
 
-#ifdef ASSERT
 inline void ContinuationHelper::set_anchor_pd(JavaFrameAnchor* anchor, intptr_t* sp) {
   intptr_t* fp = *(intptr_t**)(sp - frame::sender_sp_offset);
   anchor->set_last_Java_fp(fp);
 }
 
+#ifdef ASSERT
 inline bool ContinuationHelper::Frame::assert_frame_laid_out(frame f) {
   intptr_t* sp = f.sp();
   address pc = *(address*)(sp - frame::sender_sp_ret_address_offset());
   intptr_t* fp = *(intptr_t**)(sp - frame::sender_sp_offset);
   assert(f.raw_pc() == pc, "f.ra_pc: " INTPTR_FORMAT " actual: " INTPTR_FORMAT, p2i(f.raw_pc()), p2i(pc));
diff a/src/hotspot/cpu/x86/globalDefinitions_x86.hpp b/src/hotspot/cpu/x86/globalDefinitions_x86.hpp
--- a/src/hotspot/cpu/x86/globalDefinitions_x86.hpp
+++ b/src/hotspot/cpu/x86/globalDefinitions_x86.hpp
@@ -32,11 +32,13 @@
 // 32-bit integer argument values are extended to 64 bits.
 const bool CCallingConventionRequiresIntsAsLongs = false;
 
 #define SUPPORTS_NATIVE_CX8
 
+#ifdef _LP64
 #define SUPPORT_MONITOR_COUNT
+#endif
 
 #define CPU_MULTI_COPY_ATOMIC
 
 // The expected size in bytes of a cache line.
 #define DEFAULT_CACHE_LINE_SIZE 64
diff a/src/hotspot/cpu/x86/interp_masm_x86.cpp b/src/hotspot/cpu/x86/interp_masm_x86.cpp
--- a/src/hotspot/cpu/x86/interp_masm_x86.cpp
+++ b/src/hotspot/cpu/x86/interp_masm_x86.cpp
@@ -334,10 +334,68 @@
   // interpreter specific
   restore_bcp();
   restore_locals();
 }
 
+#ifdef _LP64
+void InterpreterMacroAssembler::call_VM_preemptable(Register oop_result,
+                                                    address entry_point,
+                                                    Register arg_1) {
+  assert(arg_1 == c_rarg1, "");
+  Label resume_pc, not_preempted;
+
+#ifdef ASSERT
+  {
+    Label L;
+    cmpptr(Address(r15_thread, JavaThread::preempt_alternate_return_offset()), NULL_WORD);
+    jcc(Assembler::equal, L);
+    stop("Should not have alternate return address set");
+    bind(L);
+  }
+#endif /* ASSERT */
+
+  push_cont_fastpath();
+
+  // Make VM call. In case of preemption set last_pc to the one we want to resume to.
+  lea(rscratch1, resume_pc);
+  push(rscratch1);
+  MacroAssembler::call_VM_helper(oop_result, entry_point, 1, false /*check_exceptions*/);
+  pop(rscratch1);
+
+  pop_cont_fastpath();
+
+  // Check if preempted.
+  movptr(rscratch1, Address(r15_thread, JavaThread::preempt_alternate_return_offset()));
+  cmpptr(rscratch1, NULL_WORD);
+  jccb(Assembler::zero, not_preempted);
+  movptr(Address(r15_thread, JavaThread::preempt_alternate_return_offset()), NULL_WORD);
+  jmp(rscratch1);
+
+  // In case of preemption, this is where we will resume once we finally acquire the monitor.
+  bind(resume_pc);
+  restore_after_resume(false /* is_native */);
+
+  bind(not_preempted);
+}
+
+void InterpreterMacroAssembler::restore_after_resume(bool is_native) {
+  lea(rscratch1, ExternalAddress(Interpreter::cont_resume_interpreter_adapter()));
+  call(rscratch1);
+  if (is_native) {
+    // On resume we need to set up stack as expected.
+    push(dtos);
+    push(ltos);
+  }
+}
+#else
+void InterpreterMacroAssembler::call_VM_preemptable(Register oop_result,
+                         address entry_point,
+                         Register arg_1) {
+  MacroAssembler::call_VM(oop_result, entry_point, arg_1);
+}
+#endif  // _LP64
+
 void InterpreterMacroAssembler::check_and_handle_popframe(Register java_thread) {
   if (JvmtiExport::can_pop_frame()) {
     Label L;
     // Initiate popframe handling only if it is not already being
     // processed.  If the flag has the popframe_processing bit set, it
@@ -1152,11 +1210,11 @@
 void InterpreterMacroAssembler::lock_object(Register lock_reg) {
   assert(lock_reg == LP64_ONLY(c_rarg1) NOT_LP64(rdx),
          "The argument is only for looks. It must be c_rarg1");
 
   if (LockingMode == LM_MONITOR) {
-    call_VM(noreg,
+    call_VM_preemptable(noreg,
             CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
             lock_reg);
   } else {
     Label count_locking, done, slow_case;
 
@@ -1239,18 +1297,18 @@
       // Save the test result, for recursive case, the result is zero
       movptr(Address(lock_reg, mark_offset), swap_reg);
       jcc(Assembler::notZero, slow_case);
 
       bind(count_locking);
+      inc_held_monitor_count();
     }
-    inc_held_monitor_count();
     jmp(done);
 
     bind(slow_case);
 
     // Call the runtime routine for slow case
-    call_VM(noreg,
+    call_VM_preemptable(noreg,
             CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
             lock_reg);
     bind(done);
   }
 }
@@ -1319,12 +1377,12 @@
 
       // zero for simple unlock of a stack-lock case
       jcc(Assembler::notZero, slow_case);
 
       bind(count_locking);
+      dec_held_monitor_count();
     }
-    dec_held_monitor_count();
     jmp(done);
 
     bind(slow_case);
     // Call the runtime routine for slow case.
     movptr(Address(lock_reg, BasicObjectLock::obj_offset()), obj_reg); // restore obj
diff a/src/hotspot/cpu/x86/interp_masm_x86.hpp b/src/hotspot/cpu/x86/interp_masm_x86.hpp
--- a/src/hotspot/cpu/x86/interp_masm_x86.hpp
+++ b/src/hotspot/cpu/x86/interp_masm_x86.hpp
@@ -61,10 +61,15 @@
  virtual void check_and_handle_popframe(Register java_thread);
  virtual void check_and_handle_earlyret(Register java_thread);
 
   void load_earlyret_value(TosState state);
 
+  void call_VM_preemptable(Register oop_result,
+                           address entry_point,
+                           Register arg_1);
+  void restore_after_resume(bool is_native);
+
   // Interpreter-specific registers
   void save_bcp() {
     movptr(Address(rbp, frame::interpreter_frame_bcp_offset * wordSize), _bcp_register);
   }
 
diff a/src/hotspot/cpu/x86/macroAssembler_x86.cpp b/src/hotspot/cpu/x86/macroAssembler_x86.cpp
--- a/src/hotspot/cpu/x86/macroAssembler_x86.cpp
+++ b/src/hotspot/cpu/x86/macroAssembler_x86.cpp
@@ -33,10 +33,11 @@
 #include "gc/shared/barrierSetAssembler.hpp"
 #include "gc/shared/collectedHeap.inline.hpp"
 #include "gc/shared/tlab_globals.hpp"
 #include "interpreter/bytecodeHistogram.hpp"
 #include "interpreter/interpreter.hpp"
+#include "interpreter/interpreterRuntime.hpp"
 #include "jvm.h"
 #include "memory/resourceArea.hpp"
 #include "memory/universe.hpp"
 #include "oops/accessDecorators.hpp"
 #include "oops/compressedKlass.inline.hpp"
@@ -526,11 +527,10 @@
 
 #ifdef _WIN64
   // restore stack pointer
   addq(rsp, frame::arg_reg_save_area_bytes);
 #endif
-
 }
 
 void MacroAssembler::cmp64(Register src1, AddressLiteral src2, Register rscratch) {
   assert(!src2.is_lval(), "should use cmpptr");
   assert(rscratch != noreg || always_reachable(src2), "missing");
@@ -3031,29 +3031,17 @@
   pop(rthread);
 #endif
 }
 
 void MacroAssembler::inc_held_monitor_count() {
-#ifndef _LP64
-  Register thread = rax;
-  push(thread);
-  get_thread(thread);
-  incrementl(Address(thread, JavaThread::held_monitor_count_offset()));
-  pop(thread);
-#else // LP64
+#ifdef _LP64
   incrementq(Address(r15_thread, JavaThread::held_monitor_count_offset()));
 #endif
 }
 
 void MacroAssembler::dec_held_monitor_count() {
-#ifndef _LP64
-  Register thread = rax;
-  push(thread);
-  get_thread(thread);
-  decrementl(Address(thread, JavaThread::held_monitor_count_offset()));
-  pop(thread);
-#else // LP64
+#ifdef _LP64
   decrementq(Address(r15_thread, JavaThread::held_monitor_count_offset()));
 #endif
 }
 
 #ifdef ASSERT
@@ -3146,10 +3134,21 @@
     lea(java_pc, InternalAddress(last_java_pc), rscratch);
   }
   movptr(Address(java_thread, JavaThread::last_Java_sp_offset()), last_java_sp);
 }
 
+#ifdef _LP64
+void MacroAssembler::set_last_Java_frame(Register last_java_sp,
+                                         Register last_java_fp,
+                                         Label &L,
+                                         Register scratch) {
+  lea(scratch, L);
+  movptr(Address(r15_thread, JavaThread::last_Java_pc_offset()), scratch);
+  set_last_Java_frame(r15_thread, last_java_sp, last_java_fp, nullptr, scratch);
+}
+#endif
+
 void MacroAssembler::shlptr(Register dst, int imm8) {
   LP64_ONLY(shlq(dst, imm8)) NOT_LP64(shll(dst, imm8));
 }
 
 void MacroAssembler::shrptr(Register dst, int imm8) {
diff a/src/hotspot/cpu/x86/macroAssembler_x86.hpp b/src/hotspot/cpu/x86/macroAssembler_x86.hpp
--- a/src/hotspot/cpu/x86/macroAssembler_x86.hpp
+++ b/src/hotspot/cpu/x86/macroAssembler_x86.hpp
@@ -109,22 +109,23 @@
     assert(op == 0xE8 /* call */ ||
         op == 0xE9 /* jmp */ ||
         op == 0xEB /* short jmp */ ||
         (op & 0xF0) == 0x70 /* short jcc */ ||
         (op == 0x0F && (branch[1] & 0xF0) == 0x80) /* jcc */ ||
-        (op == 0xC7 && branch[1] == 0xF8) /* xbegin */,
+        (op == 0xC7 && branch[1] == 0xF8) /* xbegin */ ||
+        (op == 0x4C && branch[1] == 0x8D && branch[2] == 0x15) /* lea */,
         "Invalid opcode at patch point");
 
     if (op == 0xEB || (op & 0xF0) == 0x70) {
       // short offset operators (jmp and jcc)
       char* disp = (char*) &branch[1];
       int imm8 = checked_cast<int>(target - (address) &disp[1]);
       guarantee(this->is8bit(imm8), "Short forward jump exceeds 8-bit offset at %s:%d",
                 file == nullptr ? "<null>" : file, line);
       *disp = (char)imm8;
     } else {
-      int* disp = (int*) &branch[(op == 0x0F || op == 0xC7)? 2: 1];
+      int* disp = (int*) &branch[(op == 0x4C) ? 3 : (op == 0x0F || op == 0xC7) ? 2 : 1];
       int imm32 = checked_cast<int>(target - (address) &disp[1]);
       *disp = imm32;
     }
   }
 
@@ -333,10 +334,17 @@
   void set_last_Java_frame(Register last_java_sp,
                            Register last_java_fp,
                            address  last_java_pc,
                            Register rscratch);
 
+#ifdef _LP64
+  void set_last_Java_frame(Register last_java_sp,
+                           Register last_java_fp,
+                           Label &last_java_pc,
+                           Register scratch);
+#endif
+
   void reset_last_Java_frame(Register thread, bool clear_fp);
 
   // thread in the default location (r15_thread on 64bit)
   void reset_last_Java_frame(bool clear_fp);
 
@@ -891,11 +899,11 @@
   void atomic_incq(AddressLiteral counter_addr, Register rscratch = noreg);
 #endif
   void atomic_incptr(AddressLiteral counter_addr, Register rscratch = noreg) { LP64_ONLY(atomic_incq(counter_addr, rscratch)) NOT_LP64(atomic_incl(counter_addr, rscratch)) ; }
   void atomic_incptr(Address counter_addr) { LP64_ONLY(atomic_incq(counter_addr)) NOT_LP64(atomic_incl(counter_addr)) ; }
 
-  void lea(Register dst, Address        adr) { Assembler::lea(dst, adr); }
+  using Assembler::lea;
   void lea(Register dst, AddressLiteral adr);
   void lea(Address  dst, AddressLiteral adr, Register rscratch);
 
   void leal32(Register dst, Address src) { leal(dst, src); }
 
diff a/src/hotspot/cpu/x86/sharedRuntime_x86_32.cpp b/src/hotspot/cpu/x86/sharedRuntime_x86_32.cpp
--- a/src/hotspot/cpu/x86/sharedRuntime_x86_32.cpp
+++ b/src/hotspot/cpu/x86/sharedRuntime_x86_32.cpp
@@ -2054,10 +2054,15 @@
 
 uint SharedRuntime::out_preserve_stack_slots() {
   return 0;
 }
 
+VMReg SharedRuntime::thread_register() {
+  Unimplemented();
+  return nullptr;
+}
+
 //------------------------------generate_deopt_blob----------------------------
 void SharedRuntime::generate_deopt_blob() {
   // allocate space for the code
   ResourceMark rm;
   // setup code generation tools
diff a/src/hotspot/cpu/x86/sharedRuntime_x86_64.cpp b/src/hotspot/cpu/x86/sharedRuntime_x86_64.cpp
--- a/src/hotspot/cpu/x86/sharedRuntime_x86_64.cpp
+++ b/src/hotspot/cpu/x86/sharedRuntime_x86_64.cpp
@@ -169,10 +169,11 @@
   // values on its own
 
   static int rax_offset_in_bytes(void)    { return BytesPerInt * rax_off; }
   static int rdx_offset_in_bytes(void)    { return BytesPerInt * rdx_off; }
   static int rbx_offset_in_bytes(void)    { return BytesPerInt * rbx_off; }
+  static int r15_offset_in_bytes(void)    { return BytesPerInt * r15_off; }
   static int xmm0_offset_in_bytes(void)   { return BytesPerInt * xmm0_off; }
   static int return_offset_in_bytes(void) { return BytesPerInt * return_off; }
 
   // During deoptimization only the result registers need to be restored,
   // all the other values have already been extracted.
@@ -1418,11 +1419,11 @@
 //   rsp: pointer to the spilled rbp in the entry frame
 //
 // Kills:
 //   rbx
 //
-void static continuation_enter_cleanup(MacroAssembler* masm) {
+static void continuation_enter_cleanup(MacroAssembler* masm) {
 #ifdef ASSERT
   Label L_good_sp;
   __ cmpptr(rsp, Address(r15_thread, JavaThread::cont_entry_offset()));
   __ jcc(Assembler::equal, L_good_sp);
   __ stop("Incorrect rsp at continuation_enter_cleanup");
@@ -1608,20 +1609,21 @@
 
   // --- Thawing path
 
   __ bind(L_thaw);
 
+  ContinuationEntry::_thaw_call_pc_offset = __ pc() - start;
   __ call(RuntimeAddress(StubRoutines::cont_thaw()));
 
   ContinuationEntry::_return_pc_offset = __ pc() - start;
   oop_maps->add_gc_map(__ pc() - start, map->deep_copy());
   __ post_call_nop();
 
   // --- Normal exit (resolve/thawing)
 
   __ bind(L_exit);
-
+  ContinuationEntry::_cleanup_offset = __ pc() - start;
   continuation_enter_cleanup(masm);
   __ pop(rbp);
   __ ret(0);
 
   // --- Exception handling path
@@ -1710,10 +1712,14 @@
 
   __ leave();
   __ ret(0);
 }
 
+void SharedRuntime::continuation_enter_cleanup(MacroAssembler* masm) {
+  ::continuation_enter_cleanup(masm);
+}
+
 static void gen_special_dispatch(MacroAssembler* masm,
                                  const methodHandle& method,
                                  const BasicType* sig_bt,
                                  const VMRegPair* regs) {
   verify_oop_args(masm, method, sig_bt, regs);
@@ -2178,15 +2184,20 @@
   // Change state to native (we save the return address in the thread, since it might not
   // be pushed on the stack when we do a stack traversal). It is enough that the pc()
   // points into the right code segment. It does not have to be the correct return pc.
   // We use the same pc/oopMap repeatedly when we call out
 
-  intptr_t the_pc = (intptr_t) __ pc();
-  oop_maps->add_gc_map(the_pc - start, map);
-
-  __ set_last_Java_frame(rsp, noreg, (address)the_pc, rscratch1);
+  Label native_return;
+  if (LockingMode != LM_LEGACY && method->is_object_wait0()) {
+    // For convenience we use the pc we want to resume to in case of preemption on Object.wait.
+    __ set_last_Java_frame(rsp, noreg, native_return, rscratch1);
+  } else {
+    intptr_t the_pc = (intptr_t) __ pc();
+    oop_maps->add_gc_map(the_pc - start, map);
 
+    __ set_last_Java_frame(rsp, noreg, __ pc(), rscratch1);
+  }
 
   // We have all of the arguments setup at this point. We must not touch any register
   // argument registers at this point (what if we save/restore them there are no oop?
 
   if (DTraceMethodProbes) {
@@ -2269,16 +2280,17 @@
       __ andptr(swap_reg, 3 - (int)os::vm_page_size());
 
       // Save the test result, for recursive case, the result is zero
       __ movptr(Address(lock_reg, mark_word_offset), swap_reg);
       __ jcc(Assembler::notEqual, slow_path_lock);
+
+      __ bind(count_mon);
+      __ inc_held_monitor_count();
     } else {
       assert(LockingMode == LM_LIGHTWEIGHT, "must be");
       __ lightweight_lock(lock_reg, obj_reg, swap_reg, r15_thread, rscratch1, slow_path_lock);
     }
-    __ bind(count_mon);
-    __ inc_held_monitor_count();
 
     // Slow path will re-enter here
     __ bind(lock_done);
   }
 
@@ -2365,10 +2377,24 @@
 
   // change thread state
   __ movl(Address(r15_thread, JavaThread::thread_state_offset()), _thread_in_Java);
   __ bind(after_transition);
 
+  if (LockingMode != LM_LEGACY && method->is_object_wait0()) {
+    // Check preemption for Object.wait()
+    __ movptr(rscratch1, Address(r15_thread, JavaThread::preempt_alternate_return_offset()));
+    __ cmpptr(rscratch1, NULL_WORD);
+    __ jccb(Assembler::equal, native_return);
+    __ movptr(Address(r15_thread, JavaThread::preempt_alternate_return_offset()), NULL_WORD);
+    __ jmp(rscratch1);
+    __ bind(native_return);
+
+    intptr_t the_pc = (intptr_t) __ pc();
+    oop_maps->add_gc_map(the_pc - start, map);
+  }
+
+
   Label reguard;
   Label reguard_done;
   __ cmpl(Address(r15_thread, JavaThread::stack_guard_state_offset()), StackOverflow::stack_guard_yellow_reserved_disabled);
   __ jcc(Assembler::equal, reguard);
   __ bind(reguard_done);
@@ -2414,11 +2440,10 @@
       __ jcc(Assembler::notEqual, slow_path_unlock);
       __ dec_held_monitor_count();
     } else {
       assert(LockingMode == LM_LIGHTWEIGHT, "must be");
       __ lightweight_unlock(obj_reg, swap_reg, r15_thread, lock_reg, slow_path_unlock);
-      __ dec_held_monitor_count();
     }
 
     // slow path re-enters here
     __ bind(unlock_done);
     if (ret_type != T_FLOAT && ret_type != T_DOUBLE && ret_type != T_VOID) {
@@ -2489,11 +2514,15 @@
     __ mov(c_rarg0, obj_reg);
     __ mov(c_rarg1, lock_reg);
     __ mov(c_rarg2, r15_thread);
 
     // Not a leaf but we have last_Java_frame setup as we want
+    // Force freeze slow path in case we try to preempt. We will pin the
+    // vthread to the carrier (see FreezeBase::recurse_freeze_native_frame()).
+    __ push_cont_fastpath();
     __ call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::complete_monitor_locking_C), 3);
+    __ pop_cont_fastpath();
     restore_args(masm, total_c_args, c_arg, out_regs);
 
 #ifdef ASSERT
     { Label L;
     __ cmpptr(Address(r15_thread, in_bytes(Thread::pending_exception_offset())), NULL_WORD);
@@ -2604,10 +2633,14 @@
 // return address.
 uint SharedRuntime::in_preserve_stack_slots() {
   return 4 + 2 * VerifyStackAtCalls;
 }
 
+VMReg SharedRuntime::thread_register() {
+  return r15_thread->as_VMReg();
+}
+
 //------------------------------generate_deopt_blob----------------------------
 void SharedRuntime::generate_deopt_blob() {
   // Allocate space for the code
   ResourceMark rm;
   // Setup code generation tools
diff a/src/hotspot/cpu/x86/stackChunkFrameStream_x86.inline.hpp b/src/hotspot/cpu/x86/stackChunkFrameStream_x86.inline.hpp
--- a/src/hotspot/cpu/x86/stackChunkFrameStream_x86.inline.hpp
+++ b/src/hotspot/cpu/x86/stackChunkFrameStream_x86.inline.hpp
@@ -112,10 +112,11 @@
   InterpreterOopMap mask;
   frame f = to_frame();
   f.interpreted_frame_oop_map(&mask);
   return  mask.num_oops()
         + 1 // for the mirror oop
+        + (f.interpreter_frame_method()->is_native() ? 1 : 0) // temp oop slot
         + pointer_delta_as_int((intptr_t*)f.interpreter_frame_monitor_begin(),
               (intptr_t*)f.interpreter_frame_monitor_end())/BasicObjectLock::size();
 }
 
 template<>
diff a/src/hotspot/cpu/x86/stubGenerator_x86_64.cpp b/src/hotspot/cpu/x86/stubGenerator_x86_64.cpp
--- a/src/hotspot/cpu/x86/stubGenerator_x86_64.cpp
+++ b/src/hotspot/cpu/x86/stubGenerator_x86_64.cpp
@@ -33,10 +33,11 @@
 #include "gc/shared/gc_globals.hpp"
 #include "memory/universe.hpp"
 #include "prims/jvmtiExport.hpp"
 #include "prims/upcallLinker.hpp"
 #include "runtime/arguments.hpp"
+#include "runtime/continuationEntry.hpp"
 #include "runtime/javaThread.hpp"
 #include "runtime/sharedRuntime.hpp"
 #include "runtime/stubRoutines.hpp"
 #include "stubGenerator_x86_64.hpp"
 #ifdef COMPILER2
@@ -3777,10 +3778,40 @@
 
 address StubGenerator::generate_cont_returnBarrier_exception() {
   return generate_cont_thaw("Cont thaw return barrier exception", Continuation::thaw_return_barrier_exception);
 }
 
+address StubGenerator::generate_cont_preempt_stub() {
+  if (!Continuations::enabled()) return nullptr;
+  StubCodeMark mark(this, "StubRoutines","Continuation preempt stub");
+  address start = __ pc();
+
+  __ reset_last_Java_frame(true);
+
+  // Set rsp to enterSpecial frame, i.e. remove all frames copied into the heap.
+  __ movptr(rsp, Address(r15_thread, JavaThread::cont_entry_offset()));
+
+  Label preemption_cancelled;
+  __ movbool(rscratch1, Address(r15_thread, JavaThread::preemption_cancelled_offset()));
+  __ testbool(rscratch1);
+  __ jcc(Assembler::notZero, preemption_cancelled);
+
+  // Remove enterSpecial frame from the stack and return to Continuation.run() to unmount.
+  SharedRuntime::continuation_enter_cleanup(_masm);
+  __ pop(rbp);
+  __ ret(0);
+
+  // We acquired the monitor after freezing the frames so call thaw to continue execution.
+  __ bind(preemption_cancelled);
+  __ movbool(Address(r15_thread, JavaThread::preemption_cancelled_offset()), false);
+  __ lea(rbp, Address(rsp, checked_cast<int32_t>(ContinuationEntry::size())));
+  __ movptr(rscratch1, ExternalAddress(ContinuationEntry::thaw_call_pc_address()));
+  __ jmp(rscratch1);
+
+  return start;
+}
+
 // exception handler for upcall stubs
 address StubGenerator::generate_upcall_stub_exception_handler() {
   StubCodeMark mark(this, "StubRoutines", "upcall stub exception handler");
   address start = __ pc();
 
@@ -3949,10 +3980,11 @@
 void StubGenerator::generate_continuation_stubs() {
   // Continuation stubs:
   StubRoutines::_cont_thaw          = generate_cont_thaw();
   StubRoutines::_cont_returnBarrier = generate_cont_returnBarrier();
   StubRoutines::_cont_returnBarrierExc = generate_cont_returnBarrier_exception();
+  StubRoutines::_cont_preempt_stub = generate_cont_preempt_stub();
 }
 
 void StubGenerator::generate_final_stubs() {
   // Generates the rest of stubs and initializes the entry points
 
diff a/src/hotspot/cpu/x86/stubGenerator_x86_64.hpp b/src/hotspot/cpu/x86/stubGenerator_x86_64.hpp
--- a/src/hotspot/cpu/x86/stubGenerator_x86_64.hpp
+++ b/src/hotspot/cpu/x86/stubGenerator_x86_64.hpp
@@ -596,10 +596,13 @@
 
   // TODO: will probably need multiple return barriers depending on return type
   address generate_cont_returnBarrier();
   address generate_cont_returnBarrier_exception();
 
+  address generate_cont_preempt_stub();
+  address generate_cont_resume_monitor_operation();
+
   // Continuation point for throwing of implicit exceptions that are
   // not handled in the current activation. Fabricates an exception
   // oop and initiates normal exception dispatching in this
   // frame. Since we need to preserve callee-saved values (currently
   // only for C2, but done for C1 as well) we need a callee-saved oop
diff a/src/hotspot/cpu/x86/stubRoutines_x86.hpp b/src/hotspot/cpu/x86/stubRoutines_x86.hpp
--- a/src/hotspot/cpu/x86/stubRoutines_x86.hpp
+++ b/src/hotspot/cpu/x86/stubRoutines_x86.hpp
@@ -32,11 +32,11 @@
 static bool returns_to_call_stub(address return_pc) { return return_pc == _call_stub_return_address; }
 
 enum platform_dependent_constants {
   // simply increase sizes if too small (assembler will crash if too small)
   _initial_stubs_code_size      = 20000 WINDOWS_ONLY(+1000),
-  _continuation_stubs_code_size =  1000 LP64_ONLY(+1000),
+  _continuation_stubs_code_size =  1000 LP64_ONLY(+2000),
   // AVX512 intrinsics add more code in 64-bit VM,
   // Windows have more code to save/restore registers
   _compiler_stubs_code_size     = 20000 LP64_ONLY(+46000) WINDOWS_ONLY(+2000),
   _final_stubs_code_size        = 10000 LP64_ONLY(+20000) WINDOWS_ONLY(+2000) ZGC_ONLY(+20000)
 };
diff a/src/hotspot/cpu/x86/templateInterpreterGenerator_x86.cpp b/src/hotspot/cpu/x86/templateInterpreterGenerator_x86.cpp
--- a/src/hotspot/cpu/x86/templateInterpreterGenerator_x86.cpp
+++ b/src/hotspot/cpu/x86/templateInterpreterGenerator_x86.cpp
@@ -385,10 +385,30 @@
 
   __ dispatch_via(vtos, Interpreter::_normal_table.table_for(vtos));
   return entry;
 }
 
+address TemplateInterpreterGenerator::generate_cont_resume_interpreter_adapter() {
+  if (!Continuations::enabled()) return nullptr;
+  address start = __ pc();
+
+  __ restore_bcp();
+  __ restore_locals();
+
+  // Get return address before adjusting rsp
+  __ movptr(rax, Address(rsp, 0));
+
+  // Restore stack bottom
+  __ movptr(rcx, Address(rbp, frame::interpreter_frame_last_sp_offset * wordSize));
+  __ lea(rsp, Address(rbp, rcx, Address::times_ptr));
+  // and NULL it as marker that esp is now tos until next java call
+  __ movptr(Address(rbp, frame::interpreter_frame_last_sp_offset * wordSize), NULL_WORD);
+
+  __ jmp(rax);
+
+  return start;
+}
 
 
 // Helpers for commoning out cases in the various type of method entries.
 //
 
@@ -1027,11 +1047,14 @@
 #else
    __ lea(c_rarg0, Address(r15_thread, JavaThread::jni_environment_offset()));
 
    // It is enough that the pc() points into the right code
    // segment. It does not have to be the correct return pc.
-   __ set_last_Java_frame(rsp, rbp, (address) __ pc(), rscratch1);
+   // For convenience we use the pc we want to resume to in
+   // case of preemption on Object.wait.
+   Label native_return;
+   __ set_last_Java_frame(rsp, rbp, native_return, rscratch1);
 #endif // _LP64
 
   // change thread state
 #ifdef ASSERT
   {
@@ -1047,15 +1070,19 @@
   // Change state to native
 
   __ movl(Address(thread, JavaThread::thread_state_offset()),
           _thread_in_native);
 
+  __ push_cont_fastpath();
+
   // Call the native method.
   __ call(rax);
   // 32: result potentially in rdx:rax or ST0
   // 64: result potentially in rax or xmm0
 
+  __ pop_cont_fastpath();
+
   // Verify or restore cpu control state after JNI call
   __ restore_cpu_control_state_after_jni(rscratch1);
 
   // NOTE: The order of these pushes is known to frame::interpreter_frame_result
   // in order to extract the result of a method call. If the order of these
@@ -1075,14 +1102,14 @@
 
   { Label L;
     Label push_double;
     ExternalAddress float_handler(AbstractInterpreter::result_handler(T_FLOAT));
     ExternalAddress double_handler(AbstractInterpreter::result_handler(T_DOUBLE));
-    __ cmpptr(Address(rbp, (frame::interpreter_frame_oop_temp_offset + 1)*wordSize),
+    __ cmpptr(Address(rbp, (frame::interpreter_frame_result_handler_offset)*wordSize),
               float_handler.addr(), noreg);
     __ jcc(Assembler::equal, push_double);
-    __ cmpptr(Address(rbp, (frame::interpreter_frame_oop_temp_offset + 1)*wordSize),
+    __ cmpptr(Address(rbp, (frame::interpreter_frame_result_handler_offset)*wordSize),
               double_handler.addr(), noreg);
     __ jcc(Assembler::notEqual, L);
     __ bind(push_double);
     __ push_d(); // FP values are returned using the FPU, so push FPU contents (even if UseSSE > 0).
     __ bind(L);
@@ -1148,10 +1175,28 @@
   }
 
   // change thread state
   __ movl(Address(thread, JavaThread::thread_state_offset()), _thread_in_Java);
 
+#ifdef _LP64
+  if (LockingMode != LM_LEGACY) {
+    // Check preemption for Object.wait()
+    Label not_preempted;
+    __ movptr(rscratch1, Address(r15_thread, JavaThread::preempt_alternate_return_offset()));
+    __ cmpptr(rscratch1, NULL_WORD);
+    __ jccb(Assembler::equal, not_preempted);
+    __ movptr(Address(r15_thread, JavaThread::preempt_alternate_return_offset()), NULL_WORD);
+    __ jmp(rscratch1);
+    __ bind(native_return);
+    __ restore_after_resume(true /* is_native */);
+    __ bind(not_preempted);
+  } else {
+    // any pc will do so just use this one for LM_LEGACY to keep code together.
+    __ bind(native_return);
+  }
+#endif // _LP64
+
   // reset_last_Java_frame
   __ reset_last_Java_frame(thread, true);
 
   if (CheckJNICalls) {
     // clear_pending_jni_exception_check
diff a/src/hotspot/cpu/zero/continuationFreezeThaw_zero.inline.hpp b/src/hotspot/cpu/zero/continuationFreezeThaw_zero.inline.hpp
--- a/src/hotspot/cpu/zero/continuationFreezeThaw_zero.inline.hpp
+++ b/src/hotspot/cpu/zero/continuationFreezeThaw_zero.inline.hpp
@@ -46,10 +46,14 @@
 
 void FreezeBase::adjust_interpreted_frame_unextended_sp(frame& f) {
   Unimplemented();
 }
 
+inline void FreezeBase::prepare_freeze_interpreted_top_frame(const frame& f) {
+  Unimplemented();
+}
+
 inline void FreezeBase::relativize_interpreted_frame_metadata(const frame& f, const frame& hf) {
   Unimplemented();
 }
 
 inline void FreezeBase::patch_pd(frame& hf, const frame& caller) {
@@ -81,10 +85,24 @@
 
 inline void ThawBase::patch_pd(frame& f, const frame& caller) {
   Unimplemented();
 }
 
+inline void ThawBase::patch_pd(frame& f, intptr_t* caller_sp) {
+  Unimplemented();
+}
+
+inline intptr_t* ThawBase::possibly_adjust_frame(frame& top) {
+  Unimplemented();
+  return nullptr;
+}
+
+inline intptr_t* ThawBase::push_cleanup_continuation() {
+  Unimplemented();
+  return nullptr;
+}
+
 template <typename ConfigT>
 inline void Thaw<ConfigT>::patch_caller_links(intptr_t* sp, intptr_t* bottom) {
   Unimplemented();
 }
 
diff a/src/hotspot/cpu/zero/continuationHelper_zero.inline.hpp b/src/hotspot/cpu/zero/continuationHelper_zero.inline.hpp
--- a/src/hotspot/cpu/zero/continuationHelper_zero.inline.hpp
+++ b/src/hotspot/cpu/zero/continuationHelper_zero.inline.hpp
@@ -31,10 +31,14 @@
 static inline intptr_t** link_address(const frame& f) {
   Unimplemented();
   return nullptr;
 }
 
+static inline void patch_return_pc_with_preempt_stub(frame& f) {
+  Unimplemented();
+}
+
 inline int ContinuationHelper::frame_align_words(int size) {
   Unimplemented();
   return 0;
 }
 
@@ -58,15 +62,15 @@
 
 inline void ContinuationHelper::set_anchor_to_entry_pd(JavaFrameAnchor* anchor, ContinuationEntry* cont) {
   Unimplemented();
 }
 
-#ifdef ASSERT
 inline void ContinuationHelper::set_anchor_pd(JavaFrameAnchor* anchor, intptr_t* sp) {
   Unimplemented();
 }
 
+#ifdef ASSERT
 inline bool ContinuationHelper::Frame::assert_frame_laid_out(frame f) {
   Unimplemented();
   return false;
 }
 #endif
diff a/src/hotspot/cpu/zero/globalDefinitions_zero.hpp b/src/hotspot/cpu/zero/globalDefinitions_zero.hpp
--- a/src/hotspot/cpu/zero/globalDefinitions_zero.hpp
+++ b/src/hotspot/cpu/zero/globalDefinitions_zero.hpp
@@ -34,12 +34,10 @@
 #define DEFAULT_CACHE_LINE_SIZE 64
 
 // The default padding size for data structures to avoid false sharing.
 #define DEFAULT_PADDING_SIZE DEFAULT_CACHE_LINE_SIZE
 
-#define SUPPORT_MONITOR_COUNT
-
 #include <ffi.h>
 
 // Indicates whether the C calling conventions require that
 // 32-bit integer argument values are extended to 64 bits.
 const bool CCallingConventionRequiresIntsAsLongs = false;
diff a/src/hotspot/cpu/zero/sharedRuntime_zero.cpp b/src/hotspot/cpu/zero/sharedRuntime_zero.cpp
--- a/src/hotspot/cpu/zero/sharedRuntime_zero.cpp
+++ b/src/hotspot/cpu/zero/sharedRuntime_zero.cpp
@@ -83,10 +83,15 @@
 uint SharedRuntime::out_preserve_stack_slots() {
   ShouldNotCallThis();
   return 0;
 }
 
+VMReg SharedRuntime::thread_register() {
+  Unimplemented();
+  return nullptr;
+}
+
 JRT_LEAF(void, zero_stub())
   ShouldNotCallThis();
 JRT_END
 
 static RuntimeStub* generate_empty_runtime_stub() {
diff a/src/hotspot/cpu/zero/zeroInterpreter_zero.cpp b/src/hotspot/cpu/zero/zeroInterpreter_zero.cpp
--- a/src/hotspot/cpu/zero/zeroInterpreter_zero.cpp
+++ b/src/hotspot/cpu/zero/zeroInterpreter_zero.cpp
@@ -344,13 +344,10 @@
           monitor->lock()->set_displaced_header(markWord::from_pointer(nullptr));
         } else {
           success = false;
         }
       }
-      if (success) {
-        THREAD->inc_held_monitor_count();
-      }
     }
     if (!success) {
       CALL_VM_NOCHECK(InterpreterRuntime::monitorenter(thread, monitor));
           if (HAS_PENDING_EXCEPTION)
             goto unwind_and_return;
@@ -497,13 +494,10 @@
         if (rcvr->cas_set_mark(header, old_header) != old_header) {
           monitor->set_obj(rcvr);
           success = false;
         }
       }
-      if (success) {
-        THREAD->dec_held_monitor_count();
-      }
     }
     if (!success) {
       InterpreterRuntime::monitorexit(monitor);
     }
   }
diff a/src/hotspot/share/c1/c1_MacroAssembler.hpp b/src/hotspot/share/c1/c1_MacroAssembler.hpp
--- a/src/hotspot/share/c1/c1_MacroAssembler.hpp
+++ b/src/hotspot/share/c1/c1_MacroAssembler.hpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2000, 2021, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -87,9 +87,9 @@
   int call_RT(Register oop_result1, Register metadata_result, address entry, Register arg1);
   int call_RT(Register oop_result1, Register metadata_result, address entry, Register arg1, Register arg2);
   int call_RT(Register oop_result1, Register metadata_result, address entry, Register arg1, Register arg2, Register arg3);
 
   void prologue(const char* name, bool must_gc_arguments);
-  void epilogue();
+  void epilogue(bool use_pop = false);
 };
 
 #endif // SHARE_C1_C1_MACROASSEMBLER_HPP
diff a/src/hotspot/share/c1/c1_Runtime1.hpp b/src/hotspot/share/c1/c1_Runtime1.hpp
--- a/src/hotspot/share/c1/c1_Runtime1.hpp
+++ b/src/hotspot/share/c1/c1_Runtime1.hpp
@@ -133,10 +133,12 @@
  public:
   // initialization
   static void initialize(BufferBlob* blob);
   static void initialize_pd();
 
+  static uint runtime_blob_current_thread_offset(frame f);
+
   // stubs
   static CodeBlob* blob_for (C1StubId id);
   static address   entry_for(C1StubId id)          { return blob_for(id)->code_begin(); }
   static const char* name_for (C1StubId id);
   static const char* name_for_address(address entry);
diff a/src/hotspot/share/classfile/javaClasses.cpp b/src/hotspot/share/classfile/javaClasses.cpp
--- a/src/hotspot/share/classfile/javaClasses.cpp
+++ b/src/hotspot/share/classfile/javaClasses.cpp
@@ -1684,10 +1684,11 @@
 bool java_lang_Thread::is_in_VTMS_transition(oop java_thread) {
   return java_thread->bool_field_volatile(_jvmti_is_in_VTMS_transition_offset);
 }
 
 void java_lang_Thread::set_is_in_VTMS_transition(oop java_thread, bool val) {
+  assert(is_in_VTMS_transition(java_thread) != val, "already %s transition", val ? "inside" : "outside");
   java_thread->bool_field_put_volatile(_jvmti_is_in_VTMS_transition_offset, val);
 }
 
 int java_lang_Thread::is_in_VTMS_transition_offset() {
   return _jvmti_is_in_VTMS_transition_offset;
@@ -2019,16 +2020,24 @@
 
 int java_lang_VirtualThread::static_vthread_scope_offset;
 int java_lang_VirtualThread::_carrierThread_offset;
 int java_lang_VirtualThread::_continuation_offset;
 int java_lang_VirtualThread::_state_offset;
+int java_lang_VirtualThread::_next_offset;
+int java_lang_VirtualThread::_onWaitingList_offset;
+int java_lang_VirtualThread::_notified_offset;
+int java_lang_VirtualThread::_waitTimeout_offset;
 
 #define VTHREAD_FIELDS_DO(macro) \
   macro(static_vthread_scope_offset,       k, "VTHREAD_SCOPE",      continuationscope_signature, true);  \
   macro(_carrierThread_offset,             k, "carrierThread",      thread_signature,            false); \
   macro(_continuation_offset,              k, "cont",               continuation_signature,      false); \
-  macro(_state_offset,                     k, "state",              int_signature,               false)
+  macro(_state_offset,                     k, "state",              int_signature,               false); \
+  macro(_next_offset,                      k, "next",               vthread_signature,           false); \
+  macro(_onWaitingList_offset,             k, "onWaitingList",      bool_signature,              false); \
+  macro(_notified_offset,                  k, "notified",           bool_signature,              false); \
+  macro(_waitTimeout_offset,               k, "waitTimeout",        long_signature,              false);
 
 
 void java_lang_VirtualThread::compute_offsets() {
   InstanceKlass* k = vmClasses::VirtualThread_klass();
   VTHREAD_FIELDS_DO(FIELD_COMPUTE_OFFSET);
@@ -2050,10 +2059,56 @@
 
 int java_lang_VirtualThread::state(oop vthread) {
   return vthread->int_field_acquire(_state_offset);
 }
 
+void java_lang_VirtualThread::set_state(oop vthread, int state) {
+  vthread->release_int_field_put(_state_offset, state);
+}
+
+int java_lang_VirtualThread::cmpxchg_state(oop vthread, int old_state, int new_state) {
+  jint* addr = vthread->field_addr<jint>(_state_offset);
+  int res = Atomic::cmpxchg(addr, old_state, new_state);
+  return res;
+}
+
+oop java_lang_VirtualThread::next(oop vthread) {
+  return vthread->obj_field(_next_offset);
+}
+
+void java_lang_VirtualThread::set_next(oop vthread, oop next_vthread) {
+  vthread->obj_field_put(_next_offset, next_vthread);
+}
+
+bool java_lang_VirtualThread::set_onWaitingList(oop vthread, OopHandle& list_head) {
+  jboolean* addr = vthread->field_addr<jboolean>(_onWaitingList_offset);
+  jboolean vthread_on_list = Atomic::load(addr);
+  if (!vthread_on_list) {
+    vthread_on_list = Atomic::cmpxchg(addr, (jboolean)JNI_FALSE, (jboolean)JNI_TRUE);
+    if (!vthread_on_list) {
+      for (;;) {
+        oop head = list_head.resolve();
+        java_lang_VirtualThread::set_next(vthread, head);
+        if (list_head.cmpxchg(head, vthread) == head) return true;
+      }
+    }
+  }
+  return false; // already on waiting list
+}
+
+void java_lang_VirtualThread::set_notified(oop vthread, jboolean value) {
+  vthread->bool_field_put_volatile(_notified_offset, value);
+}
+
+jlong java_lang_VirtualThread::waitTimeout(oop vthread) {
+  return vthread->long_field(_waitTimeout_offset);
+}
+
+void java_lang_VirtualThread::set_waitTimeout(oop vthread, jlong value) {
+  vthread->long_field_put(_waitTimeout_offset, value);
+}
+
 JavaThreadStatus java_lang_VirtualThread::map_state_to_thread_status(int state) {
   JavaThreadStatus status = JavaThreadStatus::NEW;
   switch (state & ~SUSPENDED) {
     case NEW:
       status = JavaThreadStatus::NEW;
@@ -2063,29 +2118,49 @@
     case PARKING:
     case TIMED_PARKING:
     case UNPARKED:
     case YIELDING:
     case YIELDED:
+    case UNBLOCKED:
+    case WAITING:
+    case TIMED_WAITING:
       status = JavaThreadStatus::RUNNABLE;
       break;
     case PARKED:
     case PINNED:
       status = JavaThreadStatus::PARKED;
       break;
     case TIMED_PARKED:
     case TIMED_PINNED:
       status = JavaThreadStatus::PARKED_TIMED;
       break;
+    case BLOCKING:
+    case BLOCKED:
+      status = JavaThreadStatus::BLOCKED_ON_MONITOR_ENTER;
+      break;
+    case WAIT:
+      status = JavaThreadStatus::IN_OBJECT_WAIT;
+      break;
+    case TIMED_WAIT:
+      status = JavaThreadStatus::IN_OBJECT_WAIT_TIMED;
+      break;
     case TERMINATED:
       status = JavaThreadStatus::TERMINATED;
       break;
     default:
       ShouldNotReachHere();
   }
   return status;
 }
 
+bool java_lang_VirtualThread::is_preempted(oop vthread) {
+  oop continuation = java_lang_VirtualThread::continuation(vthread);
+  assert(continuation != nullptr, "vthread with no continuation");
+  stackChunkOop chunk = jdk_internal_vm_Continuation::tail(continuation);
+  return chunk != nullptr && chunk->preempted();
+}
+
 #if INCLUDE_CDS
 void java_lang_VirtualThread::serialize_offsets(SerializeClosure* f) {
    VTHREAD_FIELDS_DO(FIELD_SERIALIZE_OFFSET);
 }
 #endif
diff a/src/hotspot/share/classfile/javaClasses.hpp b/src/hotspot/share/classfile/javaClasses.hpp
--- a/src/hotspot/share/classfile/javaClasses.hpp
+++ b/src/hotspot/share/classfile/javaClasses.hpp
@@ -528,10 +528,15 @@
  private:
   static int static_vthread_scope_offset;
   static int _carrierThread_offset;
   static int _continuation_offset;
   static int _state_offset;
+  static int _next_offset;
+  static int _onWaitingList_offset;
+  static int _notified_offset;
+  static int _recheckInterval_offset;
+  static int _waitTimeout_offset;
   JFR_ONLY(static int _jfr_epoch_offset;)
  public:
   enum {
     NEW           = 0,
     STARTED       = 1,
@@ -543,10 +548,17 @@
     TIMED_PARKED  = 7,
     TIMED_PINNED  = 8,
     UNPARKED      = 9,
     YIELDING      = 10,
     YIELDED       = 11,
+    BLOCKING      = 12,
+    BLOCKED       = 13,
+    UNBLOCKED     = 14,
+    WAITING       = 15,
+    WAIT          = 16,  // waiting in Object.wait
+    TIMED_WAITING = 17,
+    TIMED_WAIT    = 18,  // waiting in timed-Object.wait
     TERMINATED    = 99,
 
     // additional state bits
     SUSPENDED    = 1 << 8,   // suspended when unmounted
   };
@@ -562,10 +574,19 @@
 
   static oop vthread_scope();
   static oop carrier_thread(oop vthread);
   static oop continuation(oop vthread);
   static int state(oop vthread);
+  static void set_state(oop vthread, int state);
+  static int cmpxchg_state(oop vthread, int old_state, int new_state);
+  static oop next(oop vthread);
+  static void set_next(oop vthread, oop next_vthread);
+  static bool set_onWaitingList(oop vthread, OopHandle& list_head);
+  static jlong waitTimeout(oop vthread);
+  static void set_waitTimeout(oop vthread, jlong value);
+  static void set_notified(oop vthread, jboolean value);
+  static bool is_preempted(oop vthread);
   static JavaThreadStatus map_state_to_thread_status(int state);
 };
 
 
 // Interface to java.lang.Throwable objects
diff a/src/hotspot/share/classfile/vmIntrinsics.cpp b/src/hotspot/share/classfile/vmIntrinsics.cpp
--- a/src/hotspot/share/classfile/vmIntrinsics.cpp
+++ b/src/hotspot/share/classfile/vmIntrinsics.cpp
@@ -129,10 +129,11 @@
   case vmIntrinsics::_doubleToRawLongBits:
   case vmIntrinsics::_longBitsToDouble:
   case vmIntrinsics::_currentCarrierThread:
   case vmIntrinsics::_currentThread:
   case vmIntrinsics::_setCurrentThread:
+  case vmIntrinsics::_setCurrentLockId:
   case vmIntrinsics::_scopedValueCache:
   case vmIntrinsics::_setScopedValueCache:
   case vmIntrinsics::_dabs:
   case vmIntrinsics::_fabs:
   case vmIntrinsics::_iabs:
diff a/src/hotspot/share/classfile/vmIntrinsics.hpp b/src/hotspot/share/classfile/vmIntrinsics.hpp
--- a/src/hotspot/share/classfile/vmIntrinsics.hpp
+++ b/src/hotspot/share/classfile/vmIntrinsics.hpp
@@ -296,10 +296,12 @@
   do_intrinsic(_findScopedValueBindings,  java_lang_Thread,       findScopedValueBindings_name, void_object_signature, F_SN) \
    do_name(     findScopedValueBindings_name,                    "findScopedValueBindings")                             \
                                                                                                                         \
   do_intrinsic(_setCurrentThread,         java_lang_Thread,       setCurrentThread_name, thread_void_signature,   F_RN) \
    do_name(     setCurrentThread_name,                           "setCurrentThread")                                    \
+  do_intrinsic(_setCurrentLockId,         java_lang_Thread,       setCurrentLockId_name, long_void_signature,     F_SN) \
+   do_name(     setCurrentLockId_name,                           "setCurrentLockId")                                    \
                                                                                                                         \
   /* reflective intrinsics, for java/lang/Class, etc. */                                                                \
   do_intrinsic(_isAssignableFrom,         java_lang_Class,        isAssignableFrom_name, class_boolean_signature, F_RN) \
    do_name(     isAssignableFrom_name,                           "isAssignableFrom")                                    \
   do_intrinsic(_isInstance,               java_lang_Class,        isInstance_name, object_boolean_signature,     F_RN)  \
diff a/src/hotspot/share/classfile/vmSymbols.hpp b/src/hotspot/share/classfile/vmSymbols.hpp
--- a/src/hotspot/share/classfile/vmSymbols.hpp
+++ b/src/hotspot/share/classfile/vmSymbols.hpp
@@ -516,10 +516,12 @@
   template(data_cache_line_flush_size_name,           "DATA_CACHE_LINE_FLUSH_SIZE")               \
   template(during_unsafe_access_name,                 "during_unsafe_access")                     \
   template(checkIndex_name,                           "checkIndex")                               \
   template(jfr_epoch_name,                            "jfr_epoch")                                \
   template(maxThawingSize_name,                       "maxThawingSize")                           \
+  template(lockStackSize_name,                        "lockStackSize")                            \
+  template(objectWaiter_name,                         "objectWaiter")                             \
                                                                                                   \
   /* name symbols needed by intrinsics */                                                         \
   VM_INTRINSICS_DO(VM_INTRINSIC_IGNORE, VM_SYMBOL_IGNORE, template, VM_SYMBOL_IGNORE, VM_ALIAS_IGNORE) \
                                                                                                   \
   /* common signatures names */                                                                   \
@@ -564,10 +566,11 @@
   template(int_array_signature,                       "[I")                                       \
   template(runnable_signature,                        "Ljava/lang/Runnable;")                     \
   template(continuation_signature,                    "Ljdk/internal/vm/Continuation;")           \
   template(continuationscope_signature,               "Ljdk/internal/vm/ContinuationScope;")      \
   template(stackchunk_signature,                      "Ljdk/internal/vm/StackChunk;")             \
+  template(vthread_signature,                         "Ljava/lang/VirtualThread;")                \
   template(object_void_signature,                     "(Ljava/lang/Object;)V")                    \
   template(object_int_signature,                      "(Ljava/lang/Object;)I")                    \
   template(long_object_long_signature,                "(JLjava/lang/Object;)J")                   \
   template(object_boolean_signature,                  "(Ljava/lang/Object;)Z")                    \
   template(object_object_signature,                   "(Ljava/lang/Object;)Ljava/lang/Object;")   \
@@ -742,14 +745,18 @@
   template(java_util_ArrayList,                             "java/util/ArrayList")                                \
   template(toFileURL_name,                                  "toFileURL")                                          \
   template(toFileURL_signature,                             "(Ljava/lang/String;)Ljava/net/URL;")                 \
   template(url_array_classloader_void_signature,            "([Ljava/net/URL;Ljava/lang/ClassLoader;)V")          \
                                                                                                                   \
-  /* Thread.dump_to_file jcmd */                                                                                  \
+  /* jcmd Thread.dump_to_file */                                                                                  \
   template(jdk_internal_vm_ThreadDumper,           "jdk/internal/vm/ThreadDumper")                                \
   template(dumpThreads_name,                       "dumpThreads")                                                 \
   template(dumpThreadsToJson_name,                 "dumpThreadsToJson")                                           \
+                                                                                                                  \
+  /* jcmd Thread.vthread_summary */                                                                               \
+  template(jdk_internal_vm_VThreadSummary,         "jdk/internal/vm/VThreadSummary")                              \
+  template(print_name,                             "print")                                                       \
 
   /*end*/
 
 // enum for figuring positions and size of Symbol::_vm_symbols[]
 enum class vmSymbolID : int {
diff a/src/hotspot/share/code/nmethod.cpp b/src/hotspot/share/code/nmethod.cpp
--- a/src/hotspot/share/code/nmethod.cpp
+++ b/src/hotspot/share/code/nmethod.cpp
@@ -706,11 +706,11 @@
     return;
   }
 
   // handle the case of an anchor explicitly set in continuation code that doesn't have a callee
   JavaThread* thread = reg_map->thread();
-  if (thread->has_last_Java_frame() && fr.sp() == thread->last_Java_sp()) {
+  if ((thread->has_last_Java_frame() && fr.sp() == thread->last_Java_sp()) JVMTI_ONLY(|| (method()->is_continuation_enter_intrinsic() && thread->on_monitor_waited_event()))) {
     return;
   }
 
   if (!method()->is_native()) {
     address pc = fr.pc();
@@ -1296,11 +1296,11 @@
     _entry_bci               = InvocationEntryBci;
     _compile_id              = compile_id;
     _comp_level              = CompLevel_none;
     _compiler_type           = type;
     _orig_pc_offset          = 0;
-    _num_stack_arg_slots     = _method->constMethod()->num_stack_arg_slots();
+    _num_stack_arg_slots     = 0;
 
     if (offsets->value(CodeOffsets::Exceptions) != -1) {
       // Continuation enter intrinsic
       _exception_offset      = code_offset() + offsets->value(CodeOffsets::Exceptions);
     } else {
diff a/src/hotspot/share/include/jvm.h b/src/hotspot/share/include/jvm.h
--- a/src/hotspot/share/include/jvm.h
+++ b/src/hotspot/share/include/jvm.h
@@ -287,10 +287,13 @@
 JVM_CurrentThread(JNIEnv *env, jclass threadClass);
 
 JNIEXPORT void JNICALL
 JVM_SetCurrentThread(JNIEnv *env, jobject thisThread, jobject theThread);
 
+JNIEXPORT void JNICALL
+JVM_SetCurrentLockId(JNIEnv* env, jclass threadClass, jlong tid);
+
 JNIEXPORT void JNICALL
 JVM_Interrupt(JNIEnv *env, jobject thread);
 
 JNIEXPORT jboolean JNICALL
 JVM_HoldsLock(JNIEnv *env, jclass threadClass, jobject obj);
@@ -1146,10 +1149,16 @@
 JVM_VirtualThreadHideFrames(JNIEnv* env, jclass clazz, jboolean hide);
 
 JNIEXPORT void JNICALL
 JVM_VirtualThreadDisableSuspend(JNIEnv* env, jclass clazz, jboolean enter);
 
+JNIEXPORT void JNICALL
+JVM_VirtualThreadPinnedEvent(jint reasonCode, jstring reasonString);
+
+JNIEXPORT jobject JNICALL
+JVM_TakeVirtualThreadListToUnblock(JNIEnv* env, jclass ignored);
+
 /*
  * Core reflection support.
  */
 JNIEXPORT jint JNICALL
 JVM_GetClassFileVersion(JNIEnv *env, jclass current);
diff a/src/hotspot/share/interpreter/interpreterRuntime.cpp b/src/hotspot/share/interpreter/interpreterRuntime.cpp
--- a/src/hotspot/share/interpreter/interpreterRuntime.cpp
+++ b/src/hotspot/share/interpreter/interpreterRuntime.cpp
@@ -733,11 +733,11 @@
          "must be null or an object");
   ObjectSynchronizer::enter(h_obj, elem->lock(), current);
   assert(Universe::heap()->is_in_or_null(elem->obj()),
          "must be null or an object");
 #ifdef ASSERT
-  current->last_frame().interpreter_frame_verify_monitor(elem);
+  if (!current->preempting()) current->last_frame().interpreter_frame_verify_monitor(elem);
 #endif
 JRT_END
 
 JRT_LEAF(void, InterpreterRuntime::monitorexit(BasicObjectLock* elem))
   oop obj = elem->obj();
diff a/src/hotspot/share/interpreter/oopMapCache.cpp b/src/hotspot/share/interpreter/oopMapCache.cpp
--- a/src/hotspot/share/interpreter/oopMapCache.cpp
+++ b/src/hotspot/share/interpreter/oopMapCache.cpp
@@ -232,12 +232,14 @@
 
 class MaskFillerForNative: public NativeSignatureIterator {
  private:
   uintptr_t * _mask;                             // the bit mask to be filled
   int         _size;                             // the mask size in bits
+  int         _num_oops;
 
   void set_one(int i) {
+    _num_oops++;
     i *= InterpreterOopMap::bits_per_entry;
     assert(0 <= i && i < _size, "offset out of bounds");
     _mask[i / BitsPerWord] |= (((uintptr_t) 1 << InterpreterOopMap::oop_bit_number) << (i % BitsPerWord));
   }
 
@@ -251,18 +253,21 @@
   void pass_object()                             { set_one(offset()); }
 
   MaskFillerForNative(const methodHandle& method, uintptr_t* mask, int size) : NativeSignatureIterator(method) {
     _mask   = mask;
     _size   = size;
+    _num_oops = 0;
     // initialize with 0
     int i = (size + BitsPerWord - 1) / BitsPerWord;
     while (i-- > 0) _mask[i] = 0;
   }
 
   void generate() {
     iterate();
   }
+
+  int num_oops() { return _num_oops; }
 };
 
 bool OopMapCacheEntry::verify_mask(CellTypeState* vars, CellTypeState* stack, int max_locals, int stack_top) {
   // Check mask includes map
   VerifyClosure blk(this);
@@ -317,10 +322,11 @@
   set_mask_size(mh->size_of_parameters() * bits_per_entry);
   allocate_bit_mask();
   // fill mask for parameters
   MaskFillerForNative mf(mh, bit_mask(), mask_size());
   mf.generate();
+  _num_oops = mf.num_oops();
 }
 
 
 void OopMapCacheEntry::fill(const methodHandle& method, int bci) {
   // Flush entry to deallocate an existing entry
diff a/src/hotspot/share/interpreter/templateInterpreter.cpp b/src/hotspot/share/interpreter/templateInterpreter.cpp
--- a/src/hotspot/share/interpreter/templateInterpreter.cpp
+++ b/src/hotspot/share/interpreter/templateInterpreter.cpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 1997, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -210,10 +210,11 @@
 address    TemplateInterpreter::_throw_ArithmeticException_entry            = nullptr;
 address    TemplateInterpreter::_throw_ClassCastException_entry             = nullptr;
 address    TemplateInterpreter::_throw_NullPointerException_entry           = nullptr;
 address    TemplateInterpreter::_throw_StackOverflowError_entry             = nullptr;
 address    TemplateInterpreter::_throw_exception_entry                      = nullptr;
+address    TemplateInterpreter::_cont_resume_interpreter_adapter            = nullptr;
 
 #ifndef PRODUCT
 EntryPoint TemplateInterpreter::_trace_code;
 #endif // !PRODUCT
 EntryPoint TemplateInterpreter::_return_entry[TemplateInterpreter::number_of_return_entries];
diff a/src/hotspot/share/interpreter/templateInterpreter.hpp b/src/hotspot/share/interpreter/templateInterpreter.hpp
--- a/src/hotspot/share/interpreter/templateInterpreter.hpp
+++ b/src/hotspot/share/interpreter/templateInterpreter.hpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 1997, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -109,10 +109,12 @@
   static address    _throw_NullPointerException_entry;
   static address    _throw_exception_entry;
 
   static address    _throw_StackOverflowError_entry;
 
+  static address    _cont_resume_interpreter_adapter;
+
   static address    _remove_activation_entry;                   // continuation address if an exception is not handled by current frame
   static address    _remove_activation_preserving_args_entry;   // continuation address when current frame is being popped
 
 #ifndef PRODUCT
   static EntryPoint _trace_code;
@@ -152,10 +154,12 @@
   static address    throw_exception_entry()                     { return _throw_exception_entry; }
   static address    throw_ArithmeticException_entry()           { return _throw_ArithmeticException_entry; }
   static address    throw_NullPointerException_entry()          { return _throw_NullPointerException_entry; }
   static address    throw_StackOverflowError_entry()            { return _throw_StackOverflowError_entry; }
 
+  static address    cont_resume_interpreter_adapter()           { return _cont_resume_interpreter_adapter; }
+
   // Code generation
 #ifndef PRODUCT
   static address    trace_code    (TosState state)              { return _trace_code.entry(state); }
 #endif // !PRODUCT
   static address*   dispatch_table(TosState state)              { return _active_table.table_for(state); }
diff a/src/hotspot/share/interpreter/templateInterpreterGenerator.cpp b/src/hotspot/share/interpreter/templateInterpreterGenerator.cpp
--- a/src/hotspot/share/interpreter/templateInterpreterGenerator.cpp
+++ b/src/hotspot/share/interpreter/templateInterpreterGenerator.cpp
@@ -173,11 +173,13 @@
     Interpreter::_throw_ClassCastException_entry             = generate_ClassCastException_handler();
     Interpreter::_throw_NullPointerException_entry           = generate_exception_handler("java/lang/NullPointerException", nullptr);
     Interpreter::_throw_StackOverflowError_entry             = generate_StackOverflowError_handler();
   }
 
-
+  { CodeletMark cm(_masm, "preemption resume adapter");
+    Interpreter::_cont_resume_interpreter_adapter = generate_cont_resume_interpreter_adapter();
+  }
 
 #define method_entry(kind)                                                                          \
   { CodeletMark cm(_masm, "method entry point (kind = " #kind ")");                                 \
     Interpreter::_entry_table[Interpreter::kind] = generate_method_entry(Interpreter::kind, false); \
   }
diff a/src/hotspot/share/interpreter/templateInterpreterGenerator.hpp b/src/hotspot/share/interpreter/templateInterpreterGenerator.hpp
--- a/src/hotspot/share/interpreter/templateInterpreterGenerator.hpp
+++ b/src/hotspot/share/interpreter/templateInterpreterGenerator.hpp
@@ -54,10 +54,11 @@
   address generate_ArrayIndexOutOfBounds_handler();
   address generate_return_entry_for(TosState state, int step, size_t index_size);
   address generate_earlyret_entry_for(TosState state);
   address generate_deopt_entry_for(TosState state, int step, address continuation = nullptr);
   address generate_safept_entry_for(TosState state, address runtime_entry);
+  address generate_cont_resume_interpreter_adapter();
   void    generate_throw_exception();
 
   void lock_method();
 
   void bang_stack_shadow_pages(bool native_call);
diff a/src/hotspot/share/interpreter/zero/bytecodeInterpreter.cpp b/src/hotspot/share/interpreter/zero/bytecodeInterpreter.cpp
--- a/src/hotspot/share/interpreter/zero/bytecodeInterpreter.cpp
+++ b/src/hotspot/share/interpreter/zero/bytecodeInterpreter.cpp
@@ -639,13 +639,10 @@
               mon->lock()->set_displaced_header(markWord::from_pointer(nullptr));
             } else {
               success = false;
             }
           }
-          if (success) {
-            THREAD->inc_held_monitor_count();
-          }
         }
         if (!success) {
             CALL_VM(InterpreterRuntime::monitorenter(THREAD, mon), handle_exception);
         }
 
@@ -743,13 +740,10 @@
             entry->lock()->set_displaced_header(markWord::from_pointer(nullptr));
           } else {
             success = false;
           }
         }
-        if (success) {
-          THREAD->inc_held_monitor_count();
-        }
       }
       if (!success) {
         CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
       }
 
@@ -1678,13 +1672,10 @@
                 entry->lock()->set_displaced_header(markWord::from_pointer(nullptr));
               } else {
                 success = false;
               }
             }
-            if (success) {
-              THREAD->inc_held_monitor_count();
-            }
           }
           if (!success) {
             CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
           }
 
@@ -1718,13 +1709,10 @@
                   // restore object for the slow case
                   most_recent->set_obj(lockee);
                   success = false;
                 }
               }
-              if (success) {
-                THREAD->dec_held_monitor_count();
-              }
             }
             if (!success) {
               InterpreterRuntime::monitorexit(most_recent);
             }
             UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
@@ -3161,13 +3149,10 @@
                 // restore object for the slow case
                 end->set_obj(lockee);
                 success = false;
               }
             }
-            if (success) {
-              THREAD->dec_held_monitor_count();
-            }
           }
           if (!success) {
             InterpreterRuntime::monitorexit(end);
           }
 
@@ -3240,13 +3225,10 @@
                   if (!suppress_error) illegal_state_oop = Handle(THREAD, THREAD->pending_exception());
                   THREAD->clear_pending_exception();
                 }
               }
             }
-            if (dec_monitor_count) {
-              THREAD->dec_held_monitor_count();
-            }
           }
         }
       }
     }
     // Clear the do_not_unlock flag now.
diff a/src/hotspot/share/interpreter/zero/zeroInterpreter.hpp b/src/hotspot/share/interpreter/zero/zeroInterpreter.hpp
--- a/src/hotspot/share/interpreter/zero/zeroInterpreter.hpp
+++ b/src/hotspot/share/interpreter/zero/zeroInterpreter.hpp
@@ -69,10 +69,11 @@
                          TRAPS);
 
   static address throw_NullPointerException_entry() { return nullptr; }
   static address throw_ArithmeticException_entry()  { return nullptr; }
   static address throw_StackOverflowError_entry()   { return nullptr; }
+  static address cont_resume_interpreter_adapter()  { return nullptr; }
 
 # include "zeroInterpreter_zero.hpp"
 };
 
 #endif // ZERO
diff a/src/hotspot/share/jfr/metadata/metadata.xml b/src/hotspot/share/jfr/metadata/metadata.xml
--- a/src/hotspot/share/jfr/metadata/metadata.xml
+++ b/src/hotspot/share/jfr/metadata/metadata.xml
@@ -154,10 +154,15 @@
 
   <Event name="ContinuationThawSlow" experimental="true" category="Java Virtual Machine, Runtime" label="Continuation Thaw Slow" thread="true" stackTrace="false" startTime="false">
     <Field type="ulong" name="id" label="Continuation ID" />
   </Event>
 
+  <Event name="VirtualThreadPinned" category="Java Virtual Machine, Runtime" label="Virtual Thread Pinned" thread="true" stackTrace="true" startTime="false">
+    <Field type="string" name="pinnedReason" label="Pinned Reason" />
+    <Field type="Thread" name="carrierThread" label="Carrier Thread" />
+  </Event>
+
   <Event name="ReservedStackActivation" category="Java Virtual Machine, Runtime" label="Reserved Stack Activation"
     description="Activation of Reserved Stack Area caused by stack overflow with ReservedStackAccess annotated method in call stack" thread="true" stackTrace="true"
     startTime="false">
     <Field type="Method" name="method" label="Java Method" />
   </Event>
diff a/src/hotspot/share/jvmci/vmStructs_jvmci.cpp b/src/hotspot/share/jvmci/vmStructs_jvmci.cpp
--- a/src/hotspot/share/jvmci/vmStructs_jvmci.cpp
+++ b/src/hotspot/share/jvmci/vmStructs_jvmci.cpp
@@ -223,10 +223,11 @@
                                                                                                                                      \
   nonstatic_field(JavaThread,                  _threadObj,                                    OopHandle)                             \
   nonstatic_field(JavaThread,                  _vthread,                                      OopHandle)                             \
   nonstatic_field(JavaThread,                  _scopedValueCache,                             OopHandle)                             \
   nonstatic_field(JavaThread,                  _anchor,                                       JavaFrameAnchor)                       \
+  nonstatic_field(JavaThread,                  _lock_id,                                      int64_t)                               \
   nonstatic_field(JavaThread,                  _vm_result,                                    oop)                                   \
   nonstatic_field(JavaThread,                  _stack_overflow_state._stack_overflow_limit,   address)                               \
   volatile_nonstatic_field(JavaThread,         _exception_oop,                                oop)                                   \
   volatile_nonstatic_field(JavaThread,         _exception_pc,                                 address)                               \
   volatile_nonstatic_field(JavaThread,         _is_method_handle_return,                      int)                                   \
@@ -323,15 +324,16 @@
   nonstatic_field(nmethod,                     _verified_entry_offset,                        u2)                                    \
   nonstatic_field(nmethod,                     _comp_level,                                   CompLevel)                             \
                                                                                                                                      \
   nonstatic_field(ObjArrayKlass,               _element_klass,                                Klass*)                                \
                                                                                                                                      \
-  unchecked_nonstatic_field(ObjectMonitor,     _owner,                                        sizeof(void *)) /* NOTE: no type */    \
+  unchecked_nonstatic_field(ObjectMonitor,     _owner,                                        int64_t)                               \
   volatile_nonstatic_field(ObjectMonitor,      _recursions,                                   intptr_t)                              \
   volatile_nonstatic_field(ObjectMonitor,      _cxq,                                          ObjectWaiter*)                         \
   volatile_nonstatic_field(ObjectMonitor,      _EntryList,                                    ObjectWaiter*)                         \
-  volatile_nonstatic_field(ObjectMonitor,      _succ,                                         JavaThread*)                           \
+  volatile_nonstatic_field(ObjectMonitor,      _succ,                                         int64_t)                               \
+  volatile_nonstatic_field(ObjectMonitor,      _stack_locker,                                 BasicLock*)                            \
                                                                                                                                      \
   volatile_nonstatic_field(oopDesc,            _mark,                                         markWord)                              \
   volatile_nonstatic_field(oopDesc,            _metadata._klass,                              Klass*)                                \
                                                                                                                                      \
   static_field(StubRoutines,                _verify_oop_count,                                jint)                                  \
@@ -776,11 +778,13 @@
                                                                           \
   AARCH64_ONLY(declare_constant(NMethodPatchingType::stw_instruction_and_data_patch))  \
   AARCH64_ONLY(declare_constant(NMethodPatchingType::conc_instruction_and_data_patch)) \
   AARCH64_ONLY(declare_constant(NMethodPatchingType::conc_data_patch))                 \
                                                                           \
+  declare_constant(ObjectMonitor::NO_OWNER)                               \
   declare_constant(ObjectMonitor::ANONYMOUS_OWNER)                        \
+  declare_constant(ObjectMonitor::DEFLATER_MARKER)                        \
                                                                           \
   declare_constant(ReceiverTypeData::receiver_type_row_cell_count)        \
   declare_constant(ReceiverTypeData::receiver0_offset)                    \
   declare_constant(ReceiverTypeData::count0_offset)                       \
                                                                           \
@@ -805,10 +809,11 @@
   declare_constant(markWord::age_mask_in_place)                           \
   declare_constant(markWord::hash_mask)                                   \
   declare_constant(markWord::hash_mask_in_place)                          \
                                                                           \
   declare_constant(markWord::unlocked_value)                              \
+  declare_constant(markWord::marked_value)                                \
                                                                           \
   declare_constant(markWord::no_hash_in_place)                            \
   declare_constant(markWord::no_lock_in_place)                            \
 
 // Helper macro to support ZGC pattern where the function itself isn't exported
diff a/src/hotspot/share/oops/instanceKlass.cpp b/src/hotspot/share/oops/instanceKlass.cpp
--- a/src/hotspot/share/oops/instanceKlass.cpp
+++ b/src/hotspot/share/oops/instanceKlass.cpp
@@ -1103,10 +1103,26 @@
         log_debug(class, init)("Thread \"%s\" waiting for initialization of %s by thread \"%s\"",
                                jt->name(), external_name(), init_thread_name());
       }
       wait = true;
       jt->set_class_to_be_initialized(this);
+
+#if INCLUDE_JFR
+      ContinuationEntry* ce = jt->last_continuation();
+      if (ce != nullptr && ce->is_virtual_thread()) {
+        EventVirtualThreadPinned e;
+        if (e.should_commit()) {
+          ResourceMark rm(jt);
+          char reason[256];
+          jio_snprintf(reason, sizeof reason, "Waiting for initialization of klass %s", external_name());
+          e.set_pinnedReason(reason);
+          e.set_carrierThread(JFR_JVM_THREAD_ID(THREAD));
+          e.commit();
+        }
+      }
+ #endif
+
       ol.wait_uninterruptibly(jt);
       jt->set_class_to_be_initialized(nullptr);
     }
 
     // Step 3
diff a/src/hotspot/share/oops/instanceStackChunkKlass.cpp b/src/hotspot/share/oops/instanceStackChunkKlass.cpp
--- a/src/hotspot/share/oops/instanceStackChunkKlass.cpp
+++ b/src/hotspot/share/oops/instanceStackChunkKlass.cpp
@@ -161,10 +161,27 @@
   }
   OopIterateStackChunkFrameClosure frame_closure(closure, mr);
   chunk->iterate_stack(&frame_closure);
 }
 
+template <typename OopT>
+void InstanceStackChunkKlass::oop_oop_iterate_lockstack(stackChunkOop chunk, OopIterateClosure* closure, MemRegion mr) {
+  if (LockingMode != LM_LIGHTWEIGHT) {
+    return;
+  }
+
+  StackChunkOopIterateFilterClosure<OopIterateClosure> cl(closure, mr);
+  if (chunk->has_bitmap()) {
+    chunk->iterate_lockstack<OopT>(&cl);
+  } else {
+    chunk->iterate_lockstack<oop>(&cl);
+  }
+}
+
+template void InstanceStackChunkKlass::oop_oop_iterate_lockstack<oop>(stackChunkOop chunk, OopIterateClosure* closure, MemRegion mr);
+template void InstanceStackChunkKlass::oop_oop_iterate_lockstack<narrowOop>(stackChunkOop chunk, OopIterateClosure* closure, MemRegion mr);
+
 #ifdef ASSERT
 
 class DescribeStackChunkClosure {
   stackChunkOop _chunk;
   FrameValues _values;
@@ -222,11 +239,11 @@
   template <ChunkFrames frame_kind, typename RegisterMapT>
   bool do_frame(const StackChunkFrameStream<frame_kind>& fs, const RegisterMapT* map) {
     frame f = fs.to_frame();
     _st->print_cr("-- frame sp: " PTR_FORMAT " interpreted: %d size: %d argsize: %d",
                   p2i(fs.sp()), fs.is_interpreted(), f.frame_size(),
-                  fs.is_interpreted() ? 0 : f.compiled_frame_stack_argsize());
+                  fs.is_interpreted() || fs.is_stub() ? 0 : f.compiled_frame_stack_argsize());
   #ifdef ASSERT
     f.print_value_on(_st);
   #else
     f.print_on(_st);
   #endif
diff a/src/hotspot/share/oops/instanceStackChunkKlass.hpp b/src/hotspot/share/oops/instanceStackChunkKlass.hpp
--- a/src/hotspot/share/oops/instanceStackChunkKlass.hpp
+++ b/src/hotspot/share/oops/instanceStackChunkKlass.hpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -175,10 +175,13 @@
   inline void oop_oop_iterate_stack_bounded(stackChunkOop chunk, OopClosureType* closure, MemRegion mr);
 
   template <typename T, class OopClosureType>
   inline void oop_oop_iterate_stack_with_bitmap(stackChunkOop chunk, OopClosureType* closure, intptr_t* start, intptr_t* end);
 
+  template <typename OopT>
+  void oop_oop_iterate_lockstack(stackChunkOop chunk, OopIterateClosure* closure, MemRegion mr);
+
   void do_methods(stackChunkOop chunk, OopIterateClosure* cl);
 
   void oop_oop_iterate_stack_slow(stackChunkOop chunk, OopIterateClosure* closure, MemRegion mr);
 };
 
diff a/src/hotspot/share/oops/instanceStackChunkKlass.inline.hpp b/src/hotspot/share/oops/instanceStackChunkKlass.inline.hpp
--- a/src/hotspot/share/oops/instanceStackChunkKlass.inline.hpp
+++ b/src/hotspot/share/oops/instanceStackChunkKlass.inline.hpp
@@ -1,6 +1,6 @@
-/* Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved.
+/* Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -63,18 +63,20 @@
   if (Devirtualizer::do_metadata(closure)) {
     Devirtualizer::do_klass(closure, this);
   }
   oop_oop_iterate_stack<T>(chunk, closure);
   oop_oop_iterate_header<T>(chunk, closure);
+  oop_oop_iterate_lockstack<T>(chunk, closure, chunk->range());
 }
 
 template <typename T, class OopClosureType>
 void InstanceStackChunkKlass::oop_oop_iterate_reverse(oop obj, OopClosureType* closure) {
   assert(!Devirtualizer::do_metadata(closure), "Code to handle metadata is not implemented");
   stackChunkOop chunk = stackChunkOopDesc::cast(obj);
   oop_oop_iterate_stack<T>(chunk, closure);
   oop_oop_iterate_header<T>(chunk, closure);
+  oop_oop_iterate_lockstack<T>(chunk, closure, chunk->range());
 }
 
 template <typename T, class OopClosureType>
 void InstanceStackChunkKlass::oop_oop_iterate_bounded(oop obj, OopClosureType* closure, MemRegion mr) {
   stackChunkOop chunk = stackChunkOopDesc::cast(obj);
@@ -83,10 +85,11 @@
       Devirtualizer::do_klass(closure, this);
     }
   }
   oop_oop_iterate_stack_bounded<T>(chunk, closure, mr);
   oop_oop_iterate_header_bounded<T>(chunk, closure, mr);
+  oop_oop_iterate_lockstack<T>(chunk, closure, mr);
 }
 
 template <typename T, class OopClosureType>
 void InstanceStackChunkKlass::oop_oop_iterate_header(stackChunkOop chunk, OopClosureType* closure) {
   T* parent_addr = chunk->field_addr<T>(jdk_internal_vm_StackChunk::parent_offset());
diff a/src/hotspot/share/oops/method.cpp b/src/hotspot/share/oops/method.cpp
--- a/src/hotspot/share/oops/method.cpp
+++ b/src/hotspot/share/oops/method.cpp
@@ -865,10 +865,14 @@
 
 bool Method::needs_clinit_barrier() const {
   return is_static() && !method_holder()->is_initialized();
 }
 
+bool Method::is_object_wait0() const {
+  return name() == vmSymbols::wait_name();
+}
+
 objArrayHandle Method::resolved_checked_exceptions_impl(Method* method, TRAPS) {
   int length = method->checked_exceptions_length();
   if (length == 0) {  // common case
     return objArrayHandle(THREAD, Universe::the_empty_class_array());
   } else {
diff a/src/hotspot/share/oops/method.hpp b/src/hotspot/share/oops/method.hpp
--- a/src/hotspot/share/oops/method.hpp
+++ b/src/hotspot/share/oops/method.hpp
@@ -584,10 +584,13 @@
   bool is_static_initializer() const;
 
   // returns true if the method name is <init>
   bool is_object_initializer() const;
 
+  // returns true if the method name is wait0
+  bool is_object_wait0() const;
+
   // compiled code support
   // NOTE: code() is inherently racy as deopt can be clearing code
   // simultaneously. Use with caution.
   bool has_compiled_code() const;
 
diff a/src/hotspot/share/oops/oopHandle.hpp b/src/hotspot/share/oops/oopHandle.hpp
--- a/src/hotspot/share/oops/oopHandle.hpp
+++ b/src/hotspot/share/oops/oopHandle.hpp
@@ -69,10 +69,11 @@
   inline void release(OopStorage* storage);
 
   inline void replace(oop obj);
 
   inline oop xchg(oop new_value);
+  inline oop cmpxchg(oop old_value, oop new_value);
 
   oop* ptr_raw() const { return _obj; }
 };
 
 #endif // SHARE_OOPS_OOPHANDLE_HPP
diff a/src/hotspot/share/oops/oopHandle.inline.hpp b/src/hotspot/share/oops/oopHandle.inline.hpp
--- a/src/hotspot/share/oops/oopHandle.inline.hpp
+++ b/src/hotspot/share/oops/oopHandle.inline.hpp
@@ -63,6 +63,10 @@
 
 inline oop OopHandle::xchg(oop new_value) {
   return NativeAccess<MO_SEQ_CST>::oop_atomic_xchg(_obj, new_value);
 }
 
+inline oop OopHandle::cmpxchg(oop old_value, oop new_value) {
+  return NativeAccess<MO_SEQ_CST>::oop_atomic_cmpxchg(_obj, old_value, new_value);
+}
+
 #endif // SHARE_OOPS_OOPHANDLE_INLINE_HPP
diff a/src/hotspot/share/oops/stackChunkOop.cpp b/src/hotspot/share/oops/stackChunkOop.cpp
--- a/src/hotspot/share/oops/stackChunkOop.cpp
+++ b/src/hotspot/share/oops/stackChunkOop.cpp
@@ -63,10 +63,25 @@
       visitor.oops_do(&_f, _map, _f.oop_map());
     }
   }
 };
 
+class LockStackOopIterator : public OopIterator {
+private:
+  const stackChunkOop _chunk;
+public:
+  LockStackOopIterator(const stackChunkOop chunk) : _chunk(chunk) {}
+
+  virtual void oops_do(OopClosure* cl) override {
+    int cnt = _chunk->lockstack_size();
+    oop* lockstack_start = (oop*)_chunk->start_address();
+    for (int i = 0; i < cnt; i++) {
+      cl->do_oop(&lockstack_start[i]);
+    }
+  }
+};
+
 frame stackChunkOopDesc::top_frame(RegisterMap* map) {
   assert(!is_empty(), "");
   StackChunkFrameStream<ChunkFrames::Mixed> fs(this);
 
   map->set_stack_chunk(this);
@@ -222,10 +237,18 @@
     FrameOopIterator<RegisterMapT> iterator(fr, map);
     bs_chunk->encode_gc_mode(_chunk, &iterator);
 
     return true;
   }
+
+  bool do_lockstack() {
+    BarrierSetStackChunk* bs_chunk = BarrierSet::barrier_set()->barrier_set_stack_chunk();
+    LockStackOopIterator iterator(_chunk);
+    bs_chunk->encode_gc_mode(_chunk, &iterator);
+
+    return true;
+  }
 };
 
 bool stackChunkOopDesc::try_acquire_relativization() {
   for (;;) {
     // We use an acquiring load when reading the flags to ensure that if we leave this
@@ -296,10 +319,11 @@
   }
 
   DerivedPointersSupport::RelativizeClosure derived_cl;
   EncodeGCModeConcurrentFrameClosure<decltype(derived_cl)> frame_cl(this, &derived_cl);
   iterate_stack(&frame_cl);
+  frame_cl.do_lockstack();
 
   release_relativization();
 }
 
 class TransformStackChunkClosure {
@@ -318,10 +342,18 @@
     FrameOopIterator<RegisterMapT> iterator(fr, map);
     bs_chunk->encode_gc_mode(_chunk, &iterator);
 
     return true;
   }
+
+  bool do_lockstack() {
+    BarrierSetStackChunk* bs_chunk = BarrierSet::barrier_set()->barrier_set_stack_chunk();
+    LockStackOopIterator iterator(_chunk);
+    bs_chunk->encode_gc_mode(_chunk, &iterator);
+
+    return true;
+  }
 };
 
 void stackChunkOopDesc::transform() {
   assert(!is_gc_mode(), "Should only be called once per chunk");
   set_gc_mode(true);
@@ -330,10 +362,11 @@
   set_has_bitmap(true);
   bitmap().clear();
 
   TransformStackChunkClosure closure(this);
   iterate_stack(&closure);
+  closure.do_lockstack();
 }
 
 template <stackChunkOopDesc::BarrierType barrier, bool compressedOopsWithBitmap>
 class BarrierClosure: public OopClosure {
   NOT_PRODUCT(intptr_t* _sp;)
@@ -406,10 +439,39 @@
 }
 
 template void stackChunkOopDesc::fix_thawed_frame(const frame& f, const RegisterMap* map);
 template void stackChunkOopDesc::fix_thawed_frame(const frame& f, const SmallRegisterMap* map);
 
+void stackChunkOopDesc::copy_lockstack(oop* dst) {
+  int cnt = lockstack_size();
+
+  if (!(is_gc_mode() || requires_barriers())) {
+    oop* lockstack_start = (oop*)start_address();
+    for (int i = 0; i < cnt; i++) {
+      dst[i] = lockstack_start[i];
+      assert(oopDesc::is_oop(dst[i]), "not an oop");
+    }
+    return;
+  }
+
+  if (has_bitmap() && UseCompressedOops) {
+    intptr_t* lockstack_start = start_address();
+    for (int i = 0; i < cnt; i++) {
+      oop mon_owner = HeapAccess<>::oop_load((narrowOop*)&lockstack_start[i]);
+      assert(oopDesc::is_oop(mon_owner), "not an oop");
+      dst[i] = mon_owner;
+    }
+  } else {
+    intptr_t* lockstack_start = start_address();
+    for (int i = 0; i < cnt; i++) {
+      oop mon_owner = HeapAccess<>::oop_load((oop*)&lockstack_start[i]);
+      assert(oopDesc::is_oop(mon_owner), "not an oop");
+      dst[i] = mon_owner;
+    }
+  }
+}
+
 void stackChunkOopDesc::print_on(bool verbose, outputStream* st) const {
   if (*((juint*)this) == badHeapWordVal) {
     st->print_cr("BAD WORD");
   } else {
     InstanceStackChunkKlass::print_chunk(const_cast<stackChunkOopDesc*>(this), verbose, st);
@@ -454,13 +516,13 @@
   int _num_oops;
   int _num_frames;
   int _num_interpreted_frames;
   int _num_i2c;
 
-  VerifyStackChunkFrameClosure(stackChunkOop chunk, int num_frames, int size)
+  VerifyStackChunkFrameClosure(stackChunkOop chunk)
     : _chunk(chunk), _sp(nullptr), _cb(nullptr), _callee_interpreted(false),
-      _size(size), _argsize(0), _num_oops(0), _num_frames(num_frames), _num_interpreted_frames(0), _num_i2c(0) {}
+      _size(0), _argsize(0), _num_oops(0), _num_frames(0), _num_interpreted_frames(0), _num_i2c(0) {}
 
   template <ChunkFrames frame_kind, typename RegisterMapT>
   bool do_frame(const StackChunkFrameStream<frame_kind>& f, const RegisterMapT* map) {
     _sp = f.sp();
     _cb = f.cb();
@@ -550,15 +612,12 @@
   int size = bottom() - sp();
   assert(size >= 0, "");
   assert((size == 0) == is_empty(), "");
 
   const StackChunkFrameStream<ChunkFrames::Mixed> first(this);
-  const bool has_safepoint_stub_frame = first.is_stub();
 
-  VerifyStackChunkFrameClosure closure(this,
-                                       has_safepoint_stub_frame ? 1 : 0, // Iterate_stack skips the safepoint stub
-                                       has_safepoint_stub_frame ? first.frame_size() : 0);
+  VerifyStackChunkFrameClosure closure(this);
   iterate_stack(&closure);
 
   assert(!is_empty() || closure._cb == nullptr, "");
   if (closure._cb != nullptr && closure._cb->is_nmethod()) {
     assert(argsize() ==
diff a/src/hotspot/share/oops/stackChunkOop.hpp b/src/hotspot/share/oops/stackChunkOop.hpp
--- a/src/hotspot/share/oops/stackChunkOop.hpp
+++ b/src/hotspot/share/oops/stackChunkOop.hpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -34,10 +34,12 @@
 
 class frame;
 class MemRegion;
 class RegisterMap;
 class VMRegImpl;
+class ObjectMonitor;
+class ObjectWaiter;
 typedef VMRegImpl* VMReg;
 
 // A continuation stack-chunk oop.
 // See InstanceStackChunkKlass for a description of continuation stack-chunks.
 //
@@ -56,10 +58,12 @@
   static const uint8_t FLAG_HAS_INTERPRETED_FRAMES = 1 << 0;
   static const uint8_t FLAG_CLAIM_RELATIVIZE = 1 << 1; // Only one thread claims relativization of derived pointers
   static const uint8_t FLAG_NOTIFY_RELATIVIZE = 1 << 2; // Someone is waiting for relativization to complete
   static const uint8_t FLAG_GC_MODE = 1 << 3; // Once true it and FLAG_HAS_INTERPRETED_FRAMES can't change
   static const uint8_t FLAG_HAS_BITMAP = 1 << 4; // Can only be true if FLAG_GC_MODE is true
+  static const uint8_t FLAG_HAS_LOCKSTACK = 1 << 5; // LockStack was copied into stackChunk
+  static const uint8_t FLAG_PREEMPTED = 1 << 6; // Continuation was unmounted from inside VM
 
   bool try_acquire_relativization();
   void release_relativization();
 
 public:
@@ -89,10 +93,19 @@
   inline void release_set_flags(uint8_t value);
 
   inline int max_thawing_size() const;
   inline void set_max_thawing_size(int value);
 
+  inline uint8_t lockstack_size() const;
+  inline void set_lockstack_size(uint8_t value);
+
+  inline ObjectWaiter* object_waiter() const;
+  inline void set_object_waiter(ObjectWaiter* obj_waiter);
+
+  inline ObjectMonitor* current_pending_monitor() const;
+  inline ObjectMonitor* current_waiting_monitor() const;
+
   inline oop cont() const;
   template<typename P>
   inline oop cont() const;
   inline void set_cont(oop value);
   template<typename P>
@@ -125,10 +138,16 @@
   inline void clear_flags();
 
   inline bool has_mixed_frames() const;
   inline void set_has_mixed_frames(bool value);
 
+  inline bool preempted() const;
+  inline void set_preempted(bool value);
+
+  inline bool has_lockstack() const;
+  inline void set_has_lockstack(bool value);
+
   inline bool is_gc_mode() const;
   inline bool is_gc_mode_acquire() const;
   inline void set_gc_mode(bool value);
 
   inline bool has_bitmap() const;
@@ -145,10 +164,15 @@
   inline void do_barriers(const StackChunkFrameStream<frames>& f, const RegisterMapT* map);
 
   template <typename RegisterMapT>
   void fix_thawed_frame(const frame& f, const RegisterMapT* map);
 
+  void copy_lockstack(oop* start);
+
+  template <typename OopT, class StackChunkLockStackClosureType>
+  inline void iterate_lockstack(StackChunkLockStackClosureType* closure);
+
   template <class StackChunkFrameClosureType>
   inline void iterate_stack(StackChunkFrameClosureType* closure);
 
   // Derived pointers are relativized, with respect to their oop base.
   void relativize_derived_pointers_concurrently();
diff a/src/hotspot/share/oops/stackChunkOop.inline.hpp b/src/hotspot/share/oops/stackChunkOop.inline.hpp
--- a/src/hotspot/share/oops/stackChunkOop.inline.hpp
+++ b/src/hotspot/share/oops/stackChunkOop.inline.hpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -34,13 +34,14 @@
 #include "memory/memRegion.hpp"
 #include "memory/universe.hpp"
 #include "oops/access.inline.hpp"
 #include "oops/instanceStackChunkKlass.inline.hpp"
 #include "runtime/continuationJavaClasses.inline.hpp"
-#include "runtime/frame.inline.hpp"
+#include "runtime/frame.hpp"
 #include "runtime/globals.hpp"
 #include "runtime/handles.inline.hpp"
+#include "runtime/objectMonitor.hpp"
 #include "runtime/registerMap.hpp"
 #include "runtime/smallRegisterMap.inline.hpp"
 #include "utilities/macros.hpp"
 #include CPU_HEADER_INLINE(stackChunkOop)
 
@@ -86,10 +87,16 @@
 inline void stackChunkOopDesc::set_max_thawing_size(int value)  {
   assert(value >= 0, "size must be >= 0");
   jdk_internal_vm_StackChunk::set_maxThawingSize(this, (jint)value);
 }
 
+inline uint8_t stackChunkOopDesc::lockstack_size() const         { return jdk_internal_vm_StackChunk::lockStackSize(as_oop()); }
+inline void stackChunkOopDesc::set_lockstack_size(uint8_t value) { jdk_internal_vm_StackChunk::set_lockStackSize(this, value); }
+
+inline ObjectWaiter* stackChunkOopDesc::object_waiter() const       { return (ObjectWaiter*)jdk_internal_vm_StackChunk::objectWaiter(as_oop()); }
+inline void stackChunkOopDesc::set_object_waiter(ObjectWaiter* obj) { jdk_internal_vm_StackChunk::set_objectWaiter(this, (address)obj); }
+
 inline oop stackChunkOopDesc::cont() const                {
   if (UseZGC && !ZGenerational) {
     assert(!UseCompressedOops, "Non-generational ZGC does not support compressed oops");
     // The state of the cont oop is used by XCollectedHeap::requires_barriers,
     // to determine the age of the stackChunkOopDesc. For that to work, it is
@@ -165,14 +172,36 @@
   set_flags(0);
 }
 
 inline bool stackChunkOopDesc::has_mixed_frames() const { return is_flag(FLAG_HAS_INTERPRETED_FRAMES); }
 inline void stackChunkOopDesc::set_has_mixed_frames(bool value) {
-  assert((flags() & ~FLAG_HAS_INTERPRETED_FRAMES) == 0, "other flags should not be set");
+  assert((flags() & ~(FLAG_HAS_INTERPRETED_FRAMES | FLAG_PREEMPTED)) == 0, "other flags should not be set");
   set_flag(FLAG_HAS_INTERPRETED_FRAMES, value);
 }
 
+inline bool stackChunkOopDesc::preempted() const { return is_flag(FLAG_PREEMPTED); }
+inline void stackChunkOopDesc::set_preempted(bool value) {
+  assert(preempted() != value, "");
+  set_flag(FLAG_PREEMPTED, value);
+}
+
+inline ObjectMonitor* stackChunkOopDesc::current_pending_monitor() const {
+  ObjectWaiter* waiter = object_waiter();
+  if (waiter != nullptr && (waiter->is_monitorenter() || (waiter->is_wait() && (waiter->at_reenter() || waiter->notified())))) {
+    return waiter->monitor();
+  }
+  return nullptr;
+}
+
+inline ObjectMonitor* stackChunkOopDesc::current_waiting_monitor() const {
+  ObjectWaiter* waiter = object_waiter();
+  return waiter != nullptr && waiter->is_wait() ? waiter->monitor() : nullptr;
+}
+
+inline bool stackChunkOopDesc::has_lockstack() const         { return is_flag(FLAG_HAS_LOCKSTACK); }
+inline void stackChunkOopDesc::set_has_lockstack(bool value) { set_flag(FLAG_HAS_LOCKSTACK, value); }
+
 inline bool stackChunkOopDesc::is_gc_mode() const                  { return is_flag(FLAG_GC_MODE); }
 inline bool stackChunkOopDesc::is_gc_mode_acquire() const          { return is_flag_acquire(FLAG_GC_MODE); }
 inline void stackChunkOopDesc::set_gc_mode(bool value)             { set_flag(FLAG_GC_MODE, value); }
 
 inline bool stackChunkOopDesc::has_bitmap() const                  { return is_flag(FLAG_HAS_BITMAP); }
@@ -191,10 +220,20 @@
     f.handle_deopted();
   }
   do_barriers0<barrier>(f, map);
 }
 
+template <typename OopT, class StackChunkLockStackClosureType>
+inline void stackChunkOopDesc::iterate_lockstack(StackChunkLockStackClosureType* closure) {
+  assert(LockingMode == LM_LIGHTWEIGHT, "");
+  int cnt = lockstack_size();
+  intptr_t* lockstart_addr = start_address();
+  for (int i = 0; i < cnt; i++) {
+    closure->do_oop((OopT*)&lockstart_addr[i]);
+  }
+}
+
 template <class StackChunkFrameClosureType>
 inline void stackChunkOopDesc::iterate_stack(StackChunkFrameClosureType* closure) {
   has_mixed_frames() ? iterate_stack<ChunkFrames::Mixed>(closure)
                      : iterate_stack<ChunkFrames::CompiledOnly>(closure);
 }
@@ -211,19 +250,18 @@
     RegisterMap full_map(nullptr,
                          RegisterMap::UpdateMap::include,
                          RegisterMap::ProcessFrames::skip,
                          RegisterMap::WalkContinuation::include);
     full_map.set_include_argument_oops(false);
+    closure->do_frame(f, map);
 
     f.next(&full_map);
-
     assert(!f.is_done(), "");
     assert(f.is_compiled(), "");
 
     should_continue = closure->do_frame(f, &full_map);
     f.next(map);
-    f.handle_deopted(); // the stub caller might be deoptimized (as it's not at a call)
   }
   assert(!f.is_stub(), "");
 
   for(; should_continue && !f.is_done(); f.next(map)) {
     if (frame_kind == ChunkFrames::Mixed) {
@@ -280,11 +318,11 @@
 inline MemRegion stackChunkOopDesc::range() {
   return MemRegion((HeapWord*)this, size());
 }
 
 inline int stackChunkOopDesc::relativize_usp_offset(const frame& fr, const int usp_offset_in_bytes) const {
-  assert(fr.is_compiled_frame() || fr.cb()->is_safepoint_stub(), "");
+  assert(fr.is_compiled_frame() || fr.cb()->is_runtime_stub(), "");
   assert(is_in_chunk(fr.unextended_sp()), "");
 
   intptr_t* base = fr.real_fp(); // equal to the caller's sp
   intptr_t* loc = (intptr_t*)((address)fr.unextended_sp() + usp_offset_in_bytes);
   assert(base > loc, "");
diff a/src/hotspot/share/opto/c2compiler.cpp b/src/hotspot/share/opto/c2compiler.cpp
--- a/src/hotspot/share/opto/c2compiler.cpp
+++ b/src/hotspot/share/opto/c2compiler.cpp
@@ -728,10 +728,11 @@
   case vmIntrinsics::_storeStoreFence:
   case vmIntrinsics::_fullFence:
   case vmIntrinsics::_currentCarrierThread:
   case vmIntrinsics::_currentThread:
   case vmIntrinsics::_setCurrentThread:
+  case vmIntrinsics::_setCurrentLockId:
   case vmIntrinsics::_scopedValueCache:
   case vmIntrinsics::_setScopedValueCache:
   case vmIntrinsics::_Continuation_pin:
   case vmIntrinsics::_Continuation_unpin:
 #ifdef JFR_HAVE_INTRINSICS
diff a/src/hotspot/share/opto/library_call.cpp b/src/hotspot/share/opto/library_call.cpp
--- a/src/hotspot/share/opto/library_call.cpp
+++ b/src/hotspot/share/opto/library_call.cpp
@@ -477,10 +477,11 @@
   case vmIntrinsics::_onSpinWait:               return inline_onspinwait();
 
   case vmIntrinsics::_currentCarrierThread:     return inline_native_currentCarrierThread();
   case vmIntrinsics::_currentThread:            return inline_native_currentThread();
   case vmIntrinsics::_setCurrentThread:         return inline_native_setCurrentThread();
+  case vmIntrinsics::_setCurrentLockId:         return inline_native_setCurrentLockId();
 
   case vmIntrinsics::_scopedValueCache:          return inline_native_scopedValueCache();
   case vmIntrinsics::_setScopedValueCache:       return inline_native_setScopedValueCache();
 
   case vmIntrinsics::_Continuation_pin:          return inline_native_Continuation_pinning(false);
@@ -3701,14 +3702,27 @@
   Node* thread_obj_handle
     = make_load(nullptr, p, p->bottom_type()->is_ptr(), T_OBJECT, MemNode::unordered);
   thread_obj_handle = _gvn.transform(thread_obj_handle);
   const TypePtr *adr_type = _gvn.type(thread_obj_handle)->isa_ptr();
   access_store_at(nullptr, thread_obj_handle, adr_type, arr, _gvn.type(arr), T_OBJECT, IN_NATIVE | MO_UNORDERED);
+
+  // Change the lock_id of the JavaThread
+  Node* tid = load_field_from_object(arr, "tid", "J");
+  Node* thread_id_offset = basic_plus_adr(thread, in_bytes(JavaThread::lock_id_offset()));
+  Node* tid_memory = store_to_memory(control(), thread_id_offset, tid, T_LONG, Compile::AliasIdxRaw, MemNode::unordered, true);
+
   JFR_ONLY(extend_setCurrentThread(thread, arr);)
   return true;
 }
 
+bool LibraryCallKit::inline_native_setCurrentLockId() {
+  Node* thread = _gvn.transform(new ThreadLocalNode());
+  Node* thread_id_offset = basic_plus_adr(thread, in_bytes(JavaThread::lock_id_offset()));
+  Node* tid_memory = store_to_memory(control(), thread_id_offset, ConvL2X(argument(0)), T_LONG, Compile::AliasIdxRaw, MemNode::unordered, true);
+  return true;
+}
+
 const Type* LibraryCallKit::scopedValueCache_type() {
   ciKlass* objects_klass = ciObjArrayKlass::make(env()->Object_klass());
   const TypeOopPtr* etype = TypeOopPtr::make_from_klass(env()->Object_klass());
   const TypeAry* arr0 = TypeAry::make(etype, TypeInt::POS);
 
diff a/src/hotspot/share/opto/library_call.hpp b/src/hotspot/share/opto/library_call.hpp
--- a/src/hotspot/share/opto/library_call.hpp
+++ b/src/hotspot/share/opto/library_call.hpp
@@ -236,10 +236,11 @@
   bool inline_unsafe_setMemory();
 
   bool inline_native_currentCarrierThread();
   bool inline_native_currentThread();
   bool inline_native_setCurrentThread();
+  bool inline_native_setCurrentLockId();
 
   bool inline_native_scopedValueCache();
   const Type* scopedValueCache_type();
   Node* scopedValueCache_helper();
   bool inline_native_setScopedValueCache();
diff a/src/hotspot/share/prims/jvm.cpp b/src/hotspot/share/prims/jvm.cpp
--- a/src/hotspot/share/prims/jvm.cpp
+++ b/src/hotspot/share/prims/jvm.cpp
@@ -3087,13 +3087,21 @@
 
 JVM_ENTRY(void, JVM_SetCurrentThread(JNIEnv* env, jobject thisThread,
                                      jobject theThread))
   oop threadObj = JNIHandles::resolve(theThread);
   thread->set_vthread(threadObj);
+
+  // Set lock id of new current Thread
+  thread->set_lock_id(java_lang_Thread::thread_id(threadObj));
+
   JFR_ONLY(Jfr::on_set_current_thread(thread, threadObj);)
 JVM_END
 
+JVM_ENTRY_NO_ENV(void, JVM_SetCurrentLockId(JNIEnv* env, jclass threadClass, jlong tid))
+  thread->set_lock_id(tid);
+JVM_END
+
 JVM_ENTRY(jlong, JVM_GetNextThreadIdOffset(JNIEnv* env, jclass threadClass))
   return ThreadIdentifier::unsafe_offset();
 JVM_END
 
 JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
@@ -3969,10 +3977,43 @@
          "nested or unbalanced monitor enter/exit is not allowed");
   thread->toggle_is_disable_suspend();
 #endif
 JVM_END
 
+JVM_ENTRY_NO_ENV(void, JVM_VirtualThreadPinnedEvent(jint reasonCode, jstring reasonString))
+#if INCLUDE_JFR
+  EventVirtualThreadPinned e;
+  if (e.should_commit()) {
+    ResourceMark rm(THREAD);
+    // ignore reason code for now
+    const char *reason = java_lang_String::as_utf8_string(JNIHandles::resolve_non_null(reasonString));
+    e.set_pinnedReason(reason);
+    e.set_carrierThread(JFR_JVM_THREAD_ID(THREAD));
+    e.commit();
+  }
+#endif
+JVM_END
+
+JVM_ENTRY(jobject, JVM_TakeVirtualThreadListToUnblock(JNIEnv* env, jclass ignored))
+  ParkEvent* parkEvent = ObjectMonitor::vthread_unparker_ParkEvent();
+  assert(parkEvent != nullptr, "not initialized");
+
+  OopHandle& list_head = ObjectMonitor::vthread_cxq_head();
+  oop vthread_head = nullptr;
+  while (true) {
+    if (list_head.peek() != nullptr) {
+      for (;;) {
+        oop head = list_head.resolve();
+        if (list_head.cmpxchg(head, nullptr) == head) {
+          return JNIHandles::make_local(THREAD, head);
+        }
+      }
+    }
+    ThreadBlockInVM tbivm(THREAD);
+    parkEvent->park();
+  }
+JVM_END
 /*
  * Return the current class's class file version.  The low order 16 bits of the
  * returned jint contain the class's major version.  The high order 16 bits
  * contain the class's minor version.
  */
diff a/src/hotspot/share/prims/jvmtiEnv.cpp b/src/hotspot/share/prims/jvmtiEnv.cpp
--- a/src/hotspot/share/prims/jvmtiEnv.cpp
+++ b/src/hotspot/share/prims/jvmtiEnv.cpp
@@ -1348,37 +1348,39 @@
 jvmtiError
 JvmtiEnv::GetOwnedMonitorInfo(jthread thread, jint* owned_monitor_count_ptr, jobject** owned_monitors_ptr) {
   JavaThread* calling_thread = JavaThread::current();
   HandleMark hm(calling_thread);
 
-  // growable array of jvmti monitors info on the C-heap
-  GrowableArray<jvmtiMonitorStackDepthInfo*> *owned_monitors_list =
-      new (mtServiceability) GrowableArray<jvmtiMonitorStackDepthInfo*>(1, mtServiceability);
-
   JvmtiVTMSTransitionDisabler disabler(thread);
   ThreadsListHandle tlh(calling_thread);
 
   JavaThread* java_thread = nullptr;
   oop thread_oop = nullptr;
   jvmtiError err = get_threadOop_and_JavaThread(tlh.list(), thread, calling_thread, &java_thread, &thread_oop);
   if (err != JVMTI_ERROR_NONE) {
-    delete owned_monitors_list;
     return err;
   }
 
-  if (java_thread != nullptr) {
-    Handle thread_handle(calling_thread, thread_oop);
-    EscapeBarrier eb(true, calling_thread, java_thread);
-    if (!eb.deoptimize_objects(MaxJavaStackTraceDepth)) {
-      delete owned_monitors_list;
-      return JVMTI_ERROR_OUT_OF_MEMORY;
-    }
-    // get owned monitors info with handshake
-    GetOwnedMonitorInfoClosure op(this, calling_thread, owned_monitors_list);
-    JvmtiHandshake::execute(&op, &tlh, java_thread, thread_handle);
-    err = op.result();
+  if (LockingMode == LM_LEGACY && java_thread == nullptr) {
+    *owned_monitor_count_ptr = 0;
+    return JVMTI_ERROR_NONE;
+  }
+
+  // growable array of jvmti monitors info on the C-heap
+  GrowableArray<jvmtiMonitorStackDepthInfo*> *owned_monitors_list =
+      new (mtServiceability) GrowableArray<jvmtiMonitorStackDepthInfo*>(1, mtServiceability);
+
+  Handle thread_handle(calling_thread, thread_oop);
+  EscapeBarrier eb(java_thread != nullptr, calling_thread, java_thread);
+  if (!eb.deoptimize_objects(MaxJavaStackTraceDepth)) {
+    delete owned_monitors_list;
+    return JVMTI_ERROR_OUT_OF_MEMORY;
   }
+  // get owned monitors info with handshake
+  GetOwnedMonitorInfoClosure op(this, calling_thread, owned_monitors_list);
+  JvmtiHandshake::execute(&op, &tlh, java_thread, thread_handle);
+  err = op.result();
 
   jint owned_monitor_count = owned_monitors_list->length();
   if (err == JVMTI_ERROR_NONE) {
     if ((err = allocate(owned_monitor_count * sizeof(jobject *),
                       (unsigned char**)owned_monitors_ptr)) == JVMTI_ERROR_NONE) {
@@ -1406,37 +1408,39 @@
 jvmtiError
 JvmtiEnv::GetOwnedMonitorStackDepthInfo(jthread thread, jint* monitor_info_count_ptr, jvmtiMonitorStackDepthInfo** monitor_info_ptr) {
   JavaThread* calling_thread = JavaThread::current();
   HandleMark hm(calling_thread);
 
-  // growable array of jvmti monitors info on the C-heap
-  GrowableArray<jvmtiMonitorStackDepthInfo*> *owned_monitors_list =
-         new (mtServiceability) GrowableArray<jvmtiMonitorStackDepthInfo*>(1, mtServiceability);
-
   JvmtiVTMSTransitionDisabler disabler(thread);
   ThreadsListHandle tlh(calling_thread);
 
   JavaThread* java_thread = nullptr;
   oop thread_oop = nullptr;
   jvmtiError err = get_threadOop_and_JavaThread(tlh.list(), thread, calling_thread, &java_thread, &thread_oop);
   if (err != JVMTI_ERROR_NONE) {
-    delete owned_monitors_list;
     return err;
   }
 
-  if (java_thread != nullptr) {
-    Handle thread_handle(calling_thread, thread_oop);
-    EscapeBarrier eb(true, calling_thread, java_thread);
-    if (!eb.deoptimize_objects(MaxJavaStackTraceDepth)) {
-      delete owned_monitors_list;
-      return JVMTI_ERROR_OUT_OF_MEMORY;
-    }
-    // get owned monitors info with handshake
-    GetOwnedMonitorInfoClosure op(this, calling_thread, owned_monitors_list);
-    JvmtiHandshake::execute(&op, &tlh, java_thread, thread_handle);
-    err = op.result();
+  if (LockingMode == LM_LEGACY && java_thread == nullptr) {
+    *monitor_info_count_ptr = 0;
+    return JVMTI_ERROR_NONE;
+  }
+
+  // growable array of jvmti monitors info on the C-heap
+  GrowableArray<jvmtiMonitorStackDepthInfo*> *owned_monitors_list =
+      new (mtServiceability) GrowableArray<jvmtiMonitorStackDepthInfo*>(1, mtServiceability);
+
+  Handle thread_handle(calling_thread, thread_oop);
+  EscapeBarrier eb(java_thread != nullptr, calling_thread, java_thread);
+  if (!eb.deoptimize_objects(MaxJavaStackTraceDepth)) {
+    delete owned_monitors_list;
+    return JVMTI_ERROR_OUT_OF_MEMORY;
   }
+  // get owned monitors info with handshake
+  GetOwnedMonitorInfoClosure op(this, calling_thread, owned_monitors_list);
+  JvmtiHandshake::execute(&op, &tlh, java_thread, thread_handle);
+  err = op.result();
 
   jint owned_monitor_count = owned_monitors_list->length();
   if (err == JVMTI_ERROR_NONE) {
     if ((err = allocate(owned_monitor_count * sizeof(jvmtiMonitorStackDepthInfo),
                         (unsigned char**)monitor_info_ptr)) == JVMTI_ERROR_NONE) {
diff a/src/hotspot/share/prims/jvmtiEnvBase.cpp b/src/hotspot/share/prims/jvmtiEnvBase.cpp
--- a/src/hotspot/share/prims/jvmtiEnvBase.cpp
+++ b/src/hotspot/share/prims/jvmtiEnvBase.cpp
@@ -1014,40 +1014,41 @@
 
   return err;
 }
 
 jvmtiError
-JvmtiEnvBase::get_owned_monitors(JavaThread* calling_thread, JavaThread* java_thread, javaVFrame* jvf,
-                                 GrowableArray<jvmtiMonitorStackDepthInfo*> *owned_monitors_list) {
+JvmtiEnvBase::get_owned_monitors(JavaThread* calling_thread, JavaThread* carrier, javaVFrame* jvf,
+                                 GrowableArray<jvmtiMonitorStackDepthInfo*> *owned_monitors_list, oop vthread) {
   jvmtiError err = JVMTI_ERROR_NONE;
   Thread *current_thread = Thread::current();
-  assert(java_thread->is_handshake_safe_for(current_thread),
+  assert(carrier == nullptr || carrier->is_handshake_safe_for(current_thread),
          "call by myself or at handshake");
 
   int depth = 0;
   for ( ; jvf != nullptr; jvf = jvf->java_sender()) {
     if (MaxJavaStackTraceDepth == 0 || depth++ < MaxJavaStackTraceDepth) {  // check for stack too deep
       // Add locked objects for this frame into list.
-      err = get_locked_objects_in_frame(calling_thread, java_thread, jvf, owned_monitors_list, depth - 1);
+      err = get_locked_objects_in_frame(calling_thread, carrier, jvf, owned_monitors_list, depth - 1, vthread);
       if (err != JVMTI_ERROR_NONE) {
         return err;
       }
     }
   }
 
   // Get off stack monitors. (e.g. acquired via jni MonitorEnter).
   JvmtiMonitorClosure jmc(calling_thread, owned_monitors_list, this);
-  ObjectSynchronizer::owned_monitors_iterate(&jmc, java_thread);
+  ObjectSynchronizer::owned_monitors_iterate(&jmc, carrier != nullptr ? carrier->threadObj() : vthread);
   err = jmc.error();
 
   return err;
 }
 
 // Save JNI local handles for any objects that this frame owns.
 jvmtiError
-JvmtiEnvBase::get_locked_objects_in_frame(JavaThread* calling_thread, JavaThread* java_thread,
-                                 javaVFrame *jvf, GrowableArray<jvmtiMonitorStackDepthInfo*>* owned_monitors_list, jint stack_depth) {
+JvmtiEnvBase::get_locked_objects_in_frame(JavaThread* calling_thread, JavaThread* target,
+                                 javaVFrame *jvf, GrowableArray<jvmtiMonitorStackDepthInfo*>* owned_monitors_list,
+                                 jint stack_depth, oop vthread) {
   jvmtiError err = JVMTI_ERROR_NONE;
   Thread* current_thread = Thread::current();
   ResourceMark rm(current_thread);
   HandleMark   hm(current_thread);
 
@@ -1060,24 +1061,32 @@
   {
     // The ObjectMonitor* can't be async deflated since we are either
     // at a safepoint or the calling thread is operating on itself so
     // it cannot leave the underlying wait() call.
     // Save object of current wait() call (if any) for later comparison.
-    ObjectMonitor *mon = java_thread->current_waiting_monitor();
-    if (mon != nullptr) {
-      wait_obj = mon->object();
+    if (target != nullptr) {
+      ObjectMonitor *mon = target->current_waiting_monitor();
+      if (mon != nullptr) wait_obj = mon->object();
     }
   }
   oop pending_obj = nullptr;
   {
     // The ObjectMonitor* can't be async deflated since we are either
     // at a safepoint or the calling thread is operating on itself so
     // it cannot leave the underlying enter() call.
     // Save object of current enter() call (if any) for later comparison.
-    ObjectMonitor *mon = java_thread->current_pending_monitor();
-    if (mon != nullptr) {
-      pending_obj = mon->object();
+    if (target != nullptr) {
+      ObjectMonitor *mon = target->current_pending_monitor();
+      if (mon != nullptr) pending_obj = mon->object();
+    } else {
+      assert(vthread != nullptr, "no vthread oop");
+      oop oopCont = java_lang_VirtualThread::continuation(vthread);
+      assert(oopCont != nullptr, "vthread with no continuation");
+      stackChunkOop chunk = jdk_internal_vm_Continuation::tail(oopCont);
+      assert(chunk != nullptr, "unmounted vthread should have a chunk");
+      ObjectMonitor *mon = chunk->current_pending_monitor();
+      if (mon != nullptr) pending_obj = mon->object();
     }
   }
 
   for (int i = 0; i < mons->length(); i++) {
     MonitorInfo *mi = mons->at(i);
@@ -1522,13 +1531,17 @@
     nWait = 0;
     for (ObjectWaiter* waiter = mon->first_waiter();
          waiter != nullptr && (nWait == 0 || waiter != mon->first_waiter());
          waiter = mon->next_waiter(waiter)) {
       JavaThread *w = mon->thread_of_waiter(waiter);
-      oop thread_oop = get_vthread_or_thread_oop(w);
-      if (thread_oop->is_a(vmClasses::BaseVirtualThread_klass())) {
+      if (w == nullptr) {
         skipped++;
+      } else {
+        oop thread_oop = get_vthread_or_thread_oop(w);
+        if (thread_oop->is_a(vmClasses::BaseVirtualThread_klass())) {
+          skipped++;
+        }
       }
       nWait++;
     }
   }
   ret.waiter_count = nWant;
@@ -1572,19 +1585,23 @@
     if (ret.notify_waiter_count > 0) { // we have threads waiting to be notified in Object.wait()
       ObjectWaiter *waiter = mon->first_waiter();
       jint skipped = 0;
       for (int i = 0; i < nWait; i++) {
         JavaThread *w = mon->thread_of_waiter(waiter);
-        oop thread_oop = get_vthread_or_thread_oop(w);
-        bool is_virtual = thread_oop->is_a(vmClasses::BaseVirtualThread_klass());
-        assert(w != nullptr, "sanity check");
+        bool is_virtual;
+        if (w == nullptr) {
+          is_virtual = true;
+        } else {
+          oop thread_oop = get_vthread_or_thread_oop(w);
+          is_virtual = thread_oop->is_a(vmClasses::BaseVirtualThread_klass());
+        }
         if (is_virtual) {
           skipped++;
         } else {
           // If the thread was found on the ObjectWaiter list, then
           // it has not been notified.
-          Handle th(current_thread, get_vthread_or_thread_oop(w));
+          Handle th(current_thread, w->threadObj());
           ret.notify_waiters[i - skipped] = (jthread)jni_reference(calling_thread, th);
         }
         waiter = mon->next_waiter(waiter);
       }
     }
@@ -2499,22 +2516,22 @@
   }
 }
 
 void
 GetOwnedMonitorInfoClosure::do_vthread(Handle target_h) {
-  assert(_target_jt != nullptr, "sanity check");
   Thread* current = Thread::current();
   ResourceMark rm(current); // vframes are resource allocated
   HandleMark hm(current);
 
   javaVFrame *jvf = JvmtiEnvBase::get_vthread_jvf(target_h());
 
-  if (!_target_jt->is_exiting() && _target_jt->threadObj() != nullptr) {
+  if (_target_jt == nullptr || (!_target_jt->is_exiting() && _target_jt->threadObj() != nullptr)) {
     _result = ((JvmtiEnvBase *)_env)->get_owned_monitors(_calling_thread,
                                                          _target_jt,
                                                          jvf,
-                                                         _owned_monitors_list);
+                                                         _owned_monitors_list,
+                                                         target_h());
   }
 }
 
 void
 GetCurrentContendedMonitorClosure::do_thread(Thread *target) {
@@ -2528,10 +2545,17 @@
 }
 
 void
 GetCurrentContendedMonitorClosure::do_vthread(Handle target_h) {
   if (_target_jt == nullptr) {
+    oop cont = java_lang_VirtualThread::continuation(target_h());
+    assert(cont != nullptr, "vthread with no continuation");
+    stackChunkOop chunk = jdk_internal_vm_Continuation::tail(cont);
+    assert(chunk != nullptr, "unmounted vthread should have a chunk");
+    if (chunk->current_pending_monitor() != nullptr) {
+      *_owned_monitor_ptr = JNIHandles::make_local(_calling_thread, chunk->current_pending_monitor()->object());
+    }
     _result = JVMTI_ERROR_NONE; // target virtual thread is unmounted
     return;
   }
   // mounted virtual thread case
   do_thread(_target_jt);
diff a/src/hotspot/share/prims/jvmtiEnvBase.hpp b/src/hotspot/share/prims/jvmtiEnvBase.hpp
--- a/src/hotspot/share/prims/jvmtiEnvBase.hpp
+++ b/src/hotspot/share/prims/jvmtiEnvBase.hpp
@@ -356,11 +356,11 @@
   jint count_locked_objects(JavaThread *java_thread, Handle hobj);
   jvmtiError get_locked_objects_in_frame(JavaThread *calling_thread,
                                    JavaThread* java_thread,
                                    javaVFrame *jvf,
                                    GrowableArray<jvmtiMonitorStackDepthInfo*>* owned_monitors_list,
-                                   jint depth);
+                                   jint depth, oop vthread = nullptr);
  public:
   static javaVFrame* jvf_for_thread_and_depth(JavaThread* java_thread, jint depth);
 
   // get a field descriptor for the specified class and field
   static bool get_field_descriptor(Klass* k, jfieldID field, fieldDescriptor* fd);
@@ -420,12 +420,12 @@
                              jvmtiFrameInfo* frame_buffer, jint* count_ptr);
   jvmtiError get_current_contended_monitor(JavaThread* calling_thread, JavaThread* java_thread,
                                            jobject* monitor_ptr, bool is_virtual);
   jvmtiError get_owned_monitors(JavaThread* calling_thread, JavaThread* java_thread,
                                 GrowableArray<jvmtiMonitorStackDepthInfo*> *owned_monitors_list);
-  jvmtiError get_owned_monitors(JavaThread* calling_thread, JavaThread* java_thread, javaVFrame* jvf,
-                                GrowableArray<jvmtiMonitorStackDepthInfo*> *owned_monitors_list);
+  jvmtiError get_owned_monitors(JavaThread* calling_thread, JavaThread* carrier, javaVFrame* jvf,
+                                GrowableArray<jvmtiMonitorStackDepthInfo*> *owned_monitors_list, oop vthread);
   static jvmtiError check_top_frame(Thread* current_thread, JavaThread* java_thread,
                                     jvalue value, TosState tos, Handle* ret_ob_h);
   jvmtiError force_early_return(jthread thread, jvalue value, TosState tos);
 };
 
diff a/src/hotspot/share/prims/jvmtiExport.cpp b/src/hotspot/share/prims/jvmtiExport.cpp
--- a/src/hotspot/share/prims/jvmtiExport.cpp
+++ b/src/hotspot/share/prims/jvmtiExport.cpp
@@ -1676,12 +1676,13 @@
   }
   JavaThread *thread = JavaThread::current();
   HandleMark hm(thread);
   EVT_TRIG_TRACE(EXT_EVENT_VIRTUAL_THREAD_UNMOUNT, ("[%p] Trg Virtual Thread Unmount event triggered", vthread));
 
-  JvmtiThreadState *state = get_jvmti_thread_state(thread);
-  if (state == nullptr) {
+  // On preemption JVMTI state rebinding has already happened so get it always direclty from the oop.
+  JvmtiThreadState *state = java_lang_Thread::jvmti_thread_state(JNIHandles::resolve(vthread));
+  if (state == NULL) {
     return;
   }
 
   if (state->is_enabled((jvmtiEvent)EXT_EVENT_VIRTUAL_THREAD_UNMOUNT)) {
     JvmtiEnvThreadStateIterator it(state);
@@ -1696,11 +1697,11 @@
 
         JvmtiVirtualThreadEventMark jem(thread);
         JvmtiJavaThreadEventTransition jet(thread);
         jvmtiExtensionEvent callback = env->ext_callbacks()->VirtualThreadUnmount;
         if (callback != nullptr) {
-          (*callback)(env->jvmti_external(), jem.jni_env(), jem.jni_thread());
+          (*callback)(env->jvmti_external(), jem.jni_env(), vthread);
         }
       }
     }
   }
 }
@@ -2860,10 +2861,25 @@
       }
     }
   }
 }
 
+void JvmtiExport::vthread_post_monitor_waited(JavaThread *current, ObjectMonitor *obj_mntr, jboolean timed_out) {
+  Handle vthread(current, current->vthread());
+
+  // Finish the VTMS transition temporarily to post the event.
+  current->rebind_to_jvmti_thread_state_of(vthread());
+  JvmtiVTMSTransitionDisabler::finish_VTMS_transition((jthread)vthread.raw_value(), /* is_mount */ true);
+
+  // Post event.
+  JvmtiExport::post_monitor_waited(current, obj_mntr, timed_out);
+
+  // Go back to VTMS transition state.
+  JvmtiVTMSTransitionDisabler::start_VTMS_transition((jthread)vthread.raw_value(), /* is_mount */ true);
+  current->rebind_to_jvmti_thread_state_of(current->threadObj());
+}
+
 void JvmtiExport::post_vm_object_alloc(JavaThread *thread, oop object) {
   if (object == nullptr) {
     return;
   }
   if (thread->is_in_any_VTMS_transition()) {
diff a/src/hotspot/share/prims/jvmtiExport.hpp b/src/hotspot/share/prims/jvmtiExport.hpp
--- a/src/hotspot/share/prims/jvmtiExport.hpp
+++ b/src/hotspot/share/prims/jvmtiExport.hpp
@@ -395,10 +395,11 @@
   static void post_data_dump() NOT_JVMTI_RETURN;
   static void post_monitor_contended_enter(JavaThread *thread, ObjectMonitor *obj_mntr) NOT_JVMTI_RETURN;
   static void post_monitor_contended_entered(JavaThread *thread, ObjectMonitor *obj_mntr) NOT_JVMTI_RETURN;
   static void post_monitor_wait(JavaThread *thread, oop obj, jlong timeout) NOT_JVMTI_RETURN;
   static void post_monitor_waited(JavaThread *thread, ObjectMonitor *obj_mntr, jboolean timed_out) NOT_JVMTI_RETURN;
+  static void vthread_post_monitor_waited(JavaThread *current, ObjectMonitor *obj_mntr, jboolean timed_out) NOT_JVMTI_RETURN;
   static void post_object_free(JvmtiEnv* env, GrowableArray<jlong>* objects) NOT_JVMTI_RETURN;
   static void post_resource_exhausted(jint resource_exhausted_flags, const char* detail) NOT_JVMTI_RETURN;
   static void record_vm_internal_object_allocation(oop object) NOT_JVMTI_RETURN;
   // Post objects collected by vm_object_alloc_event_collector.
   static void post_vm_object_alloc(JavaThread *thread, oop object) NOT_JVMTI_RETURN;
diff a/src/hotspot/share/prims/jvmtiTagMap.cpp b/src/hotspot/share/prims/jvmtiTagMap.cpp
--- a/src/hotspot/share/prims/jvmtiTagMap.cpp
+++ b/src/hotspot/share/prims/jvmtiTagMap.cpp
@@ -2304,14 +2304,15 @@
 
 bool StackRefCollector::report_native_stack_refs(jmethodID method) {
   _blk->set_context(_thread_tag, _tid, _depth, method);
   if (_is_top_frame) {
     // JNI locals for the top frame.
-    assert(_java_thread != nullptr, "sanity");
-    _java_thread->active_handles()->oops_do(_blk);
-    if (_blk->stopped()) {
-      return false;
+    if (_java_thread != nullptr) {
+      _java_thread->active_handles()->oops_do(_blk);
+      if (_blk->stopped()) {
+        return false;
+      }
     }
   } else {
     if (_last_entry_frame != nullptr) {
       // JNI locals for the entry frame.
       assert(_last_entry_frame->is_entry_frame(), "checking");
diff a/src/hotspot/share/prims/jvmtiThreadState.cpp b/src/hotspot/share/prims/jvmtiThreadState.cpp
--- a/src/hotspot/share/prims/jvmtiThreadState.cpp
+++ b/src/hotspot/share/prims/jvmtiThreadState.cpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2003, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -493,10 +493,11 @@
 void
 JvmtiVTMSTransitionDisabler::finish_VTMS_transition(jthread vthread, bool is_mount) {
   JavaThread* thread = JavaThread::current();
 
   assert(thread->is_in_VTMS_transition(), "sanity check");
+  assert(!thread->is_in_tmp_VTMS_transition(), "sanity check");
   thread->set_is_in_VTMS_transition(false);
   oop vt = JNIHandles::resolve_external_guard(vthread);
   java_lang_Thread::set_is_in_VTMS_transition(vt, false);
   assert(thread->VTMS_transition_mark(), "sanity check");
   thread->set_VTMS_transition_mark(false);
@@ -661,10 +662,16 @@
 JvmtiVTMSTransitionDisabler::VTMS_unmount_end(jobject vthread) {
   JavaThread* thread = JavaThread::current();
   assert(thread->is_in_VTMS_transition(), "sanity check");
   assert(!thread->is_in_tmp_VTMS_transition(), "sanity check");
   finish_VTMS_transition(vthread, /* is_mount */ false);
+
+  if (thread->pending_jvmti_unmount_event()) {
+    assert(java_lang_VirtualThread::is_preempted(JNIHandles::resolve(vthread)), "should be marked preempted");
+    JvmtiExport::post_vthread_unmount(vthread);
+    thread->set_pending_jvmti_unmount_event(false);
+  }
 }
 
 
 //
 // Virtual Threads Suspend/Resume management
diff a/src/hotspot/share/runtime/basicLock.hpp b/src/hotspot/share/runtime/basicLock.hpp
--- a/src/hotspot/share/runtime/basicLock.hpp
+++ b/src/hotspot/share/runtime/basicLock.hpp
@@ -86,10 +86,11 @@
   oop       _obj;                                     // object holds the lock;
 
  public:
   // Manipulation
   oop      obj() const                                { return _obj;  }
+  oop*     obj_adr()                                  { return &_obj; }
   void set_obj(oop obj)                               { _obj = obj; }
   BasicLock* lock()                                   { return &_lock; }
 
   // Note: Use frame::interpreter_frame_monitor_size() for the size of BasicObjectLocks
   //       in interpreter activation frames since it includes machine-specific padding.
diff a/src/hotspot/share/runtime/continuation.cpp b/src/hotspot/share/runtime/continuation.cpp
--- a/src/hotspot/share/runtime/continuation.cpp
+++ b/src/hotspot/share/runtime/continuation.cpp
@@ -24,17 +24,20 @@
 
 #include "precompiled.hpp"
 #include "classfile/vmSymbols.hpp"
 #include "gc/shared/barrierSetNMethod.hpp"
 #include "oops/method.inline.hpp"
+#include "oops/oop.inline.hpp"
+#include "prims/jvmtiThreadState.inline.hpp"
 #include "runtime/continuation.hpp"
 #include "runtime/continuationEntry.inline.hpp"
 #include "runtime/continuationHelper.inline.hpp"
 #include "runtime/continuationJavaClasses.inline.hpp"
 #include "runtime/continuationWrapper.inline.hpp"
 #include "runtime/interfaceSupport.inline.hpp"
 #include "runtime/javaThread.inline.hpp"
+#include "runtime/jniHandles.inline.hpp"
 #include "runtime/osThread.hpp"
 #include "runtime/vframe.inline.hpp"
 #include "runtime/vframe_hp.hpp"
 
 // defined in continuationFreezeThaw.cpp
@@ -52,10 +55,124 @@
      THROW_MSG(vmSymbols::java_lang_IllegalStateException(), "pin underflow");
   }
 }
 JVM_END
 
+#if INCLUDE_JVMTI
+class JvmtiUnmountBeginMark : public StackObj {
+  Handle _vthread;
+  JavaThread* _target;
+  int _preempt_result;
+  bool _failed;
+
+ public:
+  JvmtiUnmountBeginMark(JavaThread* t) :
+    _vthread(t, t->vthread()), _target(t), _preempt_result(freeze_pinned_native), _failed(false) {
+    assert(!_target->is_in_any_VTMS_transition(), "must be");
+
+    if (JvmtiVTMSTransitionDisabler::VTMS_notify_jvmti_events()) {
+      JvmtiVTMSTransitionDisabler::start_VTMS_transition((jthread)_vthread.raw_value(), /* is_mount */ false);
+
+      // Don't preempt if there is a pending popframe or earlyret operation. This can
+      // be installed in start_VTMS_transition() so we need to check it here.
+      if (JvmtiExport::can_pop_frame() || JvmtiExport::can_force_early_return()) {
+        JvmtiThreadState* state = _target->jvmti_thread_state();
+        if (_target->has_pending_popframe() || (state != nullptr && state->is_earlyret_pending())) {
+          _failed = true;
+        }
+      }
+
+      // Don't preempt in case there is an async exception installed since
+      // we would incorrectly throw it during the unmount logic in the carrier.
+      if (_target->has_async_exception_condition()) {
+        _failed = false;
+      }
+    } else {
+      _target->set_is_in_VTMS_transition(true);
+      java_lang_Thread::set_is_in_VTMS_transition(_vthread(), true);
+    }
+  }
+  ~JvmtiUnmountBeginMark() {
+    assert(!_target->is_suspended(), "must be");
+
+    assert(_target->is_in_VTMS_transition(), "must be");
+    assert(java_lang_Thread::is_in_VTMS_transition(_vthread()), "must be");
+
+    // Read it again since for late binding agents the flag could have
+    // been set while blocked in the allocation path during freeze.
+    bool jvmti_present = JvmtiVTMSTransitionDisabler::VTMS_notify_jvmti_events();
+
+    if (_preempt_result != freeze_ok) {
+      // Undo transition
+      if (jvmti_present) {
+        JvmtiVTMSTransitionDisabler::finish_VTMS_transition((jthread)_vthread.raw_value(), false);
+      } else {
+        _target->set_is_in_VTMS_transition(false);
+        java_lang_Thread::set_is_in_VTMS_transition(_vthread(), false);
+      }
+    } else {
+      if (jvmti_present) {
+        _target->rebind_to_jvmti_thread_state_of(_target->threadObj());
+        if (JvmtiExport::should_post_vthread_mount()) {
+          _target->set_pending_jvmti_unmount_event(true);
+        }
+      }
+    }
+  }
+  void set_preempt_result(int res) { _preempt_result = res; }
+  bool failed() { return _failed; }
+};
+
+static bool is_safe_vthread_to_preempt_for_jvmti(JavaThread* target, oop vthread) {
+  if (target->is_in_any_VTMS_transition()) {
+    // We caught target at the end of a mount transition (is_in_VTMS_transition()) or at the
+    // beginning or end of a temporary switch to carrier thread (is_in_tmp_VTMS_transition()).
+    return false;
+  }
+  return true;
+}
+#endif // INCLUDE_JVMTI
+
+static bool is_safe_vthread_to_preempt(JavaThread* target, oop vthread) {
+  if (!java_lang_VirtualThread::is_instance(vthread) ||                               // inside tmp transition
+      java_lang_VirtualThread::state(vthread) != java_lang_VirtualThread::RUNNING) {  // inside transition
+    return false;
+  }
+  return JVMTI_ONLY(is_safe_vthread_to_preempt_for_jvmti(target, vthread)) NOT_JVMTI(true);
+}
+
+typedef int (*FreezeContFnT)(JavaThread*, intptr_t*);
+
+static void verify_preempt_preconditions(JavaThread* target, oop continuation) {
+  assert(target == JavaThread::current(), "no support for external preemption");
+  assert(target->has_last_Java_frame(), "");
+  assert(!target->preempting(), "");
+  assert(target->last_continuation() != nullptr, "");
+  assert(target->last_continuation()->cont_oop(target) == continuation, "");
+  assert(Continuation::continuation_scope(continuation) == java_lang_VirtualThread::vthread_scope(), "");
+  assert(!target->has_pending_exception(), "");
+}
+
+int Continuation::try_preempt(JavaThread* target, oop continuation) {
+  verify_preempt_preconditions(target, continuation);
+
+  if (LockingMode == LM_LEGACY) {
+    return freeze_unsupported;
+  }
+
+  if (!is_safe_vthread_to_preempt(target, target->vthread())) {
+    return freeze_pinned_native;
+  }
+
+  JVMTI_ONLY(JvmtiUnmountBeginMark jubm(target);)
+  JVMTI_ONLY(if (jubm.failed()) return freeze_pinned_native;)
+  int res = CAST_TO_FN_PTR(FreezeContFnT, freeze_preempt_entry())(target, target->last_Java_sp());
+  log_trace(continuations, preempt)("try_preempt: %d", res);
+  JVMTI_ONLY(jubm.set_preempt_result(res);)
+  return res;
+}
+
 #ifndef PRODUCT
 static jlong java_tid(JavaThread* thread) {
   return java_lang_Thread::thread_id(thread->threadObj());
 }
 #endif
diff a/src/hotspot/share/runtime/continuation.hpp b/src/hotspot/share/runtime/continuation.hpp
--- a/src/hotspot/share/runtime/continuation.hpp
+++ b/src/hotspot/share/runtime/continuation.hpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -45,13 +45,30 @@
 void continuations_init();
 
 class javaVFrame;
 class JavaThread;
 
+// should match Continuation.toPreemptStatus() in Continuation.java
+enum freeze_result {
+  freeze_ok = 0,
+  freeze_ok_bottom = 1,
+  freeze_pinned_cs = 2,
+  freeze_pinned_native = 3,
+  freeze_pinned_monitor = 4,
+  freeze_exception = 5,
+  freeze_not_mounted = 6,
+  freeze_unsupported = 7
+};
+
 class Continuation : AllStatic {
 public:
 
+  enum preempt_kind {
+    freeze_on_monitorenter = 1,
+    freeze_on_wait         = 2
+  };
+
   enum thaw_kind {
     thaw_top = 0,
     thaw_return_barrier = 1,
     thaw_return_barrier_exception = 2,
   };
@@ -67,13 +84,16 @@
   }
 
   static void init();
 
   static address freeze_entry();
+  static address freeze_preempt_entry();
   static int prepare_thaw(JavaThread* thread, bool return_barrier);
   static address thaw_entry();
 
+  static int try_preempt(JavaThread* target, oop continuation);
+
   static ContinuationEntry* get_continuation_entry_for_continuation(JavaThread* thread, oop continuation);
   static ContinuationEntry* get_continuation_entry_for_sp(JavaThread* thread, intptr_t* const sp);
   static ContinuationEntry* get_continuation_entry_for_entry_frame(JavaThread* thread, const frame& f);
 
   static bool is_continuation_mounted(JavaThread* thread, oop continuation);
diff a/src/hotspot/share/runtime/continuationEntry.cpp b/src/hotspot/share/runtime/continuationEntry.cpp
--- a/src/hotspot/share/runtime/continuationEntry.cpp
+++ b/src/hotspot/share/runtime/continuationEntry.cpp
@@ -34,20 +34,27 @@
 #include "runtime/stackFrameStream.inline.hpp"
 #include "runtime/stackWatermarkSet.inline.hpp"
 #include "runtime/stubRoutines.hpp"
 
 int ContinuationEntry::_return_pc_offset = 0;
+int ContinuationEntry::_thaw_call_pc_offset = 0;
+int ContinuationEntry::_cleanup_offset = 0;
 address ContinuationEntry::_return_pc = nullptr;
+address ContinuationEntry::_thaw_call_pc = nullptr;
+address ContinuationEntry::_cleanup_pc = nullptr;
 nmethod* ContinuationEntry::_enter_special = nullptr;
 int ContinuationEntry::_interpreted_entry_offset = 0;
 
 void ContinuationEntry::set_enter_code(nmethod* nm, int interpreted_entry_offset) {
   assert(_return_pc_offset != 0, "");
   _return_pc = nm->code_begin() + _return_pc_offset;
+  _thaw_call_pc = nm->code_begin() + _thaw_call_pc_offset;
+  _cleanup_pc = nm->code_begin() + _cleanup_offset;
 
   _enter_special = nm;
   _interpreted_entry_offset = interpreted_entry_offset;
+
   assert(_enter_special->code_contains(compiled_entry()),    "entry not in enterSpecial");
   assert(_enter_special->code_contains(interpreted_entry()), "entry not in enterSpecial");
   assert(interpreted_entry() < compiled_entry(), "unexpected code layout");
 }
 
diff a/src/hotspot/share/runtime/continuationEntry.hpp b/src/hotspot/share/runtime/continuationEntry.hpp
--- a/src/hotspot/share/runtime/continuationEntry.hpp
+++ b/src/hotspot/share/runtime/continuationEntry.hpp
@@ -55,15 +55,20 @@
   }
 #endif
 
 public:
   static int _return_pc_offset; // friend gen_continuation_enter
+  static int _thaw_call_pc_offset;
+  static int _cleanup_offset;
+
   static void set_enter_code(nmethod* nm, int interpreted_entry_offset);
   static bool is_interpreted_call(address call_address);
 
 private:
   static address _return_pc;
+  static address _thaw_call_pc;
+  static address _cleanup_pc;
   static nmethod* _enter_special;
   static int _interpreted_entry_offset;
 
 private:
   ContinuationEntry* _parent;
@@ -99,10 +104,13 @@
 
   static address entry_pc() { return _return_pc; }
   intptr_t* entry_sp() const { return (intptr_t*)this; }
   intptr_t* entry_fp() const;
 
+  static address thaw_call_pc_address() { return (address)&_thaw_call_pc; }
+  static address cleanup_pc() { return _cleanup_pc; }
+
   static address compiled_entry();
   static address interpreted_entry();
 
   int argsize() const { return _argsize; }
   void set_argsize(int value) { _argsize = value; }
diff a/src/hotspot/share/runtime/continuationFreezeThaw.cpp b/src/hotspot/share/runtime/continuationFreezeThaw.cpp
--- a/src/hotspot/share/runtime/continuationFreezeThaw.cpp
+++ b/src/hotspot/share/runtime/continuationFreezeThaw.cpp
@@ -53,10 +53,11 @@
 #include "runtime/frame.inline.hpp"
 #include "runtime/interfaceSupport.inline.hpp"
 #include "runtime/javaThread.inline.hpp"
 #include "runtime/jniHandles.inline.hpp"
 #include "runtime/keepStackGCProcessed.hpp"
+#include "runtime/objectMonitor.inline.hpp"
 #include "runtime/orderAccess.hpp"
 #include "runtime/prefetch.inline.hpp"
 #include "runtime/smallRegisterMap.inline.hpp"
 #include "runtime/sharedRuntime.hpp"
 #include "runtime/stackChunkFrameStream.inline.hpp"
@@ -64,10 +65,11 @@
 #include "runtime/stackOverflow.hpp"
 #include "runtime/stackWatermarkSet.inline.hpp"
 #include "utilities/debug.hpp"
 #include "utilities/exceptions.hpp"
 #include "utilities/macros.hpp"
+#include "utilities/vmError.hpp"
 #if INCLUDE_ZGC
 #include "gc/z/zStackChunkGCData.inline.hpp"
 #endif
 
 #include <type_traits>
@@ -177,10 +179,11 @@
 static void verify_continuation(oop continuation) { Continuation::debug_verify_continuation(continuation); }
 
 static void do_deopt_after_thaw(JavaThread* thread);
 static bool do_verify_after_thaw(JavaThread* thread, stackChunkOop chunk, outputStream* st);
 static void log_frames(JavaThread* thread);
+static void log_frames_after_thaw(JavaThread* thread, ContinuationWrapper& cont, intptr_t* sp, bool preempted);
 static void print_frame_layout(const frame& f, bool callee_complete, outputStream* st = tty);
 
 #define assert_pfl(p, ...) \
 do {                                           \
   if (!(p)) {                                  \
@@ -196,31 +199,12 @@
 #else
 static void verify_continuation(oop continuation) { }
 #define assert_pfl(p, ...)
 #endif
 
-// should match Continuation.preemptStatus() in Continuation.java
-enum freeze_result {
-  freeze_ok = 0,
-  freeze_ok_bottom = 1,
-  freeze_pinned_cs = 2,
-  freeze_pinned_native = 3,
-  freeze_pinned_monitor = 4,
-  freeze_exception = 5
-};
-
-const char* freeze_result_names[6] = {
-  "freeze_ok",
-  "freeze_ok_bottom",
-  "freeze_pinned_cs",
-  "freeze_pinned_native",
-  "freeze_pinned_monitor",
-  "freeze_exception"
-};
-
 static freeze_result is_pinned0(JavaThread* thread, oop cont_scope, bool safepoint);
-template<typename ConfigT> static inline int freeze_internal(JavaThread* current, intptr_t* const sp);
+template<typename ConfigT, bool preempt> static inline int freeze_internal(JavaThread* current, intptr_t* const sp);
 
 static inline int prepare_thaw_internal(JavaThread* thread, bool return_barrier);
 template<typename ConfigT> static inline intptr_t* thaw_internal(JavaThread* thread, const Continuation::thaw_kind kind);
 
 
@@ -243,12 +227,15 @@
 
 template<typename ConfigT>
 static JRT_LEAF(intptr_t*, thaw(JavaThread* thread, int kind))
   // TODO: JRT_LEAF and NoHandleMark is problematic for JFR events.
   // vFrameStreamCommon allocates Handles in RegisterMap for continuations.
+  // Also the preemption case with JVMTI events enabled might safepoint so
+  // undo the NoSafepointVerifier here and rely on handling by ContinuationWrapper.
   // JRT_ENTRY instead?
   ResetNoHandleMark rnhm;
+  DEBUG_ONLY(PauseNoSafepointVerifier pnsv(&__nsv);)
 
   // we might modify the code cache via BarrierSetNMethod::nmethod_entry_barrier
   MACOS_AARCH64_ONLY(ThreadWXEnable __wx(WXWrite, thread));
   return ConfigT::thaw(thread, (Continuation::thaw_kind)kind);
 JRT_END
@@ -267,11 +254,15 @@
 public:
   typedef Config<oops, BarrierSetT> SelfT;
   using OopT = std::conditional_t<oops == oop_kind::NARROW, narrowOop, oop>;
 
   static int freeze(JavaThread* thread, intptr_t* const sp) {
-    return freeze_internal<SelfT>(thread, sp);
+    return freeze_internal<SelfT, false>(thread, sp);
+  }
+
+  static int freeze_preempt(JavaThread* thread, intptr_t* const sp) {
+    return freeze_internal<SelfT, true>(thread, sp);
   }
 
   static intptr_t* thaw(JavaThread* thread, Continuation::thaw_kind kind) {
     return thaw_internal<SelfT>(thread, kind);
   }
@@ -310,29 +301,31 @@
 static oop get_continuation(JavaThread* thread) {
   assert(thread != nullptr, "");
   assert(thread->threadObj() != nullptr, "");
   return java_lang_Thread::continuation(thread->threadObj());
 }
+#endif // ASSERT
 
 inline void clear_anchor(JavaThread* thread) {
   thread->frame_anchor()->clear();
 }
 
-static void set_anchor(JavaThread* thread, intptr_t* sp) {
-  address pc = ContinuationHelper::return_address_at(
-                 sp - frame::sender_sp_ret_address_offset());
+static void set_anchor(JavaThread* thread, intptr_t* sp, address pc = nullptr) {
+  if (pc == nullptr) {
+    pc = ContinuationHelper::return_address_at(
+           sp - frame::sender_sp_ret_address_offset());
+  }
   assert(pc != nullptr, "");
 
   JavaFrameAnchor* anchor = thread->frame_anchor();
   anchor->set_last_Java_sp(sp);
   anchor->set_last_Java_pc(pc);
   ContinuationHelper::set_anchor_pd(anchor, sp);
 
   assert(thread->has_last_Java_frame(), "");
   assert(thread->last_frame().cb() != nullptr, "");
 }
-#endif // ASSERT
 
 static void set_anchor_to_entry(JavaThread* thread, ContinuationEntry* entry) {
   JavaFrameAnchor* anchor = thread->frame_anchor();
   anchor->set_last_Java_sp(entry->entry_sp());
   anchor->set_last_Java_pc(entry->entry_pc());
@@ -371,15 +364,20 @@
 class FreezeBase : public StackObj {
 protected:
   JavaThread* const _thread;
   ContinuationWrapper& _cont;
   bool _barriers; // only set when we allocate a chunk
-  const bool _preempt; // used only on the slow path
-  const intptr_t * const _frame_sp; // Top frame sp for this freeze
 
   intptr_t* _bottom_address;
 
+  // Used for preemption only
+  const bool _preempt;
+  frame _last_frame;
+
+  // Used to support freezing with held monitors
+  int _monitors_in_lockstack;
+
   int _freeze_size; // total size of all frames plus metadata in words.
   int _total_align_size;
 
   intptr_t* _cont_stack_top;
   intptr_t* _cont_stack_bottom;
@@ -395,21 +393,23 @@
   JvmtiSampledObjectAllocEventCollector* _jvmti_event_collector;
 
   NOT_PRODUCT(int _frames;)
   DEBUG_ONLY(intptr_t* _last_write;)
 
-  inline FreezeBase(JavaThread* thread, ContinuationWrapper& cont, intptr_t* sp);
+  inline FreezeBase(JavaThread* thread, ContinuationWrapper& cont, intptr_t* sp, bool preempt);
 
 public:
   NOINLINE freeze_result freeze_slow();
   void freeze_fast_existing_chunk();
 
   CONT_JFR_ONLY(FreezeThawJfrInfo& jfr_info() { return _jfr_info; })
   void set_jvmti_event_collector(JvmtiSampledObjectAllocEventCollector* jsoaec) { _jvmti_event_collector = jsoaec; }
 
   inline int size_if_fast_freeze_available();
 
+  inline frame& last_frame() { return _last_frame; }
+
 #ifdef ASSERT
   bool check_valid_fast_path();
 #endif
 
 protected:
@@ -427,33 +427,37 @@
   int cont_size() { return pointer_delta_as_int(_cont_stack_bottom, _cont_stack_top); }
 
 private:
   // slow path
   frame freeze_start_frame();
-  frame freeze_start_frame_safepoint_stub(frame f);
+  frame freeze_start_frame_on_preempt();
   NOINLINE freeze_result recurse_freeze(frame& f, frame& caller, int callee_argsize, bool callee_interpreted, bool top);
-  inline frame freeze_start_frame_yield_stub(frame f);
+  inline frame freeze_start_frame_yield_stub();
   template<typename FKind>
   inline freeze_result recurse_freeze_java_frame(const frame& f, frame& caller, int fsize, int argsize);
   inline void before_freeze_java_frame(const frame& f, const frame& caller, int fsize, int argsize, bool is_bottom_frame);
   inline void after_freeze_java_frame(const frame& hf, bool is_bottom_frame);
   freeze_result finalize_freeze(const frame& callee, frame& caller, int argsize);
   void patch(const frame& f, frame& hf, const frame& caller, bool is_bottom_frame);
   NOINLINE freeze_result recurse_freeze_interpreted_frame(frame& f, frame& caller, int callee_argsize, bool callee_interpreted);
   freeze_result recurse_freeze_compiled_frame(frame& f, frame& caller, int callee_argsize, bool callee_interpreted);
   NOINLINE freeze_result recurse_freeze_stub_frame(frame& f, frame& caller);
+  NOINLINE freeze_result recurse_freeze_native_frame(frame& f, frame& caller);
   NOINLINE void finish_freeze(const frame& f, const frame& top);
 
+  void freeze_lockstack();
+
   inline bool stack_overflow();
 
   static frame sender(const frame& f) { return f.is_interpreted_frame() ? sender<ContinuationHelper::InterpretedFrame>(f)
                                                                         : sender<ContinuationHelper::NonInterpretedUnknownFrame>(f); }
   template<typename FKind> static inline frame sender(const frame& f);
   template<typename FKind> frame new_heap_frame(frame& f, frame& caller);
   inline void set_top_frame_metadata_pd(const frame& hf);
   inline void patch_pd(frame& callee, const frame& caller);
   void adjust_interpreted_frame_unextended_sp(frame& f);
+  static inline void prepare_freeze_interpreted_top_frame(const frame& f);
   static inline void relativize_interpreted_frame_metadata(const frame& f, const frame& hf);
 
 protected:
   void freeze_fast_copy(stackChunkOop chunk, int chunk_start_sp CONT_JFR_ONLY(COMMA bool chunk_is_allocated));
   bool freeze_fast_new_chunk(stackChunkOop chunk);
@@ -463,21 +467,21 @@
 class Freeze : public FreezeBase {
 private:
   stackChunkOop allocate_chunk(size_t stack_size, int argsize_md);
 
 public:
-  inline Freeze(JavaThread* thread, ContinuationWrapper& cont, intptr_t* frame_sp)
-    : FreezeBase(thread, cont, frame_sp) {}
+  inline Freeze(JavaThread* thread, ContinuationWrapper& cont, intptr_t* frame_sp, bool preempt)
+    : FreezeBase(thread, cont, frame_sp, preempt) {}
 
   freeze_result try_freeze_fast();
 
 protected:
   virtual stackChunkOop allocate_chunk_slow(size_t stack_size, int argsize_md) override { return allocate_chunk(stack_size, argsize_md); }
 };
 
-FreezeBase::FreezeBase(JavaThread* thread, ContinuationWrapper& cont, intptr_t* frame_sp) :
-    _thread(thread), _cont(cont), _barriers(false), _preempt(false), _frame_sp(frame_sp) {
+FreezeBase::FreezeBase(JavaThread* thread, ContinuationWrapper& cont, intptr_t* frame_sp, bool preempt) :
+    _thread(thread), _cont(cont), _barriers(false), _preempt(preempt), _last_frame(false /* no initialization */) {
   DEBUG_ONLY(_jvmti_event_collector = nullptr;)
 
   assert(_thread != nullptr, "");
   assert(_thread->last_continuation()->entry_sp() == _cont.entrySP(), "");
 
@@ -504,28 +508,48 @@
 #if !defined(PPC64) || defined(ZERO)
   static const int doYield_stub_frame_size = frame::metadata_words;
 #else
   static const int doYield_stub_frame_size = frame::native_abi_reg_args_size >> LogBytesPerWord;
 #endif
-  assert(SharedRuntime::cont_doYield_stub()->frame_size() == doYield_stub_frame_size, "");
+  // With preemption doYield() might not have been resolved yet
+  assert(_preempt || SharedRuntime::cont_doYield_stub()->frame_size() == doYield_stub_frame_size, "");
+
+  if (preempt) {
+    _last_frame = _thread->last_frame();
+  }
 
   // properties of the continuation on the stack; all sizes are in words
-  _cont_stack_top    = frame_sp + doYield_stub_frame_size; // we don't freeze the doYield stub frame
+  _cont_stack_top    = frame_sp + (!preempt ? doYield_stub_frame_size : 0); // we don't freeze the doYield stub frame
   _cont_stack_bottom = _cont.entrySP() + (_cont.argsize() == 0 ? frame::metadata_words_at_top : 0)
       - ContinuationHelper::frame_align_words(_cont.argsize()); // see alignment in thaw
 
   log_develop_trace(continuations)("freeze size: %d argsize: %d top: " INTPTR_FORMAT " bottom: " INTPTR_FORMAT,
     cont_size(), _cont.argsize(), p2i(_cont_stack_top), p2i(_cont_stack_bottom));
   assert(cont_size() > 0, "");
+
+  if (LockingMode != LM_LIGHTWEIGHT) {
+    _monitors_in_lockstack = 0;
+  } else {
+    _monitors_in_lockstack = _thread->lock_stack().monitor_count();
+  }
 }
 
 void FreezeBase::init_rest() { // we want to postpone some initialization after chunk handling
   _freeze_size = 0;
   _total_align_size = 0;
   NOT_PRODUCT(_frames = 0;)
 }
 
+void FreezeBase::freeze_lockstack() {
+  stackChunkOop chunk = _cont.tail();
+  assert(chunk->sp_address() - chunk->start_address() >= _monitors_in_lockstack, "no room for lockstack");
+
+  _thread->lock_stack().move_to_address((oop*)chunk->start_address());
+  chunk->set_lockstack_size((uint8_t)_monitors_in_lockstack);
+  chunk->set_has_lockstack(true);
+}
+
 void FreezeBase::copy_to_chunk(intptr_t* from, intptr_t* to, int size) {
   stackChunkOop chunk = _cont.tail();
   chunk->copy_from_stack_to_chunk(from, to, size);
   CONT_JFR_ONLY(_jfr_info.record_size_copied(size);)
 
@@ -552,11 +576,11 @@
   assert(_thread->cont_fastpath(), "");
 
   DEBUG_ONLY(_fast_freeze_size = size_if_fast_freeze_available();)
   assert(_fast_freeze_size == 0, "");
 
-  stackChunkOop chunk = allocate_chunk(cont_size() + frame::metadata_words, _cont.argsize() + frame::metadata_words_at_top);
+  stackChunkOop chunk = allocate_chunk(cont_size() + frame::metadata_words + _monitors_in_lockstack, _cont.argsize() + frame::metadata_words_at_top);
   if (freeze_fast_new_chunk(chunk)) {
     return freeze_ok;
   }
   if (_thread->has_pending_exception()) {
     return freeze_exception;
@@ -585,10 +609,12 @@
   // although that would require changing stackChunkOopDesc::is_empty
   if (!chunk->is_empty()) {
     total_size_needed -= _cont.argsize() + frame::metadata_words_at_top;
   }
 
+  total_size_needed += _monitors_in_lockstack;
+
   int chunk_free_room = chunk_sp - frame::metadata_words_at_bottom;
   bool available = chunk_free_room >= total_size_needed;
   log_develop_trace(continuations)("chunk available: %s size: %d argsize: %d top: " INTPTR_FORMAT " bottom: " INTPTR_FORMAT,
     available ? "yes" : "no" , total_size_needed, _cont.argsize(), p2i(_cont_stack_top), p2i(_cont_stack_bottom));
   return available ? total_size_needed : 0;
@@ -666,11 +692,11 @@
 
   chunk->set_max_thawing_size(cont_size());
 
   // in a fresh chunk, we freeze *with* the bottom-most frame's stack arguments.
   // They'll then be stored twice: in the chunk and in the parent chunk's top frame
-  const int chunk_start_sp = cont_size() + frame::metadata_words;
+  const int chunk_start_sp = cont_size() + frame::metadata_words + _monitors_in_lockstack;
   assert(chunk_start_sp == chunk->stack_size(), "");
 
   DEBUG_ONLY(_orig_chunk_sp = chunk->start_address() + chunk_start_sp;)
 
   freeze_fast_copy(chunk, chunk_start_sp CONT_JFR_ONLY(COMMA true));
@@ -695,11 +721,11 @@
     p2i((oopDesc*)chunk), chunk->stack_size(), chunk_start_sp, _cont.argsize());
   assert(chunk_start_sp <= chunk->stack_size(), "");
   assert(chunk_start_sp >= cont_size(), "no room in the chunk");
 
   const int chunk_new_sp = chunk_start_sp - cont_size(); // the chunk's new sp, after freeze
-  assert(!(_fast_freeze_size > 0) || _orig_chunk_sp - (chunk->start_address() + chunk_new_sp) == _fast_freeze_size, "");
+  assert(!(_fast_freeze_size > 0) || (_orig_chunk_sp - (chunk->start_address() + chunk_new_sp)) == (_fast_freeze_size - _monitors_in_lockstack), "");
 
   intptr_t* chunk_top = chunk->start_address() + chunk_new_sp;
 #ifdef ASSERT
   if (!_empty) {
     intptr_t* retaddr_slot = (_orig_chunk_sp
@@ -732,13 +758,28 @@
   ContinuationHelper::patch_return_address_at(chunk_bottom_retaddr_slot,
                                               chunk->pc());
 
   // We're always writing to a young chunk, so the GC can't see it until the next safepoint.
   chunk->set_sp(chunk_new_sp);
+
   // set chunk->pc to the return address of the topmost frame in the chunk
-  chunk->set_pc(ContinuationHelper::return_address_at(
+  if (_preempt) {
+    // On aarch64/riscv64, the return pc of the top frame won't necessarily be at sp[-1].
+    // Also, on x64, if the top frame is the native wrapper frame, sp[-1] will not
+    // be the pc we used when creating the oopmap. Get the top's frame last pc from
+    // the anchor instead.
+    address last_pc = _last_frame.pc();
+    ContinuationHelper::patch_return_address_at(chunk_top - frame::sender_sp_ret_address_offset(), last_pc);
+    chunk->set_pc(last_pc);
+  } else {
+    chunk->set_pc(ContinuationHelper::return_address_at(
                   _cont_stack_top - frame::sender_sp_ret_address_offset()));
+  }
+
+  if (_monitors_in_lockstack > 0) {
+    freeze_lockstack();
+  }
 
   _cont.write();
 
   log_develop_trace(continuations)("FREEZE CHUNK #" INTPTR_FORMAT " (young)", _cont.hash());
   LogTarget(Trace, continuations) lt;
@@ -800,47 +841,36 @@
 
   return res;
 }
 
 frame FreezeBase::freeze_start_frame() {
-  frame f = _thread->last_frame();
   if (LIKELY(!_preempt)) {
-    return freeze_start_frame_yield_stub(f);
+    return freeze_start_frame_yield_stub();
   } else {
-    return freeze_start_frame_safepoint_stub(f);
+    return freeze_start_frame_on_preempt();
   }
 }
 
-frame FreezeBase::freeze_start_frame_yield_stub(frame f) {
+frame FreezeBase::freeze_start_frame_yield_stub() {
+  frame f = _thread->last_frame();
   assert(SharedRuntime::cont_doYield_stub()->contains(f.pc()), "must be");
   f = sender<ContinuationHelper::NonInterpretedUnknownFrame>(f);
   assert(Continuation::is_frame_in_continuation(_thread->last_continuation(), f), "");
   return f;
 }
 
-frame FreezeBase::freeze_start_frame_safepoint_stub(frame f) {
-#if (defined(X86) || defined(AARCH64) || defined(RISCV64)) && !defined(ZERO)
-  f.set_fp(f.real_fp()); // f.set_fp(*Frame::callee_link_address(f)); // ????
-#else
-  Unimplemented();
-#endif
-  if (!Interpreter::contains(f.pc())) {
-    assert(ContinuationHelper::Frame::is_stub(f.cb()), "must be");
-    assert(f.oop_map() != nullptr, "must be");
-
-    if (Interpreter::contains(ContinuationHelper::StubFrame::return_pc(f))) {
-      f = sender<ContinuationHelper::StubFrame>(f); // Safepoint stub in interpreter
-    }
-  }
-  assert(Continuation::is_frame_in_continuation(_thread->last_continuation(), f), "");
-  return f;
+frame FreezeBase::freeze_start_frame_on_preempt() {
+  assert(_last_frame.sp() == _thread->last_frame().sp(), "_last_frame should be already initialized");
+  assert(Continuation::is_frame_in_continuation(_thread->last_continuation(), _last_frame), "");
+  return _last_frame;
 }
 
 // The parameter callee_argsize includes metadata that has to be part of caller/callee overlap.
 NOINLINE freeze_result FreezeBase::recurse_freeze(frame& f, frame& caller, int callee_argsize, bool callee_interpreted, bool top) {
   assert(f.unextended_sp() < _bottom_address, ""); // see recurse_freeze_java_frame
-  assert(f.is_interpreted_frame() || ((top && _preempt) == ContinuationHelper::Frame::is_stub(f.cb())), "");
+  assert(f.is_interpreted_frame() || ((top && _preempt) == ContinuationHelper::Frame::is_stub(f.cb()))
+         || ((top && _preempt) == f.is_native_frame()), "");
 
   if (stack_overflow()) {
     return freeze_exception;
   }
 
@@ -849,19 +879,15 @@
       // special native frame
       return freeze_pinned_native;
     }
     return recurse_freeze_compiled_frame(f, caller, callee_argsize, callee_interpreted);
   } else if (f.is_interpreted_frame()) {
-    assert((_preempt && top) || !f.interpreter_frame_method()->is_native(), "");
-    if (_preempt && top && f.interpreter_frame_method()->is_native()) {
-      // int native entry
-      return freeze_pinned_native;
-    }
-
+    assert(!f.interpreter_frame_method()->is_native() || (top && _preempt), "");
     return recurse_freeze_interpreted_frame(f, caller, callee_argsize, callee_interpreted);
-  } else if (_preempt && top && ContinuationHelper::Frame::is_stub(f.cb())) {
-    return recurse_freeze_stub_frame(f, caller);
+  } else if (top && _preempt) {
+    assert(f.is_native_frame() || f.is_runtime_frame(), "");
+    return f.is_native_frame() ? recurse_freeze_native_frame(f, caller) : recurse_freeze_stub_frame(f, caller);
   } else {
     return freeze_pinned_native;
   }
 }
 
@@ -918,10 +944,11 @@
 // The parameter argsize_md includes metadata that has to be part of caller/callee overlap.
 // See also StackChunkFrameStream<frame_kind>::frame_size()
 freeze_result FreezeBase::finalize_freeze(const frame& callee, frame& caller, int argsize_md) {
   int argsize = argsize_md - frame::metadata_words_at_top;
   assert(callee.is_interpreted_frame()
+    || ContinuationHelper::Frame::is_stub(callee.cb())
     || callee.cb()->as_nmethod()->is_osr_method()
     || argsize == _cont.argsize(), "argsize: %d cont.argsize: %d", argsize, _cont.argsize());
   log_develop_trace(continuations)("bottom: " INTPTR_FORMAT " count %d size: %d argsize: %d",
     p2i(_bottom_address), _frames, _freeze_size << LogBytesPerWord, argsize);
 
@@ -960,10 +987,12 @@
 
   assert(chunk == nullptr || chunk->is_empty()
           || unextended_sp == chunk->to_offset(StackChunkFrameStream<ChunkFrames::Mixed>(chunk).unextended_sp()), "");
   assert(chunk != nullptr || unextended_sp < _freeze_size, "");
 
+  _freeze_size += _monitors_in_lockstack;
+
   // _barriers can be set to true by an allocation in freeze_fast, in which case the chunk is available
   bool allocated_old_in_freeze_fast = _barriers;
   assert(!allocated_old_in_freeze_fast || (unextended_sp >= _freeze_size && chunk->is_empty()),
     "Chunk allocated in freeze_fast is of insufficient size "
     "unextended_sp: %d size: %d is_empty: %d", unextended_sp, _freeze_size, chunk->is_empty());
@@ -1014,23 +1043,35 @@
   assert(!_barriers || chunk->is_empty(), "");
 
   assert(!chunk->is_empty() || StackChunkFrameStream<ChunkFrames::Mixed>(chunk).is_done(), "");
   assert(!chunk->is_empty() || StackChunkFrameStream<ChunkFrames::Mixed>(chunk).to_frame().is_empty(), "");
 
+  if (_preempt) {
+    frame f = _thread->last_frame();
+    if (f.is_interpreted_frame()) {
+      // Do it now that we know freezing will be successful.
+      prepare_freeze_interpreted_top_frame(f);
+    }
+  }
+
   // We unwind frames after the last safepoint so that the GC will have found the oops in the frames, but before
   // writing into the chunk. This is so that an asynchronous stack walk (not at a safepoint) that suspends us here
   // will either see no continuation or a consistent chunk.
   unwind_frames();
 
-  chunk->set_max_thawing_size(chunk->max_thawing_size() + _freeze_size - frame::metadata_words);
+  chunk->set_max_thawing_size(chunk->max_thawing_size() + _freeze_size - _monitors_in_lockstack - frame::metadata_words);
 
   if (lt.develop_is_enabled()) {
     LogStream ls(lt);
     ls.print_cr("top chunk:");
     chunk->print_on(&ls);
   }
 
+  if (_monitors_in_lockstack > 0) {
+    freeze_lockstack();
+  }
+
   // The topmost existing frame in the chunk; or an empty frame if the chunk is empty
   caller = StackChunkFrameStream<ChunkFrames::Mixed>(chunk).to_frame();
 
   DEBUG_ONLY(_last_write = caller.unextended_sp() + (empty_chunk ? argsize_md : overlap);)
 
@@ -1046,11 +1087,11 @@
 
   assert(!empty || Continuation::is_continuation_entry_frame(callee, nullptr), "");
 
   frame entry = sender(callee);
 
-  assert(Continuation::is_return_barrier_entry(entry.pc()) || Continuation::is_continuation_enterSpecial(entry), "");
+  assert((!empty && Continuation::is_return_barrier_entry(entry.pc())) || (empty && Continuation::is_continuation_enterSpecial(entry)), "");
   assert(callee.is_interpreted_frame() || entry.sp() == entry.unextended_sp(), "");
 #endif
 
   return freeze_ok_bottom;
 }
@@ -1111,12 +1152,12 @@
 
   Method* frame_method = ContinuationHelper::Frame::frame_method(f);
   // including metadata between f and its args
   const int argsize = ContinuationHelper::InterpretedFrame::stack_argsize(f) + frame::metadata_words_at_top;
 
-  log_develop_trace(continuations)("recurse_freeze_interpreted_frame %s _size: %d fsize: %d argsize: %d",
-    frame_method->name_and_sig_as_C_string(), _freeze_size, fsize, argsize);
+  log_develop_trace(continuations)("recurse_freeze_interpreted_frame %s _size: %d fsize: %d argsize: %d callee_interpreted: %d",
+    frame_method->name_and_sig_as_C_string(), _freeze_size, fsize, argsize, callee_interpreted);
   // we'd rather not yield inside methods annotated with @JvmtiMountTransition
   assert(!ContinuationHelper::Frame::frame_method(f)->jvmti_mount_transition(), "");
 
   freeze_result result = recurse_freeze_java_frame<ContinuationHelper::InterpretedFrame>(f, caller, fsize, argsize);
   if (UNLIKELY(result > freeze_ok_bottom)) {
@@ -1202,48 +1243,84 @@
   caller = hf;
   return freeze_ok;
 }
 
 NOINLINE freeze_result FreezeBase::recurse_freeze_stub_frame(frame& f, frame& caller) {
-  intptr_t* const stack_frame_top = ContinuationHelper::StubFrame::frame_top(f, 0, 0);
+  DEBUG_ONLY(frame fsender = sender(f);)
+  assert(fsender.is_compiled_frame(), "sender should be compiled frame");
+
+  intptr_t* const stack_frame_top = ContinuationHelper::StubFrame::frame_top(f);
   const int fsize = f.cb()->frame_size();
 
   log_develop_trace(continuations)("recurse_freeze_stub_frame %s _size: %d fsize: %d :: " INTPTR_FORMAT " - " INTPTR_FORMAT,
     f.cb()->name(), _freeze_size, fsize, p2i(stack_frame_top), p2i(stack_frame_top+fsize));
 
-  // recurse_freeze_java_frame and freeze inlined here because we need to use a full RegisterMap for lock ownership
-  NOT_PRODUCT(_frames++;)
-  _freeze_size += fsize;
+  freeze_result result = recurse_freeze_java_frame<ContinuationHelper::StubFrame>(f, caller, fsize, 0);
+  if (UNLIKELY(result > freeze_ok_bottom)) {
+    return result;
+  }
 
-  RegisterMap map(_cont.thread(),
-                  RegisterMap::UpdateMap::include,
-                  RegisterMap::ProcessFrames::skip,
-                  RegisterMap::WalkContinuation::skip);
-  map.set_include_argument_oops(false);
-  ContinuationHelper::update_register_map<ContinuationHelper::StubFrame>(f, &map);
-  f.oop_map()->update_register_map(&f, &map); // we have callee-save registers in this case
-  frame senderf = sender<ContinuationHelper::StubFrame>(f);
-  assert(senderf.unextended_sp() < _bottom_address - 1, "");
-  assert(senderf.is_compiled_frame(), "");
-
-  if (UNLIKELY(senderf.oop_map() == nullptr)) {
-    // native frame
+  assert(result == freeze_ok, "should have caller");
+  DEBUG_ONLY(before_freeze_java_frame(f, caller, fsize, 0, false /*is_bottom_frame*/);)
+
+  frame hf = new_heap_frame<ContinuationHelper::StubFrame>(f, caller);
+  intptr_t* heap_frame_top = ContinuationHelper::StubFrame::frame_top(hf);
+
+  copy_to_chunk(stack_frame_top, heap_frame_top, fsize);
+
+  if (caller.is_interpreted_frame()) {
+    _total_align_size += frame::align_wiggle;
+  }
+
+  patch(f, hf, caller, false /*is_bottom_frame*/);
+
+  DEBUG_ONLY(after_freeze_java_frame(hf, false /*is_bottom_frame*/);)
+
+  caller = hf;
+  return freeze_ok;
+}
+
+NOINLINE freeze_result FreezeBase::recurse_freeze_native_frame(frame& f, frame& caller) {
+  if (!f.cb()->as_nmethod()->method()->is_object_wait0()) {
+    assert(f.cb()->as_nmethod()->method()->is_synchronized(), "");
+    // Synchronized native method case. Unlike the interpreter native wrapper, the compiled
+    // native wrapper tries to acquire the monitor after marshalling the arguments from the
+    // caller into the native convention. This is so that we have a valid oopMap in case of
+    // having to block in the slow path. But that would require freezing those registers too
+    // and then fixing them back on thaw in case of oops. To avoid complicating things and
+    // given that this would be a rare case anyways just pin the vthread to the carrier.
     return freeze_pinned_native;
   }
 
-  freeze_result result = recurse_freeze_compiled_frame(senderf, caller, 0, 0); // This might be deoptimized
+  intptr_t* const stack_frame_top = ContinuationHelper::NativeFrame::frame_top(f);
+  // There are no stackargs but argsize must include the metadata
+  const int argsize = frame::metadata_words_at_top;
+  const int fsize = f.cb()->frame_size() + argsize;
+
+  log_develop_trace(continuations)("recurse_freeze_native_frame %s _size: %d fsize: %d :: " INTPTR_FORMAT " - " INTPTR_FORMAT,
+    f.cb()->name(), _freeze_size, fsize, p2i(stack_frame_top), p2i(stack_frame_top+fsize));
+
+  freeze_result result = recurse_freeze_java_frame<ContinuationHelper::NativeFrame>(f, caller, fsize, argsize);
   if (UNLIKELY(result > freeze_ok_bottom)) {
     return result;
   }
-  assert(result != freeze_ok_bottom, "");
-  assert(!caller.is_interpreted_frame(), "");
 
-  DEBUG_ONLY(before_freeze_java_frame(f, caller, fsize, 0, false);)
-  frame hf = new_heap_frame<ContinuationHelper::StubFrame>(f, caller);
-  intptr_t* heap_frame_top = ContinuationHelper::StubFrame::frame_top(hf, 0, 0);
+  assert(result == freeze_ok, "should have caller frame");
+  DEBUG_ONLY(before_freeze_java_frame(f, caller, fsize, argsize, false /* is_bottom_frame */);)
+
+  frame hf = new_heap_frame<ContinuationHelper::NativeFrame>(f, caller);
+  intptr_t* heap_frame_top = ContinuationHelper::NativeFrame::frame_top(hf);
+
   copy_to_chunk(stack_frame_top, heap_frame_top, fsize);
-  DEBUG_ONLY(after_freeze_java_frame(hf, false);)
+
+  if (caller.is_interpreted_frame()) {
+    _total_align_size += frame::align_wiggle;
+  }
+
+  patch(f, hf, caller, false /* is_bottom_frame */);
+
+  DEBUG_ONLY(after_freeze_java_frame(hf, false /* is_bottom_frame */);)
 
   caller = hf;
   return freeze_ok;
 }
 
@@ -1262,10 +1339,12 @@
   chunk->set_sp(chunk->to_offset(top.sp()));
   chunk->set_pc(top.pc());
 
   chunk->set_max_thawing_size(chunk->max_thawing_size() + _total_align_size);
 
+  assert(chunk->sp_address() - chunk->start_address() >= _monitors_in_lockstack, "clash with lockstack");
+
   // At this point the chunk is consistent
 
   if (UNLIKELY(_barriers)) {
     log_develop_trace(continuations)("do barriers on old chunk");
     // Serial and Parallel GC can allocate objects directly into the old generation.
@@ -1291,10 +1370,11 @@
   if (lt.develop_is_enabled()) {
     LogStream ls(lt);
     ls.print_cr("top hframe after (freeze):");
     assert(_cont.last_frame().is_heap_frame(), "should be");
     _cont.last_frame().print_on(&ls);
+    DEBUG_ONLY(print_frame_layout(top, false, &ls);)
   }
 
   assert(_cont.chunk_invariant(), "");
 }
 
@@ -1325,10 +1405,11 @@
     assert(_stack_size <= max_jint, "");
     assert(_word_size > _stack_size, "");
 
     // zero out fields (but not the stack)
     const size_t hs = oopDesc::header_size();
+    oopDesc::set_klass_gap(mem, 0);
     Copy::fill_to_aligned_words(mem + hs, vmClasses::StackChunk_klass()->size_helper() - hs);
 
     int bottom = (int)_stack_size - _argsize_md;
 
     jdk_internal_vm_StackChunk::set_size(mem, (int)_stack_size);
@@ -1433,10 +1514,12 @@
   assert(chunk->max_thawing_size() == 0, "");
   assert(chunk->pc() == nullptr, "");
   assert(chunk->is_empty(), "");
   assert(chunk->flags() == 0, "");
   assert(chunk->is_gc_mode() == false, "");
+  assert(chunk->lockstack_size() == 0, "");
+  assert(chunk->object_waiter() == nullptr, "");
 
   // fields are uninitialized
   chunk->set_parent_access<IS_DEST_UNINITIALIZED>(_cont.last_nonempty_chunk());
   chunk->set_cont_access<IS_DEST_UNINITIALIZED>(_cont.continuation());
 
@@ -1505,10 +1588,43 @@
     ContinuationWrapper::SafepointOp so(Thread::current(), cont);
     JvmtiExport::continuation_yield_cleanup(JavaThread::current(), num_frames);
   }
   invalidate_jvmti_stack(thread);
 }
+
+static void jvmti_mount_end(JavaThread* current, ContinuationWrapper& cont, frame top) {
+  assert(current->vthread() != nullptr, "must be");
+
+  HandleMarkCleaner hm(current);
+  Handle vth(current, current->vthread());
+
+  ContinuationWrapper::SafepointOp so(current, cont);
+
+  // Since we might safepoint set the anchor so that the stack can we walked.
+  set_anchor(current, top.sp());
+
+  JRT_BLOCK
+    current->rebind_to_jvmti_thread_state_of(vth());
+    JvmtiVTMSTransitionDisabler::finish_VTMS_transition((jthread)vth.raw_value(), /* is_mount */ true);
+
+    // If pending_jvmti_unmount_event() is true here we are in the preemption
+    // cancelled case. Since we never unmounted we don't post the mount event
+    // and just clear the pending unmount flag.
+    if (current->pending_jvmti_unmount_event()) {
+      current->set_pending_jvmti_unmount_event(false);
+    } else if (JvmtiExport::should_post_vthread_mount()) {
+      JvmtiExport::post_vthread_mount((jthread)vth.raw_value());
+    }
+
+    if (current->pending_contended_entered_event()) {
+      JvmtiExport::post_monitor_contended_entered(current, current->contended_entered_monitor());
+      current->set_contended_entered_monitor(nullptr);
+    }
+  JRT_BLOCK_END
+
+  clear_anchor(current);
+}
 #endif // INCLUDE_JVMTI
 
 #ifdef ASSERT
 static bool monitors_on_stack(JavaThread* thread) {
   ContinuationEntry* ce = thread->last_continuation();
@@ -1517,11 +1633,12 @@
                   RegisterMap::ProcessFrames::include,
                   RegisterMap::WalkContinuation::skip);
   map.set_include_argument_oops(false);
   for (frame f = thread->last_frame(); Continuation::is_frame_in_continuation(ce, f); f = f.sender(&map)) {
     if ((f.is_interpreted_frame() && ContinuationHelper::InterpretedFrame::is_owning_locks(f)) ||
-        (f.is_compiled_frame() && ContinuationHelper::CompiledFrame::is_owning_locks(map.thread(), &map, f))) {
+        (f.is_compiled_frame() && ContinuationHelper::CompiledFrame::is_owning_locks(map.thread(), &map, f)) ||
+        (f.is_native_frame() && ContinuationHelper::NativeFrame::is_owning_locks(map.thread(), f))) {
       return true;
     }
   }
   return false;
 }
@@ -1534,27 +1651,25 @@
   RegisterMap map(_thread,
                   RegisterMap::UpdateMap::skip,
                   RegisterMap::ProcessFrames::skip,
                   RegisterMap::WalkContinuation::skip);
   map.set_include_argument_oops(false);
-  for (frame f = freeze_start_frame(); Continuation::is_frame_in_continuation(ce, f); f = f.sender(&map)) {
-    if (!f.is_compiled_frame() || f.is_deoptimized_frame()) {
+  int i = 0;
+  for (frame f = freeze_start_frame(); Continuation::is_frame_in_continuation(ce, f); f = f.sender(&map), i++) {
+    if (!((f.is_compiled_frame() && !f.is_deoptimized_frame()) || (i == 0 && (f.is_runtime_frame() || f.is_native_frame())))) {
       return false;
     }
   }
   return true;
 }
 #endif // ASSERT
 
-static inline int freeze_epilog(JavaThread* thread, ContinuationWrapper& cont) {
+static inline int freeze_epilog(ContinuationWrapper& cont) {
   verify_continuation(cont.continuation());
   assert(!cont.is_empty(), "");
-  // This is done for the sake of the enterSpecial frame
-  StackWatermarkSet::after_unwind(thread);
 
   log_develop_debug(continuations)("=== End of freeze cont ### #" INTPTR_FORMAT, cont.hash());
-
   return 0;
 }
 
 static int freeze_epilog(JavaThread* thread, ContinuationWrapper& cont, freeze_result res) {
   if (UNLIKELY(res != freeze_ok)) {
@@ -1562,19 +1677,32 @@
     log_develop_trace(continuations)("=== end of freeze (fail %d)", res);
     return res;
   }
 
   JVMTI_ONLY(jvmti_yield_cleanup(thread, cont)); // can safepoint
-  return freeze_epilog(thread, cont);
+  return freeze_epilog(cont);
 }
 
-template<typename ConfigT>
+static int preempt_epilog(ContinuationWrapper& cont, freeze_result res, frame& old_last_frame) {
+  if (UNLIKELY(res != freeze_ok)) {
+    verify_continuation(cont.continuation());
+    log_develop_trace(continuations)("=== end of freeze (fail %d)", res);
+    return res;
+  }
+
+  patch_return_pc_with_preempt_stub(old_last_frame);
+  cont.tail()->set_preempted(true);
+
+  return freeze_epilog(cont);
+}
+
+template<typename ConfigT, bool preempt>
 static inline int freeze_internal(JavaThread* current, intptr_t* const sp) {
   assert(!current->has_pending_exception(), "");
 
 #ifdef ASSERT
-  log_trace(continuations)("~~~~ freeze sp: " INTPTR_FORMAT, p2i(current->last_continuation()->entry_sp()));
+  log_trace(continuations)("~~~~ freeze sp: " INTPTR_FORMAT "JavaThread: " INTPTR_FORMAT, p2i(current->last_continuation()->entry_sp()), p2i(current));
   log_frames(current);
 #endif
 
   CONT_JFR_ONLY(EventContinuationFreeze event;)
 
@@ -1588,39 +1716,51 @@
   ContinuationWrapper cont(current, oopCont);
   log_develop_debug(continuations)("FREEZE #" INTPTR_FORMAT " " INTPTR_FORMAT, cont.hash(), p2i((oopDesc*)oopCont));
 
   assert(entry->is_virtual_thread() == (entry->scope(current) == java_lang_VirtualThread::vthread_scope()), "");
 
-  assert(monitors_on_stack(current) == ((current->held_monitor_count() - current->jni_monitor_count()) > 0),
+  assert(LockingMode != LM_LEGACY || (monitors_on_stack(current) == ((current->held_monitor_count() - current->jni_monitor_count()) > 0)),
          "Held monitor count and locks on stack invariant: " INT64_FORMAT " JNI: " INT64_FORMAT, (int64_t)current->held_monitor_count(), (int64_t)current->jni_monitor_count());
+  assert(LockingMode == LM_LEGACY || (current->held_monitor_count() == 0 && current->jni_monitor_count() == 0),
+         "Held monitor count should only be used for LM_LEGACY: " INT64_FORMAT " JNI: " INT64_FORMAT, (int64_t)current->held_monitor_count(), (int64_t)current->jni_monitor_count());
 
   if (entry->is_pinned() || current->held_monitor_count() > 0) {
     log_develop_debug(continuations)("PINNED due to critical section/hold monitor");
     verify_continuation(cont.continuation());
     freeze_result res = entry->is_pinned() ? freeze_pinned_cs : freeze_pinned_monitor;
     log_develop_trace(continuations)("=== end of freeze (fail %d)", res);
     // Avoid Thread.yield() loops without safepoint polls.
-    if (SafepointMechanism::should_process(current)) {
+    if (SafepointMechanism::should_process(current) && !preempt) {
       cont.done(); // allow safepoint
       ThreadInVMfromJava tivmfj(current);
     }
     return res;
   }
 
-  Freeze<ConfigT> freeze(current, cont, sp);
+  Freeze<ConfigT> freeze(current, cont, sp, preempt);
 
   assert(!current->cont_fastpath() || freeze.check_valid_fast_path(), "");
   bool fast = UseContinuationFastPath && current->cont_fastpath();
   if (fast && freeze.size_if_fast_freeze_available() > 0) {
     freeze.freeze_fast_existing_chunk();
     CONT_JFR_ONLY(freeze.jfr_info().post_jfr_event(&event, oopCont, current);)
-    freeze_epilog(current, cont);
-    return 0;
+    return !preempt ? freeze_epilog(cont) : preempt_epilog(cont, freeze_ok, freeze.last_frame());
+  }
+
+  if (preempt) {
+    JvmtiSampledObjectAllocEventCollector jsoaec(false);
+    freeze.set_jvmti_event_collector(&jsoaec);
+
+    freeze_result res = fast ? freeze.try_freeze_fast() : freeze.freeze_slow();
+
+    CONT_JFR_ONLY(freeze.jfr_info().post_jfr_event(&event, oopCont, current);)
+    preempt_epilog(cont, res, freeze.last_frame());
+    return res;
   }
 
   log_develop_trace(continuations)("chunk unavailable; transitioning to VM");
-  assert(current == JavaThread::current(), "must be current thread except for preempt");
+  assert(current == JavaThread::current(), "must be current thread");
   JRT_BLOCK
     // delays a possible JvmtiSampledObjectAllocEventCollector in alloc_chunk
     JvmtiSampledObjectAllocEventCollector jsoaec(false);
     freeze.set_jvmti_event_collector(&jsoaec);
 
@@ -1696,10 +1836,11 @@
 
 static int thaw_size(stackChunkOop chunk) {
   int size = chunk->max_thawing_size();
   size += frame::metadata_words; // For the top pc+fp in push_return_frame or top = stack_sp - frame::metadata_words in thaw_fast
   size += 2*frame::align_wiggle; // in case of alignments at the top and bottom
+  size += frame::metadata_words; // for preemption case (see possibly_adjust_frame)
   return size;
 }
 
 // make room on the stack for thaw
 // returns the size in bytes, or 0 on failure
@@ -1751,10 +1892,11 @@
   ContinuationWrapper& _cont;
   CONT_JFR_ONLY(FreezeThawJfrInfo _jfr_info;)
 
   intptr_t* _fastpath;
   bool _barriers;
+  bool _preempted_case;
   intptr_t* _top_unextended_sp_before_thaw;
   int _align_size;
   DEBUG_ONLY(intptr_t* _top_stack_address);
 
   StackChunkFrameStream<ChunkFrames::Mixed> _stream;
@@ -1769,22 +1911,27 @@
     assert (cont.tail() != nullptr, "no last chunk");
     DEBUG_ONLY(_top_stack_address = _cont.entrySP() - thaw_size(cont.tail());)
   }
 
   void clear_chunk(stackChunkOop chunk);
+  template<bool check_stub>
   int remove_top_compiled_frame_from_chunk(stackChunkOop chunk, int &argsize);
   void copy_from_chunk(intptr_t* from, intptr_t* to, int size);
 
   // fast path
   inline void prefetch_chunk_pd(void* start, int size_words);
   void patch_return(intptr_t* sp, bool is_last);
 
-  // slow path
-  NOINLINE intptr_t* thaw_slow(stackChunkOop chunk, bool return_barrier);
+  intptr_t* handle_preempted_continuation(intptr_t* sp, Continuation::preempt_kind preempt_kind, bool fast_case);
+  inline intptr_t* possibly_adjust_frame(frame& top);
+  inline intptr_t* push_cleanup_continuation();
+  void throw_interrupted_exception(JavaThread* current, frame& top);
+
+  void recurse_thaw(const frame& heap_frame, frame& caller, int num_frames, bool top_on_preempt_case);
+  void finish_thaw(frame& f);
 
 private:
-  void recurse_thaw(const frame& heap_frame, frame& caller, int num_frames, bool top);
   template<typename FKind> bool recurse_thaw_java_frame(frame& caller, int num_frames);
   void finalize_thaw(frame& entry, int argsize);
 
   inline bool seen_by_gc();
 
@@ -1794,16 +1941,17 @@
   void clear_bitmap_bits(address start, address end);
 
   NOINLINE void recurse_thaw_interpreted_frame(const frame& hf, frame& caller, int num_frames);
   void recurse_thaw_compiled_frame(const frame& hf, frame& caller, int num_frames, bool stub_caller);
   void recurse_thaw_stub_frame(const frame& hf, frame& caller, int num_frames);
-  void finish_thaw(frame& f);
+  void recurse_thaw_native_frame(const frame& hf, frame& caller, int num_frames);
 
   void push_return_frame(frame& f);
   inline frame new_entry_frame();
   template<typename FKind> frame new_stack_frame(const frame& hf, frame& caller, bool bottom);
   inline void patch_pd(frame& f, const frame& sender);
+  inline void patch_pd(frame& f, intptr_t* caller_sp);
   inline intptr_t* align(const frame& hf, intptr_t* frame_sp, frame& caller, bool bottom);
 
   void maybe_set_fastpath(intptr_t* sp) { if (sp > _fastpath) _fastpath = sp; }
 
   static inline void derelativize_interpreted_frame_metadata(const frame& hf, const frame& f);
@@ -1823,11 +1971,13 @@
            && !chunk->has_thaw_slowpath_condition()
            && !PreserveFramePointer;
   }
 
   inline intptr_t* thaw(Continuation::thaw_kind kind);
+  template<bool check_stub = false>
   NOINLINE intptr_t* thaw_fast(stackChunkOop chunk);
+  NOINLINE intptr_t* thaw_slow(stackChunkOop chunk, Continuation::thaw_kind kind);
   inline void patch_caller_links(intptr_t* sp, intptr_t* bottom);
 };
 
 template <typename ConfigT>
 inline intptr_t* Thaw<ConfigT>::thaw(Continuation::thaw_kind kind) {
@@ -1839,11 +1989,11 @@
   assert(chunk != nullptr, "guaranteed by prepare_thaw");
   assert(!chunk->is_empty(), "guaranteed by prepare_thaw");
 
   _barriers = chunk->requires_barriers();
   return (LIKELY(can_thaw_fast(chunk))) ? thaw_fast(chunk)
-                                        : thaw_slow(chunk, kind != Continuation::thaw_top);
+                                        : thaw_slow(chunk, kind);
 }
 
 class ReconstructedStack : public StackObj {
   intptr_t* _base;  // _cont.entrySP(); // top of the entry frame
   int _thaw_size;
@@ -1872,21 +2022,43 @@
 inline void ThawBase::clear_chunk(stackChunkOop chunk) {
   chunk->set_sp(chunk->bottom());
   chunk->set_max_thawing_size(0);
 }
 
+template<bool check_stub>
 int ThawBase::remove_top_compiled_frame_from_chunk(stackChunkOop chunk, int &argsize) {
   bool empty = false;
   StackChunkFrameStream<ChunkFrames::CompiledOnly> f(chunk);
   DEBUG_ONLY(intptr_t* const chunk_sp = chunk->start_address() + chunk->sp();)
   assert(chunk_sp == f.sp(), "");
   assert(chunk_sp == f.unextended_sp(), "");
 
-  const int frame_size = f.cb()->frame_size();
+  int frame_size = f.cb()->frame_size();
   argsize = f.stack_argsize();
 
-  f.next(SmallRegisterMap::instance(), true /* stop */);
+  assert(!f.is_stub() || check_stub, "");
+  if (check_stub && f.is_stub()) {
+    // If we don't thaw the top compiled frame too, after restoring the saved
+    // registers back in Java, we would hit the return barrier to thaw one more
+    // frame effectively overwritting the restored registers during that call.
+    f.next(SmallRegisterMap::instance(), true /* stop */);
+    assert(!f.is_done(), "");
+
+    f.get_cb();
+    assert(f.is_compiled(), "");
+    frame_size += f.cb()->frame_size();
+    argsize = f.stack_argsize();
+
+    if (f.cb()->as_nmethod()->is_marked_for_deoptimization()) {
+      // The caller of the runtime stub when the continuation is preempted is not at a
+      // Java call instruction, and so cannot rely on nmethod patching for deopt.
+      log_develop_trace(continuations)("Deoptimizing runtime stub caller");
+      f.to_frame().deoptimize(nullptr); // the null thread simply avoids the assertion in deoptimize which we're not set up for
+    }
+  }
+
+  f.next(SmallRegisterMap::instance, true /* stop */);
   empty = f.is_done();
   assert(!empty || argsize == chunk->argsize(), "");
 
   if (empty) {
     clear_chunk(chunk);
@@ -1927,10 +2099,11 @@
     sp - frame::sender_sp_ret_address_offset(),
     pc);
 }
 
 template <typename ConfigT>
+template<bool check_stub>
 NOINLINE intptr_t* Thaw<ConfigT>::thaw_fast(stackChunkOop chunk) {
   assert(chunk == _cont.tail(), "");
   assert(!chunk->has_mixed_frames(), "");
   assert(!chunk->requires_barriers(), "");
   assert(!chunk->has_bitmap(), "");
@@ -1960,11 +2133,11 @@
     clear_chunk(chunk);
     thaw_size = full_chunk_size;
     empty = true;
   } else { // thaw a single frame
     partial = true;
-    thaw_size = remove_top_compiled_frame_from_chunk(chunk, argsize);
+    thaw_size = remove_top_compiled_frame_from_chunk<check_stub>(chunk, argsize);
     empty = chunk->is_empty();
   }
 
   // Are we thawing the last frame(s) in the continuation
   const bool is_last = empty && chunk->parent() == nullptr;
@@ -2016,15 +2189,82 @@
 
 inline bool ThawBase::seen_by_gc() {
   return _barriers || _cont.tail()->is_gc_mode();
 }
 
-NOINLINE intptr_t* ThawBase::thaw_slow(stackChunkOop chunk, bool return_barrier) {
+static inline void relativize_chunk_concurrently(stackChunkOop chunk) {
+#if INCLUDE_ZGC || INCLUDE_SHENANDOAHGC
+  if (UseZGC || UseShenandoahGC) {
+    chunk->relativize_derived_pointers_concurrently();
+  }
+#endif
+}
+
+template <typename ConfigT>
+NOINLINE intptr_t* Thaw<ConfigT>::thaw_slow(stackChunkOop chunk, Continuation::thaw_kind kind) {
+  Continuation::preempt_kind preempt_kind;
+  bool retry_fast_path = false;
+
+  _preempted_case = chunk->preempted();
+  if (_preempted_case) {
+    if (chunk->object_waiter() != nullptr) {
+      // Mounted again after preemption. Resume the pending monitor operation,
+      // which will be either a monitorenter or Object.wait() call.
+      assert(chunk->current_pending_monitor() != nullptr || chunk->current_waiting_monitor() != nullptr, "");
+      ObjectWaiter* waiter = chunk->object_waiter();
+      ObjectMonitor* mon = waiter->monitor();
+      preempt_kind = waiter->is_wait() ? Continuation::freeze_on_wait : Continuation::freeze_on_monitorenter;
+
+      bool mon_acquired = mon->resume_operation(_thread, waiter, _cont);
+      assert(!mon_acquired || mon->is_owner(_thread), "invariant");
+      if (!mon_acquired) {
+        // Failed to aquire monitor. Return to enterSpecial to unmount again.
+        return push_cleanup_continuation();
+      }
+      chunk = _cont.tail();  // reload oop in case of safepoint in resume_operation (if posting JVMTI events).
+    } else {
+      // Preemption cancelled in moniterenter case. We actually acquired
+      // the monitor after freezing all frames so nothing to do.
+      preempt_kind = Continuation::freeze_on_monitorenter;
+    }
+    // Call this first to avoid racing with GC threads later when modifying the chunk flags.
+    relativize_chunk_concurrently(chunk);
+    chunk->set_preempted(false);
+    retry_fast_path = true;
+  } else {
+    relativize_chunk_concurrently(chunk);
+  }
+
+  // On first thaw after freeze restore oops to the lockstack if any.
+  assert(chunk->lockstack_size() == 0 || kind == Continuation::thaw_top, "");
+  if (kind == Continuation::thaw_top && chunk->lockstack_size() > 0) {
+    int lockStackSize = chunk->lockstack_size();
+    assert(lockStackSize > 0, "should be");
+
+    oop tmp_lockstack[8];
+    chunk->copy_lockstack(tmp_lockstack);
+    _thread->lock_stack().move_from_address(tmp_lockstack, lockStackSize);
+
+    chunk->set_lockstack_size(0);
+    chunk->set_has_lockstack(false);
+    retry_fast_path = true;
+  }
+
+  // Retry the fast path now that we possibly cleared the FLAG_HAS_LOCKSTACK
+  // and FLAG_PREEMPTED flags from the stackChunk.
+  if (retry_fast_path && can_thaw_fast(chunk)) {
+    intptr_t* sp = thaw_fast<true>(chunk);
+    if (_preempted_case) {
+      return handle_preempted_continuation(sp, preempt_kind, true /* fast_case */);
+    }
+    return sp;
+  }
+
   LogTarget(Trace, continuations) lt;
   if (lt.develop_is_enabled()) {
     LogStream ls(lt);
-    ls.print_cr("thaw slow return_barrier: %d " INTPTR_FORMAT, return_barrier, p2i(chunk));
+    ls.print_cr("thaw slow return_barrier: %d " INTPTR_FORMAT, kind, p2i(chunk));
     chunk->print_on(true, &ls);
   }
 
 #if CONT_JFR
   EventContinuationThawSlow e;
@@ -2034,11 +2274,11 @@
   }
 #endif
 
   DEBUG_ONLY(_frames = 0;)
   _align_size = 0;
-  int num_frames = (return_barrier ? 1 : 2);
+  int num_frames = kind == Continuation::thaw_top ? 2 : 1;
 
   _stream = StackChunkFrameStream<ChunkFrames::Mixed>(chunk);
   _top_unextended_sp_before_thaw = _stream.unextended_sp();
 
   frame heap_frame = _stream.to_frame();
@@ -2047,40 +2287,37 @@
     ls.print_cr("top hframe before (thaw):");
     assert(heap_frame.is_heap_frame(), "should have created a relative frame");
     heap_frame.print_value_on(&ls);
   }
 
-#if INCLUDE_ZGC || INCLUDE_SHENANDOAHGC
-  if (UseZGC || UseShenandoahGC) {
-    _cont.tail()->relativize_derived_pointers_concurrently();
-  }
-#endif
-
   frame caller; // the thawed caller on the stack
-  recurse_thaw(heap_frame, caller, num_frames, true);
+  recurse_thaw(heap_frame, caller, num_frames, _preempted_case);
   finish_thaw(caller); // caller is now the topmost thawed frame
   _cont.write();
 
   assert(_cont.chunk_invariant(), "");
 
   JVMTI_ONLY(invalidate_jvmti_stack(_thread));
 
   _thread->set_cont_fastpath(_fastpath);
 
   intptr_t* sp = caller.sp();
+
+  if (_preempted_case) {
+    return handle_preempted_continuation(sp, preempt_kind, false /* fast_case */);
+  }
   return sp;
 }
 
-void ThawBase::recurse_thaw(const frame& heap_frame, frame& caller, int num_frames, bool top) {
+void ThawBase::recurse_thaw(const frame& heap_frame, frame& caller, int num_frames, bool top_on_preempt_case) {
   log_develop_debug(continuations)("thaw num_frames: %d", num_frames);
   assert(!_cont.is_empty(), "no more frames");
   assert(num_frames > 0, "");
   assert(!heap_frame.is_empty(), "");
 
-  if (top && heap_frame.is_safepoint_blob_frame()) {
-    assert(ContinuationHelper::Frame::is_stub(heap_frame.cb()), "cb: %s", heap_frame.cb()->name());
-    recurse_thaw_stub_frame(heap_frame, caller, num_frames);
+  if (top_on_preempt_case && (heap_frame.is_native_frame() || heap_frame.is_runtime_frame())) {
+    heap_frame.is_native_frame() ? recurse_thaw_native_frame(heap_frame, caller, 2) : recurse_thaw_stub_frame(heap_frame, caller, 2);
   } else if (!heap_frame.is_interpreted_frame()) {
     recurse_thaw_compiled_frame(heap_frame, caller, num_frames, false);
   } else {
     recurse_thaw_interpreted_frame(heap_frame, caller, num_frames);
   }
@@ -2106,11 +2343,11 @@
 
   if (num_frames == 1 || _stream.is_done()) { // end recursion
     finalize_thaw(caller, FKind::interpreted ? 0 : argsize);
     return true; // bottom
   } else { // recurse
-    recurse_thaw(_stream.to_frame(), caller, num_frames - 1, false);
+    recurse_thaw(_stream.to_frame(), caller, num_frames - 1, false /* top_on_preempt_case */);
     return false;
   }
 }
 
 void ThawBase::finalize_thaw(frame& entry, int argsize) {
@@ -2196,10 +2433,65 @@
   stackChunkOop chunk = _cont.tail();
   chunk->bitmap().clear_range(chunk->bit_index_for(start), chunk->bit_index_for(effective_end));
   assert(effective_end == end || !chunk->bitmap().at(chunk->bit_index_for(effective_end)), "bit should not be set");
 }
 
+intptr_t* ThawBase::handle_preempted_continuation(intptr_t* sp, Continuation::preempt_kind preempt_kind, bool fast_case) {
+  assert(preempt_kind == Continuation::freeze_on_wait || preempt_kind == Continuation::freeze_on_monitorenter, "");
+  frame top(sp);
+  assert(top.pc() == *(address*)(sp - frame::sender_sp_ret_address_offset()), "");
+
+#if INCLUDE_JVMTI
+  // Finish the VTMS transition.
+  assert(_thread->is_in_VTMS_transition(), "must be");
+  bool is_vthread = Continuation::continuation_scope(_cont.continuation()) == java_lang_VirtualThread::vthread_scope();
+  if (is_vthread) {
+    if (JvmtiVTMSTransitionDisabler::VTMS_notify_jvmti_events()) {
+      jvmti_mount_end(_thread, _cont, top);
+    } else {
+      _thread->set_is_in_VTMS_transition(false);
+      java_lang_Thread::set_is_in_VTMS_transition(_thread->vthread(), false);
+    }
+  }
+#endif
+
+  if (fast_case) {
+    // If we thawed in the slow path the runtime stub/native wrapper frame already
+    // has the correct fp (see ThawBase::new_stack_frame). On the fast path though,
+    // we copied the original fp at the time of freeze which now will have to be fixed.
+    assert(top.is_runtime_frame() || top.is_native_frame(), "");
+    int fsize = top.cb()->frame_size();
+    patch_pd(top, sp + fsize);
+  }
+
+  if (preempt_kind == Continuation::freeze_on_wait) {
+    // Check now if we need to throw IE exception.
+    if (_thread->pending_interrupted_exception()) {
+      throw_interrupted_exception(_thread, top);
+      _thread->set_pending_interrupted_exception(false);
+    }
+  } else if (top.is_runtime_frame()) {
+    // The continuation might now run on a different platform thread than the previous time so
+    // we need to adjust the current thread saved in the stub frame before restoring registers.
+    JavaThread** thread_addr = frame::saved_thread_address(top);
+    if (thread_addr != nullptr) *thread_addr = _thread;
+    // Some platforms require the size of the runtime frame to be adjusted.
+    sp = possibly_adjust_frame(top);
+  }
+  return sp;
+}
+
+void ThawBase::throw_interrupted_exception(JavaThread* current, frame& top) {
+  ContinuationWrapper::SafepointOp so(current, _cont);
+  // Since we might safepoint set the anchor so that the stack can we walked.
+  set_anchor(current, top.sp());
+  JRT_BLOCK
+    THROW(vmSymbols::java_lang_InterruptedException());
+  JRT_BLOCK_END
+  clear_anchor(current);
+}
+
 NOINLINE void ThawBase::recurse_thaw_interpreted_frame(const frame& hf, frame& caller, int num_frames) {
   assert(hf.is_interpreted_frame(), "");
 
   if (UNLIKELY(seen_by_gc())) {
     _cont.tail()->do_barriers<stackChunkOopDesc::BarrierType::Store>(_stream, SmallRegisterMap::instance());
@@ -2239,11 +2531,13 @@
 
   CONT_JFR_ONLY(_jfr_info.record_interpreted_frame();)
 
   maybe_set_fastpath(f.sp());
 
-  const int locals = hf.interpreter_frame_method()->max_locals();
+  Method* m = hf.interpreter_frame_method();
+  // For native frames we need to count parameters, possible alignment, plus the 2 extra words (temp oop/result handler).
+  const int locals = !m->is_native() ? m->max_locals() : m->size_of_parameters() + frame::align_wiggle + 2;
 
   if (!is_bottom_frame) {
     // can only fix caller once this frame is thawed (due to callee saved regs)
     _cont.tail()->fix_thawed_frame(caller, SmallRegisterMap::instance());
   } else if (_cont.tail()->has_bitmap() && locals > 0) {
@@ -2256,12 +2550,12 @@
   DEBUG_ONLY(after_thaw_java_frame(f, is_bottom_frame);)
   caller = f;
 }
 
 void ThawBase::recurse_thaw_compiled_frame(const frame& hf, frame& caller, int num_frames, bool stub_caller) {
-  assert(!hf.is_interpreted_frame(), "");
-  assert(_cont.is_preempted() || !stub_caller, "stub caller not at preemption");
+  assert(hf.is_compiled_frame(), "");
+  assert(_preempted_case || !stub_caller, "stub caller not at preemption");
 
   if (!stub_caller && UNLIKELY(seen_by_gc())) { // recurse_thaw_stub_frame already invoked our barriers with a full regmap
     _cont.tail()->do_barriers<stackChunkOopDesc::BarrierType::Store>(_stream, SmallRegisterMap::instance());
   }
 
@@ -2270,11 +2564,11 @@
   DEBUG_ONLY(before_thaw_java_frame(hf, caller, is_bottom_frame, num_frames);)
 
   assert(caller.sp() == caller.unextended_sp(), "");
 
   if ((!is_bottom_frame && caller.is_interpreted_frame()) || (is_bottom_frame && Interpreter::contains(_cont.tail()->pc()))) {
-    _align_size += frame::align_wiggle; // we add one whether or not we've aligned because we add it in freeze_interpreted_frame
+    _align_size += frame::align_wiggle; // we add one whether or not we've aligned because we add it in recurse_freeze_compiled_frame
   }
 
   // new_stack_frame must construct the resulting frame using hf.pc() rather than hf.raw_pc() because the frame is not
   // yet laid out in the stack, and so the original_pc is not stored in it.
   // As a result, f.is_deoptimized_frame() is always false and we must test hf to know if the frame is deoptimized.
@@ -2303,14 +2597,13 @@
   // f.is_deoptimized_frame() is always false and we must test hf.is_deoptimized_frame() (see comment above)
   assert(!f.is_deoptimized_frame(), "");
   if (hf.is_deoptimized_frame()) {
     maybe_set_fastpath(f.sp());
   } else if (_thread->is_interp_only_mode()
-              || (_cont.is_preempted() && f.cb()->as_nmethod()->is_marked_for_deoptimization())) {
+              || (stub_caller && f.cb()->as_nmethod()->is_marked_for_deoptimization())) {
     // The caller of the safepoint stub when the continuation is preempted is not at a call instruction, and so
     // cannot rely on nmethod patching for deopt.
-    assert(_thread->is_interp_only_mode() || stub_caller, "expected a stub-caller");
 
     log_develop_trace(continuations)("Deoptimizing thawed frame");
     DEBUG_ONLY(ContinuationHelper::Frame::patch_pc(f, nullptr));
 
     f.deoptimize(nullptr); // the null thread simply avoids the assertion in deoptimize which we're not set up for
@@ -2334,53 +2627,106 @@
 }
 
 void ThawBase::recurse_thaw_stub_frame(const frame& hf, frame& caller, int num_frames) {
   DEBUG_ONLY(_frames++;)
 
-  {
+  if (UNLIKELY(seen_by_gc())) {
+    // Process the stub's caller here since we might need the full map.
     RegisterMap map(nullptr,
                     RegisterMap::UpdateMap::include,
                     RegisterMap::ProcessFrames::skip,
                     RegisterMap::WalkContinuation::skip);
     map.set_include_argument_oops(false);
     _stream.next(&map);
     assert(!_stream.is_done(), "");
-    if (UNLIKELY(seen_by_gc())) { // we're now doing this on the stub's caller
-      _cont.tail()->do_barriers<stackChunkOopDesc::BarrierType::Store>(_stream, &map);
-    }
+    _cont.tail()->do_barriers<stackChunkOopDesc::BarrierType::Store>(_stream, &map);
+  } else {
+    _stream.next(SmallRegisterMap::instance);
     assert(!_stream.is_done(), "");
   }
 
-  recurse_thaw_compiled_frame(_stream.to_frame(), caller, num_frames, true); // this could be deoptimized
-
-  DEBUG_ONLY(before_thaw_java_frame(hf, caller, false, num_frames);)
+  recurse_thaw_compiled_frame(_stream.to_frame(), caller, num_frames, true);
 
-  assert(ContinuationHelper::Frame::is_stub(hf.cb()), "");
+  assert(caller.is_compiled_frame(), "");
   assert(caller.sp() == caller.unextended_sp(), "");
-  assert(!caller.is_interpreted_frame(), "");
 
-  int fsize = ContinuationHelper::StubFrame::size(hf);
+  DEBUG_ONLY(before_thaw_java_frame(hf, caller, false /*is_bottom_frame*/, num_frames);)
+
+  if (caller.is_interpreted_frame()) {
+    _align_size += frame::align_wiggle; // we add one whether or not we've aligned because we add it in recurse_freeze_stub_frame
+  }
 
   frame f = new_stack_frame<ContinuationHelper::StubFrame>(hf, caller, false);
   intptr_t* stack_frame_top = f.sp();
   intptr_t* heap_frame_top = hf.sp();
+  int fsize = ContinuationHelper::StubFrame::size(hf);
 
   copy_from_chunk(heap_frame_top - frame::metadata_words, stack_frame_top - frame::metadata_words,
                   fsize + frame::metadata_words);
 
-  { // can only fix caller once this frame is thawed (due to callee saved regs)
-    RegisterMap map(nullptr,
-                    RegisterMap::UpdateMap::include,
-                    RegisterMap::ProcessFrames::skip,
-                    RegisterMap::WalkContinuation::skip); // map.clear();
-    map.set_include_argument_oops(false);
-    f.oop_map()->update_register_map(&f, &map);
-    ContinuationHelper::update_register_map_with_callee(caller, &map);
-    _cont.tail()->fix_thawed_frame(caller, &map);
+  patch(f, caller, false /*is_bottom_frame*/);
+
+  // can only fix caller once this frame is thawed (due to callee saved regs)
+  RegisterMap map(nullptr,
+                  RegisterMap::UpdateMap::include,
+                  RegisterMap::ProcessFrames::skip,
+                  RegisterMap::WalkContinuation::skip);
+  map.set_include_argument_oops(false);
+  f.oop_map()->update_register_map(&f, &map);
+  ContinuationHelper::update_register_map_with_callee(caller, &map);
+  _cont.tail()->fix_thawed_frame(caller, &map);
+
+  DEBUG_ONLY(after_thaw_java_frame(f, false /*is_bottom_frame*/);)
+  caller = f;
+}
+
+void ThawBase::recurse_thaw_native_frame(const frame& hf, frame& caller, int num_frames) {
+  assert(hf.is_native_frame(), "");
+  assert(_preempted_case && hf.cb()->as_nmethod()->method()->is_object_wait0(), "");
+
+  if (UNLIKELY(seen_by_gc())) { // recurse_thaw_stub_frame already invoked our barriers with a full regmap
+    _cont.tail()->do_barriers<stackChunkOopDesc::BarrierType::Store>(_stream, SmallRegisterMap::instance());
   }
 
-  DEBUG_ONLY(after_thaw_java_frame(f, false);)
+  const bool is_bottom_frame = recurse_thaw_java_frame<ContinuationHelper::NativeFrame>(caller, num_frames);
+  assert(!is_bottom_frame, "");
+
+  DEBUG_ONLY(before_thaw_java_frame(hf, caller, is_bottom_frame, num_frames);)
+
+  assert(caller.sp() == caller.unextended_sp(), "");
+
+  if (caller.is_interpreted_frame()) {
+    _align_size += frame::align_wiggle; // we add one whether or not we've aligned because we add it in recurse_freeze_native_frame
+  }
+
+  // new_stack_frame must construct the resulting frame using hf.pc() rather than hf.raw_pc() because the frame is not
+  // yet laid out in the stack, and so the original_pc is not stored in it.
+  // As a result, f.is_deoptimized_frame() is always false and we must test hf to know if the frame is deoptimized.
+  frame f = new_stack_frame<ContinuationHelper::NativeFrame>(hf, caller, false /* bottom */);
+  intptr_t* const stack_frame_top = f.sp();
+  intptr_t* const heap_frame_top = hf.unextended_sp();
+
+  int fsize = ContinuationHelper::NativeFrame::size(hf);
+  assert(fsize <= (int)(caller.unextended_sp() - f.unextended_sp()), "");
+
+  intptr_t* from = heap_frame_top - frame::metadata_words_at_bottom;
+  intptr_t* to   = stack_frame_top - frame::metadata_words_at_bottom;
+  int sz = fsize + frame::metadata_words_at_bottom;
+
+  copy_from_chunk(from, to, sz); // copying good oops because we invoked barriers above
+
+  patch(f, caller, false /* bottom */);
+
+  // f.is_deoptimized_frame() is always false and we must test hf.is_deoptimized_frame() (see comment above)
+  assert(!f.is_deoptimized_frame(), "");
+  assert(!hf.is_deoptimized_frame(), "");
+  assert(!f.cb()->as_nmethod()->is_marked_for_deoptimization(), "");
+
+  // can only fix caller once this frame is thawed (due to callee saved regs); this happens on the stack
+  _cont.tail()->fix_thawed_frame(caller, SmallRegisterMap::instance());
+
+  DEBUG_ONLY(after_thaw_java_frame(f, false /* bottom */);)
   caller = f;
 }
 
 void ThawBase::finish_thaw(frame& f) {
   stackChunkOop chunk = _cont.tail();
@@ -2463,34 +2809,15 @@
   set_anchor_to_entry(thread, cont.entry());
   log_frames(thread);
   clear_anchor(thread);
 #endif
 
+  DEBUG_ONLY(bool preempted = cont.tail()->preempted();)
   Thaw<ConfigT> thw(thread, cont);
   intptr_t* const sp = thw.thaw(kind);
   assert(is_aligned(sp, frame::frame_alignment), "");
-
-  // All or part of the frames have been thawed so we know they don't hold any monitors except JNI monitors.
-  assert(thread->held_monitor_count() == thread->jni_monitor_count(), "Must be");
-
-#ifdef ASSERT
-  intptr_t* sp0 = sp;
-  set_anchor(thread, sp0);
-  log_frames(thread);
-  if (LoomVerifyAfterThaw) {
-    assert(do_verify_after_thaw(thread, cont.tail(), tty), "");
-  }
-  assert(ContinuationEntry::assert_entry_frame_laid_out(thread), "");
-  clear_anchor(thread);
-
-  LogTarget(Trace, continuations) lt;
-  if (lt.develop_is_enabled()) {
-    LogStream ls(lt);
-    ls.print_cr("Jumping to frame (thaw):");
-    frame(sp).print_value_on(&ls);
-  }
-#endif
+  DEBUG_ONLY(log_frames_after_thaw(thread, cont, sp, preempted);)
 
   CONT_JFR_ONLY(thw.jfr_info().post_jfr_event(&event, cont.continuation(), thread);)
 
   verify_continuation(cont.continuation());
   log_develop_debug(continuations)("=== End of thaw #" INTPTR_FORMAT, cont.hash());
@@ -2591,11 +2918,11 @@
   if (!lt.develop_is_enabled()) {
     return;
   }
   LogStream ls(lt);
 
-  ls.print_cr("------- frames ---------");
+  ls.print_cr("------- frames --------- for thread " INTPTR_FORMAT, p2i(thread));
   if (!thread->has_last_Java_frame()) {
     ls.print_cr("NO ANCHOR!");
   }
 
   RegisterMap map(thread,
@@ -2615,22 +2942,63 @@
     HandleMark hm(Thread::current());
     FrameValues values;
 
     int i = 0;
     int post_entry = -1;
-    for (frame f = thread->last_frame(); !f.is_entry_frame(); f = f.sender(&map)) {
-      f.describe(values, i++, &map);
+    for (frame f = thread->last_frame(); !f.is_first_frame(); f = f.sender(&map), i++) {
+      f.describe(values, i, &map, i == 0);
       if (post_entry >= 0 || Continuation::is_continuation_enterSpecial(f))
         post_entry++;
       if (post_entry >= show_entry_callers)
         break;
     }
     values.print_on(thread, &ls);
   }
 
   ls.print_cr("======= end frames =========");
 }
+
+static void log_frames_after_thaw(JavaThread* thread, ContinuationWrapper& cont, intptr_t* sp, bool preempted) {
+  intptr_t* sp0 = sp;
+  address pc0 = *(address*)(sp - frame::sender_sp_ret_address_offset());
+  bool use_cont_entry = false;
+
+  // Some preemption cases need to use and adjusted version of sp.
+  if (preempted) {
+    if (sp0 == cont.entrySP()) {
+      // Still preempted (monitor not acquired) so no frames were thawed.
+      assert(cont.tail()->preempted(), "");
+      use_cont_entry = true;
+    }
+#if defined (AARCH64) || defined (RISCV64)
+    else {
+      CodeBlob* cb = CodeCache::find_blob(pc0);
+      if (cb->frame_size() == 2) {
+        assert(cb->is_runtime_stub(), "");
+        // Returning to c2 runtime stub requires extra adjustment on aarch64
+        // and riscv64 (see possibly_adjust_frame()).
+        sp0 += frame::metadata_words;
+      }
+    }
+#endif
+  }
+
+  set_anchor(thread, use_cont_entry ? cont.entrySP() : sp0, use_cont_entry ? cont.entryPC() : nullptr);
+  log_frames(thread);
+  if (LoomVerifyAfterThaw) {
+    assert(do_verify_after_thaw(thread, cont.tail(), tty), "");
+  }
+  assert(ContinuationEntry::assert_entry_frame_laid_out(thread), "");
+  clear_anchor(thread);
+
+  LogTarget(Trace, continuations) lt;
+  if (lt.develop_is_enabled()) {
+    LogStream ls(lt);
+    ls.print_cr("Jumping to frame (thaw):");
+    frame(sp).print_value_on(&ls);
+  }
+}
 #endif // ASSERT
 
 #include CPU_HEADER_INLINE(continuationFreezeThaw)
 
 #ifdef ASSERT
@@ -2647,26 +3015,31 @@
   map.set_include_argument_oops(false);
   map.set_skip_missing(true);
   if (callee_complete) {
     frame::update_map_with_saved_link(&map, ContinuationHelper::Frame::callee_link_address(f));
   }
-  const_cast<frame&>(f).describe(values, 0, &map);
+  const_cast<frame&>(f).describe(values, 0, &map, true);
   values.print_on(static_cast<JavaThread*>(nullptr), st);
 }
 #endif
 
 static address thaw_entry   = nullptr;
 static address freeze_entry = nullptr;
+static address freeze_preempt_entry = nullptr;
 
 address Continuation::thaw_entry() {
   return ::thaw_entry;
 }
 
 address Continuation::freeze_entry() {
   return ::freeze_entry;
 }
 
+address Continuation::freeze_preempt_entry() {
+  return ::freeze_preempt_entry;
+}
+
 class ConfigResolve {
 public:
   static void resolve() { resolve_compressed(); }
 
   static void resolve_compressed() {
@@ -2696,10 +3069,11 @@
   template <bool use_compressed, typename BarrierSetT>
   static void resolve() {
     typedef Config<use_compressed ? oop_kind::NARROW : oop_kind::WIDE, BarrierSetT> SelectedConfigT;
 
     freeze_entry = (address)freeze<SelectedConfigT>;
+    freeze_preempt_entry = (address)SelectedConfigT::freeze_preempt;
 
     // If we wanted, we could templatize by kind and have three different thaw entries
     thaw_entry   = (address)thaw<SelectedConfigT>;
   }
 };
diff a/src/hotspot/share/runtime/continuationHelper.hpp b/src/hotspot/share/runtime/continuationHelper.hpp
--- a/src/hotspot/share/runtime/continuationHelper.hpp
+++ b/src/hotspot/share/runtime/continuationHelper.hpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -56,17 +56,19 @@
 
   // Concrete helpers for describing concrete types of frames
   class InterpretedFrame;
   class NonInterpretedUnknownFrame;
   class CompiledFrame;
+  class NativeFrame;
   class StubFrame;
 };
 
 class ContinuationHelper::Frame : public AllStatic {
 public:
   static const bool interpreted = false;
   static const bool stub = false;
+  static const bool native = false;
 
   static inline intptr_t** callee_link_address(const frame& f);
   static Method* frame_method(const frame& f);
   static inline address real_pc(const frame& f);
   static inline void patch_pc(const frame& f, address pc);
@@ -131,13 +133,27 @@
   template <typename RegisterMapT>
   static bool is_owning_locks(JavaThread* thread, RegisterMapT* map, const frame& f);
 #endif
 };
 
+class ContinuationHelper::NativeFrame : public ContinuationHelper::NonInterpretedFrame {
+public:
+  static const bool native = true;
+
+  static bool is_instance(const frame& f);
+
+#ifdef ASSERT
+  static bool is_owning_locks(JavaThread* current, const frame& f);
+#endif
+
+  static int stack_argsize(const frame& f) { return 0; }
+};
+
 class ContinuationHelper::StubFrame : public ContinuationHelper::NonInterpretedFrame {
 public:
   static const bool stub = true;
 
   static bool is_instance(const frame& f);
+  static int stack_argsize(const frame& f) { return 0; }
 };
 
 #endif // SHARE_VM_RUNTIME_CONTINUATIONHELPER_HPP
diff a/src/hotspot/share/runtime/continuationHelper.inline.hpp b/src/hotspot/share/runtime/continuationHelper.inline.hpp
--- a/src/hotspot/share/runtime/continuationHelper.inline.hpp
+++ b/src/hotspot/share/runtime/continuationHelper.inline.hpp
@@ -30,11 +30,13 @@
 #include "code/scopeDesc.hpp"
 #include "compiler/oopMap.hpp"
 #include "compiler/oopMap.inline.hpp"
 #include "interpreter/oopMapCache.hpp"
 #include "runtime/frame.inline.hpp"
+#include "runtime/objectMonitor.hpp"
 #include "runtime/stackValue.hpp"
+#include "runtime/synchronizer.hpp"
 #include "utilities/macros.hpp"
 
 #include CPU_HEADER_INLINE(continuationHelper)
 
 #ifndef CPU_OVERRIDES_RETURN_ADDRESS_ACCESSORS
@@ -51,11 +53,11 @@
 inline bool ContinuationHelper::NonInterpretedUnknownFrame::is_instance(const frame& f) {
   return !f.is_interpreted_frame();
 }
 
 inline bool ContinuationHelper::Frame::is_stub(CodeBlob* cb) {
-  return cb != nullptr && (cb->is_safepoint_stub() || cb->is_runtime_stub());
+  return cb != nullptr && cb->is_runtime_stub();
 }
 
 inline Method* ContinuationHelper::Frame::frame_method(const frame& f) {
   return f.is_interpreted_frame() ? f.interpreter_frame_method() : f.cb()->as_nmethod()->method();
 }
@@ -193,10 +195,30 @@
   }
   return false;
 }
 #endif
 
+inline bool ContinuationHelper::NativeFrame::is_instance(const frame& f) {
+  return f.is_native_frame();
+}
+
+#ifdef ASSERT
+inline bool ContinuationHelper::NativeFrame::is_owning_locks(JavaThread* thread, const frame& f) {
+  assert(NativeFrame::is_instance(f), "");
+
+  Method* method = f.cb()->as_nmethod()->method();
+  if (!method->is_synchronized()) {
+    return false;
+  } else {
+    // Just verify we are actually the owner
+    oop synced_obj = f.get_native_receiver();
+    assert(ObjectSynchronizer::current_thread_holds_lock(thread, Handle(thread, synced_obj)), "must be owner");
+    return true;
+  }
+}
+#endif
+
 inline bool ContinuationHelper::StubFrame::is_instance(const frame& f) {
   return !f.is_interpreted_frame() && is_stub(f.cb());
 }
 
 #endif // SHARE_VM_RUNTIME_CONTINUATIONHELPER_INLINE_HPP
diff a/src/hotspot/share/runtime/continuationJavaClasses.cpp b/src/hotspot/share/runtime/continuationJavaClasses.cpp
--- a/src/hotspot/share/runtime/continuationJavaClasses.cpp
+++ b/src/hotspot/share/runtime/continuationJavaClasses.cpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 1997, 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -85,10 +85,12 @@
 int jdk_internal_vm_StackChunk::_sp_offset;
 int jdk_internal_vm_StackChunk::_pc_offset;
 int jdk_internal_vm_StackChunk::_bottom_offset;
 int jdk_internal_vm_StackChunk::_flags_offset;
 int jdk_internal_vm_StackChunk::_maxThawingSize_offset;
+int jdk_internal_vm_StackChunk::_lockStackSize_offset;
+int jdk_internal_vm_StackChunk::_objectWaiter_offset;
 int jdk_internal_vm_StackChunk::_cont_offset;
 
 #define STACKCHUNK_FIELDS_DO(macro) \
   macro(_parent_offset,  k, vmSymbols::parent_name(),  stackchunk_signature, false); \
   macro(_size_offset,    k, vmSymbols::size_name(),    int_signature,        false); \
diff a/src/hotspot/share/runtime/continuationJavaClasses.hpp b/src/hotspot/share/runtime/continuationJavaClasses.hpp
--- a/src/hotspot/share/runtime/continuationJavaClasses.hpp
+++ b/src/hotspot/share/runtime/continuationJavaClasses.hpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -69,14 +69,16 @@
   static inline void set_preempted(oop continuation, bool value);
 };
 
 // Interface to jdk.internal.vm.StackChunk objects
 #define STACKCHUNK_INJECTED_FIELDS(macro)                                          \
-  macro(jdk_internal_vm_StackChunk, cont,           continuation_signature, false) \
-  macro(jdk_internal_vm_StackChunk, flags,          byte_signature,         false) \
-  macro(jdk_internal_vm_StackChunk, pc,             intptr_signature,       false) \
-  macro(jdk_internal_vm_StackChunk, maxThawingSize, int_signature,          false) \
+  macro(jdk_internal_vm_StackChunk, cont,            continuation_signature, false) \
+  macro(jdk_internal_vm_StackChunk, flags,           byte_signature,         false) \
+  macro(jdk_internal_vm_StackChunk, pc,              intptr_signature,       false) \
+  macro(jdk_internal_vm_StackChunk, maxThawingSize,  int_signature,          false) \
+  macro(jdk_internal_vm_StackChunk, lockStackSize,   byte_signature,         false) \
+  macro(jdk_internal_vm_StackChunk, objectWaiter,    intptr_signature,       false) \
 
 class jdk_internal_vm_StackChunk: AllStatic {
   friend class JavaClasses;
  private:
   static int _parent_offset;
@@ -84,10 +86,12 @@
   static int _sp_offset;
   static int _pc_offset;
   static int _bottom_offset;
   static int _flags_offset;
   static int _maxThawingSize_offset;
+  static int _lockStackSize_offset;
+  static int _objectWaiter_offset;
   static int _cont_offset;
 
 
   static void compute_offsets();
  public:
@@ -122,10 +126,16 @@
   static inline bool try_set_flags(oop chunk, uint8_t expected_value, uint8_t new_value);
 
   static inline int maxThawingSize(oop chunk);
   static inline void set_maxThawingSize(oop chunk, int value);
 
+  static inline uint8_t lockStackSize(oop chunk);
+  static inline void set_lockStackSize(oop chunk, uint8_t value);
+
+  static inline address objectWaiter(oop chunk);
+  static inline void set_objectWaiter(oop chunk, address mon);
+
   // cont oop's processing is essential for the chunk's GC protocol
   static inline oop cont(oop chunk);
   template<typename P>
   static inline oop cont_raw(oop chunk);
   static inline void set_cont(oop chunk, oop value);
diff a/src/hotspot/share/runtime/continuationJavaClasses.inline.hpp b/src/hotspot/share/runtime/continuationJavaClasses.inline.hpp
--- a/src/hotspot/share/runtime/continuationJavaClasses.inline.hpp
+++ b/src/hotspot/share/runtime/continuationJavaClasses.inline.hpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 1997, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -183,6 +183,22 @@
   log_develop_trace(continuations)("%s max_size: %d -> %d", value >= old ? "add" : "sub", old, value);
 #endif
   chunk->int_field_put(_maxThawingSize_offset, value);
 }
 
+inline uint8_t jdk_internal_vm_StackChunk::lockStackSize(oop chunk) {
+  return Atomic::load(chunk->field_addr<uint8_t>(_lockStackSize_offset));
+}
+
+inline void jdk_internal_vm_StackChunk::set_lockStackSize(oop chunk, uint8_t value) {
+  Atomic::store(chunk->field_addr<uint8_t>(_lockStackSize_offset), value);
+}
+
+inline address jdk_internal_vm_StackChunk::objectWaiter(oop chunk) {
+  return chunk->address_field(_objectWaiter_offset);
+}
+
+inline void jdk_internal_vm_StackChunk::set_objectWaiter(oop chunk, address value) {
+  chunk->address_field_put(_objectWaiter_offset, value);
+}
+
 #endif // SHARE_RUNTIME_CONTINUATIONJAVACLASSES_INLINE_HPP
diff a/src/hotspot/share/runtime/deoptimization.cpp b/src/hotspot/share/runtime/deoptimization.cpp
--- a/src/hotspot/share/runtime/deoptimization.cpp
+++ b/src/hotspot/share/runtime/deoptimization.cpp
@@ -120,10 +120,14 @@
 DeoptimizationScope::~DeoptimizationScope() {
   assert(_deopted, "Deopt not executed");
 }
 
 void DeoptimizationScope::mark(nmethod* nm, bool inc_recompile_counts) {
+  if (!nm->can_be_deoptimized()) {
+    return;
+  }
+
   ConditionalMutexLocker ml(NMethodState_lock, !NMethodState_lock->owned_by_self(), Mutex::_no_safepoint_check_flag);
 
   // If it's already marked but we still need it to be deopted.
   if (nm->is_marked_for_deoptimization()) {
     dependent(nm);
@@ -1664,11 +1668,11 @@
                                                                 deoptee_thread, thread);
           }
           assert(mon_info->owner()->is_locked(), "object must be locked now");
           assert(obj->mark().has_monitor(), "must be");
           assert(!deoptee_thread->lock_stack().contains(obj()), "must be");
-          assert(ObjectSynchronizer::read_monitor(thread, obj(), obj->mark())->owner() == deoptee_thread, "must be");
+          assert(ObjectSynchronizer::read_monitor(thread, obj(), obj->mark())->is_owner(deoptee_thread), "must be");
         } else {
           ObjectSynchronizer::enter_for(obj, lock, deoptee_thread);
           assert(mon_info->owner()->is_locked(), "object must be locked now");
         }
       }
diff a/src/hotspot/share/runtime/frame.cpp b/src/hotspot/share/runtime/frame.cpp
--- a/src/hotspot/share/runtime/frame.cpp
+++ b/src/hotspot/share/runtime/frame.cpp
@@ -58,10 +58,13 @@
 #include "runtime/stubCodeGenerator.hpp"
 #include "runtime/stubRoutines.hpp"
 #include "utilities/debug.hpp"
 #include "utilities/decoder.hpp"
 #include "utilities/formatBuffer.hpp"
+#ifdef COMPILER1
+#include "c1/c1_Runtime1.hpp"
+#endif
 
 RegisterMap::RegisterMap(JavaThread *thread, UpdateMap update_map, ProcessFrames process_frames, WalkContinuation walk_cont) {
   _thread         = thread;
   _update_map     = update_map == UpdateMap::include;
   _process_frames = process_frames == ProcessFrames::include;
@@ -495,10 +498,44 @@
   }
   assert(stack_size <= (size_t)max_jint, "stack size too big");
   return (jint)stack_size;
 }
 
+#if defined(ASSERT) && !defined(PPC64)
+static address get_register_address_in_stub(const frame& stub_fr, VMReg reg) {
+  RegisterMap map(nullptr,
+                  RegisterMap::UpdateMap::include,
+                  RegisterMap::ProcessFrames::skip,
+                  RegisterMap::WalkContinuation::skip);
+  stub_fr.oop_map()->update_register_map(&stub_fr, &map);
+  return map.location(reg, stub_fr.sp());
+}
+#endif
+
+JavaThread** frame::saved_thread_address(const frame& f) {
+#if defined(PPC64)
+  // The current thread (JavaThread*) is never stored on the stack
+  return nullptr;
+#else
+  CodeBlob* cb = f.cb();
+  assert(cb != nullptr && cb->is_runtime_stub(), "invalid frame");
+
+  JavaThread** thread_addr;
+#ifdef COMPILER1
+  if (cb == Runtime1::blob_for(C1StubId::monitorenter_id) ||
+      cb == Runtime1::blob_for(C1StubId::monitorenter_nofpu_id)) {
+    thread_addr = (JavaThread**)(f.sp() + Runtime1::runtime_blob_current_thread_offset(f));
+  } else
+#endif
+  {
+    // c2 only saves rbp in the stub frame so nothing to do.
+    thread_addr = nullptr;
+  }
+  assert(get_register_address_in_stub(f, SharedRuntime::thread_register()) == (address)thread_addr, "wrong thread address");
+  return thread_addr;
+#endif
+}
 
 // (frame::interpreter_frame_sender_sp accessor is in frame_<arch>.cpp)
 
 const char* frame::print_name() const {
   if (is_native_frame())      return "Native";
@@ -575,14 +612,25 @@
   // locks for synchronization
   for (BasicObjectLock* current = interpreter_frame_monitor_end();
        current < interpreter_frame_monitor_begin();
        current = next_monitor_in_interpreter_frame(current)) {
     st->print(" - obj    [%s", current->obj() == nullptr ? "null" : "");
-    if (current->obj() != nullptr) current->obj()->print_value_on(st);
+    oop obj = current->obj();
+    if (obj != nullptr) {
+      if (!is_heap_frame()) {
+        obj->print_value_on(st);
+      } else {
+        // Might be an invalid oop. We don't have the
+        // stackChunk to correct it so just print address.
+        st->print(INTPTR_FORMAT, p2i(obj));
+      }
+    }
     st->print_cr("]");
     st->print(" - lock   [");
-    current->lock()->print_on(st, current->obj());
+    if (!is_heap_frame()) {
+      current->lock()->print_on(st, obj);
+    }
     st->print_cr("]");
   }
   // monitor
   st->print_cr(" - monitor[" INTPTR_FORMAT "]", p2i(interpreter_frame_monitor_begin()));
   // bcp
@@ -1083,20 +1131,20 @@
   assert(Universe::heap()->is_in_or_null(r), "bad receiver: " INTPTR_FORMAT " (" INTX_FORMAT ")", p2i(r), p2i(r));
   return r;
 }
 
 
-BasicLock* frame::get_native_monitor() {
+BasicLock* frame::get_native_monitor() const {
   nmethod* nm = (nmethod*)_cb;
   assert(_cb != nullptr && _cb->is_nmethod() && nm->method()->is_native(),
          "Should not call this unless it's a native nmethod");
   int byte_offset = in_bytes(nm->native_basic_lock_sp_offset());
   assert(byte_offset >= 0, "should not see invalid offset");
   return (BasicLock*) &sp()[byte_offset / wordSize];
 }
 
-oop frame::get_native_receiver() {
+oop frame::get_native_receiver() const {
   nmethod* nm = (nmethod*)_cb;
   assert(_cb != nullptr && _cb->is_nmethod() && nm->method()->is_native(),
          "Should not call this unless it's a native nmethod");
   int byte_offset = in_bytes(nm->native_receiver_sp_offset());
   assert(byte_offset >= 0, "should not see invalid offset");
@@ -1338,13 +1386,18 @@
   }
 };
 
 // callers need a ResourceMark because of name_and_sig_as_C_string() usage,
 // RA allocated string is returned to the caller
-void frame::describe(FrameValues& values, int frame_no, const RegisterMap* reg_map) {
+void frame::describe(FrameValues& values, int frame_no, const RegisterMap* reg_map, bool top) {
   // boundaries: sp and the 'real' frame pointer
   values.describe(-1, sp(), err_msg("sp for #%d", frame_no), 0);
+  if (top) {
+    values.describe(-1, sp() - 1, err_msg("sp[-1] for #%d", frame_no), 0);
+    values.describe(-1, sp() - 2, err_msg("sp[-2] for #%d", frame_no), 0);
+  }
+
   intptr_t* frame_pointer = real_fp(); // Note: may differ from fp()
 
   // print frame info at the highest boundary
   intptr_t* info_address = MAX2(sp(), frame_pointer);
 
@@ -1412,11 +1465,11 @@
       oopsFn.describe(values, frame_no);
     }
   } else if (is_entry_frame()) {
     // For now just label the frame
     values.describe(-1, info_address, err_msg("#%d entry frame", frame_no), 2);
-  } else if (cb()->is_nmethod()) {
+  } else if (is_compiled_frame()) {
     // For now just label the frame
     nmethod* nm = cb()->as_nmethod();
     values.describe(-1, info_address,
                     FormatBuffer<1024>("#%d nmethod " INTPTR_FORMAT " for method J %s%s", frame_no,
                                        p2i(nm),
@@ -1515,21 +1568,20 @@
         FrameValuesOopMapClosure valuesFn(this, reg_map, values, frame_no);
         // also OopMapValue::live_value ??
         oop_map()->all_type_do(this, OopMapValue::callee_saved_value, &valuesFn);
       }
     }
-
-    if (nm->method()->is_continuation_enter_intrinsic()) {
-      ContinuationEntry* ce = Continuation::get_continuation_entry_for_entry_frame(reg_map->thread(), *this); // (ContinuationEntry*)unextended_sp();
-      ce->describe(values, frame_no);
-    }
   } else if (is_native_frame()) {
     // For now just label the frame
     nmethod* nm = cb()->as_nmethod_or_null();
     values.describe(-1, info_address,
                     FormatBuffer<1024>("#%d nmethod " INTPTR_FORMAT " for native method %s", frame_no,
                                        p2i(nm), nm->method()->name_and_sig_as_C_string()), 2);
+    if (nm->method()->is_continuation_enter_intrinsic()) {
+      ContinuationEntry* ce = Continuation::get_continuation_entry_for_entry_frame(reg_map->thread(), *this); // (ContinuationEntry*)unextended_sp();
+      ce->describe(values, frame_no);
+    }
   } else {
     // provide default info if not handled before
     char *info = (char *) "special frame";
     if ((_cb != nullptr) &&
         (_cb->name() != nullptr)) {
diff a/src/hotspot/share/runtime/frame.hpp b/src/hotspot/share/runtime/frame.hpp
--- a/src/hotspot/share/runtime/frame.hpp
+++ b/src/hotspot/share/runtime/frame.hpp
@@ -335,12 +335,12 @@
   oop retrieve_receiver(RegisterMap *reg_map);
 
   // Return the monitor owner and BasicLock for compiled synchronized
   // native methods. Used by JVMTI's GetLocalInstance method
   // (via VM_GetReceiver) to retrieve the receiver from a native wrapper frame.
-  BasicLock* get_native_monitor();
-  oop        get_native_receiver();
+  BasicLock* get_native_monitor() const;
+  oop        get_native_receiver() const;
 
   // Find receiver for an invoke when arguments are just pushed on stack (i.e., callee stack-frame is
   // not setup)
   oop interpreter_callee_receiver(Symbol* signature);
 
@@ -424,10 +424,12 @@
 
  public:
   oop saved_oop_result(RegisterMap* map) const;
   void set_saved_oop_result(RegisterMap* map, oop obj);
 
+  static JavaThread** saved_thread_address(const frame& f);
+
   // For debugging
  private:
   const char* print_name() const;
 
   void describe_pd(FrameValues& values, int frame_no);
@@ -440,11 +442,11 @@
   void print_on_error(outputStream* st, char* buf, int buflen, bool verbose = false) const;
   static void print_C_frame(outputStream* st, char* buf, int buflen, address pc);
 
 #ifndef PRODUCT
   // Add annotated descriptions of memory locations belonging to this frame to values
-  void describe(FrameValues& values, int frame_no, const RegisterMap* reg_map=nullptr);
+  void describe(FrameValues& values, int frame_no, const RegisterMap* reg_map=nullptr, bool top = false);
 #endif
 
   // Conversion from a VMReg to physical stack location
   template <typename RegisterMapT>
   address oopmapreg_to_location(VMReg reg, const RegisterMapT* reg_map) const;
diff a/src/hotspot/share/runtime/globals.hpp b/src/hotspot/share/runtime/globals.hpp
--- a/src/hotspot/share/runtime/globals.hpp
+++ b/src/hotspot/share/runtime/globals.hpp
@@ -1902,11 +1902,11 @@
           "Deopt stack after thaw")                                         \
                                                                             \
   develop(bool, LoomVerifyAfterThaw, false,                                 \
           "Verify stack after thaw")                                        \
                                                                             \
-  develop(bool, VerifyContinuations, false,                                 \
+  develop(bool, VerifyContinuations, true,                                  \
           "Verify continuation consistency")                                \
                                                                             \
   develop(bool, UseContinuationFastPath, true,                              \
           "Use fast-path frame walking in continuations")                   \
                                                                             \
diff a/src/hotspot/share/runtime/javaThread.cpp b/src/hotspot/share/runtime/javaThread.cpp
--- a/src/hotspot/share/runtime/javaThread.cpp
+++ b/src/hotspot/share/runtime/javaThread.cpp
@@ -81,10 +81,11 @@
 #include "runtime/serviceThread.hpp"
 #include "runtime/stackFrameStream.inline.hpp"
 #include "runtime/stackWatermarkSet.hpp"
 #include "runtime/synchronizer.hpp"
 #include "runtime/threadCritical.hpp"
+#include "runtime/threadIdentifier.hpp"
 #include "runtime/threadSMR.inline.hpp"
 #include "runtime/threadStatisticalInfo.hpp"
 #include "runtime/threadWXSetters.inline.hpp"
 #include "runtime/timer.hpp"
 #include "runtime/timerTrace.hpp"
@@ -233,10 +234,12 @@
   // We are called from jni_AttachCurrentThread/jni_AttachCurrentThreadAsDaemon.
   // We cannot use JavaCalls::construct_new_instance because the java.lang.Thread
   // constructor calls Thread.current(), which must be set here.
   java_lang_Thread::set_thread(thread_oop(), this);
   set_threadOopHandles(thread_oop());
+  // Set the lock_id to the next thread_id temporarily while initialization runs.
+  set_lock_id(ThreadIdentifier::next());
 
   JavaValue result(T_VOID);
   if (thread_name != nullptr) {
     Handle name = java_lang_String::create_from_str(thread_name, CHECK);
     // Thread gets assigned specified name and null target
@@ -258,10 +261,13 @@
                             vmSymbols::threadgroup_runnable_void_signature(),
                             thread_group,
                             Handle(),
                             CHECK);
   }
+  // Update the lock_id with the tid value.
+  set_lock_id(java_lang_Thread::thread_id(thread_oop()));
+
   os::set_priority(this, NormPriority);
 
   if (daemon) {
     java_lang_Thread::set_daemon(thread_oop());
   }
@@ -427,10 +433,11 @@
   _current_pending_monitor(nullptr),
   _current_pending_monitor_is_from_java(true),
   _current_waiting_monitor(nullptr),
   _active_handles(nullptr),
   _free_handle_block(nullptr),
+  _lock_id(0),
 
   _suspend_flags(0),
 
   _thread_state(_thread_new),
   _saved_exception_pc(nullptr),
@@ -447,10 +454,13 @@
   _carrier_thread_suspended(false),
   _is_in_VTMS_transition(false),
   _is_in_tmp_VTMS_transition(false),
   _is_disable_suspend(false),
   _VTMS_transition_mark(false),
+  _pending_jvmti_unmount_event(false),
+  _on_monitor_waited_event(false),
+  _contended_entered_monitor(nullptr),
 #ifdef ASSERT
   _is_VTMS_transition_disabler(false),
 #endif
 #endif
   _jni_attach_state(_not_attaching_via_jni),
@@ -487,10 +497,14 @@
   _cont_fastpath_thread_state(1),
   _held_monitor_count(0),
   _jni_monitor_count(0),
   _unlocked_inflated_monitor(nullptr),
 
+  _preempt_alternate_return(nullptr),
+  _preemption_cancelled(false),
+  _pending_interrupted_exception(false),
+
   _handshake(this),
 
   _popframe_preserved_args(nullptr),
   _popframe_preserved_args_size(0),
 
@@ -1161,10 +1175,11 @@
   Handshake::execute(&iaeh, target);
 }
 
 #if INCLUDE_JVMTI
 void JavaThread::set_is_in_VTMS_transition(bool val) {
+  assert(is_in_VTMS_transition() != val, "already %s transition", val ? "inside" : "outside");
   _is_in_VTMS_transition = val;
 }
 
 #ifdef ASSERT
 void JavaThread::set_is_VTMS_transition_disabler(bool val) {
@@ -1524,13 +1539,12 @@
   Thread::print_on(st, print_extended_info);
   // print guess for valid stack memory region (assume 4K pages); helps lock debugging
   st->print_cr("[" INTPTR_FORMAT "]", (intptr_t)last_Java_sp() & ~right_n_bits(12));
   if (thread_oop != nullptr) {
     if (is_vthread_mounted()) {
-      oop vt = vthread();
-      assert(vt != nullptr, "");
-      st->print_cr("   Carrying virtual thread #" INT64_FORMAT, (int64_t)java_lang_Thread::thread_id(vt));
+      // _lock_id is the thread ID of the mounted virtual thread
+      st->print_cr("   Carrying virtual thread #" INT64_FORMAT, lock_id());
     } else {
       st->print_cr("   java.lang.Thread.State: %s", java_lang_Thread::thread_status_name(thread_oop));
     }
   }
 #ifndef PRODUCT
@@ -1710,10 +1724,11 @@
   Handle thread_oop(Thread::current(),
                     JNIHandles::resolve_non_null(jni_thread));
   assert(InstanceKlass::cast(thread_oop->klass())->is_linked(),
          "must be initialized");
   set_threadOopHandles(thread_oop());
+  set_lock_id(java_lang_Thread::thread_id(thread_oop()));
 
   if (prio == NoPriority) {
     prio = java_lang_Thread::priority(thread_oop());
     assert(prio != NoPriority, "A valid priority should be present");
   }
@@ -1989,38 +2004,54 @@
 
 // Slow-path increment of the held monitor counts. JNI locking is always
 // this slow-path.
 void JavaThread::inc_held_monitor_count(intx i, bool jni) {
 #ifdef SUPPORT_MONITOR_COUNT
+
+  if (LockingMode != LM_LEGACY) {
+    // Nothing to do. Just do some sanity check.
+    assert(_held_monitor_count == 0, "counter should not be used");
+    assert(_jni_monitor_count == 0, "counter should not be used");
+    return;
+  }
+
   assert(_held_monitor_count >= 0, "Must always be non-negative: " INTX_FORMAT, _held_monitor_count);
   _held_monitor_count += i;
   if (jni) {
     assert(_jni_monitor_count >= 0, "Must always be non-negative: " INTX_FORMAT, _jni_monitor_count);
     _jni_monitor_count += i;
   }
   assert(_held_monitor_count >= _jni_monitor_count, "Monitor count discrepancy detected - held count "
          INTX_FORMAT " is less than JNI count " INTX_FORMAT, _held_monitor_count, _jni_monitor_count);
-#endif
+#endif // SUPPORT_MONITOR_COUNT
 }
 
 // Slow-path decrement of the held monitor counts. JNI unlocking is always
 // this slow-path.
 void JavaThread::dec_held_monitor_count(intx i, bool jni) {
 #ifdef SUPPORT_MONITOR_COUNT
+
+  if (LockingMode != LM_LEGACY) {
+    // Nothing to do. Just do some sanity check.
+    assert(_held_monitor_count == 0, "counter should not be used");
+    assert(_jni_monitor_count == 0, "counter should not be used");
+    return;
+  }
+
   _held_monitor_count -= i;
-  assert(_held_monitor_count >= 0, "Must always be greater than 0: " INTX_FORMAT, _held_monitor_count);
+  assert(_held_monitor_count >= 0, "Must always be non-negative: " INTX_FORMAT, _held_monitor_count);
   if (jni) {
     _jni_monitor_count -= i;
-    assert(_jni_monitor_count >= 0, "Must always be greater than 0: " INTX_FORMAT, _jni_monitor_count);
+    assert(_jni_monitor_count >= 0, "Must always be non-negative: " INTX_FORMAT, _jni_monitor_count);
   }
   // When a thread is detaching with still owned JNI monitors, the logic that releases
   // the monitors doesn't know to set the "jni" flag and so the counts can get out of sync.
   // So we skip this assert if the thread is exiting. Once all monitors are unlocked the
   // JNI count is directly set to zero.
   assert(_held_monitor_count >= _jni_monitor_count || is_exiting(), "Monitor count discrepancy detected - held count "
          INTX_FORMAT " is less than JNI count " INTX_FORMAT, _held_monitor_count, _jni_monitor_count);
-#endif
+#endif // SUPPORT_MONITOR_COUNT
 }
 
 frame JavaThread::vthread_last_frame() {
   assert (is_vthread_mounted(), "Virtual thread not mounted");
   return last_frame();
@@ -2198,10 +2229,11 @@
 
   java_lang_Thread::set_daemon(thread_oop());
 
   // Now bind the thread_oop to the target JavaThread.
   target->set_threadOopHandles(thread_oop());
+  target->set_lock_id(java_lang_Thread::thread_id(thread_oop()));
 
   Threads::add(target); // target is now visible for safepoint/handshake
   // Publish the JavaThread* in java.lang.Thread after the JavaThread* is
   // on a ThreadsList. We don't want to wait for the release when the
   // Theads_lock is dropped when the 'mu' destructor is run since the
diff a/src/hotspot/share/runtime/javaThread.hpp b/src/hotspot/share/runtime/javaThread.hpp
--- a/src/hotspot/share/runtime/javaThread.hpp
+++ b/src/hotspot/share/runtime/javaThread.hpp
@@ -28,10 +28,11 @@
 
 #include "jni.h"
 #include "memory/allocation.hpp"
 #include "oops/oop.hpp"
 #include "oops/oopHandle.hpp"
+#include "runtime/continuationEntry.hpp"
 #include "runtime/frame.hpp"
 #include "runtime/globals.hpp"
 #include "runtime/handshake.hpp"
 #include "runtime/javaFrameAnchor.hpp"
 #include "runtime/lockStack.hpp"
@@ -39,20 +40,20 @@
 #include "runtime/safepointMechanism.hpp"
 #include "runtime/stackWatermarkSet.hpp"
 #include "runtime/stackOverflow.hpp"
 #include "runtime/thread.hpp"
 #include "runtime/threadHeapSampler.hpp"
+#include "runtime/threadIdentifier.hpp"
 #include "runtime/threadStatisticalInfo.hpp"
 #include "utilities/exceptions.hpp"
 #include "utilities/globalDefinitions.hpp"
 #include "utilities/macros.hpp"
 #if INCLUDE_JFR
 #include "jfr/support/jfrThreadExtension.hpp"
 #endif
 
 class AsyncExceptionHandshake;
-class ContinuationEntry;
 class DeoptResourceMark;
 class InternalOOMEMark;
 class JNIHandleBlock;
 class JVMCIRuntime;
 
@@ -157,11 +158,22 @@
   JNIHandleBlock* _active_handles;
 
   // One-element thread local free list
   JNIHandleBlock* _free_handle_block;
 
+  // ID used as owner for inflated monitors. Same as the j.l.Thread.tid of the
+  // current _vthread object, except during creation of the primordial and JNI
+  // attached thread cases where this field can have a temporal value.
+  int64_t _lock_id;
+
  public:
+  void set_lock_id(int64_t tid) {
+    assert(tid >= ThreadIdentifier::initial() && tid < ThreadIdentifier::current(), "invalid tid");
+    _lock_id = tid;
+  }
+  int64_t lock_id() const { return _lock_id; }
+
   // For tracking the heavyweight monitor the thread is pending on.
   ObjectMonitor* current_pending_monitor() {
     // Use Atomic::load() to prevent data race between concurrent modification and
     // concurrent readers, e.g. ThreadService::get_current_contended_monitor().
     // Especially, reloading pointer from thread after null check must be prevented.
@@ -312,10 +324,13 @@
   volatile bool         _carrier_thread_suspended;       // Carrier thread is externally suspended
   bool                  _is_in_VTMS_transition;          // thread is in virtual thread mount state transition
   bool                  _is_in_tmp_VTMS_transition;      // thread is in temporary virtual thread mount state transition
   bool                  _is_disable_suspend;             // JVMTI suspend is temporarily disabled; used on current thread only
   bool                  _VTMS_transition_mark;           // used for sync between VTMS transitions and disablers
+  bool                  _pending_jvmti_unmount_event;    // When preempting we post unmount event at unmount end rather than start
+  bool                  _on_monitor_waited_event;        // Avoid callee arg processing for enterSpecial when posting waited event
+  ObjectMonitor*        _contended_entered_monitor;      // Monitor por pending monitor_contended_entered callback
 #ifdef ASSERT
   bool                  _is_VTMS_transition_disabler;    // thread currently disabled VTMS transitions
 #endif
 #endif
 
@@ -462,10 +477,32 @@
   // It's signed for error detection.
   intx _held_monitor_count;  // used by continuations for fast lock detection
   intx _jni_monitor_count;
   ObjectMonitor* _unlocked_inflated_monitor;
 
+  // This is the field we poke in the interpreter and native
+  // wrapper (Object.wait) to check for preemption.
+  address _preempt_alternate_return;
+  // When preempting on monitorenter we could have acquired the
+  // monitor after freezing all vthread frames. In that case we
+  // set this field so that in the preempt stub we call thaw again
+  // instead of unmounting.
+  bool _preemption_cancelled;
+  // For Object.wait() we set this field to know if we need to
+  // throw IE at the end of thawing before returning to Java.
+  bool _pending_interrupted_exception;
+
+ public:
+  bool preemption_cancelled()           { return _preemption_cancelled; }
+  void set_preemption_cancelled(bool b) { _preemption_cancelled = b; }
+
+  bool pending_interrupted_exception()           { return _pending_interrupted_exception; }
+  void set_pending_interrupted_exception(bool b) { _pending_interrupted_exception = b; }
+
+  bool preempting()           { return _preempt_alternate_return != nullptr; }
+  void set_preempt_alternate_return(address val) { _preempt_alternate_return = val; }
+
 private:
 
   friend class VMThread;
   friend class ThreadWaitTransition;
   friend class VM_Exit;
@@ -626,11 +663,11 @@
   void inc_held_monitor_count(intx i = 1, bool jni = false);
   void dec_held_monitor_count(intx i = 1, bool jni = false);
 
   intx held_monitor_count() { return _held_monitor_count; }
   intx jni_monitor_count()  { return _jni_monitor_count;  }
-  void clear_jni_monitor_count() { _jni_monitor_count = 0;   }
+  void clear_jni_monitor_count() { _jni_monitor_count = 0; }
 
   // Support for SharedRuntime::monitor_exit_helper()
   ObjectMonitor* unlocked_inflated_monitor() const { return _unlocked_inflated_monitor; }
   void clear_unlocked_inflated_monitor() {
     _unlocked_inflated_monitor = nullptr;
@@ -685,16 +722,26 @@
   void toggle_is_disable_suspend()               { _is_disable_suspend = !_is_disable_suspend; };
 
   bool VTMS_transition_mark() const              { return Atomic::load(&_VTMS_transition_mark); }
   void set_VTMS_transition_mark(bool val)        { Atomic::store(&_VTMS_transition_mark, val); }
 
+  bool pending_jvmti_unmount_event()             { return _pending_jvmti_unmount_event; }
+  void set_pending_jvmti_unmount_event(bool val) { _pending_jvmti_unmount_event = val; }
+
+  bool on_monitor_waited_event()             { return _on_monitor_waited_event; }
+  void set_on_monitor_waited_event(bool val) { _on_monitor_waited_event = val; }
+
+  bool pending_contended_entered_event()         { return _contended_entered_monitor != nullptr; }
+  ObjectMonitor* contended_entered_monitor()     { return _contended_entered_monitor; }
 #ifdef ASSERT
   bool is_VTMS_transition_disabler() const       { return _is_VTMS_transition_disabler; }
   void set_is_VTMS_transition_disabler(bool val);
 #endif
 #endif
 
+  void set_contended_entered_monitor(ObjectMonitor* val) NOT_JVMTI_RETURN JVMTI_ONLY({ _contended_entered_monitor = val; })
+
   // Support for object deoptimization and JFR suspension
   void handle_special_runtime_exit_condition();
   bool has_special_runtime_exit_condition() {
     return (_suspend_flags & (_obj_deopt JFR_ONLY(| _trace_flag))) != 0;
   }
@@ -841,14 +888,18 @@
     return byte_offset_of(JavaThread, _should_post_on_exceptions_flag);
   }
   static ByteSize doing_unsafe_access_offset() { return byte_offset_of(JavaThread, _doing_unsafe_access); }
   NOT_PRODUCT(static ByteSize requires_cross_modify_fence_offset()  { return byte_offset_of(JavaThread, _requires_cross_modify_fence); })
 
+  static ByteSize lock_id_offset()            { return byte_offset_of(JavaThread, _lock_id); }
+
   static ByteSize cont_entry_offset()         { return byte_offset_of(JavaThread, _cont_entry); }
   static ByteSize cont_fastpath_offset()      { return byte_offset_of(JavaThread, _cont_fastpath); }
   static ByteSize held_monitor_count_offset() { return byte_offset_of(JavaThread, _held_monitor_count); }
   static ByteSize jni_monitor_count_offset()  { return byte_offset_of(JavaThread, _jni_monitor_count); }
+  static ByteSize preemption_cancelled_offset()  { return byte_offset_of(JavaThread, _preemption_cancelled); }
+  static ByteSize preempt_alternate_return_offset() { return byte_offset_of(JavaThread, _preempt_alternate_return); }
   static ByteSize unlocked_inflated_monitor_offset() { return byte_offset_of(JavaThread, _unlocked_inflated_monitor); }
 
 #if INCLUDE_JVMTI
   static ByteSize is_in_VTMS_transition_offset()     { return byte_offset_of(JavaThread, _is_in_VTMS_transition); }
   static ByteSize is_in_tmp_VTMS_transition_offset() { return byte_offset_of(JavaThread, _is_in_tmp_VTMS_transition); }
@@ -1265,6 +1316,25 @@
     thread->push_jni_handle_block();
   }
   ~JNIHandleMark() { _thread->pop_jni_handle_block(); }
 };
 
+class NoPreemptMark {
+  ContinuationEntry* _ce;
+  bool _unpin;
+ public:
+  NoPreemptMark(JavaThread* thread) : _ce(thread->last_continuation()), _unpin(false) {
+    if (_ce != nullptr) _unpin = _ce->pin();
+  }
+  ~NoPreemptMark() { if (_unpin) _ce->unpin(); }
+};
+
+class ThreadOnMonitorWaitedEvent {
+  JavaThread* _thread;
+ public:
+  ThreadOnMonitorWaitedEvent(JavaThread* thread) : _thread(thread) {
+    JVMTI_ONLY(_thread->set_on_monitor_waited_event(true);)
+  }
+  ~ThreadOnMonitorWaitedEvent() { JVMTI_ONLY(_thread->set_on_monitor_waited_event(false);) }
+};
+
 #endif // SHARE_RUNTIME_JAVATHREAD_HPP
diff a/src/hotspot/share/runtime/javaThread.inline.hpp b/src/hotspot/share/runtime/javaThread.inline.hpp
--- a/src/hotspot/share/runtime/javaThread.inline.hpp
+++ b/src/hotspot/share/runtime/javaThread.inline.hpp
@@ -242,11 +242,11 @@
 }
 
 inline void JavaThread::om_set_monitor_cache(ObjectMonitor* monitor) {
   assert(UseObjectMonitorTable, "must be");
   assert(monitor != nullptr, "use om_clear_monitor_cache to clear");
-  assert(this == current() || monitor->owner_raw() == this, "only add owned monitors for other threads");
+  assert(this == current() || monitor->is_owner(this), "only add owned monitors for other threads");
   assert(this == current() || is_obj_deopt_suspend(), "thread must not run concurrently");
 
   _om_cache.set_monitor(monitor);
 }
 
diff a/src/hotspot/share/runtime/lightweightSynchronizer.cpp b/src/hotspot/share/runtime/lightweightSynchronizer.cpp
--- a/src/hotspot/share/runtime/lightweightSynchronizer.cpp
+++ b/src/hotspot/share/runtime/lightweightSynchronizer.cpp
@@ -621,12 +621,10 @@
 
   if (obj->klass()->is_value_based()) {
     ObjectSynchronizer::handle_sync_on_value_based_class(obj, locking_thread);
   }
 
-  locking_thread->inc_held_monitor_count();
-
   CacheSetter cache_setter(locking_thread, lock);
 
   LockStack& lock_stack = locking_thread->lock_stack();
 
   ObjectMonitor* monitor = nullptr;
@@ -652,12 +650,10 @@
 
   if (obj->klass()->is_value_based()) {
     ObjectSynchronizer::handle_sync_on_value_based_class(obj, current);
   }
 
-  current->inc_held_monitor_count();
-
   CacheSetter cache_setter(current, lock);
 
   // Used when deflation is observed. Progress here requires progress
   // from the deflator. After observing that the deflator is not
   // making progress (after two yields), switch to sleeping.
@@ -861,11 +857,11 @@
       ObjectMonitor* monitor = new ObjectMonitor(object);
       monitor->set_header(mark.set_unlocked());
       bool own = inflating_thread != nullptr && inflating_thread->lock_stack().contains(object);
       if (own) {
         // Owned by inflating_thread.
-        monitor->set_owner_from(nullptr, inflating_thread);
+        monitor->set_owner(inflating_thread);
       } else {
         // Owned by somebody else.
         monitor->set_owner_anonymous();
       }
       markWord monitor_mark = markWord::encode(monitor);
@@ -1187,20 +1183,18 @@
 
 #ifndef _LP64
   // Only for 32bit which has limited support for fast locking outside the runtime.
   if (lock_stack.try_recursive_enter(obj)) {
     // Recursive lock successful.
-    current->inc_held_monitor_count();
     return true;
   }
 
   if (mark.is_unlocked()) {
     markWord locked_mark = mark.set_fast_locked();
     if (obj->cas_set_mark(locked_mark, mark) == mark) {
       // Successfully fast-locked, push object to lock-stack and return.
       lock_stack.push(obj);
-      current->inc_held_monitor_count();
       return true;
     }
   }
 #endif
 
@@ -1214,11 +1208,10 @@
     }
 
     if (monitor->try_enter(current)) {
       // ObjectMonitor enter successful.
       cache_setter.set_monitor(monitor);
-      current->inc_held_monitor_count();
       return true;
     }
   }
 
   // Slow-path.
diff a/src/hotspot/share/runtime/lockStack.hpp b/src/hotspot/share/runtime/lockStack.hpp
--- a/src/hotspot/share/runtime/lockStack.hpp
+++ b/src/hotspot/share/runtime/lockStack.hpp
@@ -116,10 +116,14 @@
   inline size_t remove(oop o);
 
   // Tests whether the oop is on this lock-stack.
   inline bool contains(oop o) const;
 
+  inline int monitor_count() const;
+  inline void move_to_address(oop* start);
+  inline void move_from_address(oop* start, int count);
+
   // GC support
   inline void oops_do(OopClosure* cl);
 
   // Printing
   void print_on(outputStream* st);
diff a/src/hotspot/share/runtime/lockStack.inline.hpp b/src/hotspot/share/runtime/lockStack.inline.hpp
--- a/src/hotspot/share/runtime/lockStack.inline.hpp
+++ b/src/hotspot/share/runtime/lockStack.inline.hpp
@@ -213,10 +213,33 @@
   }
   verify("post-contains");
   return false;
 }
 
+inline int LockStack::monitor_count() const {
+  int end = to_index(_top);
+  assert(end <= CAPACITY, "invariant");
+  return end;
+}
+
+inline void LockStack::move_to_address(oop* start) {
+  int end = to_index(_top);
+  for (int i = 0; i < end; i++) {
+    start[i] = _base[i];
+    DEBUG_ONLY(_base[i] = nullptr;)
+  }
+  _top = lock_stack_base_offset;
+}
+
+inline void LockStack::move_from_address(oop* start, int count) {
+  assert(to_index(_top) == 0, "lockstack should be empty");
+  for (int i = 0; i < count; i++) {
+    _base[i] = start[i];
+    _top += oopSize;
+  }
+}
+
 inline void LockStack::oops_do(OopClosure* cl) {
   verify("pre-oops-do");
   int end = to_index(_top);
   for (int i = 0; i < end; i++) {
     cl->do_oop(&_base[i]);
diff a/src/hotspot/share/runtime/objectMonitor.cpp b/src/hotspot/share/runtime/objectMonitor.cpp
--- a/src/hotspot/share/runtime/objectMonitor.cpp
+++ b/src/hotspot/share/runtime/objectMonitor.cpp
@@ -37,10 +37,11 @@
 #include "oops/oopHandle.inline.hpp"
 #include "oops/weakHandle.inline.hpp"
 #include "prims/jvmtiDeferredUpdates.hpp"
 #include "prims/jvmtiExport.hpp"
 #include "runtime/atomic.hpp"
+#include "runtime/continuationWrapper.inline.hpp"
 #include "runtime/globals.hpp"
 #include "runtime/handles.inline.hpp"
 #include "runtime/interfaceSupport.inline.hpp"
 #include "runtime/javaThread.inline.hpp"
 #include "runtime/lightweightSynchronizer.hpp"
@@ -51,10 +52,11 @@
 #include "runtime/osThread.hpp"
 #include "runtime/perfData.hpp"
 #include "runtime/safefetch.hpp"
 #include "runtime/safepointMechanism.inline.hpp"
 #include "runtime/sharedRuntime.hpp"
+#include "runtime/threads.hpp"
 #include "services/threadService.hpp"
 #include "utilities/debug.hpp"
 #include "utilities/dtrace.hpp"
 #include "utilities/globalDefinitions.hpp"
 #include "utilities/macros.hpp"
@@ -112,10 +114,22 @@
 
 DEBUG_ONLY(static volatile bool InitDone = false;)
 
 OopStorage* ObjectMonitor::_oop_storage = nullptr;
 
+OopHandle ObjectMonitor::_vthread_cxq_head;
+ParkEvent* ObjectMonitor::_vthread_unparker_ParkEvent = nullptr;
+
+static void post_virtual_thread_pinned_event(JavaThread* current, const char* reason) {
+  EventVirtualThreadPinned e;
+  if (e.should_commit()) {
+    e.set_pinnedReason(reason);
+    e.set_carrierThread(JFR_JVM_THREAD_ID(current));
+    e.commit();
+  }
+}
+
 // -----------------------------------------------------------------------------
 // Theory of operations -- Monitors lists, thread residency, etc:
 //
 // * A thread acquires ownership of a monitor by successfully
 //   CAS()ing the _owner field from null to non-null.
@@ -235,22 +249,23 @@
 }
 
 ObjectMonitor::ObjectMonitor(oop object) :
   _metadata(0),
   _object(_oop_storage, object),
-  _owner(nullptr),
+  _owner(NO_OWNER),
   _previous_owner_tid(0),
   _next_om(nullptr),
   _recursions(0),
   _EntryList(nullptr),
   _cxq(nullptr),
-  _succ(nullptr),
+  _succ(NO_OWNER),
   _SpinDuration(ObjectMonitor::Knob_SpinLimit),
   _contentions(0),
   _WaitSet(nullptr),
   _waiters(0),
-  _WaitSetLock(0)
+  _WaitSetLock(0),
+  _stack_locker(nullptr)
 { }
 
 ObjectMonitor::~ObjectMonitor() {
   _object.release(_oop_storage);
 }
@@ -261,23 +276,23 @@
 }
 
 void ObjectMonitor::ExitOnSuspend::operator()(JavaThread* current) {
   if (current->is_suspended()) {
     _om->_recursions = 0;
-    _om->_succ = nullptr;
+    _om->clear_succesor();
     // Don't need a full fence after clearing successor here because of the call to exit().
     _om->exit(current, false /* not_suspended */);
     _om_exited = true;
 
     current->set_current_pending_monitor(_om);
   }
 }
 
 void ObjectMonitor::ClearSuccOnSuspend::operator()(JavaThread* current) {
   if (current->is_suspended()) {
-    if (_om->_succ == current) {
-      _om->_succ = nullptr;
+    if (_om->is_succesor(current)) {
+      _om->clear_succesor();
       OrderAccess::fence(); // always do a full fence when successor is cleared
     }
   }
 }
 
@@ -308,17 +323,17 @@
 
 bool ObjectMonitor::TryLockWithContentionMark(JavaThread* locking_thread, ObjectMonitorContentionMark& contention_mark) {
   assert(contention_mark._monitor == this, "must be");
   assert(!is_being_async_deflated(), "must be");
 
-  void* prev_owner = try_set_owner_from(nullptr, locking_thread);
+  int64_t prev_owner = try_set_owner_from(NO_OWNER, locking_thread);
   bool success = false;
 
-  if (prev_owner == nullptr) {
+  if (prev_owner == NO_OWNER) {
     assert(_recursions == 0, "invariant");
     success = true;
-  } else if (prev_owner == locking_thread) {
+  } else if (prev_owner == owner_for(locking_thread)) {
     _recursions++;
     success = true;
   } else if (prev_owner == DEFLATER_MARKER) {
     // Racing with deflation.
     prev_owner = try_set_owner_from(DEFLATER_MARKER, locking_thread);
@@ -332,25 +347,20 @@
       // scope. ObjectMonitor::deflate_monitor() which is called by
       // the deflater thread will decrement contentions after it
       // recognizes that the async deflation was cancelled.
       contention_mark.extend();
       success = true;
-    } else if (prev_owner == nullptr) {
+    } else if (prev_owner == NO_OWNER) {
       // At this point we cannot race with deflation as we have both incremented
       // contentions, seen contention > 0 and seen a DEFLATER_MARKER.
       // success will only be false if this races with something other than
       // deflation.
-      prev_owner = try_set_owner_from(nullptr, locking_thread);
-      success = prev_owner == nullptr;
+      prev_owner = try_set_owner_from(NO_OWNER, locking_thread);
+      success = prev_owner == NO_OWNER;
     }
-  } else if (LockingMode == LM_LEGACY && locking_thread->is_lock_owned((address)prev_owner)) {
-    assert(_recursions == 0, "must be");
-    _recursions = 1;
-    set_owner_from_BasicLock(prev_owner, locking_thread);
-    success = true;
   }
-  assert(!success || owner_raw() == locking_thread, "must be");
+  assert(!success || is_owner(locking_thread), "must be");
 
   return success;
 }
 
 void ObjectMonitor::enter_for_with_contention_mark(JavaThread* locking_thread, ObjectMonitorContentionMark& contention_mark) {
@@ -359,12 +369,12 @@
   // So this code may only contend with deflation.
   assert(locking_thread == Thread::current() || locking_thread->is_obj_deopt_suspend(), "must be");
   bool success = TryLockWithContentionMark(locking_thread, contention_mark);
 
   assert(success, "Failed to enter_for: locking_thread=" INTPTR_FORMAT
-         ", this=" INTPTR_FORMAT "{owner=" INTPTR_FORMAT "}",
-         p2i(locking_thread), p2i(this), p2i(owner_raw()));
+         ", this=" INTPTR_FORMAT "{owner=" INT64_FORMAT "}",
+         p2i(locking_thread), p2i(this), owner_raw());
 }
 
 bool ObjectMonitor::enter_for(JavaThread* locking_thread) {
   // Used by ObjectSynchronizer::enter_for() to enter for another thread.
   // The monitor is private to or already owned by locking_thread which must be suspended.
@@ -380,13 +390,13 @@
   }
 
   bool success = TryLockWithContentionMark(locking_thread, contention_mark);
 
   assert(success, "Failed to enter_for: locking_thread=" INTPTR_FORMAT
-         ", this=" INTPTR_FORMAT "{owner=" INTPTR_FORMAT "}",
-         p2i(locking_thread), p2i(this), p2i(owner_raw()));
-  assert(owner_raw() == locking_thread, "must be");
+         ", this=" INTPTR_FORMAT "{owner=" INT64_FORMAT "}",
+         p2i(locking_thread), p2i(this), owner_raw());
+  assert(is_owner(locking_thread), "must be");
   return true;
 }
 
 bool ObjectMonitor::try_enter(JavaThread* current, bool check_for_recursion) {
   // TryLock avoids the CAS and handles deflation.
@@ -400,22 +410,15 @@
   // this thread doesn't already own the lock.
   if (!check_for_recursion) {
     return false;
   }
 
-  if (r == TryLockResult::HasOwner && owner() == current) {
+  if (r == TryLockResult::HasOwner && is_owner(current)) {
     _recursions++;
     return true;
   }
 
-  void* cur = owner_raw();
-  if (LockingMode == LM_LEGACY && current->is_lock_owned((address)cur)) {
-    assert(_recursions == 0, "internal state error");
-    _recursions = 1;
-    set_owner_from_BasicLock(cur, current);  // Convert from BasicLock* to Thread*.
-    return true;
-  }
   return false;
 }
 
 bool ObjectMonitor::spin_enter(JavaThread* current) {
   assert(current == JavaThread::current(), "must be");
@@ -434,11 +437,11 @@
 
   // Do one round of spinning.
   // Note that if we acquire the monitor from an initial spin
   // we forgo posting JVMTI events and firing DTRACE probes.
   if (TrySpin(current)) {
-    assert(owner_raw() == current, "must be current: owner=" INTPTR_FORMAT, p2i(owner_raw()));
+    assert(is_owner(current), "must be current: owner=" INT64_FORMAT, owner_raw());
     assert(_recursions == 0, "must be 0: recursions=" INTX_FORMAT, _recursions);
     assert_mark_word_consistency();
     return true;
   }
 
@@ -450,12 +453,12 @@
 
   if (spin_enter(current)) {
     return true;
   }
 
-  assert(owner_raw() != current, "invariant");
-  assert(_succ != current, "invariant");
+  assert(!is_owner(current), "invariant");
+  assert(!is_succesor(current), "invariant");
   assert(!SafepointSynchronize::is_at_safepoint(), "invariant");
   assert(current->thread_state() != _thread_blocked, "invariant");
 
   // Keep is_being_async_deflated stable across the rest of enter
   ObjectMonitorContentionMark contention_mark(this);
@@ -470,11 +473,11 @@
   return true;
 }
 
 void ObjectMonitor::enter_with_contention_mark(JavaThread *current, ObjectMonitorContentionMark &cm) {
   assert(current == JavaThread::current(), "must be");
-  assert(owner_raw() != current, "must be");
+  assert(!is_owner(current), "must be");
   assert(cm._monitor == this, "must be");
   assert(!is_being_async_deflated(), "must be");
 
   JFR_ONLY(JfrConditionalFlush<EventJavaMonitorEnter> flush(current);)
   EventJavaMonitorEnter event;
@@ -501,10 +504,39 @@
       // This means that the JVMTI_EVENT_MONITOR_CONTENDED_ENTER event
       // handler cannot accidentally consume an unpark() meant for the
       // ParkEvent associated with this ObjectMonitor.
     }
 
+    ContinuationEntry* ce = current->last_continuation();
+    if (ce != nullptr && ce->is_virtual_thread()) {
+      int result = Continuation::try_preempt(current, ce->cont_oop(current));
+      if (result == freeze_ok) {
+        bool acquired = VThreadMonitorEnter(current);
+        if (acquired) {
+          // We actually acquired the monitor while trying to add the vthread to the
+          // _cxq so cancel preemption. We will still go through the preempt stub
+          // but instead of unmounting we will call thaw to continue execution.
+          current->set_preemption_cancelled(true);
+          if (JvmtiExport::should_post_monitor_contended_entered()) {
+            // We are going to call thaw again after this and finish the VMTS
+            // transition so no need to do it here. We will post the event there.
+            current->set_contended_entered_monitor(this);
+          }
+        }
+        current->set_current_pending_monitor(nullptr);
+        DEBUG_ONLY(int state = java_lang_VirtualThread::state(current->vthread()));
+        assert((acquired && current->preemption_cancelled() && state == java_lang_VirtualThread::RUNNING) ||
+               (!acquired && !current->preemption_cancelled() && state == java_lang_VirtualThread::BLOCKING), "invariant");
+        return;
+      }
+      if (result == freeze_pinned_native) {
+        post_virtual_thread_pinned_event(current, "Native frame or <clinit> on stack");
+      } else if (result == freeze_unsupported) {
+        post_virtual_thread_pinned_event(current, "Native frame or <clinit> or monitors on stack");
+      }
+    }
+
     OSThreadContendState osts(current->osthread());
 
     assert(current->thread_state() == _thread_in_vm, "invariant");
 
     for (;;) {
@@ -521,11 +553,11 @@
         // If there is a suspend request, ExitOnSuspend will exit the OM
         // and set the OM as pending.
       }
       if (!eos.exited()) {
         // ExitOnSuspend did not exit the OM
-        assert(owner_raw() == current, "invariant");
+        assert(is_owner(current), "invariant");
         break;
       }
     }
 
     // We've just gotten past the enter-check-for-suspend dance and we now own
@@ -534,12 +566,12 @@
 
   assert(contentions() >= 0, "must not be negative: contentions=%d", contentions());
 
   // Must either set _recursions = 0 or ASSERT _recursions == 0.
   assert(_recursions == 0, "invariant");
-  assert(owner_raw() == current, "invariant");
-  assert(_succ != current, "invariant");
+  assert(is_owner(current), "invariant");
+  assert(!is_succesor(current), "invariant");
   assert_mark_word_consistency();
 
   // The thread -- now the owner -- is back in vm mode.
   // Report the glorious news via TI,DTrace and jvmstat.
   // The probe effect is non-trivial.  All the reportage occurs
@@ -571,12 +603,12 @@
 
 // Caveat: TryLock() is not necessarily serializing if it returns failure.
 // Callers must compensate as needed.
 
 ObjectMonitor::TryLockResult ObjectMonitor::TryLock(JavaThread* current) {
-  void* own = owner_raw();
-  void* first_own = own;
+  int64_t own = owner_raw();
+  int64_t first_own = own;
 
   for (;;) {
     if (own == DEFLATER_MARKER) {
       // Block out deflation as soon as possible.
       ObjectMonitorContentionMark contention_mark(this);
@@ -591,13 +623,13 @@
         return TryLockResult::Success;
       } else {
         // Deflation won or change of owner; dont spin
         break;
       }
-    } else if (own == nullptr) {
-      void* prev_own = try_set_owner_from(nullptr, current);
-      if (prev_own == nullptr) {
+    } else if (own == NO_OWNER) {
+      int64_t prev_own = try_set_owner_from(NO_OWNER, current);
+      if (prev_own == NO_OWNER) {
         assert(_recursions == 0, "invariant");
         return TryLockResult::Success;
       } else {
         // The lock had been free momentarily, but we lost the race to the lock.
         own = prev_own;
@@ -632,31 +664,31 @@
   const oop obj = object_peek();
 
   if (obj == nullptr) {
     // If the object died, we can recycle the monitor without racing with
     // Java threads. The GC already broke the association with the object.
-    set_owner_from(nullptr, DEFLATER_MARKER);
+    set_owner_from_raw(NO_OWNER, DEFLATER_MARKER);
     assert(contentions() >= 0, "must be non-negative: contentions=%d", contentions());
     _contentions = INT_MIN; // minimum negative int
   } else {
     // Attempt async deflation protocol.
 
     // Set a null owner to DEFLATER_MARKER to force any contending thread
     // through the slow path. This is just the first part of the async
     // deflation dance.
-    if (try_set_owner_from(nullptr, DEFLATER_MARKER) != nullptr) {
+    if (try_set_owner_from_raw(NO_OWNER, DEFLATER_MARKER) != NO_OWNER) {
       // The owner field is no longer null so we lost the race since the
       // ObjectMonitor is now busy.
       return false;
     }
 
     if (contentions() > 0 || _waiters != 0) {
       // Another thread has raced to enter the ObjectMonitor after
       // is_busy() above or has already entered and waited on
       // it which makes it busy so no deflation. Restore owner to
       // null if it is still DEFLATER_MARKER.
-      if (try_set_owner_from(DEFLATER_MARKER, nullptr) != DEFLATER_MARKER) {
+      if (try_set_owner_from_raw(DEFLATER_MARKER, NO_OWNER) != DEFLATER_MARKER) {
         // Deferred decrement for the JT EnterI() that cancelled the async deflation.
         add_to_contentions(-1);
       }
       return false;
     }
@@ -665,11 +697,11 @@
     // to retry. This is the second part of the async deflation dance.
     if (Atomic::cmpxchg(&_contentions, 0, INT_MIN) != 0) {
       // Contentions was no longer 0 so we lost the race since the
       // ObjectMonitor is now busy. Restore owner to null if it is
       // still DEFLATER_MARKER:
-      if (try_set_owner_from(DEFLATER_MARKER, nullptr) != DEFLATER_MARKER) {
+      if (try_set_owner_from_raw(DEFLATER_MARKER, NO_OWNER) != DEFLATER_MARKER) {
         // Deferred decrement for the JT EnterI() that cancelled the async deflation.
         add_to_contentions(-1);
       }
       return false;
     }
@@ -766,32 +798,32 @@
 // Convert the fields used by is_busy() to a string that can be
 // used for diagnostic output.
 const char* ObjectMonitor::is_busy_to_string(stringStream* ss) {
   ss->print("is_busy: waiters=%d"
             ", contentions=%d"
-            ", owner=" PTR_FORMAT
+            ", owner=" INT64_FORMAT
             ", cxq=" PTR_FORMAT
             ", EntryList=" PTR_FORMAT,
             _waiters,
             (contentions() > 0 ? contentions() : 0),
             owner_is_DEFLATER_MARKER()
                 // We report null instead of DEFLATER_MARKER here because is_busy()
                 // ignores DEFLATER_MARKER values.
-                ? p2i(nullptr)
-                : p2i(owner_raw()),
+                ? NO_OWNER
+                : owner_raw(),
             p2i(_cxq),
             p2i(_EntryList));
   return ss->base();
 }
 
 void ObjectMonitor::EnterI(JavaThread* current) {
   assert(current->thread_state() == _thread_blocked, "invariant");
 
   // Try the lock - TATAS
   if (TryLock(current) == TryLockResult::Success) {
-    assert(_succ != current, "invariant");
-    assert(owner_raw() == current, "invariant");
+    assert(!is_succesor(current), "invariant");
+    assert(is_owner(current), "invariant");
     return;
   }
 
   assert(InitDone, "Unexpectedly not initialized");
 
@@ -801,18 +833,18 @@
   // operation to donate the remainder of this thread's quantum
   // to the owner.  This has subtle but beneficial affinity
   // effects.
 
   if (TrySpin(current)) {
-    assert(owner_raw() == current, "invariant");
-    assert(_succ != current, "invariant");
+    assert(is_owner(current), "invariant");
+    assert(!is_succesor(current), "invariant");
     return;
   }
 
   // The Spin failed -- Enqueue and park the thread ...
-  assert(_succ != current, "invariant");
-  assert(owner_raw() != current, "invariant");
+  assert(!is_succesor(current), "invariant");
+  assert(!is_owner(current), "invariant");
 
   // Enqueue "current" on ObjectMonitor's _cxq.
   //
   // Node acts as a proxy for current.
   // As an aside, if were to ever rewrite the synchronization code mostly
@@ -836,12 +868,12 @@
     if (Atomic::cmpxchg(&_cxq, nxt, &node) == nxt) break;
 
     // Interference - the CAS failed because _cxq changed.  Just retry.
     // As an optional optimization we retry the lock.
     if (TryLock(current) == TryLockResult::Success) {
-      assert(_succ != current, "invariant");
-      assert(owner_raw() == current, "invariant");
+      assert(!is_succesor(current), "invariant");
+      assert(is_owner(current), "invariant");
       return;
     }
   }
 
   // The lock might have been released while this thread was occupied queueing
@@ -853,19 +885,42 @@
   // TODO: Defer all thread state transitions until park-time.
   // Since state transitions are heavy and inefficient we'd like
   // to defer the state transitions until absolutely necessary,
   // and in doing so avoid some transitions ...
 
+  // For virtual threads that are pinned do a timed-park instead, to
+  // alleviate some deadlocks cases where the succesor is an unmounted
+  // virtual thread that cannot run. This can happen in particular when
+  // this virtual thread is currently loading/initializing a class, and
+  // all other carriers have a vthread pinned to it waiting for said class
+  // to be loaded/initialized.
+  static int MAX_RECHECK_INTERVAL = 1000;
+  int recheck_interval = 1;
+  bool do_timed_parked = false;
+  ContinuationEntry* ce = current->last_continuation();
+  if (ce != nullptr && ce->is_virtual_thread()) {
+    do_timed_parked = true;
+  }
+
   for (;;) {
 
     if (TryLock(current) == TryLockResult::Success) {
       break;
     }
-    assert(owner_raw() != current, "invariant");
+    assert(!is_owner(current), "invariant");
 
     // park self
-    current->_ParkEvent->park();
+    if (do_timed_parked) {
+      current->_ParkEvent->park((jlong) recheck_interval);
+      // Increase the recheck_interval, but clamp the value.
+      recheck_interval *= 8;
+      if (recheck_interval > MAX_RECHECK_INTERVAL) {
+        recheck_interval = MAX_RECHECK_INTERVAL;
+      }
+    } else {
+      current->_ParkEvent->park();
+    }
 
     if (TryLock(current) == TryLockResult::Success) {
       break;
     }
 
@@ -891,11 +946,11 @@
     // we were spinning.  That's harmless.  If we iterate and call park(),
     // park() will consume the event and return immediately and we'll
     // just spin again.  This pattern can repeat, leaving _succ to simply
     // spin on a CPU.
 
-    if (_succ == current) _succ = nullptr;
+    if (is_succesor(current)) clear_succesor();
 
     // Invariant: after clearing _succ a thread *must* retry _owner before parking.
     OrderAccess::fence();
   }
 
@@ -905,15 +960,15 @@
   // From the perspective of the lock owner (this thread), the
   // EntryList is stable and cxq is prepend-only.
   // The head of cxq is volatile but the interior is stable.
   // In addition, current.TState is stable.
 
-  assert(owner_raw() == current, "invariant");
+  assert(is_owner(current), "invariant");
 
   UnlinkAfterAcquire(current, &node);
-  if (_succ == current) {
-    _succ = nullptr;
+  if (is_succesor(current)) {
+    clear_succesor();
     // Note that we don't need to do OrderAccess::fence() after clearing
     // _succ here, since we own the lock.
   }
 
   // We've acquired ownership with CAS().
@@ -956,11 +1011,11 @@
   assert_mark_word_consistency();
 
   for (;;) {
     ObjectWaiter::TStates v = currentNode->TState;
     guarantee(v == ObjectWaiter::TS_ENTER || v == ObjectWaiter::TS_CXQ, "invariant");
-    assert(owner_raw() != current, "invariant");
+    assert(!is_owner(current), "invariant");
 
     // This thread has been notified so try to reacquire the lock.
     if (TryLock(current) == TryLockResult::Success) {
       break;
     }
@@ -992,11 +1047,11 @@
 
     // The lock is still contested.
 
     // Assuming this is not a spurious wakeup we'll normally
     // find that _succ == current.
-    if (_succ == current) _succ = nullptr;
+    if (is_succesor(current)) clear_succesor();
 
     // Invariant: after clearing _succ a contending thread
     // *must* retry  _owner before parking.
     OrderAccess::fence();
 
@@ -1015,26 +1070,153 @@
   // From the perspective of the lock owner (this thread), the
   // EntryList is stable and cxq is prepend-only.
   // The head of cxq is volatile but the interior is stable.
   // In addition, current.TState is stable.
 
-  assert(owner_raw() == current, "invariant");
+  assert(is_owner(current), "invariant");
   assert_mark_word_consistency();
   UnlinkAfterAcquire(current, currentNode);
-  if (_succ == current) _succ = nullptr;
-  assert(_succ != current, "invariant");
+  if (is_succesor(current)) clear_succesor();
+  assert(!is_succesor(current), "invariant");
   currentNode->TState = ObjectWaiter::TS_RUN;
   OrderAccess::fence();      // see comments at the end of EnterI()
 }
 
+// This method is called from two places:
+// - On monitorenter contention with a null waiter.
+// - After Object.wait() times out or the target is interrupted to reenter the
+//   monitor, with the existing waiter.
+// For the Object.wait() case we do not delete the ObjectWaiter in case we
+// succesfully acquire the monitor since we are going to need it on return.
+bool ObjectMonitor::VThreadMonitorEnter(JavaThread* current, ObjectWaiter* waiter) {
+  if (TryLock(current) == TryLockResult::Success) {
+    assert(is_owner(current), "invariant");
+    assert(!is_succesor(current), "invariant");
+    return true;
+  }
+
+  oop vthread = current->vthread();
+  ObjectWaiter* node = waiter != nullptr ? waiter : new ObjectWaiter(vthread, this);
+  node->_prev   = (ObjectWaiter*) 0xBAD;
+  node->TState  = ObjectWaiter::TS_CXQ;
+
+  // Push node associated with vthread onto the front of the _cxq.
+  ObjectWaiter* nxt;
+  for (;;) {
+    node->_next = nxt = _cxq;
+    if (Atomic::cmpxchg(&_cxq, nxt, node) == nxt) break;
+
+    // Interference - the CAS failed because _cxq changed.  Just retry.
+    // As an optional optimization we retry the lock.
+    if (TryLock(current) == TryLockResult::Success) {
+      assert(is_owner(current), "invariant");
+      assert(!is_succesor(current), "invariant");
+      if (waiter == nullptr) delete node;  // for Object.wait() don't delete yet
+      return true;
+    }
+  }
+
+  // We have to try once more since owner could have exited monitor and checked
+  // _cxq before we added the node to the queue.
+  if (TryLock(current) == TryLockResult::Success) {
+    assert(is_owner(current), "invariant");
+    UnlinkAfterAcquire(current, node);
+    if (is_succesor(current)) clear_succesor();
+    if (waiter == nullptr) delete node;  // for Object.wait() don't delete yet
+    return true;
+  }
+
+  assert(java_lang_VirtualThread::state(vthread) == java_lang_VirtualThread::RUNNING, "wrong state for vthread");
+  java_lang_VirtualThread::set_state(vthread, java_lang_VirtualThread::BLOCKING);
+
+  // We didn't succeed in acquiring the monitor so increment _contentions and
+  // save ObjectWaiter* in the chunk since we will need it when resuming execution.
+  add_to_contentions(1);
+  oop cont = java_lang_VirtualThread::continuation(vthread);
+  stackChunkOop chunk  = jdk_internal_vm_Continuation::tail(cont);
+  chunk->set_object_waiter(node);
+  return false;
+}
+
+bool ObjectMonitor::resume_operation(JavaThread* current, ObjectWaiter* node, ContinuationWrapper& cont) {
+  assert(java_lang_VirtualThread::state(current->vthread()) == java_lang_VirtualThread::RUNNING, "wrong state for vthread");
+
+  if (node->is_wait() && !node->at_reenter()) {
+    bool acquired_monitor = VThreadWaitReenter(current, node, cont);
+    if (acquired_monitor) return true;
+  }
+
+  // Retry acquiring monitor...
+
+  int state = node->TState;
+  guarantee(state == ObjectWaiter::TS_ENTER || state == ObjectWaiter::TS_CXQ, "invariant");
+
+  if (TryLock(current) == TryLockResult::Success) {
+    VThreadEpilog(current, node);
+    return true;
+  }
+
+  oop vthread = current->vthread();
+  if (is_succesor(current)) clear_succesor();
+
+  // Invariant: after clearing _succ a thread *must* retry acquiring the monitor.
+  OrderAccess::fence();
+
+  if (TryLock(current) == TryLockResult::Success) {
+    VThreadEpilog(current, node);
+    return true;
+  }
+
+  // We will return to Continuation.run() and unmount so set the right state.
+  java_lang_VirtualThread::set_state(vthread, java_lang_VirtualThread::BLOCKING);
+
+  return false;
+}
+
+void ObjectMonitor::VThreadEpilog(JavaThread* current, ObjectWaiter* node) {
+  assert(is_owner(current), "invariant");
+  add_to_contentions(-1);
+
+  if (is_succesor(current)) clear_succesor();
+
+  guarantee(_recursions == 0, "invariant");
+
+  if (node->is_wait()) {
+    _recursions = node->_recursions;   // restore the old recursion count
+    _waiters--;                        // decrement the number of waiters
+
+    if (node->_interrupted) {
+      // We will throw at thaw end after finishing the mount transition.
+      current->set_pending_interrupted_exception(true);
+    }
+  }
+
+  assert(node->TState == ObjectWaiter::TS_ENTER || node->TState == ObjectWaiter::TS_CXQ, "");
+  UnlinkAfterAcquire(current, node);
+  delete node;
+
+  // Remove the ObjectWaiter* from the stackChunk.
+  oop vthread = current->vthread();
+  oop cont = java_lang_VirtualThread::continuation(vthread);
+  stackChunkOop chunk  = jdk_internal_vm_Continuation::tail(cont);
+  chunk->set_object_waiter(nullptr);
+
+  if (JvmtiExport::should_post_monitor_contended_entered()) {
+    // We are going to call thaw again after this and finish the VMTS
+    // transition so no need to do it here. We will post the event there.
+    current->set_contended_entered_monitor(this);
+  }
+}
+
 // By convention we unlink a contending thread from EntryList|cxq immediately
 // after the thread acquires the lock in ::enter().  Equally, we could defer
 // unlinking the thread until ::exit()-time.
 
 void ObjectMonitor::UnlinkAfterAcquire(JavaThread* current, ObjectWaiter* currentNode) {
-  assert(owner_raw() == current, "invariant");
-  assert(currentNode->_thread == current, "invariant");
+  assert(is_owner(current), "invariant");
+  assert((!currentNode->is_vthread() && currentNode->thread() == current) ||
+         (currentNode->is_vthread() && currentNode->vthread() == current->vthread()), "invariant");
 
   if (currentNode->TState == ObjectWaiter::TS_ENTER) {
     // Normal case: remove current from the DLL EntryList .
     // This is a constant-time operation.
     ObjectWaiter* nxt = currentNode->_next;
@@ -1141,36 +1323,30 @@
 // then wake a thread unnecessarily. This is benign, and we've
 // structured the code so the windows are short and the frequency
 // of such futile wakups is low.
 
 void ObjectMonitor::exit(JavaThread* current, bool not_suspended) {
-  void* cur = owner_raw();
-  if (current != cur) {
-    if (LockingMode != LM_LIGHTWEIGHT && current->is_lock_owned((address)cur)) {
-      assert(_recursions == 0, "invariant");
-      set_owner_from_BasicLock(cur, current);  // Convert from BasicLock* to Thread*.
-      _recursions = 0;
-    } else {
-      // Apparent unbalanced locking ...
-      // Naively we'd like to throw IllegalMonitorStateException.
-      // As a practical matter we can neither allocate nor throw an
-      // exception as ::exit() can be called from leaf routines.
-      // see x86_32.ad Fast_Unlock() and the I1 and I2 properties.
-      // Upon deeper reflection, however, in a properly run JVM the only
-      // way we should encounter this situation is in the presence of
-      // unbalanced JNI locking. TODO: CheckJNICalls.
-      // See also: CR4414101
+  int64_t cur = owner_raw();
+  if (owner_for(current) != cur) {
+    // Apparent unbalanced locking ...
+    // Naively we'd like to throw IllegalMonitorStateException.
+    // As a practical matter we can neither allocate nor throw an
+    // exception as ::exit() can be called from leaf routines.
+    // see x86_32.ad Fast_Unlock() and the I1 and I2 properties.
+    // Upon deeper reflection, however, in a properly run JVM the only
+    // way we should encounter this situation is in the presence of
+    // unbalanced JNI locking. TODO: CheckJNICalls.
+    // See also: CR4414101
 #ifdef ASSERT
-      LogStreamHandle(Error, monitorinflation) lsh;
-      lsh.print_cr("ERROR: ObjectMonitor::exit(): thread=" INTPTR_FORMAT
-                    " is exiting an ObjectMonitor it does not own.", p2i(current));
-      lsh.print_cr("The imbalance is possibly caused by JNI locking.");
-      print_debug_style_on(&lsh);
-      assert(false, "Non-balanced monitor enter/exit!");
+    LogStreamHandle(Error, monitorinflation) lsh;
+    lsh.print_cr("ERROR: ObjectMonitor::exit(): thread=" INTPTR_FORMAT
+                  " is exiting an ObjectMonitor it does not own.", p2i(current));
+    lsh.print_cr("The imbalance is possibly caused by JNI locking.");
+    print_debug_style_on(&lsh);
+    assert(false, "Non-balanced monitor enter/exit!");
 #endif
-      return;
-    }
+    return;
   }
 
   if (_recursions != 0) {
     _recursions--;        // this is simple recursive enter
     return;
@@ -1183,22 +1359,22 @@
     _previous_owner_tid = JFR_THREAD_ID(current);
   }
 #endif
 
   for (;;) {
-    assert(current == owner_raw(), "invariant");
+    assert(is_owner(current), "invariant");
 
     // Drop the lock.
     // release semantics: prior loads and stores from within the critical section
     // must not float (reorder) past the following store that drops the lock.
     // Uses a storeload to separate release_store(owner) from the
-    // successor check. The try_set_owner() below uses cmpxchg() so
+    // successor check. The try_set_owner_from() below uses cmpxchg() so
     // we get the fence down there.
     release_clear_owner(current);
     OrderAccess::storeload();
 
-    if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != nullptr) {
+    if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || has_succesor()) {
       return;
     }
     // Other threads are blocked trying to acquire the lock.
 
     // Normally the exiting thread is responsible for ensuring succession,
@@ -1236,11 +1412,11 @@
       // Some other thread acquired the lock (or the monitor was
       // deflated). Either way we are done.
       return;
     }
 
-    guarantee(owner_raw() == current, "invariant");
+    guarantee(is_owner(current), "invariant");
 
     ObjectWaiter* w = nullptr;
 
     w = _EntryList;
     if (w != nullptr) {
@@ -1301,11 +1477,11 @@
     // The MEMBAR is satisfied by the release_store() operation in ExitEpilog().
 
     // See if we can abdicate to a spinner instead of waking a thread.
     // A primary goal of the implementation is to reduce the
     // context-switch rate.
-    if (_succ != nullptr) continue;
+    if (has_succesor()) continue;
 
     w = _EntryList;
     if (w != nullptr) {
       guarantee(w->TState == ObjectWaiter::TS_ENTER, "invariant");
       ExitEpilog(current, w);
@@ -1313,20 +1489,31 @@
     }
   }
 }
 
 void ObjectMonitor::ExitEpilog(JavaThread* current, ObjectWaiter* Wakee) {
-  assert(owner_raw() == current, "invariant");
+  assert(is_owner(current), "invariant");
 
   // Exit protocol:
   // 1. ST _succ = wakee
   // 2. membar #loadstore|#storestore;
   // 2. ST _owner = nullptr
   // 3. unpark(wakee)
 
-  _succ = Wakee->_thread;
-  ParkEvent * Trigger = Wakee->_event;
+  oop vthread = nullptr;
+  ParkEvent * Trigger;
+  if (!Wakee->is_vthread()) {
+    JavaThread* t = Wakee->thread();
+    assert(t != nullptr, "");
+    Trigger = t->_ParkEvent;
+    set_succesor(t);
+  } else {
+    vthread = Wakee->vthread();
+    assert(vthread != nullptr, "");
+    Trigger = ObjectMonitor::vthread_unparker_ParkEvent();
+    set_succesor(vthread);
+  }
 
   // Hygiene -- once we've set _owner = nullptr we can't safely dereference Wakee again.
   // The thread associated with Wakee may have grabbed the lock and "Wakee" may be
   // out-of-scope (non-extant).
   Wakee  = nullptr;
@@ -1335,11 +1522,18 @@
   // Uses a fence to separate release_store(owner) from the LD in unpark().
   release_clear_owner(current);
   OrderAccess::fence();
 
   DTRACE_MONITOR_PROBE(contended__exit, this, object(), current);
-  Trigger->unpark();
+
+  if (vthread == nullptr) {
+    // Platform thread case.
+    Trigger->unpark();
+  } else if (java_lang_VirtualThread::set_onWaitingList(vthread, vthread_cxq_head())) {
+    // Virtual thread case.
+    Trigger->unpark();
+  }
 
   // Maintain stats and report events to JVMTI
   OM_PERFDATA_OP(Parks, inc());
 }
 
@@ -1349,24 +1543,24 @@
 // inflated monitor, e.g. the monitor can be inflated by a non-owning
 // thread due to contention.
 intx ObjectMonitor::complete_exit(JavaThread* current) {
   assert(InitDone, "Unexpectedly not initialized");
 
-  void* cur = owner_raw();
-  if (current != cur) {
-    if (LockingMode != LM_LIGHTWEIGHT && current->is_lock_owned((address)cur)) {
+  int64_t cur = owner_raw();
+  if (owner_for(current) != cur) {
+    if (LockingMode == LM_LEGACY && is_stack_locker(current)) {
       assert(_recursions == 0, "internal state error");
-      set_owner_from_BasicLock(cur, current);  // Convert from BasicLock* to Thread*.
+      set_owner_from_BasicLock(current);  // Convert from BasicLock* to Thread*.
       _recursions = 0;
     }
   }
 
-  guarantee(current == owner_raw(), "complete_exit not owner");
+  guarantee(is_owner(current), "complete_exit not owner");
   intx save = _recursions; // record the old recursion count
   _recursions = 0;         // set the recursion level to be 0
   exit(current);           // exit the monitor
-  guarantee(owner_raw() != current, "invariant");
+  guarantee(!is_owner(current), "invariant");
   return save;
 }
 
 // Checks that the current THREAD owns this monitor and causes an
 // immediate return if it doesn't. We don't use the CHECK macro
@@ -1385,18 +1579,12 @@
 // Otherwise returns false and throws IllegalMonitorStateException
 // (IMSE). If there is a pending exception and the specified thread
 // is not the owner, that exception will be replaced by the IMSE.
 bool ObjectMonitor::check_owner(TRAPS) {
   JavaThread* current = THREAD;
-  void* cur = owner_raw();
-  assert(cur != anon_owner_ptr(), "no anon owner here");
-  if (cur == current) {
-    return true;
-  }
-  if (LockingMode != LM_LIGHTWEIGHT && current->is_lock_owned((address)cur)) {
-    set_owner_from_BasicLock(cur, current);  // Convert from BasicLock* to Thread*.
-    _recursions = 0;
+  int64_t cur = owner_raw();
+  if (cur == owner_for(current)) {
     return true;
   }
   THROW_MSG_(vmSymbols::java_lang_IllegalMonitorStateException(),
              "current thread is not owner", false);
 }
@@ -1427,10 +1615,36 @@
   event->set_notifier(notifier_tid);
   event->set_timedOut(timedout);
   event->commit();
 }
 
+static void vthread_monitor_waited_event(JavaThread *current, ObjectWaiter* node, ContinuationWrapper& cont, EventJavaMonitorWait* event, jboolean timed_out) {
+  // Since we might safepoint set the anchor so that the stack can we walked.
+  assert(current->last_continuation() != nullptr, "");
+  JavaFrameAnchor* anchor = current->frame_anchor();
+  anchor->set_last_Java_sp(current->last_continuation()->entry_sp());
+  anchor->set_last_Java_pc(current->last_continuation()->entry_pc());
+
+  ContinuationWrapper::SafepointOp so(current, cont);
+
+  JRT_BLOCK
+    if (event->should_commit()) {
+      long timeout = java_lang_VirtualThread::waitTimeout(current->vthread());
+      post_monitor_wait_event(event, node->_monitor, node->_notifier_tid, timeout, timed_out);
+    }
+    if (JvmtiExport::should_post_monitor_waited()) {
+      // We mark this call in case of an upcall to Java while posting the event.
+      // If somebody walks the stack in that case, processing the enterSpecial
+      // frame should not include processing callee arguments since there is no
+      // actual callee (see nmethod::preserve_callee_argument_oops()).
+      ThreadOnMonitorWaitedEvent tmwe(current);
+      JvmtiExport::vthread_post_monitor_waited(current, node->_monitor, timed_out);
+    }
+  JRT_BLOCK_END
+  current->frame_anchor()->clear();
+}
+
 // -----------------------------------------------------------------------------
 // Wait/Notify/NotifyAll
 //
 // Note: a subset of changes to ObjectMonitor::wait()
 // will need to be replicated in complete_exit
@@ -1466,10 +1680,30 @@
     return;
   }
 
   current->set_current_waiting_monitor(this);
 
+  ContinuationEntry* ce = current->last_continuation();
+  if (interruptible && ce != nullptr && ce->is_virtual_thread()) {
+    int result = Continuation::try_preempt(current, ce->cont_oop(current));
+    if (result == freeze_ok) {
+      VThreadWait(current, millis);
+      current->set_current_waiting_monitor(nullptr);
+      return;
+    }
+    if (result == freeze_pinned_native || result == freeze_unsupported) {
+      const Klass* monitor_klass = object()->klass();
+      if (!is_excluded(monitor_klass)) {
+        if (result == freeze_pinned_native) {
+          post_virtual_thread_pinned_event(current,"Native frame or <clinit> on stack");
+        } else if (result == freeze_unsupported) {
+          post_virtual_thread_pinned_event(current, "Native frame or <clinit> or monitors on stack");
+        }
+      }
+    }
+  }
+
   // create a node to be put into the queue
   // Critically, after we reset() the event but prior to park(), we must check
   // for a pending interrupt.
   ObjectWaiter node(current);
   node.TState = ObjectWaiter::TS_WAIT;
@@ -1489,11 +1723,11 @@
 
   intx save = _recursions;     // record the old recursion count
   _waiters++;                  // increment the number of waiters
   _recursions = 0;             // set the recursion level to be 1
   exit(current);               // exit the monitor
-  guarantee(owner_raw() != current, "invariant");
+  guarantee(!is_owner(current), "invariant");
 
   // The thread is on the WaitSet list - now park() it.
   // On MP systems it's conceivable that a brief spin before we park
   // could be profitable.
   //
@@ -1515,11 +1749,11 @@
     {
       ClearSuccOnSuspend csos(this);
       ThreadBlockInVMPreprocess<ClearSuccOnSuspend> tbivs(current, csos, true /* allow_suspend */);
       if (interrupted || HAS_PENDING_EXCEPTION) {
         // Intentionally empty
-      } else if (node._notified == 0) {
+      } else if (!node._notified) {
         if (millis <= 0) {
           current->_ParkEvent->park();
         } else {
           ret = current->_ParkEvent->park(millis);
         }
@@ -1543,11 +1777,11 @@
 
     if (node.TState == ObjectWaiter::TS_WAIT) {
       Thread::SpinAcquire(&_WaitSetLock, "WaitSet - unlink");
       if (node.TState == ObjectWaiter::TS_WAIT) {
         DequeueSpecificWaiter(&node);       // unlink from WaitSet
-        assert(node._notified == 0, "invariant");
+        assert(!node._notified, "invariant");
         node.TState = ObjectWaiter::TS_RUN;
       }
       Thread::SpinRelease(&_WaitSetLock);
     }
 
@@ -1555,11 +1789,11 @@
     // on the EntryList (TS_ENTER), or on the cxq (TS_CXQ).
     // The Node's TState variable is stable from the perspective of this thread.
     // No other threads will asynchronously modify TState.
     guarantee(node.TState != ObjectWaiter::TS_WAIT, "invariant");
     OrderAccess::loadload();
-    if (_succ == current) _succ = nullptr;
+    if (is_succesor(current)) clear_succesor();
     WasNotified = node._notified;
 
     // Reentry phase -- reacquire the monitor.
     // re-enter contended monitor after object.wait().
     // retain OBJECT_WAIT state until re-enter successfully completes
@@ -1569,11 +1803,11 @@
 
     // post monitor waited event. Note that this is past-tense, we are done waiting.
     if (JvmtiExport::should_post_monitor_waited()) {
       JvmtiExport::post_monitor_waited(current, this, ret == OS_TIMEOUT);
 
-      if (node._notified != 0 && _succ == current) {
+      if (node._notified && is_succesor(current)) {
         // In this part of the monitor wait-notify-reenter protocol it
         // is possible (and normal) for another thread to do a fastpath
         // monitor enter-exit while this thread is still trying to get
         // to the reenter portion of the protocol.
         //
@@ -1585,23 +1819,27 @@
         // monitors and JVM/TI RawMonitors (for now).
         //
         // We redo the unpark() to ensure forward progress, i.e., we
         // don't want all pending threads hanging (parked) with none
         // entering the unlocked monitor.
-        node._event->unpark();
+        current->_ParkEvent->unpark();
       }
     }
 
     if (event.should_commit()) {
       post_monitor_wait_event(&event, this, node._notifier_tid, millis, ret == OS_TIMEOUT);
     }
 
     OrderAccess::fence();
 
-    assert(owner_raw() != current, "invariant");
+    assert(!is_owner(current), "invariant");
     ObjectWaiter::TStates v = node.TState;
     if (v == ObjectWaiter::TS_RUN) {
+      // We use the NoPreemptMark for the very rare case where the previous
+      // preempt attempt failed due to OOM. The preempt on monitor contention
+      // could succeed but we can't unmount now.
+      NoPreemptMark npm(current);
       enter(current);
     } else {
       guarantee(v == ObjectWaiter::TS_ENTER || v == ObjectWaiter::TS_CXQ, "invariant");
       ReenterI(current, &node);
       node.wait_reenter_end(this);
@@ -1610,12 +1848,12 @@
     // current has reacquired the lock.
     // Lifecycle - the node representing current must not appear on any queues.
     // Node is about to go out-of-scope, but even if it were immortal we wouldn't
     // want residual elements associated with this thread left on any lists.
     guarantee(node.TState == ObjectWaiter::TS_RUN, "invariant");
-    assert(owner_raw() == current, "invariant");
-    assert(_succ != current, "invariant");
+    assert(is_owner(current), "invariant");
+    assert(!is_succesor(current), "invariant");
   } // OSThreadWaitState()
 
   current->set_current_waiting_monitor(nullptr);
 
   guarantee(_recursions == 0, "invariant");
@@ -1624,12 +1862,12 @@
                 + relock_count; //  increased by the deferred relock count
   current->inc_held_monitor_count(relock_count); // Deopt never entered these counts.
   _waiters--;             // decrement the number of waiters
 
   // Verify a few postconditions
-  assert(owner_raw() == current, "invariant");
-  assert(_succ != current, "invariant");
+  assert(is_owner(current), "invariant");
+  assert(!is_succesor(current), "invariant");
   assert_mark_word_consistency();
 
   // check if the notification happened
   if (!WasNotified) {
     // no, it could be timeout or Thread.interrupt() or both
@@ -1641,31 +1879,46 @@
 
   // NOTE: Spurious wake up will be consider as timeout.
   // Monitor notify has precedence over thread interrupt.
 }
 
-
 // Consider:
 // If the lock is cool (cxq == null && succ == null) and we're on an MP system
 // then instead of transferring a thread from the WaitSet to the EntryList
 // we might just dequeue a thread from the WaitSet and directly unpark() it.
 
 void ObjectMonitor::INotify(JavaThread* current) {
   Thread::SpinAcquire(&_WaitSetLock, "WaitSet - notify");
   ObjectWaiter* iterator = DequeueWaiter();
   if (iterator != nullptr) {
     guarantee(iterator->TState == ObjectWaiter::TS_WAIT, "invariant");
-    guarantee(iterator->_notified == 0, "invariant");
+    guarantee(!iterator->_notified, "invariant");
     // Disposition - what might we do with iterator ?
     // a.  add it directly to the EntryList - either tail (policy == 1)
     //     or head (policy == 0).
     // b.  push it onto the front of the _cxq (policy == 2).
     // For now we use (b).
 
+    if (iterator->is_vthread()) {
+      oop vthread = iterator->vthread();
+      java_lang_VirtualThread::set_notified(vthread, true);
+      int old_state = java_lang_VirtualThread::state(vthread);
+      // If state is not WAIT/TIMED_WAIT then target could still be on
+      // unmount transition, or wait could have already timed-out or target
+      // could have been interrupted. In the first case, the target itself
+      // will set the state to BLOCKED at the end of the unmount transition.
+      // In the other cases the target would have been already unblocked so
+      // there is nothing to do.
+      if (old_state == java_lang_VirtualThread::WAIT ||
+          old_state == java_lang_VirtualThread::TIMED_WAIT) {
+        java_lang_VirtualThread::cmpxchg_state(vthread, old_state, java_lang_VirtualThread::BLOCKED);
+      }
+    }
+
     iterator->TState = ObjectWaiter::TS_ENTER;
 
-    iterator->_notified = 1;
+    iterator->_notified = true;
     iterator->_notifier_tid = JFR_THREAD_ID(current);
 
     ObjectWaiter* list = _EntryList;
     if (list != nullptr) {
       assert(list->_prev == nullptr, "invariant");
@@ -1693,12 +1946,13 @@
     // protected by _WaitSetLock.  In practice that's not useful.  With the
     // exception of  wait() timeouts and interrupts the monitor owner
     // is the only thread that grabs _WaitSetLock.  There's almost no contention
     // on _WaitSetLock so it's not profitable to reduce the length of the
     // critical section.
-
-    iterator->wait_reenter_begin(this);
+    if (!iterator->is_vthread()) {
+      iterator->wait_reenter_begin(this);
+    }
   }
   Thread::SpinRelease(&_WaitSetLock);
 }
 
 // Consider: a not-uncommon synchronization bug is to use notify() when
@@ -1745,10 +1999,99 @@
   }
 
   OM_PERFDATA_OP(Notifications, inc(tally));
 }
 
+void ObjectMonitor::VThreadWait(JavaThread* current, jlong millis) {
+  oop vthread = current->vthread();
+  ObjectWaiter* node = new ObjectWaiter(vthread, this);
+  node->_is_wait = true;
+  node->TState = ObjectWaiter::TS_WAIT;
+  java_lang_VirtualThread::set_notified(vthread, false);  // Reset notified flag
+
+  // Enter the waiting queue, which is a circular doubly linked list in this case
+  // but it could be a priority queue or any data structure.
+  // _WaitSetLock protects the wait queue.  Normally the wait queue is accessed only
+  // by the owner of the monitor *except* in the case where park()
+  // returns because of a timeout or interrupt.  Contention is exceptionally rare
+  // so we use a simple spin-lock instead of a heavier-weight blocking lock.
+
+  Thread::SpinAcquire(&_WaitSetLock, "WaitSet - add");
+  AddWaiter(node);
+  Thread::SpinRelease(&_WaitSetLock);
+
+  node->_recursions = _recursions;   // record the old recursion count
+  _recursions = 0;                   // set the recursion level to be 0
+  _waiters++;                        // increment the number of waiters
+  exit(current);                     // exit the monitor
+  guarantee(!is_owner(current), "invariant");
+
+  assert(java_lang_VirtualThread::state(vthread) == java_lang_VirtualThread::RUNNING, "wrong state for vthread");
+  java_lang_VirtualThread::set_state(vthread, millis == 0 ? java_lang_VirtualThread::WAITING : java_lang_VirtualThread::TIMED_WAITING);
+  java_lang_VirtualThread::set_waitTimeout(vthread, millis);
+
+  // Save the ObjectWaiter* in the chunk since we will need it when resuming execution.
+  oop cont = java_lang_VirtualThread::continuation(vthread);
+  stackChunkOop chunk  = jdk_internal_vm_Continuation::tail(cont);
+  chunk->set_object_waiter(node);
+}
+
+bool ObjectMonitor::VThreadWaitReenter(JavaThread* current, ObjectWaiter* node, ContinuationWrapper& cont) {
+  // First time we run after being preempted on Object.wait().
+  // Check if we were interrupted or the wait timed-out, and in
+  // that case remove ourselves from the _WaitSet queue.
+  if (node->TState == ObjectWaiter::TS_WAIT) {
+    Thread::SpinAcquire(&_WaitSetLock, "WaitSet - unlink");
+    if (node->TState == ObjectWaiter::TS_WAIT) {
+      DequeueSpecificWaiter(node);       // unlink from WaitSet
+      assert(!node->_notified, "invariant");
+      node->TState = ObjectWaiter::TS_RUN;
+    }
+    Thread::SpinRelease(&_WaitSetLock);
+  }
+
+  // If this was an interrupted case, set the _interrupted boolean so that
+  // once we re-acquire the monitor we know if we need to throw IE or not.
+  ObjectWaiter::TStates state = node->TState;
+  bool was_notified = state == ObjectWaiter::TS_ENTER || state == ObjectWaiter::TS_CXQ;
+  assert(was_notified || state == ObjectWaiter::TS_RUN, "");
+  node->_interrupted = !was_notified && current->is_interrupted(false);
+
+  // Post JFR and JVMTI events.
+  EventJavaMonitorWait event;
+  if (event.should_commit() || JvmtiExport::should_post_monitor_waited()) {
+    vthread_monitor_waited_event(current, node, cont, &event, !was_notified && !node->_interrupted);
+  }
+
+  // Mark that we are at reenter so that we don't call this method again.
+  node->_at_reenter = true;
+  assert(!is_owner(current), "invariant");
+
+  if (!was_notified) {
+    bool acquired = VThreadMonitorEnter(current, node);
+    if (acquired) {
+      guarantee(_recursions == 0, "invariant");
+      _recursions = node->_recursions;   // restore the old recursion count
+      _waiters--;                        // decrement the number of waiters
+
+      if (node->_interrupted) {
+        // We will throw at thaw end after finishing the mount transition.
+        current->set_pending_interrupted_exception(true);
+      }
+
+      delete node;
+      stackChunkOop chunk  = cont.tail();
+      chunk->set_object_waiter(nullptr);
+      return true;
+    }
+  } else {
+    // Already moved to _cxq or _EntryList by notifier, so just add to contentions.
+    add_to_contentions(1);
+  }
+  return false;
+}
+
 // -----------------------------------------------------------------------------
 // Adaptive Spinning Support
 //
 // Adaptive spin-then-block - rational spinning
 //
@@ -1898,14 +2241,14 @@
 
   // We're good to spin ... spin ingress.
   // CONSIDER: use Prefetch::write() to avoid RTS->RTO upgrades
   // when preparing to LD...CAS _owner, etc and the CAS is likely
   // to succeed.
-  if (_succ == nullptr) {
-    _succ = current;
+  if (!has_succesor()) {
+    set_succesor(current);
   }
-  Thread* prv = nullptr;
+  int64_t prv = NO_OWNER;
 
   // There are three ways to exit the following loop:
   // 1.  A successful spin where this thread has acquired the lock.
   // 2.  Spin failure with prejudice
   // 3.  Spin failure without prejudice
@@ -1939,18 +2282,18 @@
     // or if we observe _owner change from one non-null value to
     // another non-null value.   In such cases we might abort
     // the spin without prejudice or apply a "penalty" to the
     // spin count-down variable "ctr", reducing it by 100, say.
 
-    JavaThread* ox = static_cast<JavaThread*>(owner_raw());
-    if (ox == nullptr) {
-      ox = static_cast<JavaThread*>(try_set_owner_from(nullptr, current));
-      if (ox == nullptr) {
+    int64_t ox = owner_raw();
+    if (ox == NO_OWNER) {
+      ox = try_set_owner_from(NO_OWNER, current);
+      if (ox == NO_OWNER) {
         // The CAS succeeded -- this thread acquired ownership
         // Take care of some bookkeeping to exit spin state.
-        if (_succ == current) {
-          _succ = nullptr;
+        if (is_succesor(current)) {
+          clear_succesor();
         }
 
         // Increase _SpinDuration :
         // The spin was successful (profitable) so we tend toward
         // longer spin attempts in the future.
@@ -1969,27 +2312,27 @@
       // * Since CAS is high-latency, retry again immediately.
       break;
     }
 
     // Did lock ownership change hands ?
-    if (ox != prv && prv != nullptr) {
+    if (ox != prv && prv != NO_OWNER) {
       break;
     }
     prv = ox;
 
-    if (_succ == nullptr) {
-      _succ = current;
+    if (!has_succesor()) {
+      set_succesor(current);
     }
   }
 
   // Spin failed with prejudice -- reduce _SpinDuration.
   if (ctr < 0) {
     _SpinDuration = adjust_down(_SpinDuration);
   }
 
-  if (_succ == current) {
-    _succ = nullptr;
+  if (is_succesor(current)) {
+    clear_succesor();
     // Invariant: after setting succ=null a contending thread
     // must recheck-retry _owner before parking.  This usually happens
     // in the normal usage of TrySpin(), but it's safest
     // to make TrySpin() as foolproof as possible.
     OrderAccess::fence();
@@ -2006,17 +2349,37 @@
 // WaitSet management ...
 
 ObjectWaiter::ObjectWaiter(JavaThread* current) {
   _next     = nullptr;
   _prev     = nullptr;
-  _notified = 0;
+  _thread   = current;
+  _monitor  = nullptr;
   _notifier_tid = 0;
+  _recursions = 0;
   TState    = TS_RUN;
-  _thread   = current;
-  _event    = _thread->_ParkEvent;
+  _notified = false;
+  _is_wait  = false;
+  _at_reenter = false;
+  _interrupted = false;
   _active   = false;
-  assert(_event != nullptr, "invariant");
+}
+
+ObjectWaiter::ObjectWaiter(oop vthread, ObjectMonitor* mon) : ObjectWaiter(nullptr) {
+  assert(oopDesc::is_oop(vthread), "");
+  _vthread = OopHandle(JavaThread::thread_oop_storage(), vthread);
+  _monitor = mon;
+}
+
+ObjectWaiter::~ObjectWaiter() {
+  if (is_vthread()) {
+    assert(vthread() != nullptr, "");
+    _vthread.release(JavaThread::thread_oop_storage());
+  }
+}
+
+oop ObjectWaiter::vthread() {
+  return _vthread.resolve();
 }
 
 void ObjectWaiter::wait_reenter_begin(ObjectMonitor * const mon) {
   _active = JavaThreadBlockedOnMonitorEnterState::wait_reenter_begin(_thread, mon);
 }
@@ -2130,16 +2493,21 @@
   _oop_storage = OopStorageSet::create_weak("ObjectSynchronizer Weak", mtSynchronizer);
 
   DEBUG_ONLY(InitDone = true;)
 }
 
+void ObjectMonitor::Initialize2() {
+  _vthread_cxq_head = OopHandle(JavaThread::thread_oop_storage(), nullptr);
+  _vthread_unparker_ParkEvent = ParkEvent::Allocate(nullptr);
+}
+
 void ObjectMonitor::print_on(outputStream* st) const {
   // The minimal things to print for markWord printing, more can be added for debugging and logging.
   st->print("{contentions=0x%08x,waiters=0x%08x"
-            ",recursions=" INTX_FORMAT ",owner=" INTPTR_FORMAT "}",
+            ",recursions=" INTX_FORMAT ",owner=" INT64_FORMAT "}",
             contentions(), waiters(), recursions(),
-            p2i(owner()));
+            owner());
 }
 void ObjectMonitor::print() const { print_on(tty); }
 
 #ifdef ASSERT
 // Print the ObjectMonitor like a debugger would:
@@ -2178,22 +2546,22 @@
   st->print_cr("  _pad_buf0 = {");
   st->print_cr("    [0] = '\\0'");
   st->print_cr("    ...");
   st->print_cr("    [%d] = '\\0'", (int)sizeof(_pad_buf0) - 1);
   st->print_cr("  }");
-  st->print_cr("  _owner = " INTPTR_FORMAT, p2i(owner_raw()));
+  st->print_cr("  _owner = " INT64_FORMAT, owner_raw());
   st->print_cr("  _previous_owner_tid = " UINT64_FORMAT, _previous_owner_tid);
   st->print_cr("  _pad_buf1 = {");
   st->print_cr("    [0] = '\\0'");
   st->print_cr("    ...");
   st->print_cr("    [%d] = '\\0'", (int)sizeof(_pad_buf1) - 1);
   st->print_cr("  }");
   st->print_cr("  _next_om = " INTPTR_FORMAT, p2i(next_om()));
   st->print_cr("  _recursions = " INTX_FORMAT, _recursions);
   st->print_cr("  _EntryList = " INTPTR_FORMAT, p2i(_EntryList));
   st->print_cr("  _cxq = " INTPTR_FORMAT, p2i(_cxq));
-  st->print_cr("  _succ = " INTPTR_FORMAT, p2i(_succ));
+  st->print_cr("  _succ = " INT64_FORMAT, _succ);
   st->print_cr("  _SpinDuration = %d", _SpinDuration);
   st->print_cr("  _contentions = %d", contentions());
   st->print_cr("  _WaitSet = " INTPTR_FORMAT, p2i(_WaitSet));
   st->print_cr("  _waiters = %d", _waiters);
   st->print_cr("  _WaitSetLock = %d", _WaitSetLock);
diff a/src/hotspot/share/runtime/objectMonitor.hpp b/src/hotspot/share/runtime/objectMonitor.hpp
--- a/src/hotspot/share/runtime/objectMonitor.hpp
+++ b/src/hotspot/share/runtime/objectMonitor.hpp
@@ -26,38 +26,57 @@
 #define SHARE_RUNTIME_OBJECTMONITOR_HPP
 
 #include "memory/allocation.hpp"
 #include "memory/padded.hpp"
 #include "oops/markWord.hpp"
+#include "oops/oopHandle.hpp"
 #include "oops/weakHandle.hpp"
+#include "runtime/javaThread.hpp"
 #include "runtime/perfDataTypes.hpp"
 #include "utilities/checkedCast.hpp"
 
 class ObjectMonitor;
 class ObjectMonitorContentionMark;
 class ParkEvent;
+class BasicLock;
+class ContinuationWrapper;
 
 // ObjectWaiter serves as a "proxy" or surrogate thread.
 // TODO-FIXME: Eliminate ObjectWaiter and use the thread-specific
 // ParkEvent instead.  Beware, however, that the JVMTI code
 // knows about ObjectWaiters, so we'll have to reconcile that code.
 // See next_waiter(), first_waiter(), etc.
 
-class ObjectWaiter : public StackObj {
+class ObjectWaiter : public CHeapObj<mtThread> {
  public:
-  enum TStates { TS_UNDEF, TS_READY, TS_RUN, TS_WAIT, TS_ENTER, TS_CXQ };
+  enum TStates : uint8_t { TS_UNDEF, TS_READY, TS_RUN, TS_WAIT, TS_ENTER, TS_CXQ };
   ObjectWaiter* volatile _next;
   ObjectWaiter* volatile _prev;
-  JavaThread*   _thread;
-  uint64_t      _notifier_tid;
-  ParkEvent *   _event;
-  volatile int  _notified;
+  JavaThread*     _thread;
+  OopHandle      _vthread;
+  ObjectMonitor* _monitor;
+  uint64_t  _notifier_tid;
+  int         _recursions;
   volatile TStates TState;
-  bool          _active;           // Contention monitoring is enabled
+  volatile bool _notified;
+  bool           _is_wait;
+  bool        _at_reenter;
+  bool       _interrupted;
+  bool            _active;    // Contention monitoring is enabled
  public:
   ObjectWaiter(JavaThread* current);
-
+  ObjectWaiter(oop vthread, ObjectMonitor* mon);
+  ~ObjectWaiter();
+  JavaThread* thread() { return _thread; }
+  bool is_vthread()    { return _thread == nullptr; }
+  uint8_t state()      { return TState; }
+  ObjectMonitor* monitor() { return _monitor; }
+  bool is_monitorenter()   { return !_is_wait; }
+  bool is_wait()           { return _is_wait; }
+  bool notified()          { return _notified; }
+  bool at_reenter()        { return _at_reenter; }
+  oop vthread();
   void wait_reenter_begin(ObjectMonitor *mon);
   void wait_reenter_end(ObjectMonitor *mon);
 };
 
 // The ObjectMonitor class implements the heavyweight version of a
@@ -130,10 +149,15 @@
   friend class VMStructs;
   JVMCI_ONLY(friend class JVMCIVMStructs;)
 
   static OopStorage* _oop_storage;
 
+  // List of j.l.VirtualThread waiting to be unblocked by unblocker thread.
+  static OopHandle _vthread_cxq_head;
+  // ParkEvent of unblocker thread.
+  static ParkEvent* _vthread_unparker_ParkEvent;
+
   // The sync code expects the metadata field to be at offset zero (0).
   // Enforced by the assert() in metadata_addr().
   // * LM_LIGHTWEIGHT with UseObjectMonitorTable:
   // Contains the _object's hashCode.
   // * LM_LEGACY, LM_MONITOR, LM_LIGHTWEIGHT without UseObjectMonitorTable:
@@ -144,28 +168,24 @@
   // have busy multi-threaded access. _metadata and _object are set at initial
   // inflation. The _object does not change, so it is a good choice to share
   // its cache line with _metadata.
   DEFINE_PAD_MINUS_SIZE(0, OM_CACHE_LINE_SIZE, sizeof(_metadata) +
                         sizeof(WeakHandle));
+
+  static const int64_t NO_OWNER = 0;
+  static const int64_t ANONYMOUS_OWNER = 1;
   // Used by async deflation as a marker in the _owner field.
   // Note that the choice of the two markers is peculiar:
   // - They need to represent values that cannot be pointers. In particular,
   //   we achieve this by using the lowest two bits.
   // - ANONYMOUS_OWNER should be a small value, it is used in generated code
   //   and small values encode much better.
   // - We test for anonymous owner by testing for the lowest bit, therefore
   //   DEFLATER_MARKER must *not* have that bit set.
-  static const uintptr_t DEFLATER_MARKER_VALUE = 2;
-  #define DEFLATER_MARKER reinterpret_cast<void*>(DEFLATER_MARKER_VALUE)
- public:
-  // NOTE: Typed as uintptr_t so that we can pick it up in SA, via vmStructs.
-  static const uintptr_t ANONYMOUS_OWNER = 1;
+  static const int64_t DEFLATER_MARKER = 2;
 
- private:
-  static void* anon_owner_ptr() { return reinterpret_cast<void*>(ANONYMOUS_OWNER); }
-
-  void* volatile _owner;            // pointer to owning thread OR BasicLock
+  int64_t volatile _owner;  // Either tid of owner, ANONYMOUS_OWNER_MARKER or DEFLATER_MARKER.
   volatile uint64_t _previous_owner_tid;  // thread id of the previous owner of the monitor
   // Separate _owner and _next_om on different cache lines since
   // both can have busy multi-threaded access. _previous_owner_tid is only
   // changed by ObjectMonitor::exit() so it is a good choice to share the
   // cache line with _owner.
@@ -176,11 +196,11 @@
   ObjectWaiter* volatile _EntryList;  // Threads blocked on entry or reentry.
                                       // The list is actually composed of WaitNodes,
                                       // acting as proxies for Threads.
 
   ObjectWaiter* volatile _cxq;      // LL of recently-arrived threads blocked on entry.
-  JavaThread* volatile _succ;       // Heir presumptive thread - used for futile wakeup throttling
+  int64_t volatile _succ;           // Heir presumptive thread - used for futile wakeup throttling
 
   volatile int _SpinDuration;
 
   int _contentions;                 // Number of active contentions in enter(). It is used by is_busy()
                                     // along with other fields to determine if an ObjectMonitor can be
@@ -189,13 +209,20 @@
 
   ObjectWaiter* volatile _WaitSet;  // LL of threads wait()ing on the monitor
   volatile int  _waiters;           // number of waiting threads
   volatile int _WaitSetLock;        // protects Wait Queue - simple spinlock
 
+  // Used in LM_LEGACY mode to store BasicLock* in case of inflation by contending thread.
+  BasicLock* volatile _stack_locker;
+
  public:
 
   static void Initialize();
+  static void Initialize2();
+
+  static OopHandle& vthread_cxq_head() { return _vthread_cxq_head; }
+  static ParkEvent* vthread_unparker_ParkEvent() { return _vthread_unparker_ParkEvent; }
 
   // Only perform a PerfData operation if the PerfData object has been
   // allocated and if the PerfDataManager has not freed the PerfData
   // objects which can happen at normal VM shutdown.
   //
@@ -264,52 +291,69 @@
 
   bool is_entered(JavaThread* current) const;
 
   // Returns true if this OM has an owner, false otherwise.
   bool      has_owner() const;
-  void*     owner() const;  // Returns null if DEFLATER_MARKER is observed.
-  void*     owner_raw() const;
+  int64_t   owner() const;  // Returns null if DEFLATER_MARKER is observed.
+  int64_t   owner_raw() const;
+
+  static int64_t owner_for(JavaThread* thread);
+  static int64_t owner_for_oop(oop vthread);
+
   // Returns true if owner field == DEFLATER_MARKER and false otherwise.
   bool      owner_is_DEFLATER_MARKER() const;
   // Returns true if 'this' is being async deflated and false otherwise.
   bool      is_being_async_deflated();
   // Clear _owner field; current value must match old_value.
-  void      release_clear_owner(void* old_value);
+  void      release_clear_owner(JavaThread* old_value);
   // Simply set _owner field to new_value; current value must match old_value.
-  void      set_owner_from(void* old_value, void* new_value);
+  void      set_owner_from_raw(int64_t old_value, int64_t new_value);
+  void      set_owner_from(int64_t old_value, JavaThread* current);
   // Simply set _owner field to current; current value must match basic_lock_p.
-  void      set_owner_from_BasicLock(void* basic_lock_p, JavaThread* current);
+  void      set_owner_from_BasicLock(JavaThread* current);
   // Try to set _owner field to new_value if the current value matches
   // old_value, using Atomic::cmpxchg(). Otherwise, does not change the
   // _owner field. Returns the prior value of the _owner field.
-  void*     try_set_owner_from(void* old_value, void* new_value);
+  int64_t   try_set_owner_from_raw(int64_t old_value, int64_t new_value);
+  int64_t   try_set_owner_from(int64_t old_value, JavaThread* current);
+
+  bool      is_succesor(JavaThread* thread);
+  void      set_succesor(JavaThread* thread);
+  void      set_succesor(oop vthread);
+  void      clear_succesor();
+  bool      has_succesor();
+
+  bool is_owner(JavaThread* thread) const { return owner() == owner_for(thread); }
+  void set_owner(JavaThread* thread) { set_owner_from(NO_OWNER, thread); }
+  bool try_set_owner(JavaThread* thread) {
+    return try_set_owner_from(NO_OWNER, thread) == NO_OWNER;
+  }
 
+  bool is_owner_anonymous() const { return owner_raw() == ANONYMOUS_OWNER; }
   void set_owner_anonymous() {
-    set_owner_from(nullptr, anon_owner_ptr());
+    set_owner_from_raw(NO_OWNER, ANONYMOUS_OWNER);
   }
-
-  bool is_owner_anonymous() const {
-    return owner_raw() == anon_owner_ptr();
+  void set_owner_from_anonymous(JavaThread* owner) {
+    set_owner_from(ANONYMOUS_OWNER, owner);
   }
 
-  void set_owner_from_anonymous(Thread* owner) {
-    set_owner_from(anon_owner_ptr(), owner);
-  }
+  bool is_stack_locker(JavaThread* current);
+  BasicLock* stack_locker() const;
+  void set_stack_locker(BasicLock* locker);
 
   // Simply get _next_om field.
   ObjectMonitor* next_om() const;
   // Simply set _next_om field to new_value.
   void set_next_om(ObjectMonitor* new_value);
 
-  int       waiters() const;
-
   int       contentions() const;
   void      add_to_contentions(int value);
   intx      recursions() const                                         { return _recursions; }
   void      set_recursions(size_t recursions);
 
   // JVM/TI GetObjectMonitorUsage() needs this:
+  int waiters() const;
   ObjectWaiter* first_waiter()                                         { return _WaitSet; }
   ObjectWaiter* next_waiter(ObjectWaiter* o)                           { return o->_next; }
   JavaThread* thread_of_waiter(ObjectWaiter* o)                        { return o->_thread; }
 
   ObjectMonitor(oop object);
@@ -349,10 +393,11 @@
   bool      enter(JavaThread* current);
   bool      try_enter(JavaThread* current, bool check_for_recursion = true);
   bool      spin_enter(JavaThread* current);
   void      enter_with_contention_mark(JavaThread* current, ObjectMonitorContentionMark& contention_mark);
   void      exit(JavaThread* current, bool not_suspended = true);
+  bool      resume_operation(JavaThread* current, ObjectWaiter* node, ContinuationWrapper& cont);
   void      wait(jlong millis, bool interruptible, TRAPS);
   void      notify(TRAPS);
   void      notifyAll(TRAPS);
 
   void      print() const;
@@ -371,10 +416,14 @@
   void      DequeueSpecificWaiter(ObjectWaiter* waiter);
   void      EnterI(JavaThread* current);
   void      ReenterI(JavaThread* current, ObjectWaiter* current_node);
   void      UnlinkAfterAcquire(JavaThread* current, ObjectWaiter* current_node);
 
+  bool      VThreadMonitorEnter(JavaThread* current, ObjectWaiter* node = nullptr);
+  void      VThreadWait(JavaThread* current, jlong millis);
+  bool      VThreadWaitReenter(JavaThread* current, ObjectWaiter* node, ContinuationWrapper& cont);
+  void      VThreadEpilog(JavaThread* current, ObjectWaiter* node);
 
   enum class TryLockResult { Interference = -1, HasOwner = 0, Success = 1 };
 
   bool           TryLockWithContentionMark(JavaThread* locking_thread, ObjectMonitorContentionMark& contention_mark);
   TryLockResult  TryLock(JavaThread* current);
diff a/src/hotspot/share/runtime/objectMonitor.inline.hpp b/src/hotspot/share/runtime/objectMonitor.inline.hpp
--- a/src/hotspot/share/runtime/objectMonitor.inline.hpp
+++ b/src/hotspot/share/runtime/objectMonitor.inline.hpp
@@ -30,27 +30,38 @@
 #include "logging/log.hpp"
 #include "oops/access.inline.hpp"
 #include "oops/markWord.hpp"
 #include "runtime/atomic.hpp"
 #include "runtime/globals.hpp"
+#include "runtime/javaThread.inline.hpp"
 #include "runtime/lockStack.inline.hpp"
 #include "runtime/synchronizer.hpp"
+#include "runtime/threadIdentifier.hpp"
 #include "utilities/checkedCast.hpp"
 #include "utilities/globalDefinitions.hpp"
 
+inline int64_t ObjectMonitor::owner_for(JavaThread* thread) {
+  int64_t tid = thread->lock_id();
+  assert(tid >= 3 && tid < ThreadIdentifier::current(), "must be reasonable");
+  return tid;
+}
+
+inline int64_t ObjectMonitor::owner_for_oop(oop vthread) {
+  int64_t tid = java_lang_Thread::thread_id(vthread);
+  assert(tid >= 3 && tid < ThreadIdentifier::current(), "must be reasonable");
+  return tid;
+}
+
 inline bool ObjectMonitor::is_entered(JavaThread* current) const {
-  if (LockingMode == LM_LIGHTWEIGHT) {
-    if (is_owner_anonymous()) {
+  if (is_owner_anonymous()) {
+    if (LockingMode == LM_LIGHTWEIGHT) {
       return current->lock_stack().contains(object());
     } else {
-      return current == owner_raw();
+      return current->is_lock_owned((address)stack_locker());
     }
   } else {
-    void* owner = owner_raw();
-    if (current == owner || current->is_lock_owned((address)owner)) {
-      return true;
-    }
+    return is_owner(current);
   }
   return false;
 }
 
 inline uintptr_t ObjectMonitor::metadata() const {
@@ -90,24 +101,36 @@
 inline int ObjectMonitor::waiters() const {
   return _waiters;
 }
 
 inline bool ObjectMonitor::has_owner() const {
-  void* owner = owner_raw();
-  return owner != nullptr && owner != DEFLATER_MARKER;
+  int64_t owner = owner_raw();
+  return owner != NO_OWNER && owner != DEFLATER_MARKER;
 }
 
 // Returns null if DEFLATER_MARKER is observed.
-inline void* ObjectMonitor::owner() const {
-  void* owner = owner_raw();
-  return owner != DEFLATER_MARKER ? owner : nullptr;
+inline int64_t ObjectMonitor::owner() const {
+  int64_t owner = owner_raw();
+  return owner != DEFLATER_MARKER ? owner : NO_OWNER;
 }
 
-inline void* ObjectMonitor::owner_raw() const {
+inline int64_t ObjectMonitor::owner_raw() const {
   return Atomic::load(&_owner);
 }
 
+inline BasicLock* ObjectMonitor::stack_locker() const {
+  return Atomic::load(&_stack_locker);
+}
+
+inline void ObjectMonitor::set_stack_locker(BasicLock* locker) {
+  Atomic::store(&_stack_locker, locker);
+}
+
+inline bool ObjectMonitor::is_stack_locker(JavaThread* current) {
+  return is_owner_anonymous() && current->is_lock_owned((address)stack_locker());
+}
+
 // Returns true if owner field == DEFLATER_MARKER and false otherwise.
 // This accessor is called when we really need to know if the owner
 // field == DEFLATER_MARKER and any non-null value won't do the trick.
 inline bool ObjectMonitor::owner_is_DEFLATER_MARKER() const {
   return owner_raw() == DEFLATER_MARKER;
@@ -133,67 +156,98 @@
   assert(has_owner(), "must be owned");
   _recursions = checked_cast<intx>(recursions);
 }
 
 // Clear _owner field; current value must match old_value.
-inline void ObjectMonitor::release_clear_owner(void* old_value) {
+inline void ObjectMonitor::release_clear_owner(JavaThread* old_owner) {
+  int64_t old_value = owner_for(old_owner);
 #ifdef ASSERT
-  void* prev = Atomic::load(&_owner);
-  assert(prev == old_value, "unexpected prev owner=" INTPTR_FORMAT
-         ", expected=" INTPTR_FORMAT, p2i(prev), p2i(old_value));
+  int64_t prev = Atomic::load(&_owner);
+  assert(prev == old_value, "unexpected prev owner=" INT64_FORMAT
+         ", expected=" INT64_FORMAT, prev, old_value);
 #endif
-  Atomic::release_store(&_owner, (void*)nullptr);
+  Atomic::release_store(&_owner, NO_OWNER);
   log_trace(monitorinflation, owner)("release_clear_owner(): mid="
-                                     INTPTR_FORMAT ", old_value=" INTPTR_FORMAT,
-                                     p2i(this), p2i(old_value));
+                                     INTPTR_FORMAT ", old_value=" INT64_FORMAT,
+                                     p2i(this), old_value);
 }
 
 // Simply set _owner field to new_value; current value must match old_value.
 // (Simple means no memory sync needed.)
-inline void ObjectMonitor::set_owner_from(void* old_value, void* new_value) {
+inline void ObjectMonitor::set_owner_from_raw(int64_t old_value, int64_t new_value) {
 #ifdef ASSERT
-  void* prev = Atomic::load(&_owner);
-  assert(prev == old_value, "unexpected prev owner=" INTPTR_FORMAT
-         ", expected=" INTPTR_FORMAT, p2i(prev), p2i(old_value));
+  int64_t prev = Atomic::load(&_owner);
+  assert((int64_t)prev < ThreadIdentifier::current(), "must be reasonable");
+  assert(prev == old_value, "unexpected prev owner=" INT64_FORMAT
+         ", expected=" INT64_FORMAT, prev, old_value);
 #endif
   Atomic::store(&_owner, new_value);
   log_trace(monitorinflation, owner)("set_owner_from(): mid="
-                                     INTPTR_FORMAT ", old_value=" INTPTR_FORMAT
-                                     ", new_value=" INTPTR_FORMAT, p2i(this),
-                                     p2i(old_value), p2i(new_value));
+                                     INTPTR_FORMAT ", old_value=" INT64_FORMAT
+                                     ", new_value=" INT64_FORMAT, p2i(this),
+                                     old_value, new_value);
+}
+
+inline void ObjectMonitor::set_owner_from(int64_t old_value, JavaThread* current) {
+  set_owner_from_raw(old_value, owner_for(current));
 }
 
 // Simply set _owner field to self; current value must match basic_lock_p.
-inline void ObjectMonitor::set_owner_from_BasicLock(void* basic_lock_p, JavaThread* current) {
-#ifdef ASSERT
-  void* prev = Atomic::load(&_owner);
-  assert(prev == basic_lock_p, "unexpected prev owner=" INTPTR_FORMAT
-         ", expected=" INTPTR_FORMAT, p2i(prev), p2i(basic_lock_p));
-#endif
+inline void ObjectMonitor::set_owner_from_BasicLock(JavaThread* current) {
+  BasicLock* basic_lock_p = stack_locker();
+
+  set_stack_locker(nullptr); // first
+  assert(is_owner_anonymous(), "");
+
   // Non-null owner field to non-null owner field is safe without
   // cmpxchg() as long as all readers can tolerate either flavor.
-  Atomic::store(&_owner, current);
+  Atomic::store(&_owner, owner_for(current));
   log_trace(monitorinflation, owner)("set_owner_from_BasicLock(): mid="
                                      INTPTR_FORMAT ", basic_lock_p="
-                                     INTPTR_FORMAT ", new_value=" INTPTR_FORMAT,
-                                     p2i(this), p2i(basic_lock_p), p2i(current));
+                                     INTPTR_FORMAT ", new_value=" INT64_FORMAT,
+                                     p2i(this), p2i(basic_lock_p), owner_for(current));
 }
 
 // Try to set _owner field to new_value if the current value matches
 // old_value. Otherwise, does not change the _owner field. Returns
 // the prior value of the _owner field.
-inline void* ObjectMonitor::try_set_owner_from(void* old_value, void* new_value) {
-  void* prev = Atomic::cmpxchg(&_owner, old_value, new_value);
+inline int64_t ObjectMonitor::try_set_owner_from_raw(int64_t old_value, int64_t new_value) {
+  assert((int64_t)new_value < ThreadIdentifier::current(), "must be reasonable");
+  int64_t prev = Atomic::cmpxchg(&_owner, old_value, new_value);
   if (prev == old_value) {
     log_trace(monitorinflation, owner)("try_set_owner_from(): mid="
-                                       INTPTR_FORMAT ", prev=" INTPTR_FORMAT
-                                       ", new=" INTPTR_FORMAT, p2i(this),
-                                       p2i(prev), p2i(new_value));
+                                       INTPTR_FORMAT ", prev=" INT64_FORMAT
+                                       ", new=" INT64_FORMAT, p2i(this),
+                                       prev, new_value);
   }
   return prev;
 }
 
+inline int64_t ObjectMonitor::try_set_owner_from(int64_t old_value, JavaThread* current) {
+  return try_set_owner_from_raw(old_value, owner_for(current));
+}
+
+inline bool ObjectMonitor::is_succesor(JavaThread* thread) {
+  return owner_for(thread) == Atomic::load(&_succ);
+}
+
+inline void ObjectMonitor::set_succesor(JavaThread* thread) {
+  Atomic::store(&_succ, owner_for(thread));
+}
+
+inline void ObjectMonitor::set_succesor(oop vthread) {
+  Atomic::store(&_succ, java_lang_Thread::thread_id(vthread));
+}
+
+inline void ObjectMonitor::clear_succesor() {
+  Atomic::store(&_succ, NO_OWNER);
+}
+
+inline bool ObjectMonitor::has_succesor() {
+  return Atomic::load(&_succ) != NO_OWNER;
+}
+
 // The _next_om field can be concurrently read and modified so we
 // use Atomic operations to disable compiler optimizations that
 // might try to elide loading and/or storing this field.
 
 // Simply get _next_om field.
diff a/src/hotspot/share/runtime/sharedRuntime.cpp b/src/hotspot/share/runtime/sharedRuntime.cpp
--- a/src/hotspot/share/runtime/sharedRuntime.cpp
+++ b/src/hotspot/share/runtime/sharedRuntime.cpp
@@ -1969,11 +1969,11 @@
   // monitor before going slow path.  Since there is no safepoint
   // polling when calling into the VM, we can be sure that the monitor
   // hasn't been deallocated.
   ObjectMonitor* m = current->unlocked_inflated_monitor();
   if (m != nullptr) {
-    assert(m->owner_raw() != current, "must be");
+    assert(!m->is_owner(current), "must be");
     current->clear_unlocked_inflated_monitor();
 
     // We need to reacquire the lock before we can call ObjectSynchronizer::exit().
     if (!m->try_enter(current, /*check_for_recursion*/ false)) {
       // Some other thread acquired the lock (or the monitor was
diff a/src/hotspot/share/runtime/sharedRuntime.hpp b/src/hotspot/share/runtime/sharedRuntime.hpp
--- a/src/hotspot/share/runtime/sharedRuntime.hpp
+++ b/src/hotspot/share/runtime/sharedRuntime.hpp
@@ -510,10 +510,14 @@
   // Stack slots that may be unused by the calling convention but must
   // otherwise be preserved.  On Intel this includes the return address.
   // On PowerPC it includes the 4 words holding the old TOC & LR glue.
   static uint in_preserve_stack_slots();
 
+  static VMReg thread_register();
+
+  static void continuation_enter_cleanup(MacroAssembler* masm);
+
   // Is vector's size (in bytes) bigger than a size saved by default?
   // For example, on x86 16 bytes XMM registers are saved by default.
   static bool is_wide_vector(int size);
 
   // Save and restore a native result
diff a/src/hotspot/share/runtime/stackChunkFrameStream.hpp b/src/hotspot/share/runtime/stackChunkFrameStream.hpp
--- a/src/hotspot/share/runtime/stackChunkFrameStream.hpp
+++ b/src/hotspot/share/runtime/stackChunkFrameStream.hpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -73,10 +73,11 @@
 
   inline bool is_interpreted() const;
   inline bool is_stub() const;
   inline bool is_compiled() const;
   CodeBlob* cb() const { return _cb; }
+  inline void get_cb();
   const ImmutableOopMap* oopmap() const { if (_oopmap == nullptr) get_oopmap(); return _oopmap; }
   inline int frame_size() const;
   inline int stack_argsize() const;
   inline int num_oops() const;
 
@@ -96,11 +97,10 @@
 
   void print_on(outputStream* st) const PRODUCT_RETURN;
 
  private:
   inline address get_pc() const;
-  inline void get_cb();
 
   inline int interpreter_frame_size() const;
   inline int interpreter_frame_num_oops() const;
   inline int interpreter_frame_stack_argsize() const;
   inline void next_for_interpreter_frame();
diff a/src/hotspot/share/runtime/stackChunkFrameStream.inline.hpp b/src/hotspot/share/runtime/stackChunkFrameStream.inline.hpp
--- a/src/hotspot/share/runtime/stackChunkFrameStream.inline.hpp
+++ b/src/hotspot/share/runtime/stackChunkFrameStream.inline.hpp
@@ -104,11 +104,11 @@
   } DEBUG_ONLY(else _has_stub = false;)
 }
 
 template <ChunkFrames frame_kind>
 inline bool StackChunkFrameStream<frame_kind>::is_stub() const {
-  return cb() != nullptr && (_cb->is_safepoint_stub() || _cb->is_runtime_stub());
+  return cb() != nullptr && _cb->is_runtime_stub();
 }
 
 template <ChunkFrames frame_kind>
 inline bool StackChunkFrameStream<frame_kind>::is_compiled() const {
   return cb() != nullptr && _cb->is_nmethod();
@@ -194,11 +194,18 @@
   return (cb()->as_nmethod()->num_stack_arg_slots() * VMRegImpl::stack_slot_size) >> LogBytesPerWord;
 }
 
 template <ChunkFrames frame_kind>
 inline int StackChunkFrameStream<frame_kind>::num_oops() const {
-  return is_interpreted() ? interpreter_frame_num_oops() : oopmap()->num_oops();
+  if (is_interpreted()) {
+    return interpreter_frame_num_oops();
+  } else if (is_compiled()) {
+    return oopmap()->num_oops();
+  } else {
+    assert(is_stub(), "invariant");
+    return 0;
+  }
 }
 
 template <ChunkFrames frame_kind>
 inline void StackChunkFrameStream<frame_kind>::initialize_register_map(RegisterMap* map) {
   update_reg_map_pd(map);
@@ -206,11 +213,11 @@
 
 template <ChunkFrames frame_kind>
 template <typename RegisterMapT>
 inline void StackChunkFrameStream<frame_kind>::next(RegisterMapT* map, bool stop) {
   update_reg_map(map);
-  bool safepoint = is_stub();
+  bool is_runtime_stub = is_stub();
   if (frame_kind == ChunkFrames::Mixed) {
     if (is_interpreted()) {
       next_for_interpreter_frame();
     } else {
       _sp = _unextended_sp + cb()->frame_size();
@@ -230,12 +237,13 @@
     return;
   }
 
   get_cb();
   update_reg_map_pd(map);
-  if (safepoint && cb() != nullptr) { // there's no post-call nop and no fast oopmap lookup
-    _oopmap = cb()->oop_map_for_return_address(pc());
+  if (is_runtime_stub && cb() != nullptr) { // there's no post-call nop and no fast oopmap lookup
+    // caller could have been deoptimized so use orig_pc()
+    _oopmap = cb()->oop_map_for_return_address(orig_pc());
   }
 }
 
 template <ChunkFrames frame_kind>
 inline void StackChunkFrameStream<frame_kind>::get_cb() {
@@ -298,13 +306,12 @@
 }
 
 template<>
 template<>
 inline void StackChunkFrameStream<ChunkFrames::CompiledOnly>::update_reg_map(RegisterMap* map) {
-  assert(map->in_cont(), "");
-  assert(map->stack_chunk()() == _chunk, "");
-  if (map->update_map()) {
+  assert(!map->in_cont() || map->stack_chunk() == _chunk, "");
+  if (map->update_map() && is_stub()) {
     frame f = to_frame();
     oopmap()->update_register_map(&f, map); // we have callee-save registers in this case
   }
 }
 
diff a/src/hotspot/share/runtime/stackValue.cpp b/src/hotspot/share/runtime/stackValue.cpp
--- a/src/hotspot/share/runtime/stackValue.cpp
+++ b/src/hotspot/share/runtime/stackValue.cpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 1997, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -291,22 +291,22 @@
 
   assert(value_addr == nullptr || Continuation::is_in_usable_stack(value_addr, reg_map->as_RegisterMap()) || (reg_map->thread() != nullptr && reg_map->thread()->is_in_usable_stack(value_addr)), INTPTR_FORMAT, p2i(value_addr));
   return value_addr;
 }
 
-BasicLock* StackValue::resolve_monitor_lock(const frame* fr, Location location) {
+BasicLock* StackValue::resolve_monitor_lock(const frame& fr, Location location) {
   assert(location.is_stack(), "for now we only look at the stack");
   int word_offset = location.stack_offset() / wordSize;
   // (stack picture)
   // high: [     ]  word_offset + 1
   // low   [     ]  word_offset
   //
   // sp->  [     ]  0
   // the word_offset is the distance from the stack pointer to the lowest address
   // The frame's original stack pointer, before any extension by its callee
   // (due to Compiler1 linkage on SPARC), must be used.
-  return (BasicLock*) (fr->unextended_sp() + word_offset);
+  return (BasicLock*) (fr.unextended_sp() + word_offset);
 }
 
 
 #ifndef PRODUCT
 
diff a/src/hotspot/share/runtime/stackValue.hpp b/src/hotspot/share/runtime/stackValue.hpp
--- a/src/hotspot/share/runtime/stackValue.hpp
+++ b/src/hotspot/share/runtime/stackValue.hpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 1997, 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -121,11 +121,11 @@
   }
 
   static StackValue* create_stack_value_from_oop_location(stackChunkOop chunk, void* addr);
   static StackValue* create_stack_value_from_narrowOop_location(stackChunkOop chunk, void* addr, bool is_register);
 
-  static BasicLock*  resolve_monitor_lock(const frame* fr, Location location);
+  static BasicLock*  resolve_monitor_lock(const frame& fr, Location location);
 
   template<typename RegisterMapT>
   static StackValue* create_stack_value(const frame* fr, const RegisterMapT* reg_map, ScopeValue* sv);
 
   template<typename RegisterMapT>
diff a/src/hotspot/share/runtime/stubRoutines.cpp b/src/hotspot/share/runtime/stubRoutines.cpp
--- a/src/hotspot/share/runtime/stubRoutines.cpp
+++ b/src/hotspot/share/runtime/stubRoutines.cpp
@@ -184,10 +184,11 @@
 address StubRoutines::_array_partition  = nullptr;
 
 address StubRoutines::_cont_thaw          = nullptr;
 address StubRoutines::_cont_returnBarrier = nullptr;
 address StubRoutines::_cont_returnBarrierExc = nullptr;
+address StubRoutines::_cont_preempt_stub = nullptr;
 
 address StubRoutines::_upcall_stub_exception_handler = nullptr;
 address StubRoutines::_upcall_stub_load_target = nullptr;
 
 address StubRoutines::_lookup_secondary_supers_table_slow_path_stub = nullptr;
diff a/src/hotspot/share/runtime/stubRoutines.hpp b/src/hotspot/share/runtime/stubRoutines.hpp
--- a/src/hotspot/share/runtime/stubRoutines.hpp
+++ b/src/hotspot/share/runtime/stubRoutines.hpp
@@ -290,10 +290,11 @@
   static address _method_entry_barrier;
 
   static address _cont_thaw;
   static address _cont_returnBarrier;
   static address _cont_returnBarrierExc;
+  static address _cont_preempt_stub;
 
   // Vector Math Routines
   static address _vector_f_math[VectorSupport::NUM_VEC_SIZES][VectorSupport::NUM_VECTOR_OP_MATH];
   static address _vector_d_math[VectorSupport::NUM_VEC_SIZES][VectorSupport::NUM_VECTOR_OP_MATH];
 
@@ -499,10 +500,11 @@
   static address method_entry_barrier() { return _method_entry_barrier; }
 
   static address cont_thaw()           { return _cont_thaw; }
   static address cont_returnBarrier()  { return _cont_returnBarrier; }
   static address cont_returnBarrierExc(){return _cont_returnBarrierExc; }
+  static address cont_preempt_stub()   { return _cont_preempt_stub; }
 
   static address upcall_stub_exception_handler() {
     assert(_upcall_stub_exception_handler != nullptr, "not implemented");
     return _upcall_stub_exception_handler;
   }
diff a/src/hotspot/share/runtime/synchronizer.cpp b/src/hotspot/share/runtime/synchronizer.cpp
--- a/src/hotspot/share/runtime/synchronizer.cpp
+++ b/src/hotspot/share/runtime/synchronizer.cpp
@@ -359,11 +359,11 @@
     if (LockingMode == LM_LIGHTWEIGHT && mon == nullptr) {
       // Racing with inflation/deflation go slow path
       return false;
     }
     assert(mon->object() == oop(obj), "invariant");
-    if (mon->owner() != current) return false;  // slow-path for IMS exception
+    if (!mon->is_owner(current)) return false;  // slow-path for IMS exception
 
     if (mon->first_waiter() != nullptr) {
       // We have one or more waiters. Since this is an inflated monitor
       // that we own, we can transfer one or more threads from the waitset
       // to the entrylist here and now, avoiding the slow-path.
@@ -422,18 +422,17 @@
     // the ObjectMonitor busy by setting the owner below. If we detect
     // that race we just bail out to the slow-path here.
     if (m->object_peek() == nullptr) {
       return false;
     }
-    JavaThread* const owner = static_cast<JavaThread*>(m->owner_raw());
 
     // Lock contention and Transactional Lock Elision (TLE) diagnostics
     // and observability
     // Case: light contention possibly amenable to TLE
     // Case: TLE inimical operations such as nested/recursive synchronization
 
-    if (owner == current) {
+    if (m->is_owner(current)) {
       m->_recursions++;
       current->inc_held_monitor_count();
       return true;
     }
 
@@ -446,11 +445,11 @@
     // stack-locking in the object's header, the second check is for
     // recursive stack-locking in the displaced header in the BasicLock,
     // and last are the inflated Java Monitor (ObjectMonitor) checks.
     lock->set_displaced_header(markWord::unused_mark());
 
-    if (owner == nullptr && m->try_set_owner_from(nullptr, current) == nullptr) {
+    if (!m->has_owner() && m->try_set_owner(current)) {
       assert(m->_recursions == 0, "invariant");
       current->inc_held_monitor_count();
       return true;
     }
   }
@@ -664,10 +663,14 @@
 
 // -----------------------------------------------------------------------------
 // JNI locks on java objects
 // NOTE: must use heavy weight monitor to handle jni monitor enter
 void ObjectSynchronizer::jni_enter(Handle obj, JavaThread* current) {
+  // Top native frames in the stack will not be seen if we attempt
+  // preemption, since we start walking from the last Java anchor.
+  NoPreemptMark npm(current);
+
   if (obj->klass()->is_value_based()) {
     handle_sync_on_value_based_class(obj, current);
   }
 
   // the current locking is from JNI instead of Java code
@@ -715,11 +718,11 @@
 }
 
 // -----------------------------------------------------------------------------
 // Internal VM locks on java objects
 // standard constructor, allows locking failures
-ObjectLocker::ObjectLocker(Handle obj, JavaThread* thread) {
+ObjectLocker::ObjectLocker(Handle obj, JavaThread* thread) : _npm(thread) {
   _thread = thread;
   _thread->check_for_valid_safepoint_state();
   _obj = obj;
 
   if (_obj() != nullptr) {
@@ -1163,11 +1166,11 @@
   markWord mark = read_stable_mark(obj);
 
   if (LockingMode == LM_LEGACY && mark.has_locker()) {
     // stack-locked so header points into owner's stack.
     // owning_thread_from_monitor_owner() may also return null here:
-    return Threads::owning_thread_from_monitor_owner(t_list, (address) mark.locker());
+    return Threads::owning_thread_from_stacklock(t_list, (address) mark.locker());
   }
 
   if (LockingMode == LM_LIGHTWEIGHT && mark.is_fast_locked()) {
     // fast-locked so get owner from the object.
     // owning_thread_from_object() may also return null here:
@@ -1227,28 +1230,35 @@
     // target thread is suspended or when the target thread is
     // operating on itself. The current closures in use today are
     // only interested in an owned ObjectMonitor and ownership
     // cannot be dropped under the calling contexts so the
     // ObjectMonitor cannot be async deflated.
-    if (monitor->has_owner() && filter(monitor->owner_raw())) {
+    if (monitor->has_owner() && filter(monitor)) {
       assert(!monitor->is_being_async_deflated(), "Owned monitors should not be deflating");
 
       closure->do_monitor(monitor);
     }
   });
 }
 
 // Iterate ObjectMonitors where the owner == thread; this does NOT include
 // ObjectMonitors where owner is set to a stack-lock address in thread.
 void ObjectSynchronizer::owned_monitors_iterate(MonitorClosure* closure, JavaThread* thread) {
-  auto thread_filter = [&](void* owner) { return owner == thread; };
+  int64_t key = ObjectMonitor::owner_for(thread);
+  auto thread_filter = [&](ObjectMonitor* monitor) { return monitor->owner() == key; };
+  return owned_monitors_iterate_filtered(closure, thread_filter);
+}
+
+void ObjectSynchronizer::owned_monitors_iterate(MonitorClosure* closure, oop vthread) {
+  int64_t key = ObjectMonitor::owner_for_oop(vthread);
+  auto thread_filter = [&](ObjectMonitor* monitor) { return monitor->owner() == key; };
   return owned_monitors_iterate_filtered(closure, thread_filter);
 }
 
 // Iterate ObjectMonitors owned by any thread.
 void ObjectSynchronizer::owned_monitors_iterate(MonitorClosure* closure) {
-  auto all_filter = [&](void* owner) { return true; };
+  auto all_filter = [&](ObjectMonitor* monitor) { return true; };
   return owned_monitors_iterate_filtered(closure, all_filter);
 }
 
 static bool monitors_used_above_threshold(MonitorList* list) {
   if (MonitorUsedDeflationThreshold == 0) {  // disabled case is easy
@@ -1416,38 +1426,52 @@
 }
 
 ObjectMonitor* ObjectSynchronizer::inflate(Thread* current, oop obj, const InflateCause cause) {
   assert(current == Thread::current(), "must be");
   assert(LockingMode != LM_LIGHTWEIGHT, "only inflate through enter");
-  return inflate_impl(obj, cause);
+  return inflate_impl(current->is_Java_thread() ? JavaThread::cast(current) : nullptr, obj, cause);
 }
 
 ObjectMonitor* ObjectSynchronizer::inflate_for(JavaThread* thread, oop obj, const InflateCause cause) {
   assert(thread == Thread::current() || thread->is_obj_deopt_suspend(), "must be");
   assert(LockingMode != LM_LIGHTWEIGHT, "LM_LIGHTWEIGHT cannot use inflate_for");
-  return inflate_impl(obj, cause);
+  return inflate_impl(thread, obj, cause);
 }
 
-ObjectMonitor* ObjectSynchronizer::inflate_impl(oop object, const InflateCause cause) {
+ObjectMonitor* ObjectSynchronizer::inflate_impl(JavaThread* inflating_thread, oop object, const InflateCause cause) {
+  // The JavaThread* inflating_thread requires that the inflating_thread == Thread::current() or
+  // is suspended throughout the call by some other mechanism.
+  // The thread might be nullptr when called from a non JavaThread. (As may still be
+  // the case from FastHashCode). However it is only important for correctness that the
+  // thread is set when called from ObjectSynchronizer::enter from the owning thread,
+  // ObjectSynchronizer::enter_for from any thread, or ObjectSynchronizer::exit.
   assert(LockingMode != LM_LIGHTWEIGHT, "LM_LIGHTWEIGHT cannot use inflate_impl");
   EventJavaMonitorInflate event;
 
   for (;;) {
     const markWord mark = object->mark_acquire();
 
     // The mark can be in one of the following states:
-    // *  inflated     - Just return it.
+    // *  inflated     - If the ObjectMonitor owner is anonymous and the
+    //                   inflating_thread owns the object lock, then we
+    //                   make the inflating_thread the ObjectMonitor owner.
     // *  stack-locked - Coerce it to inflated from stack-locked.
     // *  INFLATING    - Busy wait for conversion from stack-locked to
     //                   inflated.
     // *  unlocked     - Aggressively inflate the object.
 
     // CASE: inflated
     if (mark.has_monitor()) {
       ObjectMonitor* inf = mark.monitor();
       markWord dmw = inf->header();
       assert(dmw.is_neutral(), "invariant: header=" INTPTR_FORMAT, dmw.value());
+      if (inf->is_owner_anonymous() && inflating_thread != nullptr) {
+        assert(LockingMode == LM_LEGACY, "invariant");
+        if (inflating_thread->is_lock_owned((address)inf->stack_locker())) {
+          inf->set_owner_from_BasicLock(inflating_thread);
+        }
+      }
       return inf;
     }
 
     // CASE: inflation in progress - inflating over a stack-lock.
     // Some other thread is converting from stack-locked to inflated.
@@ -1520,16 +1544,21 @@
       assert(dmw.is_neutral(), "invariant: header=" INTPTR_FORMAT, dmw.value());
 
       // Setup monitor fields to proper values -- prepare the monitor
       m->set_header(dmw);
 
-      // Optimization: if the mark.locker stack address is associated
-      // with this thread we could simply set m->_owner = current.
       // Note that a thread can inflate an object
       // that it has stack-locked -- as might happen in wait() -- directly
       // with CAS.  That is, we can avoid the xchg-nullptr .... ST idiom.
-      m->set_owner_from(nullptr, mark.locker());
+      if (inflating_thread != nullptr && inflating_thread->is_lock_owned((address)mark.locker())) {
+        m->set_owner(inflating_thread);
+      } else {
+        // Use ANONYMOUS_OWNER to indicate that the owner is the BasicLock on the stack,
+        // and set the stack locker field in the monitor.
+        m->set_stack_locker(mark.locker());
+        m->set_owner_anonymous();  // second
+      }
       // TODO-FIXME: assert BasicLock->dhw != 0.
 
       // Must preserve store ordering. The monitor state must
       // be stable at the time of publishing the monitor address.
       guarantee(object->mark() == markWord::INFLATING(), "invariant");
@@ -2048,11 +2077,11 @@
       if (is_interesting(monitor)) {
         const oop obj = monitor->object_peek();
         const intptr_t hash = UseObjectMonitorTable ? monitor->hash() : monitor->header().hash();
         ResourceMark rm;
         out->print(INTPTR_FORMAT "  %d%d%d  " INTPTR_FORMAT "  %s", p2i(monitor),
-                   monitor->is_busy(), hash != 0, monitor->owner() != nullptr,
+                   monitor->is_busy(), hash != 0, monitor->has_owner(),
                    p2i(obj), obj == nullptr ? "" : obj->klass()->external_name());
         if (monitor->is_busy()) {
           out->print(" (%s)", monitor->is_busy_to_string(&ss));
           ss.reset();
         }
diff a/src/hotspot/share/runtime/synchronizer.hpp b/src/hotspot/share/runtime/synchronizer.hpp
--- a/src/hotspot/share/runtime/synchronizer.hpp
+++ b/src/hotspot/share/runtime/synchronizer.hpp
@@ -135,11 +135,11 @@
   // Used to inflate a monitor as if it was done from the thread JavaThread.
   static ObjectMonitor* inflate_for(JavaThread* thread, oop obj, const InflateCause cause);
 
 private:
   // Shared implementation between the different LockingMode.
-  static ObjectMonitor* inflate_impl(oop obj, const InflateCause cause);
+  static ObjectMonitor* inflate_impl(JavaThread* inflating_thread, oop obj, const InflateCause cause);
 
 public:
   // This version is only for internal use
   static void inflate_helper(oop obj);
   static const char* inflate_cause_name(const InflateCause cause);
@@ -166,14 +166,17 @@
   // Iterate ObjectMonitors owned by any thread and where the owner `filter`
   // returns true.
   template <typename OwnerFilter>
   static void owned_monitors_iterate_filtered(MonitorClosure* closure, OwnerFilter filter);
 
-  // Iterate ObjectMonitors where the owner == thread; this does NOT include
+  // Iterate ObjectMonitors where the owner is thread; this does NOT include
   // ObjectMonitors where owner is set to a stack lock address in thread.
   static void owned_monitors_iterate(MonitorClosure* m, JavaThread* thread);
 
+  // Iterate ObjectMonitors where the owner is vthread.
+  static void owned_monitors_iterate(MonitorClosure* m, oop vthread);
+
   // Iterate ObjectMonitors owned by any thread.
   static void owned_monitors_iterate(MonitorClosure* closure);
 
   // Initialize the gInflationLocks
   static void initialize();
@@ -227,15 +230,19 @@
 // ObjectLocker enforces balanced locking and can never throw an
 // IllegalMonitorStateException. However, a pending exception may
 // have to pass through, and we must also be able to deal with
 // asynchronous exceptions. The caller is responsible for checking
 // the thread's pending exception if needed.
+// When using ObjectLocker the top native frames in the stack will
+// not be seen in case we attempt preemption, since we start walking
+// from the last Java anchor, so we disable it with NoPreemptMark.
 class ObjectLocker : public StackObj {
  private:
   JavaThread* _thread;
   Handle      _obj;
   BasicLock   _lock;
+  NoPreemptMark _npm;
  public:
   ObjectLocker(Handle obj, JavaThread* current);
   ~ObjectLocker();
 
   // Monitor behavior
diff a/src/hotspot/share/runtime/threadIdentifier.cpp b/src/hotspot/share/runtime/threadIdentifier.cpp
--- a/src/hotspot/share/runtime/threadIdentifier.cpp
+++ b/src/hotspot/share/runtime/threadIdentifier.cpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -24,16 +24,28 @@
 
 #include "precompiled.hpp"
 #include "runtime/atomic.hpp"
 #include "runtime/threadIdentifier.hpp"
 
-static volatile int64_t next_thread_id = 2; // starting at 2, excluding the primordial thread id
+// starting at 3, excluding reserved values defined in ObjectMonitor.hpp
+static const int64_t INITIAL_TID = 3;
+static volatile int64_t next_thread_id = INITIAL_TID;
+
+#ifdef ASSERT
+int64_t ThreadIdentifier::initial() {
+  return INITIAL_TID;
+}
+#endif
 
 int64_t ThreadIdentifier::unsafe_offset() {
   return reinterpret_cast<int64_t>(&next_thread_id);
 }
 
+int64_t ThreadIdentifier::current() {
+  return Atomic::load(&next_thread_id);
+}
+
 int64_t ThreadIdentifier::next() {
   int64_t next_tid;
   do {
     next_tid = Atomic::load(&next_thread_id);
   } while (Atomic::cmpxchg(&next_thread_id, next_tid, next_tid + 1) != next_tid);
diff a/src/hotspot/share/runtime/threadIdentifier.hpp b/src/hotspot/share/runtime/threadIdentifier.hpp
--- a/src/hotspot/share/runtime/threadIdentifier.hpp
+++ b/src/hotspot/share/runtime/threadIdentifier.hpp
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -34,9 +34,11 @@
  * JFR uses next() for a non-reusable id for non-java threads.
  */
 class ThreadIdentifier : AllStatic {
  public:
   static int64_t next();
+  static int64_t current();
   static int64_t unsafe_offset();
+  DEBUG_ONLY(static int64_t initial();)
 };
 
 #endif // SHARE_RUNTIME_THREADIDENTIFIER_HPP
diff a/src/hotspot/share/runtime/threads.cpp b/src/hotspot/share/runtime/threads.cpp
--- a/src/hotspot/share/runtime/threads.cpp
+++ b/src/hotspot/share/runtime/threads.cpp
@@ -164,10 +164,12 @@
                           vmSymbols::threadgroup_string_void_signature(),
                           thread_group,
                           string,
                           CHECK);
 
+  assert(thread->lock_id() == ThreadIdentifier::initial(), "invariant");
+
   // Set thread status to running since main thread has
   // been started and running.
   java_lang_Thread::set_thread_status(thread_oop(),
                                       JavaThreadStatus::RUNNABLE);
 }
@@ -530,10 +532,15 @@
   main_thread->record_stack_base_and_size();
   main_thread->register_thread_stack_with_NMT();
   main_thread->set_active_handles(JNIHandleBlock::allocate_block());
   MACOS_AARCH64_ONLY(main_thread->init_wx());
 
+  // Set the lock_id now since we will run Java code before the Thread instance
+  // is even created. The same value will be assigned to the Thread instance on init.
+  main_thread->set_lock_id(ThreadIdentifier::next());
+  assert(main_thread->lock_id() == ThreadIdentifier::initial(), "invariant");
+
   if (!main_thread->set_as_starting_thread()) {
     vm_shutdown_during_initialization(
                                       "Failed necessary internal allocation. Out of swap space");
     main_thread->smr_delete();
     *canTryAgain = false; // don't let caller call JNI_CreateJavaVM again
@@ -579,10 +586,12 @@
     }
     *canTryAgain = false; // don't let caller call JNI_CreateJavaVM again
     return status;
   }
 
+  ObjectMonitor::Initialize2();
+
   JFR_ONLY(Jfr::on_create_vm_1();)
 
   // Should be done after the heap is fully created
   main_thread->cache_global_variables();
 
@@ -1224,39 +1233,20 @@
 
   return result;
 }
 #endif // INCLUDE_JVMTI
 
-JavaThread *Threads::owning_thread_from_monitor_owner(ThreadsList * t_list,
-                                                      address owner) {
-  assert(LockingMode != LM_LIGHTWEIGHT, "Not with new lightweight locking");
-  // null owner means not locked so we can skip the search
-  if (owner == nullptr) return nullptr;
-
-  for (JavaThread* p : *t_list) {
-    // first, see if owner is the address of a Java thread
-    if (owner == (address)p) return p;
-  }
-
-  // Cannot assert on lack of success here since this function may be
-  // used by code that is trying to report useful problem information
-  // like deadlock detection.
-  if (LockingMode == LM_MONITOR) return nullptr;
+JavaThread *Threads::owning_thread_from_stacklock(ThreadsList * t_list, address basicLock) {
+  assert(LockingMode == LM_LEGACY, "Not with new lightweight locking");
 
-  // If we didn't find a matching Java thread and we didn't force use of
-  // heavyweight monitors, then the owner is the stack address of the
-  // Lock Word in the owning Java thread's stack.
-  //
   JavaThread* the_owner = nullptr;
   for (JavaThread* q : *t_list) {
-    if (q->is_lock_owned(owner)) {
+    if (q->is_lock_owned(basicLock)) {
       the_owner = q;
       break;
     }
   }
-
-  // cannot assert on lack of success here; see above comment
   return the_owner;
 }
 
 JavaThread* Threads::owning_thread_from_object(ThreadsList * t_list, oop obj) {
   assert(LockingMode == LM_LIGHTWEIGHT, "Only with new lightweight locking");
@@ -1273,21 +1263,26 @@
   }
   return nullptr;
 }
 
 JavaThread* Threads::owning_thread_from_monitor(ThreadsList* t_list, ObjectMonitor* monitor) {
-  if (LockingMode == LM_LIGHTWEIGHT) {
-    if (monitor->is_owner_anonymous()) {
+  if (monitor->is_owner_anonymous()) {
+    if (LockingMode == LM_LIGHTWEIGHT) {
       return owning_thread_from_object(t_list, monitor->object());
     } else {
-      Thread* owner = reinterpret_cast<Thread*>(monitor->owner());
-      assert(owner == nullptr || owner->is_Java_thread(), "only JavaThreads own monitors");
-      return reinterpret_cast<JavaThread*>(owner);
+      assert(LockingMode == LM_LEGACY, "invariant");
+      return owning_thread_from_stacklock(t_list, (address)monitor->stack_locker());
     }
   } else {
-    address owner = (address)monitor->owner();
-    return owning_thread_from_monitor_owner(t_list, owner);
+    JavaThread* the_owner = nullptr;
+    for (JavaThread* q : *t_list) {
+      if (monitor->is_owner(q)) {
+        the_owner = q;
+        break;
+      }
+    }
+    return the_owner;
   }
 }
 
 class PrintOnClosure : public ThreadClosure {
 private:
@@ -1335,21 +1330,14 @@
     if (print_stacks) {
       if (internal_format) {
         p->trace_stack();
       } else {
         p->print_stack_on(st);
-        const oop thread_oop = p->threadObj();
-        if (thread_oop != nullptr) {
-          if (p->is_vthread_mounted()) {
-            const oop vt = p->vthread();
-            assert(vt != nullptr, "vthread should not be null when vthread is mounted");
-            // JavaThread._vthread can refer to the carrier thread. Print only if _vthread refers to a virtual thread.
-            if (vt != thread_oop) {
-              st->print_cr("   Mounted virtual thread #" INT64_FORMAT, (int64_t)java_lang_Thread::thread_id(vt));
-              p->print_vthread_stack_on(st);
-            }
-          }
+        if (p->is_vthread_mounted()) {
+          // _lock_id is the thread ID of the mounted virtual thread
+          st->print_cr("   Mounted virtual thread #" INT64_FORMAT, p->lock_id());
+          p->print_vthread_stack_on(st);
         }
       }
     }
     st->cr();
 #if INCLUDE_SERVICES
diff a/src/hotspot/share/runtime/threads.hpp b/src/hotspot/share/runtime/threads.hpp
--- a/src/hotspot/share/runtime/threads.hpp
+++ b/src/hotspot/share/runtime/threads.hpp
@@ -132,13 +132,12 @@
   // Get Java threads that are waiting to enter or re-enter the specified monitor.
   // Java threads that are executing mounted virtual threads are not included.
   static GrowableArray<JavaThread*>* get_pending_threads(ThreadsList * t_list,
                                                          int count, address monitor);
 
-  // Get owning Java thread from the monitor's owner field.
-  static JavaThread *owning_thread_from_monitor_owner(ThreadsList * t_list,
-                                                      address owner);
+  // Get owning Java thread from the basicLock address.
+  static JavaThread *owning_thread_from_stacklock(ThreadsList * t_list, address basicLock);
 
   static JavaThread* owning_thread_from_object(ThreadsList* t_list, oop obj);
   static JavaThread* owning_thread_from_monitor(ThreadsList* t_list, ObjectMonitor* owner);
 
   // Number of threads on the active threads list
diff a/src/hotspot/share/runtime/vframe.cpp b/src/hotspot/share/runtime/vframe.cpp
--- a/src/hotspot/share/runtime/vframe.cpp
+++ b/src/hotspot/share/runtime/vframe.cpp
@@ -278,17 +278,18 @@
   assert(fr().is_interpreted_frame(), "frame should be an interpreted frame");
   return fr().interpreter_frame_local_at(offset);
 }
 
 GrowableArray<MonitorInfo*>* interpretedVFrame::monitors() const {
+  bool heap_frame = stack_chunk() != nullptr;
+  frame f = !heap_frame ? _fr : stack_chunk()->derelativize(_fr);
   GrowableArray<MonitorInfo*>* result = new GrowableArray<MonitorInfo*>(5);
-  if (stack_chunk() == nullptr) { // no monitors in continuations
-    for (BasicObjectLock* current = (fr().previous_monitor_in_interpreter_frame(fr().interpreter_frame_monitor_begin()));
-        current >= fr().interpreter_frame_monitor_end();
-        current = fr().previous_monitor_in_interpreter_frame(current)) {
-      result->push(new MonitorInfo(current->obj(), current->lock(), false, false));
-    }
+  for (BasicObjectLock* current = (f.previous_monitor_in_interpreter_frame(f.interpreter_frame_monitor_begin()));
+      current >= f.interpreter_frame_monitor_end();
+      current = f.previous_monitor_in_interpreter_frame(current)) {
+      oop owner = !heap_frame ? current->obj() : StackValue::create_stack_value_from_oop_location(stack_chunk(), (void*)current->obj_adr())->get_obj()();
+    result->push(new MonitorInfo(owner, current->lock(), false, false));
   }
   return result;
 }
 
 int interpretedVFrame::bci() const {
diff a/src/hotspot/share/runtime/vframe.inline.hpp b/src/hotspot/share/runtime/vframe.inline.hpp
--- a/src/hotspot/share/runtime/vframe.inline.hpp
+++ b/src/hotspot/share/runtime/vframe.inline.hpp
@@ -120,10 +120,17 @@
     return;
   }
 
   if (thread->is_vthread_mounted()) {
     _frame = vthread_carrier ? _thread->carrier_last_frame(&_reg_map) : _thread->vthread_last_frame();
+    if (Continuation::is_continuation_enterSpecial(_frame)) {
+      // This can happen when calling async_get_stack_trace() and catching the target
+      // vthread at the JRT_BLOCK_END in freeze_internal() or when posting the Monitor
+      // Waited event after target vthread was preempted. Since all continuation frames
+      // are freezed we get the top frame from the stackChunk instead.
+      _frame = Continuation::last_frame(java_lang_VirtualThread::continuation(_thread->vthread()), &_reg_map);
+    }
   } else {
     _frame = _thread->last_frame();
   }
 
   _cont_entry = _thread->last_continuation();
diff a/src/hotspot/share/runtime/vframe_hp.cpp b/src/hotspot/share/runtime/vframe_hp.cpp
--- a/src/hotspot/share/runtime/vframe_hp.cpp
+++ b/src/hotspot/share/runtime/vframe_hp.cpp
@@ -233,11 +233,11 @@
   const_cast<RegisterMap*>(&_reg_map)->set_stack_chunk_index(index);
   return res;
 }
 
 BasicLock* compiledVFrame::resolve_monitor_lock(Location location) const {
-  return StackValue::resolve_monitor_lock(&_fr, location);
+  return StackValue::resolve_monitor_lock(stack_chunk() == nullptr ? _fr : stack_chunk()->derelativize(_fr), location);
 }
 
 
 GrowableArray<MonitorInfo*>* compiledVFrame::monitors() const {
   // Natives has no scope
@@ -282,10 +282,11 @@
     }
   }
 
   // Replace the original values with any stores that have been
   // performed through compiledVFrame::update_monitors.
+  if (thread() == nullptr) return result; // Unmounted continuations have no thread so nothing to do.
   GrowableArrayView<jvmtiDeferredLocalVariableSet*>* list = JvmtiDeferredUpdates::deferred_locals(thread());
   if (list != nullptr ) {
     // In real life this never happens or is typically a single element search
     for (int i = 0; i < list->length(); i++) {
       if (list->at(i)->matches(this)) {
diff a/src/hotspot/share/runtime/vmOperations.cpp b/src/hotspot/share/runtime/vmOperations.cpp
--- a/src/hotspot/share/runtime/vmOperations.cpp
+++ b/src/hotspot/share/runtime/vmOperations.cpp
@@ -273,17 +273,17 @@
     // Release Heap_lock
     Heap_lock->unlock();
   }
 }
 
-// Hash table of void* to a list of ObjectMonitor* owned by the JavaThread.
+// Hash table of int64_t to a list of ObjectMonitor* owned by the JavaThread.
 // The JavaThread's owner key is either a JavaThread* or a stack lock
-// address in the JavaThread so we use "void*".
+// address in the JavaThread so we use "int64_t".
 //
 class ObjectMonitorsDump : public MonitorClosure, public ObjectMonitorsView {
  private:
-  static unsigned int ptr_hash(void* const& s1) {
+  static unsigned int ptr_hash(int64_t const& s1) {
     // 2654435761 = 2^32 * Phi (golden ratio)
     return (unsigned int)(((uint32_t)(uintptr_t)s1) * 2654435761u);
   }
 
  private:
@@ -292,28 +292,28 @@
                           AnyObj::C_HEAP, mtThread,
                           AllocFailStrategy::RETURN_NULL> {};
 
   // ResourceHashtable SIZE is specified at compile time so we
   // use 1031 which is the first prime after 1024.
-  typedef ResourceHashtable<void*, ObjectMonitorLinkedList*, 1031, AnyObj::C_HEAP, mtThread,
+  typedef ResourceHashtable<int64_t, ObjectMonitorLinkedList*, 1031, AnyObj::C_HEAP, mtThread,
                             &ObjectMonitorsDump::ptr_hash> PtrTable;
   PtrTable* _ptrs;
   size_t _key_count;
   size_t _om_count;
 
-  void add_list(void* key, ObjectMonitorLinkedList* list) {
+  void add_list(int64_t key, ObjectMonitorLinkedList* list) {
     _ptrs->put(key, list);
     _key_count++;
   }
 
-  ObjectMonitorLinkedList* get_list(void* key) {
+  ObjectMonitorLinkedList* get_list(int64_t key) {
     ObjectMonitorLinkedList** listpp = _ptrs->get(key);
     return (listpp == nullptr) ? nullptr : *listpp;
   }
 
   void add(ObjectMonitor* monitor) {
-    void* key = monitor->owner();
+    int64_t key = monitor->owner();
 
     ObjectMonitorLinkedList* list = get_list(key);
     if (list == nullptr) {
       // Create new list and add it to the hash table:
       list = new (mtThread) ObjectMonitorLinkedList;
@@ -333,11 +333,11 @@
   ObjectMonitorsDump() : _ptrs(new (mtThread) PtrTable), _key_count(0), _om_count(0) {}
 
   ~ObjectMonitorsDump() {
     class CleanupObjectMonitorsDump: StackObj {
      public:
-      bool do_entry(void*& key, ObjectMonitorLinkedList*& list) {
+      bool do_entry(int64_t& key, ObjectMonitorLinkedList*& list) {
         list->clear();  // clear the LinkListNodes
         delete list;    // then delete the LinkedList
         return true;
       }
     } cleanup;
@@ -366,11 +366,12 @@
     add(monitor);
   }
 
   // Implements the ObjectMonitorsView interface
   void visit(MonitorClosure* closure, JavaThread* thread) override {
-    ObjectMonitorLinkedList* list = get_list(thread);
+    int64_t key = ObjectMonitor::owner_for(thread);
+    ObjectMonitorLinkedList* list = get_list(key);
     LinkedListIterator<ObjectMonitor*> iter(list != nullptr ? list->head() : nullptr);
     while (!iter.is_empty()) {
       ObjectMonitor* monitor = *iter.next();
       closure->do_monitor(monitor);
     }
diff a/src/hotspot/share/runtime/vmStructs.cpp b/src/hotspot/share/runtime/vmStructs.cpp
--- a/src/hotspot/share/runtime/vmStructs.cpp
+++ b/src/hotspot/share/runtime/vmStructs.cpp
@@ -663,10 +663,11 @@
   nonstatic_field(JavaThread,                  _stack_base,                                   address)                               \
   nonstatic_field(JavaThread,                  _stack_size,                                   size_t)                                \
   nonstatic_field(JavaThread,                  _vframe_array_head,                            vframeArray*)                          \
   nonstatic_field(JavaThread,                  _vframe_array_last,                            vframeArray*)                          \
   nonstatic_field(JavaThread,                  _active_handles,                               JNIHandleBlock*)                       \
+  nonstatic_field(JavaThread,                  _lock_id,                                      int64_t)                               \
   volatile_nonstatic_field(JavaThread,         _terminated,                                   JavaThread::TerminatedTypes)           \
   nonstatic_field(Thread,                      _resource_area,                                ResourceArea*)                         \
   nonstatic_field(CompilerThread,              _env,                                          ciEnv*)                                \
                                                                                                                                      \
   /************/                                                                                                                     \
@@ -783,11 +784,12 @@
   /* Monitors */                                                                                                                     \
   /************/                                                                                                                     \
                                                                                                                                      \
   volatile_nonstatic_field(ObjectMonitor,      _metadata,                                     uintptr_t)                             \
   unchecked_nonstatic_field(ObjectMonitor,     _object,                                       sizeof(void *)) /* NOTE: no type */    \
-  unchecked_nonstatic_field(ObjectMonitor,     _owner,                                        sizeof(void *)) /* NOTE: no type */    \
+  unchecked_nonstatic_field(ObjectMonitor,     _owner,                                        int64_t)                               \
+  unchecked_nonstatic_field(ObjectMonitor,     _stack_locker,                                 BasicLock*)                            \
   volatile_nonstatic_field(ObjectMonitor,      _next_om,                                      ObjectMonitor*)                        \
   volatile_nonstatic_field(BasicLock,          _metadata,                                     uintptr_t)                             \
   nonstatic_field(ObjectMonitor,               _contentions,                                  int)                                   \
   volatile_nonstatic_field(ObjectMonitor,      _waiters,                                      int)                                   \
   volatile_nonstatic_field(ObjectMonitor,      _recursions,                                   intx)                                  \
@@ -2531,11 +2533,13 @@
   /* InvocationCounter constants */                                       \
   declare_constant(InvocationCounter::count_increment)                    \
   declare_constant(InvocationCounter::count_shift)                        \
                                                                           \
   /* ObjectMonitor constants */                                           \
+  declare_constant(ObjectMonitor::NO_OWNER)                               \
   declare_constant(ObjectMonitor::ANONYMOUS_OWNER)                        \
+  declare_constant(ObjectMonitor::DEFLATER_MARKER)                        \
 
 //--------------------------------------------------------------------------------
 //
 
 // Generate and check a nonstatic field in non-product builds
diff a/src/hotspot/share/services/diagnosticCommand.cpp b/src/hotspot/share/services/diagnosticCommand.cpp
--- a/src/hotspot/share/services/diagnosticCommand.cpp
+++ b/src/hotspot/share/services/diagnosticCommand.cpp
@@ -127,10 +127,11 @@
 #if INCLUDE_JVMTI
   DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<JVMTIDataDumpDCmd>(full_export, true, false));
 #endif // INCLUDE_JVMTI
   DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<ThreadDumpDCmd>(full_export, true, false));
   DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<ThreadDumpToFileDCmd>(full_export, true, false));
+  DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<VThreadSummaryDCmd>(full_export, true, false));
   DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<ClassLoaderStatsDCmd>(full_export, true, false));
   DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<ClassLoaderHierarchyDCmd>(full_export, true, false));
   DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<CompileQueueDCmd>(full_export, true, false));
   DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<CodeListDCmd>(full_export, true, false));
   DCmdFactory::register_DCmdFactory(new DCmdFactoryImpl<CodeCacheDCmd>(full_export, true, false));
@@ -1121,11 +1122,10 @@
 
   Handle h_path = java_lang_String::create_from_str(path, CHECK);
 
   Symbol* sym = vmSymbols::jdk_internal_vm_ThreadDumper();
   Klass* k = SystemDictionary::resolve_or_fail(sym, true, CHECK);
-  InstanceKlass* ik = InstanceKlass::cast(k);
   if (HAS_PENDING_EXCEPTION) {
     java_lang_Throwable::print(PENDING_EXCEPTION, output());
     output()->cr();
     CLEAR_PENDING_EXCEPTION;
     return;
@@ -1152,10 +1152,50 @@
   // check that result is byte array
   oop res = cast_to_oop(result.get_jobject());
   assert(res->is_typeArray(), "just checking");
   assert(TypeArrayKlass::cast(res->klass())->element_type() == T_BYTE, "just checking");
 
+  // copy the bytes to the output stream
+  typeArrayOop ba = typeArrayOop(res);
+  jbyte* addr = typeArrayOop(res)->byte_at_addr(0);
+  output()->print_raw((const char*)addr, ba->length());
+}
+
+void VThreadSummaryDCmd::execute(DCmdSource source, TRAPS) {
+  ResourceMark rm(THREAD);
+  HandleMark hm(THREAD);
+
+  Symbol* sym = vmSymbols::jdk_internal_vm_VThreadSummary();
+  Klass* k = SystemDictionary::resolve_or_fail(sym, true, CHECK);
+  if (HAS_PENDING_EXCEPTION) {
+    java_lang_Throwable::print(PENDING_EXCEPTION, output());
+    output()->cr();
+    CLEAR_PENDING_EXCEPTION;
+    return;
+  }
+
+  // invoke VThreadSummary.print method
+  JavaValue result(T_OBJECT);
+  JavaCallArguments args;
+  JavaCalls::call_static(&result,
+                         k,
+                         vmSymbols::print_name(),
+                         vmSymbols::void_byte_array_signature(),
+                         &args,
+                         THREAD);
+  if (HAS_PENDING_EXCEPTION) {
+    java_lang_Throwable::print(PENDING_EXCEPTION, output());
+    output()->cr();
+    CLEAR_PENDING_EXCEPTION;
+    return;
+  }
+
+  // check that result is byte array
+  oop res = cast_to_oop(result.get_jobject());
+  assert(res->is_typeArray(), "just checking");
+  assert(TypeArrayKlass::cast(res->klass())->element_type() == T_BYTE, "just checking");
+
   // copy the bytes to the output stream
   typeArrayOop ba = typeArrayOop(res);
   jbyte* addr = typeArrayOop(res)->byte_at_addr(0);
   output()->print_raw((const char*)addr, ba->length());
 }
diff a/src/hotspot/share/services/diagnosticCommand.hpp b/src/hotspot/share/services/diagnosticCommand.hpp
--- a/src/hotspot/share/services/diagnosticCommand.hpp
+++ b/src/hotspot/share/services/diagnosticCommand.hpp
@@ -429,11 +429,11 @@
 public:
   static int num_arguments() { return 2; }
   ThreadDumpDCmd(outputStream* output, bool heap);
   static const char* name() { return "Thread.print"; }
   static const char* description() {
-    return "Print all threads with stacktraces.";
+    return "Print all platform threads with stacktraces.";
   }
   static const char* impact() {
     return "Medium: Depends on the number of threads.";
   }
   static const JavaPermission permission() {
@@ -955,10 +955,23 @@
     return p;
   }
   virtual void execute(DCmdSource source, TRAPS);
 };
 
+class VThreadSummaryDCmd : public DCmd {
+public:
+  VThreadSummaryDCmd(outputStream* output, bool heap) : DCmd(output, heap) { }
+  static const char* name() {
+    return "Thread.vthread_summary";
+  }
+  static const char* description() {
+    return "Print the virtual thread scheduler, timeout schedulers, I/O pollers, and thread groupings.";
+  }
+  static const char* impact() { return "Low"; }
+  virtual void execute(DCmdSource source, TRAPS);
+};
+
 class CompilationMemoryStatisticDCmd: public DCmdWithParser {
 protected:
   DCmdArgument<bool> _human_readable;
   DCmdArgument<MemorySizeArgument> _minsize;
 public:
diff a/src/hotspot/share/services/threadService.cpp b/src/hotspot/share/services/threadService.cpp
--- a/src/hotspot/share/services/threadService.cpp
+++ b/src/hotspot/share/services/threadService.cpp
@@ -462,27 +462,10 @@
           currentThread = JavaThread::cast(owner);
         }
       } else if (waitingToLockMonitor != nullptr) {
         if (waitingToLockMonitor->has_owner()) {
           currentThread = Threads::owning_thread_from_monitor(t_list, waitingToLockMonitor);
-          if (currentThread == nullptr) {
-            // This function is called at a safepoint so the JavaThread
-            // that owns waitingToLockMonitor should be findable, but
-            // if it is not findable, then the previous currentThread is
-            // blocked permanently. We record this as a deadlock.
-            num_deadlocks++;
-
-            // add this cycle to the deadlocks list
-            if (deadlocks == nullptr) {
-              deadlocks = cycle;
-            } else {
-              last->set_next(cycle);
-            }
-            last = cycle;
-            cycle = new DeadlockCycle();
-            break;
-          }
         }
       } else {
         if (concurrent_locks) {
           if (waitingToLockBlocker->is_a(vmClasses::java_util_concurrent_locks_AbstractOwnableSynchronizer_klass())) {
             oop threadObj = java_util_concurrent_locks_AbstractOwnableSynchronizer::get_owner_threadObj(waitingToLockBlocker);
@@ -1051,12 +1034,12 @@
       if (currentThread == nullptr) {
         // The deadlock was detected at a safepoint so the JavaThread
         // that owns waitingToLockMonitor should be findable, but
         // if it is not findable, then the previous currentThread is
         // blocked permanently.
-        st->print_cr("%s UNKNOWN_owner_addr=" PTR_FORMAT, owner_desc,
-                  p2i(waitingToLockMonitor->owner()));
+        st->print_cr("%s UNKNOWN_owner_addr=" INT64_FORMAT, owner_desc,
+                     waitingToLockMonitor->owner());
         continue;
       }
     } else {
       st->print("  waiting for ownable synchronizer " INTPTR_FORMAT ", (a %s)",
                 p2i(waitingToLockBlocker),
diff a/src/java.base/linux/classes/sun/nio/ch/EPollSelectorImpl.java b/src/java.base/linux/classes/sun/nio/ch/EPollSelectorImpl.java
--- a/src/java.base/linux/classes/sun/nio/ch/EPollSelectorImpl.java
+++ b/src/java.base/linux/classes/sun/nio/ch/EPollSelectorImpl.java
@@ -33,11 +33,10 @@
 import java.util.Deque;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
-import jdk.internal.misc.Blocker;
 
 import static sun.nio.ch.EPoll.EPOLLIN;
 import static sun.nio.ch.EPoll.EPOLL_CTL_ADD;
 import static sun.nio.ch.EPoll.EPOLL_CTL_DEL;
 import static sun.nio.ch.EPoll.EPOLL_CTL_MOD;
@@ -103,40 +102,75 @@
         boolean timedPoll = (to > 0);
 
         int numEntries;
         processUpdateQueue();
         processDeregisterQueue();
-        try {
-            begin(blocking);
 
-            do {
-                long startTime = timedPoll ? System.nanoTime() : 0;
-                boolean attempted = Blocker.begin(blocking);
-                try {
+        if (Thread.currentThread().isVirtual()) {
+            numEntries = (timedPoll)
+                    ? timedPoll(TimeUnit.MILLISECONDS.toNanos(to))
+                    : untimedPoll(blocking);
+        } else {
+            try {
+                begin(blocking);
+                do {
+                    long startTime = timedPoll ? System.nanoTime() : 0;
                     numEntries = EPoll.wait(epfd, pollArrayAddress, NUM_EPOLLEVENTS, to);
-                } finally {
-                    Blocker.end(attempted);
-                }
-                if (numEntries == IOStatus.INTERRUPTED && timedPoll) {
-                    // timed poll interrupted so need to adjust timeout
-                    long adjust = System.nanoTime() - startTime;
-                    to -= (int) TimeUnit.NANOSECONDS.toMillis(adjust);
-                    if (to <= 0) {
-                        // timeout expired so no retry
-                        numEntries = 0;
+                    if (numEntries == IOStatus.INTERRUPTED && timedPoll) {
+                        // timed poll interrupted so need to adjust timeout
+                        long adjust = System.nanoTime() - startTime;
+                        to -= (int) TimeUnit.NANOSECONDS.toMillis(adjust);
+                        if (to <= 0) {
+                            // timeout expired so no retry
+                            numEntries = 0;
+                        }
                     }
-                }
-            } while (numEntries == IOStatus.INTERRUPTED);
-            assert IOStatus.check(numEntries);
-
-        } finally {
-            end(blocking);
+                } while (numEntries == IOStatus.INTERRUPTED);
+            } finally {
+                end(blocking);
+            }
         }
+        assert IOStatus.check(numEntries);
+
         processDeregisterQueue();
         return processEvents(numEntries, action);
     }
 
+    /**
+     * If blocking, parks the current virtual thread until a file descriptor is polled
+     * or the thread is interrupted.
+     */
+    private int untimedPoll(boolean block) throws IOException {
+        int numEntries = EPoll.wait(epfd, pollArrayAddress, NUM_EPOLLEVENTS, 0);
+        if (block) {
+            while (numEntries == 0 && !Thread.currentThread().isInterrupted()) {
+                Poller.pollSelector(epfd, 0);
+                numEntries = EPoll.wait(epfd, pollArrayAddress, NUM_EPOLLEVENTS, 0);
+            }
+        }
+        return numEntries;
+    }
+
+    /**
+     * Parks the current virtual thread until a file descriptor is polled, or the thread
+     * is interrupted, for up to the specified waiting time.
+     */
+    private int timedPoll(long nanos) throws IOException {
+        long startNanos = System.nanoTime();
+        int numEntries = EPoll.wait(epfd, pollArrayAddress, NUM_EPOLLEVENTS, 0);
+        while (numEntries == 0 && !Thread.currentThread().isInterrupted()) {
+            long remainingNanos = nanos - (System.nanoTime() - startNanos);
+            if (remainingNanos <= 0) {
+                // timeout
+                break;
+            }
+            Poller.pollSelector(epfd, remainingNanos);
+            numEntries = EPoll.wait(epfd, pollArrayAddress, NUM_EPOLLEVENTS, 0);
+        }
+        return numEntries;
+    }
+
     /**
      * Process changes to the interest ops.
      */
     private void processUpdateQueue() {
         assert Thread.holdsLock(this);
diff a/src/java.base/macosx/classes/sun/nio/ch/KQueueSelectorImpl.java b/src/java.base/macosx/classes/sun/nio/ch/KQueueSelectorImpl.java
--- a/src/java.base/macosx/classes/sun/nio/ch/KQueueSelectorImpl.java
+++ b/src/java.base/macosx/classes/sun/nio/ch/KQueueSelectorImpl.java
@@ -33,11 +33,10 @@
 import java.util.Deque;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
-import jdk.internal.misc.Blocker;
 
 import static sun.nio.ch.KQueue.EVFILT_READ;
 import static sun.nio.ch.KQueue.EVFILT_WRITE;
 import static sun.nio.ch.KQueue.EV_ADD;
 import static sun.nio.ch.KQueue.EV_DELETE;
@@ -107,40 +106,75 @@
         boolean timedPoll = (to > 0);
 
         int numEntries;
         processUpdateQueue();
         processDeregisterQueue();
-        try {
-            begin(blocking);
 
-            do {
-                long startTime = timedPoll ? System.nanoTime() : 0;
-                boolean attempted = Blocker.begin(blocking);
-                try {
+        if (Thread.currentThread().isVirtual()) {
+            numEntries = (timedPoll)
+                    ? timedPoll(TimeUnit.MILLISECONDS.toNanos(to))
+                    : untimedPoll(blocking);
+        } else {
+            try {
+                begin(blocking);
+                do {
+                    long startTime = timedPoll ? System.nanoTime() : 0;
                     numEntries = KQueue.poll(kqfd, pollArrayAddress, MAX_KEVENTS, to);
-                } finally {
-                    Blocker.end(attempted);
-                }
-                if (numEntries == IOStatus.INTERRUPTED && timedPoll) {
-                    // timed poll interrupted so need to adjust timeout
-                    long adjust = System.nanoTime() - startTime;
-                    to -= TimeUnit.NANOSECONDS.toMillis(adjust);
-                    if (to <= 0) {
-                        // timeout expired so no retry
-                        numEntries = 0;
+                    if (numEntries == IOStatus.INTERRUPTED && timedPoll) {
+                        // timed poll interrupted so need to adjust timeout
+                        long adjust = System.nanoTime() - startTime;
+                        to -= TimeUnit.NANOSECONDS.toMillis(adjust);
+                        if (to <= 0) {
+                            // timeout expired so no retry
+                            numEntries = 0;
+                        }
                     }
-                }
-            } while (numEntries == IOStatus.INTERRUPTED);
-            assert IOStatus.check(numEntries);
-
-        } finally {
-            end(blocking);
+                } while (numEntries == IOStatus.INTERRUPTED);
+            } finally {
+                end(blocking);
+            }
         }
+        assert IOStatus.check(numEntries);
+
         processDeregisterQueue();
         return processEvents(numEntries, action);
     }
 
+    /**
+     * If blocking, parks the current virtual thread until a file descriptor is polled
+     * or the thread is interrupted.
+     */
+    private int untimedPoll(boolean block) throws IOException {
+        int numEntries = KQueue.poll(kqfd, pollArrayAddress, MAX_KEVENTS, 0);
+        if (block) {
+            while (numEntries == 0 && !Thread.currentThread().isInterrupted()) {
+                Poller.pollSelector(kqfd, 0);
+                numEntries = KQueue.poll(kqfd, pollArrayAddress, MAX_KEVENTS, 0);
+            }
+        }
+        return numEntries;
+    }
+
+    /**
+     * Parks the current virtual thread until a file descriptor is polled, or the thread
+     * is interrupted, for up to the specified waiting time.
+     */
+    private int timedPoll(long nanos) throws IOException {
+        long startNanos = System.nanoTime();
+        int numEntries = KQueue.poll(kqfd, pollArrayAddress, MAX_KEVENTS, 0);
+        while (numEntries == 0 && !Thread.currentThread().isInterrupted()) {
+            long remainingNanos = nanos - (System.nanoTime() - startNanos);
+            if (remainingNanos <= 0) {
+                // timeout
+                break;
+            }
+            Poller.pollSelector(kqfd, remainingNanos);
+            numEntries = KQueue.poll(kqfd, pollArrayAddress, MAX_KEVENTS, 0);
+        }
+        return numEntries;
+    }
+
     /**
      * Process changes to the interest ops.
      */
     private void processUpdateQueue() {
         assert Thread.holdsLock(this);
diff a/src/java.base/share/classes/java/io/ByteArrayOutputStream.java b/src/java.base/share/classes/java/io/ByteArrayOutputStream.java
--- a/src/java.base/share/classes/java/io/ByteArrayOutputStream.java
+++ b/src/java.base/share/classes/java/io/ByteArrayOutputStream.java
@@ -157,20 +157,12 @@
      *
      * @param   out   the output stream to which to write the data.
      * @throws  NullPointerException if {@code out} is {@code null}.
      * @throws  IOException if an I/O error occurs.
      */
-    public void writeTo(OutputStream out) throws IOException {
-        if (Thread.currentThread().isVirtual()) {
-            byte[] bytes;
-            synchronized (this) {
-                bytes = Arrays.copyOf(buf, count);
-            }
-            out.write(bytes);
-        } else synchronized (this) {
-            out.write(buf, 0, count);
-        }
+    public synchronized void writeTo(OutputStream out) throws IOException {
+        out.write(buf, 0, count);
     }
 
     /**
      * Resets the {@code count} field of this {@code ByteArrayOutputStream}
      * to zero, so that all currently accumulated output in the
diff a/src/java.base/share/classes/java/lang/Object.java b/src/java.base/share/classes/java/lang/Object.java
--- a/src/java.base/share/classes/java/lang/Object.java
+++ b/src/java.base/share/classes/java/lang/Object.java
@@ -23,11 +23,10 @@
  * questions.
  */
 
 package java.lang;
 
-import jdk.internal.misc.Blocker;
 import jdk.internal.vm.annotation.IntrinsicCandidate;
 
 /**
  * Class {@code Object} is the root of the class hierarchy.
  * Every class has {@code Object} as a superclass. All objects,
@@ -372,25 +371,28 @@
      * @see    #notifyAll()
      * @see    #wait()
      * @see    #wait(long, int)
      */
     public final void wait(long timeoutMillis) throws InterruptedException {
-        if (!Thread.currentThread().isVirtual()) {
-            wait0(timeoutMillis);
-            return;
+        if (timeoutMillis < 0) {
+            throw new IllegalArgumentException("timeout value is negative");
         }
 
-        // virtual thread waiting
-        boolean attempted = Blocker.begin();
-        try {
+        if (Thread.currentThread() instanceof VirtualThread vthread) {
+            try {
+                wait0(timeoutMillis);
+            } catch (InterruptedException e) {
+                // virtual thread's interrupt status needs to be cleared
+                vthread.getAndClearInterrupt();
+                throw e;
+            } finally {
+                if (timeoutMillis > 0) {
+                    vthread.cancelWaitTimeout();
+                }
+            }
+        } else {
             wait0(timeoutMillis);
-        } catch (InterruptedException e) {
-            // virtual thread's interrupt status needs to be cleared
-            Thread.currentThread().getAndClearInterrupt();
-            throw e;
-        } finally {
-            Blocker.end(attempted);
         }
     }
 
     // final modifier so method not in vtable
     private final native void wait0(long timeoutMillis) throws InterruptedException;
diff a/src/java.base/share/classes/java/lang/PinnedThreadPrinter.java b/src/java.base/share/classes/java/lang/PinnedThreadPrinter.java
--- a/src/java.base/share/classes/java/lang/PinnedThreadPrinter.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.  Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-package java.lang;
-
-import java.io.PrintStream;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
-import static java.lang.StackWalker.Option.*;
-import jdk.internal.access.JavaIOPrintStreamAccess;
-import jdk.internal.access.SharedSecrets;
-import jdk.internal.misc.InternalLock;
-import jdk.internal.vm.Continuation;
-
-/**
- * Helper class to print the virtual thread stack trace when pinned.
- *
- * The class maintains a ClassValue with the hashes of stack traces that are pinned by
- * code in that Class. This is used to avoid printing the same stack trace many times.
- */
-class PinnedThreadPrinter {
-    private static final JavaIOPrintStreamAccess JIOPSA = SharedSecrets.getJavaIOPrintStreamAccess();
-    private static final StackWalker STACK_WALKER;
-    static {
-        var options = Set.of(SHOW_REFLECT_FRAMES, RETAIN_CLASS_REFERENCE);
-        PrivilegedAction<StackWalker> pa = () ->
-            LiveStackFrame.getStackWalker(options, VirtualThread.continuationScope());
-        @SuppressWarnings("removal")
-        var stackWalker = AccessController.doPrivileged(pa);
-        STACK_WALKER = stackWalker;
-    }
-
-    private static final ClassValue<Hashes> HASHES = new ClassValue<>() {
-        @Override
-        protected Hashes computeValue(Class<?> type) {
-            return new Hashes();
-        }
-    };
-
-    @SuppressWarnings("serial")
-    private static class Hashes extends LinkedHashMap<Integer, Boolean> {
-        boolean add(int hash) {
-            return (putIfAbsent(hash, Boolean.TRUE) == null);
-        }
-        @Override
-        protected boolean removeEldestEntry(Map.Entry<Integer, Boolean> oldest) {
-            // limit number of hashes
-            return size() > 8;
-        }
-    }
-
-    /**
-     * Returns a hash of the given stack trace. The hash is based on the class,
-     * method and bytecode index.
-     */
-    private static int hash(List<LiveStackFrame> stack) {
-        int hash = 0;
-        for (LiveStackFrame frame : stack) {
-            hash = (31 * hash) + Objects.hash(frame.getDeclaringClass(),
-                    frame.getMethodName(),
-                    frame.getByteCodeIndex());
-        }
-        return hash;
-    }
-
-    /**
-     * Returns true if the frame is native, a class initializer, or holds monitors.
-     */
-    private static boolean isInterestingFrame(LiveStackFrame f) {
-        return f.isNativeMethod()
-                || "<clinit>".equals(f.getMethodName())
-                || (f.getMonitors().length > 0);
-    }
-
-    /**
-     * Prints the current thread's stack trace.
-     *
-     * @param printAll true to print all stack frames, false to only print the
-     *        frames that are native or holding a monitor
-     */
-    static void printStackTrace(PrintStream out, Continuation.Pinned reason, boolean printAll) {
-        List<LiveStackFrame> stack = STACK_WALKER.walk(s ->
-            s.map(f -> (LiveStackFrame) f)
-                    .filter(f -> f.getDeclaringClass() != PinnedThreadPrinter.class)
-                    .collect(Collectors.toList())
-        );
-        Object lockObj = JIOPSA.lock(out);
-        if (lockObj instanceof InternalLock lock && lock.tryLock()) {
-            try {
-                // find the closest frame that is causing the thread to be pinned
-                stack.stream()
-                    .filter(f -> isInterestingFrame(f))
-                    .map(LiveStackFrame::getDeclaringClass)
-                    .findFirst()
-                    .ifPresentOrElse(klass -> {
-                        // print the stack trace if not already seen
-                        int hash = hash(stack);
-                        if (HASHES.get(klass).add(hash)) {
-                            printStackTrace(out, reason, stack, printAll);
-                        }
-                    }, () -> printStackTrace(out, reason, stack, true));  // not found
-
-            } finally {
-                lock.unlock();
-            }
-        }
-    }
-
-    private static void printStackTrace(PrintStream out,
-                                        Continuation.Pinned reason,
-                                        List<LiveStackFrame> stack,
-                                        boolean printAll) {
-        out.format("%s reason:%s%n", Thread.currentThread(), reason);
-        for (LiveStackFrame frame : stack) {
-            var ste = frame.toStackTraceElement();
-            int monitorCount = frame.getMonitors().length;
-            if (monitorCount > 0) {
-                out.format("    %s <== monitors:%d%n", ste, monitorCount);
-            } else if (printAll || isInterestingFrame(frame)) {
-                out.format("    %s%n", ste);
-            }
-        }
-    }
-}
diff a/src/java.base/share/classes/java/lang/ScopedValue.java b/src/java.base/share/classes/java/lang/ScopedValue.java
--- a/src/java.base/share/classes/java/lang/ScopedValue.java
+++ b/src/java.base/share/classes/java/lang/ScopedValue.java
@@ -73,38 +73,38 @@
  *
  * {@snippet lang=java :
  *     // @link substring="newInstance" target="#newInstance" :
  *     private static final ScopedValue<String> NAME = ScopedValue.newInstance();
  *
- *     // @link substring="runWhere" target="#runWhere(ScopedValue, Object, Runnable)" :
- *     ScopedValue.runWhere(NAME, "duke", () -> doSomething());
+ *     // @link substring="run" target="Carrier#run(Runnable)" :
+ *     ScopedValue.where(NAME, "duke").run(() -> doSomething());
  * }
  * Code executed directly or indirectly by {@code doSomething}, with access to the field
  * {@code NAME}, can invoke {@code NAME.get()} to read the value "{@code duke}". {@code
  * NAME} is bound while executing the {@code run} method. It reverts to being unbound when
  * the {@code run} method completes.
  *
- * <p> The example using {@code runWhere} invokes a method that does not return a result.
- * The {@link #callWhere(ScopedValue, Object, CallableOp) callWhere} method can be used
+ * <p> The example using {@code run} invokes a method that does not return a result.
+ * The {@link Carrier#call(CallableOp) call} method can be used
  * to invoke a method that returns a result.
- * In addition, {@code ScopedValue} defines the {@link #where(ScopedValue, Object)} method
+ * {@code ScopedValue} defines the {@link #where(ScopedValue, Object)} method
  * for cases where multiple mappings (of {@code ScopedValue} to value) are accumulated
  * in advance of calling a method with all {@code ScopedValue}s bound to their value.
  *
  * <h2>Bindings are per-thread</h2>
  *
- * A {@code ScopedValue} binding to a value is per-thread. Invoking {@code xxxWhere}
+ * A {@code ScopedValue} binding to a value is per-thread. Invoking {@code run}
  * executes a method with a {@code ScopedValue} bound to a value for the current thread.
  * The {@link #get() get} method returns the value bound for the current thread.
  *
  * <p> In the example, if code executed by one thread invokes this:
  * {@snippet lang=java :
- *     ScopedValue.runWhere(NAME, "duke1", () -> doSomething());
+ *     ScopedValue.where(NAME, "duke1").run(() -> doSomething());
  * }
  * and code executed by another thread invokes:
  * {@snippet lang=java :
- *     ScopedValue.runWhere(NAME, "duke2", () -> doSomething());
+ *     ScopedValue.where(NAME, "duke2").run(() -> doSomething());
  * }
  * then code in {@code doSomething} (or any method that it calls) invoking {@code NAME.get()}
  * will read the value "{@code duke1}" or "{@code duke2}", depending on which thread is
  * executing.
  *
@@ -127,11 +127,11 @@
  * to its previous value.
  *
  * <p> In the above example, suppose that code executed by {@code doSomething} binds
  * {@code NAME} to a new value with:
  * {@snippet lang=java :
- *     ScopedValue.runWhere(NAME, "duchess", () -> doMore());
+ *     ScopedValue.where(NAME, "duchess").run(() -> doMore());
  * }
  * Code executed directly or indirectly by {@code doMore()} that invokes {@code
  * NAME.get()} will read the value "{@code duchess}". When {@code doMore()} completes
  * then the value of {@code NAME} reverts to "{@code duke}".
  *
@@ -155,18 +155,23 @@
  * "{@code duke}".
  *
  * {@snippet lang=java :
  *     private static final ScopedValue<String> NAME = ScopedValue.newInstance();
 
- *     ScopedValue.runWhere(NAME, "duke", () -> {
- *         try (var scope = new StructuredTaskScope<String>()) {
+ *     ScopedValue.where(NAME, "duke").run(() -> {
+ *         // @link substring="open" target="StructuredTaskScope#open()" :
+ *         try (var scope = StructuredTaskScope.open()) {
  *
- *             scope.fork(() -> childTask1());
- *             scope.fork(() -> childTask2());
- *             scope.fork(() -> childTask3());
+ *              // @link substring="fork" target="StructuredTaskScope#fork(java.util.concurrent.Callable)" :
+ *              scope.fork(() -> childTask1());
+ *              scope.fork(() -> childTask2());
+ *              scope.fork(() -> childTask3());
  *
- *             ...
+ *              // @link substring="join" target="StructuredTaskScope#join()" :
+ *              scope.join();
+ *
+ *              ..
  *          }
  *     });
  * }
  *
  * <p> Unless otherwise specified, passing a {@code null} argument to a method in this
@@ -383,11 +388,11 @@
                 if (carrier.getKey() == key) {
                     Object value = carrier.get();
                     return (T) value;
                 }
             }
-            throw new NoSuchElementException();
+            throw new NoSuchElementException("No mapping present");
         }
 
         /**
          * Calls a value-returning operation with each scoped value in this mapping bound
          * to its value in the current thread.
@@ -407,11 +412,10 @@
          * @param <R> the type of the result of the operation
          * @param <X> type of the exception thrown by the operation
          * @return the result
          * @throws StructureViolationException if a structure violation is detected
          * @throws X if {@code op} completes with an exception
-         * @see ScopedValue#callWhere(ScopedValue, Object, CallableOp)
          * @since 23
          */
         public <R, X extends Throwable> R call(CallableOp<? extends R, X> op) throws X {
             Objects.requireNonNull(op);
             Cache.invalidate(bitmask);
@@ -456,11 +460,10 @@
          * exception). In that case, the underlying construct of the {@code StructuredTaskScope}
          * is closed and {@link StructureViolationException} is thrown.
          *
          * @param op the operation to run
          * @throws StructureViolationException if a structure violation is detected
-         * @see ScopedValue#runWhere(ScopedValue, Object, Runnable)
          */
         public void run(Runnable op) {
             Objects.requireNonNull(op);
             Cache.invalidate(bitmask);
             var prevSnapshot = scopedValueBindings();
@@ -526,81 +529,10 @@
      */
     public static <T> Carrier where(ScopedValue<T> key, T value) {
         return Carrier.of(key, value);
     }
 
-    /**
-     * Calls a value-returning operation with a {@code ScopedValue} bound to a value
-     * in the current thread. When the operation completes (normally or with an
-     * exception), the {@code ScopedValue} will revert to being unbound, or revert to
-     * its previous value when previously bound, in the current thread. If {@code op}
-     * completes with an exception then it propagated by this method.
-     *
-     * <p> Scoped values are intended to be used in a <em>structured manner</em>. If code
-     * invoked directly or indirectly by the operation creates a {@link StructuredTaskScope}
-     * but does not {@linkplain StructuredTaskScope#close() close} it, then it is detected
-     * as a <em>structure violation</em> when the operation completes (normally or with an
-     * exception). In that case, the underlying construct of the {@code StructuredTaskScope}
-     * is closed and {@link StructureViolationException} is thrown.
-     *
-     * @implNote
-     * This method is implemented to be equivalent to:
-     * {@snippet lang=java :
-     *     // @link substring="call" target="Carrier#call(CallableOp)" :
-     *     ScopedValue.where(key, value).call(op);
-     * }
-     *
-     *
-     *
-     * @param key the {@code ScopedValue} key
-     * @param value the value, can be {@code null}
-     * @param <T> the type of the value
-     * @param <R> the result type
-     * @param <X> type of the exception thrown by the operation
-     * @param op the operation to call
-     * @return the result
-     * @throws StructureViolationException if a structure violation is detected
-     * @throws X if the operation completes with an exception
-     * @since 23
-     */
-    public static <T, R, X extends Throwable> R callWhere(ScopedValue<T> key,
-                                                          T value,
-                                                          CallableOp<? extends R, X> op) throws X {
-        return where(key, value).call(op);
-    }
-
-    /**
-     * Run an operation with a {@code ScopedValue} bound to a value in the current
-     * thread. When the operation completes (normally or with an exception), the
-     * {@code ScopedValue} will revert to being unbound, or revert to its previous value
-     * when previously bound, in the current thread. If {@code op} completes with an
-     * exception then it propagated by this method.
-     *
-     * <p> Scoped values are intended to be used in a <em>structured manner</em>. If code
-     * invoked directly or indirectly by the operation creates a {@link StructuredTaskScope}
-     * but does not {@linkplain StructuredTaskScope#close() close} it, then it is detected
-     * as a <em>structure violation</em> when the operation completes (normally or with an
-     * exception). In that case, the underlying construct of the {@code StructuredTaskScope}
-     * is closed and {@link StructureViolationException} is thrown.
-     *
-     * @implNote
-     * This method is implemented to be equivalent to:
-     * {@snippet lang=java :
-     *     // @link substring="run" target="Carrier#run(Runnable)" :
-     *     ScopedValue.where(key, value).run(op);
-     * }
-     *
-     * @param key the {@code ScopedValue} key
-     * @param value the value, can be {@code null}
-     * @param <T> the type of the value
-     * @param op the operation to call
-     * @throws StructureViolationException if a structure violation is detected
-     */
-    public static <T> void runWhere(ScopedValue<T> key, T value, Runnable op) {
-        where(key, value).run(op);
-    }
-
     private ScopedValue() {
         this.hash = generateKey();
     }
 
     /**
@@ -640,11 +572,11 @@
 
     @SuppressWarnings("unchecked")
     private T slowGet() {
         var value = findBinding();
         if (value == Snapshot.NIL) {
-            throw new NoSuchElementException();
+            throw new NoSuchElementException("ScopedValue not bound");
         }
         Cache.put(this, value);
         return (T)value;
     }
 
diff a/src/java.base/share/classes/java/lang/System.java b/src/java.base/share/classes/java/lang/System.java
--- a/src/java.base/share/classes/java/lang/System.java
+++ b/src/java.base/share/classes/java/lang/System.java
@@ -64,10 +64,11 @@
 import java.util.PropertyPermission;
 import java.util.ResourceBundle;
 import java.util.Set;
 import java.util.WeakHashMap;
 import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.function.Supplier;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.Stream;
 
 import jdk.internal.javac.Restricted;
@@ -2773,10 +2774,14 @@
 
             public Executor virtualThreadDefaultScheduler() {
                 return VirtualThread.defaultScheduler();
             }
 
+            public Stream<ScheduledExecutorService> virtualThreadDelayedTaskSchedulers() {
+                return VirtualThread.delayedTaskSchedulers();
+            }
+
             public StackWalker newStackWalkerInstance(Set<StackWalker.Option> options,
                                                       ContinuationScope contScope,
                                                       Continuation continuation) {
                 return StackWalker.newInstance(options, null, contScope, continuation);
             }
diff a/src/java.base/share/classes/java/lang/Thread.java b/src/java.base/share/classes/java/lang/Thread.java
--- a/src/java.base/share/classes/java/lang/Thread.java
+++ b/src/java.base/share/classes/java/lang/Thread.java
@@ -199,12 +199,12 @@
  *   <tbody>
  *   <tr>
  *     <th scope="row">
  *       {@systemProperty jdk.virtualThreadScheduler.parallelism}
  *     </th>
- *     <td> The number of platform threads available for scheduling virtual
- *       threads. It defaults to the number of available processors. </td>
+ *     <td> The scheduler's target parallelism. It defaults to the number of
+ *       available processors. </td>
  *   </tr>
  *   <tr>
  *     <th scope="row">
  *       {@systemProperty jdk.virtualThreadScheduler.maxPoolSize}
  *     </th>
@@ -414,10 +414,16 @@
      * Sets the Thread object to be returned by Thread.currentThread().
      */
     @IntrinsicCandidate
     native void setCurrentThread(Thread thread);
 
+    /**
+     * Sets the current thread's lock ID.
+     */
+    @IntrinsicCandidate
+    static native void setCurrentLockId(long tid);
+
     // ScopedValue support:
 
     @IntrinsicCandidate
     static native Object[] scopedValueCache();
 
@@ -720,14 +726,15 @@
             int priority = Math.min(parent.getPriority(), g.getMaxPriority());
             this.holder = new FieldHolder(g, task, stackSize, priority, parent.isDaemon());
         }
 
         if (attached && VM.initLevel() < 1) {
-            this.tid = 1;  // primordial thread
+            this.tid = 3;  // primordial thread
         } else {
             this.tid = ThreadIdentifiers.next();
         }
+
         this.name = (name != null) ? name : genThreadName();
 
         if (acc != null) {
             this.inheritedAccessControlContext = acc;
         } else {
diff a/src/java.base/share/classes/java/lang/VirtualThread.java b/src/java.base/share/classes/java/lang/VirtualThread.java
--- a/src/java.base/share/classes/java/lang/VirtualThread.java
+++ b/src/java.base/share/classes/java/lang/VirtualThread.java
@@ -22,12 +22,14 @@
  * or visit www.oracle.com if you need additional information or have any
  * questions.
  */
 package java.lang;
 
+import java.lang.reflect.Constructor;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
+import java.util.Arrays;
 import java.util.Locale;
 import java.util.Objects;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
@@ -39,12 +41,12 @@
 import java.util.concurrent.Future;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
 import jdk.internal.event.VirtualThreadEndEvent;
-import jdk.internal.event.VirtualThreadPinnedEvent;
 import jdk.internal.event.VirtualThreadStartEvent;
 import jdk.internal.event.VirtualThreadSubmitFailedEvent;
 import jdk.internal.misc.CarrierThread;
 import jdk.internal.misc.InnocuousThread;
 import jdk.internal.misc.Unsafe;
@@ -66,18 +68,18 @@
  * A thread that is scheduled by the Java virtual machine rather than the operating system.
  */
 final class VirtualThread extends BaseVirtualThread {
     private static final Unsafe U = Unsafe.getUnsafe();
     private static final ContinuationScope VTHREAD_SCOPE = new ContinuationScope("VirtualThreads");
-    private static final ForkJoinPool DEFAULT_SCHEDULER = createDefaultScheduler();
+    private static final Executor DEFAULT_SCHEDULER = createDefaultScheduler();
     private static final ScheduledExecutorService[] DELAYED_TASK_SCHEDULERS = createDelayedTaskSchedulers();
-    private static final int TRACE_PINNING_MODE = tracePinningMode();
 
     private static final long STATE = U.objectFieldOffset(VirtualThread.class, "state");
     private static final long PARK_PERMIT = U.objectFieldOffset(VirtualThread.class, "parkPermit");
     private static final long CARRIER_THREAD = U.objectFieldOffset(VirtualThread.class, "carrierThread");
     private static final long TERMINATION = U.objectFieldOffset(VirtualThread.class, "termination");
+    private static final long ON_WAITING_LIST = U.objectFieldOffset(VirtualThread.class, "onWaitingList");
 
     // scheduler and continuation
     private final Executor scheduler;
     private final Continuation cont;
     private final Runnable runContinuation;
@@ -104,10 +106,25 @@
      * TIMED_PARKING -> TIMED_PARKED    // cont.yield successful, timed-parked
      * TIMED_PARKING -> TIMED_PINNED    // cont.yield failed, timed-parked on carrier
      *  TIMED_PARKED -> UNPARKED        // unparked, may be scheduled to continue
      *  TIMED_PINNED -> RUNNING         // unparked, continue execution on same carrier
      *
+     *   RUNNING -> BLOCKING       // blocking on monitor enter
+     *  BLOCKING -> BLOCKED        // blocked on monitor enter
+     *   BLOCKED -> UNBLOCKED      // unblocked, may be scheduled to continue
+     * UNBLOCKED -> RUNNING        // continue execution after blocked on monitor enter
+     *
+     *   RUNNING -> WAITING        // transitional state during wait on monitor
+     *   WAITING -> WAITED         // waiting on monitor
+     *    WAITED -> BLOCKED        // notified, waiting to be unblocked by monitor owner
+     *    WAITED -> UNBLOCKED      // timed-out/interrupted
+     *
+     *       RUNNING -> TIMED_WAITING   // transition state during timed-waiting on monitor
+     * TIMED_WAITING -> TIMED_WAITED    // timed-waiting on monitor
+     *  TIMED_WAITED -> BLOCKED         // notified, waiting to be unblocked by monitor owner
+     *  TIMED_WAITED -> UNBLOCKED       // timed-out/interrupted
+     *
      *  RUNNING -> YIELDING        // Thread.yield
      * YIELDING -> YIELDED         // cont.yield successful, may be scheduled to continue
      * YIELDING -> RUNNING         // cont.yield failed
      *  YIELDED -> RUNNING         // continue execution after Thread.yield
      */
@@ -126,32 +143,66 @@
 
     // Thread.yield
     private static final int YIELDING = 10;
     private static final int YIELDED  = 11;         // unmounted but runnable
 
+    // monitor enter
+    private static final int BLOCKING  = 12;
+    private static final int BLOCKED   = 13;        // unmounted
+    private static final int UNBLOCKED = 14;        // unmounted but runnable
+
+    // monitor wait/timed-wait
+    private static final int WAITING       = 15;
+    private static final int WAIT          = 16;    // waiting in Object.wait
+    private static final int TIMED_WAITING = 17;
+    private static final int TIMED_WAIT    = 18;    // waiting in timed-Object.wait
+
     private static final int TERMINATED = 99;  // final state
 
     // can be suspended from scheduling when unmounted
     private static final int SUSPENDED = 1 << 8;
 
-    // parking permit
+    // parking permit made available by LockSupport.unpark
     private volatile boolean parkPermit;
 
+    // blocking permit made available by unblocker thread when another thread exits monitor
+    private volatile boolean blockPermit;
+
+    // true when on the list of virtual threads waiting to be unblocked
+    private volatile boolean onWaitingList;
+
+    // next virtual thread on the list of virtual threads waiting to be unblocked
+    private volatile VirtualThread next;
+
+    // notified by Object.notify/notifyAll while waiting in Object.wait
+    private volatile boolean notified;
+
+    // timed-wait support
+    private long waitTimeout;
+    private byte timedWaitNonce;
+    private volatile Future<?> waitTimeoutTask;
+
     // carrier thread when mounted, accessed by VM
     private volatile Thread carrierThread;
 
     // termination object when joining, created lazily if needed
     private volatile CountDownLatch termination;
 
-
     /**
      * Returns the default scheduler.
      */
     static Executor defaultScheduler() {
         return DEFAULT_SCHEDULER;
     }
 
+    /**
+     * Returns a stream of the delayed task schedulers used to support timed operations.
+     */
+    static Stream<ScheduledExecutorService> delayedTaskSchedulers() {
+        return Arrays.stream(DELAYED_TASK_SCHEDULERS);
+    }
+
     /**
      * Returns the continuation scope used for virtual threads.
      */
     static ContinuationScope continuationScope() {
         return VTHREAD_SCOPE;
@@ -195,22 +246,12 @@
         VThreadContinuation(VirtualThread vthread, Runnable task) {
             super(VTHREAD_SCOPE, wrap(vthread, task));
         }
         @Override
         protected void onPinned(Continuation.Pinned reason) {
-            if (TRACE_PINNING_MODE > 0) {
-                boolean printAll = (TRACE_PINNING_MODE == 1);
-                VirtualThread vthread = (VirtualThread) Thread.currentThread();
-                int oldState = vthread.state();
-                try {
-                    // avoid printing when in transition states
-                    vthread.setState(RUNNING);
-                    PinnedThreadPrinter.printStackTrace(System.out, reason, printAll);
-                } finally {
-                    vthread.setState(oldState);
-                }
-            }
+            // emit JFR event
+            virtualThreadPinnedEvent(reason.reasonCode(), reason.reasonString());
         }
         private static Runnable wrap(VirtualThread vthread, Runnable task) {
             return new Runnable() {
                 @Hidden
                 public void run() {
@@ -218,10 +259,17 @@
                 }
             };
         }
     }
 
+    /**
+     * jdk.VirtualThreadPinned is emitted by HotSpot VM when pinned. Call into VM to
+     * emit event to avoid having a JFR event in Java with the same name (but different ID)
+     * to events emitted by the VM.
+     */
+    private static native void virtualThreadPinnedEvent(int reason, String reasonString);
+
     /**
      * Runs or continues execution on the current thread. The virtual thread is mounted
      * on the current thread before the task runs or continues. It unmounts when the
      * task completes or yields.
      */
@@ -232,18 +280,21 @@
             throw new WrongThreadException();
         }
 
         // set state to RUNNING
         int initialState = state();
-        if (initialState == STARTED || initialState == UNPARKED || initialState == YIELDED) {
+        if (initialState == STARTED || initialState == UNPARKED
+                || initialState == UNBLOCKED || initialState == YIELDED) {
             // newly started or continue after parking/blocking/Thread.yield
             if (!compareAndSetState(initialState, RUNNING)) {
                 return;
             }
-            // consume parking permit when continuing after parking
+            // consume permit when continuing after parking or blocking
             if (initialState == UNPARKED) {
                 setParkPermit(false);
+            } if (initialState == UNBLOCKED) {
+                blockPermit = false;
             }
         } else {
             // not runnable
             return;
         }
@@ -272,22 +323,21 @@
     @ChangesCurrentThread
     private void submitRunContinuation(Executor scheduler, boolean retryOnOOME) {
         boolean done = false;
         while (!done) {
             try {
-                // The scheduler's execute method is invoked in the context of the
-                // carrier thread. For the default scheduler this ensures that the
-                // current thread is a ForkJoinWorkerThread so the task will be pushed
-                // to the local queue. For other schedulers, it avoids deadlock that
-                // would arise due to platform and virtual threads contending for a
-                // lock on the scheduler's submission queue.
-                if (currentThread() instanceof VirtualThread vthread) {
-                    vthread.switchToCarrierThread();
+                // Pin the continuation to prevent the virtual thread from unmounting
+                // when submitting a task. For the default scheduler this ensures that
+                // the carrier doesn't change when pushing a task. For other schedulers
+                // it avoids deadlock that could arise due to carriers and virtual
+                // threads contending for a lock.
+                if (currentThread().isVirtual()) {
+                    Continuation.pin();
                     try {
                         scheduler.execute(runContinuation);
                     } finally {
-                        switchToVirtualThread(vthread);
+                        Continuation.unpin();
                     }
                 } else {
                     scheduler.execute(runContinuation);
                 }
                 done = true;
@@ -485,10 +535,11 @@
         notifyJvmtiHideFrames(true);
         Thread carrier = this.carrierThread;
         assert Thread.currentThread() == this
                 && carrier == Thread.currentCarrierThread();
         carrier.setCurrentThread(carrier);
+        Thread.setCurrentLockId(this.threadId()); // keep lock ID of virtual thread
     }
 
     /**
      * Sets the current thread to the given virtual thread.
      */
@@ -572,10 +623,59 @@
                 submitRunContinuation();
             }
             return;
         }
 
+        // blocking on monitorenter
+        if (s == BLOCKING) {
+            setState(BLOCKED);
+
+            // may have been unblocked while blocking
+            if (blockPermit && compareAndSetState(BLOCKED, UNBLOCKED)) {
+                submitRunContinuation();
+            }
+            return;
+        }
+
+        // Object.wait
+        if (s == WAITING || s == TIMED_WAITING) {
+            byte nonce;
+            int newState;
+            if (s == WAITING) {
+                nonce = 0;  // not used
+                setState(newState = WAIT);
+            } else {
+                // synchronize with timeout task (previous timed-wait may be running)
+                synchronized (timedWaitLock()) {
+                    nonce = ++timedWaitNonce;
+                    setState(newState = TIMED_WAIT);
+                }
+            }
+
+            // may have been notified while in transition to wait state
+            if (notified && compareAndSetState(newState, BLOCKED)) {
+                // may have even been unblocked already
+                if (blockPermit && compareAndSetState(BLOCKED, UNBLOCKED)) {
+                    submitRunContinuation();
+                }
+                return;
+            }
+
+            // may have been interrupted while in transition to wait state
+            if (interrupted && compareAndSetState(newState, UNBLOCKED)) {
+                submitRunContinuation();
+                return;
+            }
+
+            // schedule wakeup
+            if (newState == TIMED_WAIT) {
+                assert waitTimeout > 0;
+                waitTimeoutTask = schedule(() -> waitTimeoutExpired(nonce), waitTimeout, MILLISECONDS);
+            }
+            return;
+        }
+
         assert false;
     }
 
     /**
      * Invoked after the continuation completes.
@@ -672,11 +772,13 @@
 
         // park the thread
         boolean yielded = false;
         setState(PARKING);
         try {
-            yielded = yieldContinuation();  // may throw
+            yielded = yieldContinuation();
+        } catch (OutOfMemoryError e) {
+            // park on carrier
         } finally {
             assert (Thread.currentThread() == this) && (yielded == (state() == RUNNING));
             if (!yielded) {
                 assert state() == PARKING;
                 setState(RUNNING);
@@ -708,24 +810,33 @@
         // park the thread for the waiting time
         if (nanos > 0) {
             long startTime = System.nanoTime();
 
             boolean yielded = false;
-            Future<?> unparker = scheduleUnpark(nanos);  // may throw OOME
-            setState(TIMED_PARKING);
+            Future<?> unparker = null;
             try {
-                yielded = yieldContinuation();  // may throw
-            } finally {
-                assert (Thread.currentThread() == this) && (yielded == (state() == RUNNING));
-                if (!yielded) {
-                    assert state() == TIMED_PARKING;
-                    setState(RUNNING);
+                 unparker = scheduleUnpark(nanos);
+            } catch (OutOfMemoryError e) {
+                // park on carrier
+            }
+            if (unparker != null) {
+                setState(TIMED_PARKING);
+                try {
+                    yielded = yieldContinuation();
+                } catch (OutOfMemoryError e) {
+                    // park on carrier
+                } finally {
+                    assert (Thread.currentThread() == this) && (yielded == (state() == RUNNING));
+                    if (!yielded) {
+                        assert state() == TIMED_PARKING;
+                        setState(RUNNING);
+                    }
+                    cancel(unparker);
                 }
-                cancel(unparker);
             }
 
-            // park on carrier thread for remaining time when pinned
+            // park on carrier thread for remaining time when pinned (or OOME)
             if (!yielded) {
                 long remainingNanos = nanos - (System.nanoTime() - startTime);
                 parkOnCarrierThread(true, remainingNanos);
             }
         }
@@ -739,18 +850,10 @@
      * @param nanos the waiting time in nanoseconds
      */
     private void parkOnCarrierThread(boolean timed, long nanos) {
         assert state() == RUNNING;
 
-        VirtualThreadPinnedEvent event;
-        try {
-            event = new VirtualThreadPinnedEvent();
-            event.begin();
-        } catch (OutOfMemoryError e) {
-            event = null;
-        }
-
         setState(timed ? TIMED_PINNED : PINNED);
         try {
             if (!parkPermit) {
                 if (!timed) {
                     U.park(false, 0);
@@ -762,22 +865,15 @@
             setState(RUNNING);
         }
 
         // consume parking permit
         setParkPermit(false);
-
-        if (event != null) {
-            try {
-                event.commit();
-            } catch (OutOfMemoryError e) {
-                // ignore
-            }
-        }
     }
 
     /**
-     * Schedule this virtual thread to be unparked after a given delay.
+     * Invoked by parkNanos to schedule this virtual thread to be unparked after
+     * a given delay.
      */
     @ChangesCurrentThread
     private Future<?> scheduleUnpark(long nanos) {
         assert Thread.currentThread() == this;
         // need to switch to current carrier thread to avoid nested parking
@@ -788,11 +884,11 @@
             switchToVirtualThread(this);
         }
     }
 
     /**
-     * Cancels a task if it has not completed.
+     * Invoked by parkNanos to cancel the unpark timer.
      */
     @ChangesCurrentThread
     private void cancel(Future<?> future) {
         assert Thread.currentThread() == this;
         if (!future.isDone()) {
@@ -840,10 +936,73 @@
                 return;
             }
         }
     }
 
+    /**
+     * Invoked by unblocker thread to unblock this virtual thread.
+     */
+    private void unblock() {
+        assert !Thread.currentThread().isVirtual();
+        blockPermit = true;
+        if (state() == BLOCKED && compareAndSetState(BLOCKED, UNBLOCKED)) {
+            submitRunContinuation();
+        }
+    }
+
+    /**
+     * Invoked by timer thread when wait timeout for virtual thread has expired.
+     * If the virtual thread is in timed-wait then this method will unblock the thread
+     * and submit its task so that it continues and attempts to reenter the monitor.
+     * This method does nothing if the thread has been woken by notify or interrupt.
+     */
+    private void waitTimeoutExpired(byte nounce) {
+        assert !Thread.currentThread().isVirtual();
+        for (;;) {
+            boolean unblocked = false;
+            synchronized (timedWaitLock()) {
+                if (nounce != timedWaitNonce) {
+                    // this timeout task is for a past timed-wait
+                    return;
+                }
+                int s = state();
+                if (s == TIMED_WAIT) {
+                    unblocked = compareAndSetState(TIMED_WAIT, UNBLOCKED);
+                } else if (s != (TIMED_WAIT | SUSPENDED)) {
+                    // notified or interrupted, no longer waiting
+                    return;
+                }
+            }
+            if (unblocked) {
+                submitRunContinuation();
+                return;
+            }
+            // need to retry when thread is suspended in time-wait
+            Thread.yield();
+        }
+    }
+
+    /**
+     * Invoked by Object.wait to cancel the wait timer.
+     */
+    void cancelWaitTimeout() {
+        assert Thread.currentThread() == this;
+        Future<?> timeoutTask = this.waitTimeoutTask;
+        if (timeoutTask != null) {
+            // Pin the continuation to prevent the virtual thread from unmounting
+            // when there is contention removing the task. This avoids deadlock that
+            // could arise due to carriers and virtual threads contending for a
+            // lock on the delay queue.
+            Continuation.pin();
+            try {
+                timeoutTask.cancel(false);
+            } finally {
+                Continuation.unpin();
+            }
+        }
+    }
+
     /**
      * Attempts to yield the current virtual thread (Thread.yield).
      */
     void tryYield() {
         assert Thread.currentThread() == this;
@@ -970,10 +1129,16 @@
             }
 
             // make available parking permit, unpark thread if parked
             unpark();
 
+            // if thread is waiting in Object.wait then schedule to try to reenter
+            int s = state();
+            if ((s == WAIT || s == TIMED_WAIT) && compareAndSetState(s, UNBLOCKED)) {
+                submitRunContinuation();
+            }
+
         } else {
             interrupted = true;
             carrierThread.setInterrupt();
             setParkPermit(true);
         }
@@ -1014,10 +1179,11 @@
                     return Thread.State.NEW;
                 } else {
                     return Thread.State.RUNNABLE;
                 }
             case UNPARKED:
+            case UNBLOCKED:
             case YIELDED:
                 // runnable, not mounted
                 return Thread.State.RUNNABLE;
             case RUNNING:
                 // if mounted then return state of carrier thread
@@ -1036,19 +1202,26 @@
                 }
                 // runnable, mounted
                 return Thread.State.RUNNABLE;
             case PARKING:
             case TIMED_PARKING:
+            case WAITING:
+            case TIMED_WAITING:
             case YIELDING:
                 // runnable, in transition
                 return Thread.State.RUNNABLE;
             case PARKED:
             case PINNED:
-                return State.WAITING;
+            case WAIT:
+                return Thread.State.WAITING;
             case TIMED_PARKED:
             case TIMED_PINNED:
-                return State.TIMED_WAITING;
+            case TIMED_WAIT:
+                return Thread.State.TIMED_WAITING;
+            case BLOCKING:
+            case BLOCKED:
+                return Thread.State.BLOCKED;
             case TERMINATED:
                 return Thread.State.TERMINATED;
             default:
                 throw new InternalError();
         }
@@ -1090,17 +1263,17 @@
                 return new StackTraceElement[0];  // unmounted, empty stack
             }
             case RUNNING, PINNED, TIMED_PINNED -> {
                 return null;   // mounted
             }
-            case PARKED, TIMED_PARKED -> {
+            case PARKED, TIMED_PARKED, BLOCKED, WAIT, TIMED_WAIT -> {
                 // unmounted, not runnable
             }
-            case UNPARKED, YIELDED -> {
+            case UNPARKED, UNBLOCKED, YIELDED -> {
                 // unmounted, runnable
             }
-            case PARKING, TIMED_PARKING, YIELDING -> {
+            case PARKING, TIMED_PARKING, BLOCKING, YIELDING, WAITING, TIMED_WAITING -> {
                 return null;  // in transition
             }
             default -> throw new InternalError("" + initialState);
         }
 
@@ -1117,18 +1290,27 @@
         } finally {
             assert state == suspendedState;
             setState(initialState);
         }
         boolean resubmit = switch (initialState) {
-            case UNPARKED, YIELDED -> {
+            case UNPARKED, UNBLOCKED, YIELDED -> {
                 // resubmit as task may have run while suspended
                 yield true;
             }
             case PARKED, TIMED_PARKED -> {
                 // resubmit if unparked while suspended
                 yield parkPermit && compareAndSetState(initialState, UNPARKED);
             }
+            case BLOCKED -> {
+                // resubmit if unblocked while suspended
+                yield blockPermit && compareAndSetState(BLOCKED, UNBLOCKED);
+            }
+            case WAIT, TIMED_WAIT -> {
+                // resubmit if notified or interrupted while waiting (Object.wait)
+                // waitTimeoutExpired will retry if the timed expired when suspended
+                yield (notified || interrupted) && compareAndSetState(initialState, UNBLOCKED);
+            }
             default -> throw new InternalError();
         };
         if (resubmit) {
             submitRunContinuation();
         }
@@ -1219,10 +1401,18 @@
     private Object carrierThreadAccessLock() {
         // return interruptLock as unmount has to coordinate with interrupt
         return interruptLock;
     }
 
+    /**
+     * Returns a lock object to coordinating timed-wait setup and timeout handling.
+     */
+    private Object timedWaitLock() {
+        // use this object for now to avoid the overhead of introducing another lock
+        return runContinuation;
+    }
+
     /**
      * Disallow the current thread be suspended or preempted.
      */
     private void disableSuspendAndPreempt() {
         notifyJvmtiDisableSuspend(true);
@@ -1249,10 +1439,14 @@
 
     private boolean compareAndSetState(int expectedValue, int newValue) {
         return U.compareAndSetInt(this, STATE, expectedValue, newValue);
     }
 
+    private boolean compareAndSetOnWaitingList(boolean expectedValue, boolean newValue) {
+        return U.compareAndSetBoolean(this, ON_WAITING_LIST, expectedValue, newValue);
+    }
+
     private void setParkPermit(boolean newValue) {
         if (parkPermit != newValue) {
             parkPermit = newValue;
         }
     }
@@ -1299,20 +1493,46 @@
     static {
         registerNatives();
 
         // ensure VTHREAD_GROUP is created, may be accessed by JVMTI
         var group = Thread.virtualThreadGroup();
+    }
 
-        // ensure VirtualThreadPinnedEvent is loaded/initialized
-        U.ensureClassInitialized(VirtualThreadPinnedEvent.class);
+    /**
+     * Creates the default scheduler.
+     * If the system property {@code jdk.virtualThreadScheduler.implClass} is set then
+     * its value is the name of a class that implements java.util.concurrent.Executor.
+     * The class is public in an exported package, has a public no-arg constructor,
+     * and is visible to the system class loader.
+     * If the system property is not set then the default scheduler will be a
+     * ForkJoinPool instance.
+     */
+    private static Executor createDefaultScheduler() {
+        String propName = "jdk.virtualThreadScheduler.implClass";
+        String propValue = GetPropertyAction.privilegedGetProperty(propName);
+        if (propValue != null) {
+            try {
+                Class<?> clazz = Class.forName(propValue, true,
+                        ClassLoader.getSystemClassLoader());
+                Constructor<?> ctor = clazz.getConstructor();
+                var scheduler = (Executor) ctor.newInstance();
+                System.err.println("""
+                    WARNING: Using custom scheduler, this is an experimental feature.""");
+                return scheduler;
+            } catch (Exception ex) {
+                throw new Error(ex);
+            }
+        } else {
+            return createDefaultForkJoinPoolScheduler();
+        }
     }
 
     /**
      * Creates the default ForkJoinPool scheduler.
      */
     @SuppressWarnings("removal")
-    private static ForkJoinPool createDefaultScheduler() {
+    private static ForkJoinPool createDefaultForkJoinPoolScheduler() {
         ForkJoinWorkerThreadFactory factory = pool -> {
             PrivilegedAction<ForkJoinWorkerThread> pa = () -> new CarrierThread(pool);
             return AccessController.doPrivileged(pa);
         };
         PrivilegedAction<ForkJoinPool> pa = () -> {
@@ -1382,20 +1602,39 @@
         }
         return schedulers;
     }
 
     /**
-     * Reads the value of the jdk.tracePinnedThreads property to determine if stack
-     * traces should be printed when a carrier thread is pinned when a virtual thread
-     * attempts to park.
+     * Schedule virtual threads that are ready to be scheduled after they blocked on
+     * monitor enter.
      */
-    private static int tracePinningMode() {
-        String propValue = GetPropertyAction.privilegedGetProperty("jdk.tracePinnedThreads");
-        if (propValue != null) {
-            if (propValue.length() == 0 || "full".equalsIgnoreCase(propValue))
-                return 1;
-            if ("short".equalsIgnoreCase(propValue))
-                return 2;
+    private static void unblockVirtualThreads() {
+        while (true) {
+            VirtualThread vthread = takeVirtualThreadListToUnblock();
+            while (vthread != null) {
+                assert vthread.onWaitingList;
+                VirtualThread nextThread = vthread.next;
+
+                // remove from list and unblock
+                vthread.next = null;
+                boolean changed = vthread.compareAndSetOnWaitingList(true, false);
+                assert changed;
+                vthread.unblock();
+
+                vthread = nextThread;
+            }
         }
-        return 0;
+    }
+
+    /**
+     * Retrieves the list of virtual threads that are waiting to be unblocked, waiting
+     * if necessary until a list of one or more threads becomes available.
+     */
+    private static native VirtualThread takeVirtualThreadListToUnblock();
+
+    static {
+        var unblocker = InnocuousThread.newThread("VirtualThread-unblocker",
+                VirtualThread::unblockVirtualThreads);
+        unblocker.setDaemon(true);
+        unblocker.start();
     }
 }
diff a/src/java.base/share/classes/java/lang/invoke/MethodType.java b/src/java.base/share/classes/java/lang/invoke/MethodType.java
--- a/src/java.base/share/classes/java/lang/invoke/MethodType.java
+++ b/src/java.base/share/classes/java/lang/invoke/MethodType.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2008, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2008, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.  Oracle designates this
@@ -229,11 +229,11 @@
         if (num instanceof Integer)  num = "bad index: "+num;
         return new IndexOutOfBoundsException(num.toString());
     }
 
     static final ReferencedKeySet<MethodType> internTable =
-        ReferencedKeySet.create(false, true, new Supplier<>() {
+        ReferencedKeySet.create(false, new Supplier<>() {
             @Override
             public Map<ReferenceKey<MethodType>, ReferenceKey<MethodType>> get() {
                 return new ConcurrentHashMap<>(512);
             }
         });
diff a/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java b/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java
--- a/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java
+++ b/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java
@@ -1132,11 +1132,11 @@
                   .return_();
             }
         };
 
         static final ReferencedKeyMap<MethodType, SoftReference<MethodHandlePair>> CACHE =
-                ReferencedKeyMap.create(true, true,
+                ReferencedKeyMap.create(true,
                         new Supplier<>() {
                             @Override
                             public Map<ReferenceKey<MethodType>, SoftReference<MethodHandlePair>> get() {
                                 return new ConcurrentHashMap<>(64);
                             }
diff a/src/java.base/share/classes/java/lang/ref/Finalizer.java b/src/java.base/share/classes/java/lang/ref/Finalizer.java
--- a/src/java.base/share/classes/java/lang/ref/Finalizer.java
+++ b/src/java.base/share/classes/java/lang/ref/Finalizer.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 1997, 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.  Oracle designates this
@@ -33,11 +33,11 @@
 
 final class Finalizer extends FinalReference<Object> { /* Package-private; must be in
                                                           same package as the Reference
                                                           class */
 
-    private static ReferenceQueue<Object> queue = new NativeReferenceQueue<>();
+    private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
 
     /** Head of doubly linked list of Finalizers awaiting finalization. */
     private static Finalizer unfinalized = null;
 
     /** Lock guarding access to unfinalized list. */
diff a/src/java.base/share/classes/java/lang/ref/NativeReferenceQueue.java b/src/java.base/share/classes/java/lang/ref/NativeReferenceQueue.java
--- a/src/java.base/share/classes/java/lang/ref/NativeReferenceQueue.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.  Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-package java.lang.ref;
-
-/**
- * An implementation of a ReferenceQueue that uses native monitors.
- * The use of java.util.concurrent.lock locks interacts with various mechanisms,
- * such as virtual threads and ForkJoinPool, that might not be appropriate for some
- * low-level mechanisms, in particular MethodType's weak intern set.
- */
-final class NativeReferenceQueue<T> extends ReferenceQueue<T> {
-    public NativeReferenceQueue() {
-        super(0);
-    }
-
-    private static class Lock { };
-    private final Lock lock = new Lock();
-
-    @Override
-    void signal() {
-        lock.notifyAll();
-    }
-    @Override
-    void await() throws InterruptedException {
-        lock.wait();
-    }
-
-    @Override
-    void await(long timeoutMillis) throws InterruptedException {
-        lock.wait(timeoutMillis);
-    }
-
-    @Override
-    boolean enqueue(Reference<? extends T> r) {
-        synchronized(lock) {
-            return enqueue0(r);
-        }
-    }
-
-    @Override
-    public Reference<? extends T> poll() {
-        if (headIsNull())
-            return null;
-
-        synchronized(lock) {
-            return poll0();
-        }
-    }
-
-    @Override
-    public Reference<? extends T> remove(long timeout)
-            throws IllegalArgumentException, InterruptedException {
-        if (timeout < 0)
-            throw new IllegalArgumentException("Negative timeout value");
-        if (timeout == 0)
-            return remove();
-
-        synchronized(lock) {
-            return remove0(timeout);
-        }
-    }
-
-    @Override
-    public Reference<? extends T> remove() throws InterruptedException {
-        synchronized(lock) {
-            return remove0();
-        }
-    }
-}
diff a/src/java.base/share/classes/java/lang/ref/Reference.java b/src/java.base/share/classes/java/lang/ref/Reference.java
--- a/src/java.base/share/classes/java/lang/ref/Reference.java
+++ b/src/java.base/share/classes/java/lang/ref/Reference.java
@@ -328,15 +328,10 @@
 
             @Override
             public void runFinalization() {
                 Finalizer.runFinalization();
             }
-
-            @Override
-            public <T> ReferenceQueue<T> newNativeReferenceQueue() {
-                return new NativeReferenceQueue<T>();
-            }
         });
     }
 
     /* -- Referent accessor and setters -- */
 
diff a/src/java.base/share/classes/java/lang/ref/ReferenceQueue.java b/src/java.base/share/classes/java/lang/ref/ReferenceQueue.java
--- a/src/java.base/share/classes/java/lang/ref/ReferenceQueue.java
+++ b/src/java.base/share/classes/java/lang/ref/ReferenceQueue.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 1997, 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.  Oracle designates this
@@ -23,15 +23,14 @@
  * questions.
  */
 
 package java.lang.ref;
 
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Consumer;
 import jdk.internal.misc.VM;
+import jdk.internal.vm.Continuation;
+import jdk.internal.vm.ContinuationSupport;
 
 /**
  * Reference queues, to which registered reference objects are appended by the
  * garbage collector after the appropriate reachability changes are detected.
  *
@@ -48,12 +47,10 @@
  * @since    1.2
  */
 
 public class ReferenceQueue<T> {
     private static class Null extends ReferenceQueue<Object> {
-        public Null() { super(0); }
-
         @Override
         boolean enqueue(Reference<?> r) {
             return false;
         }
     }
@@ -62,39 +59,20 @@
     static final ReferenceQueue<Object> ENQUEUED = new Null();
 
     private volatile Reference<? extends T> head;
     private long queueLength = 0;
 
-    private final ReentrantLock lock;
-    private final Condition notEmpty;
-
-    void signal() {
-        notEmpty.signalAll();
-    }
-
-    void await() throws InterruptedException {
-        notEmpty.await();
-    }
-
-    void await(long timeoutMillis) throws InterruptedException {
-        notEmpty.await(timeoutMillis, TimeUnit.MILLISECONDS);
-    }
+    private static class Lock { };
+    private final Lock lock = new Lock();
 
     /**
      * Constructs a new reference-object queue.
      */
     public ReferenceQueue() {
-        this.lock = new ReentrantLock();
-        this.notEmpty = lock.newCondition();
     }
 
-    ReferenceQueue(int dummy) {
-        this.lock = null;
-        this.notEmpty = null;
-    }
-
-    final boolean enqueue0(Reference<? extends T> r) { // must hold lock
+    private boolean enqueue0(Reference<? extends T> r) { // must hold lock
         // Check that since getting the lock this reference hasn't already been
         // enqueued (and even then removed)
         ReferenceQueue<?> queue = r.queue;
         if ((queue == NULL) || (queue == ENQUEUED)) {
             return false;
@@ -109,19 +87,15 @@
         // Volatiles ensure ordering.
         r.queue = ENQUEUED;
         if (r instanceof FinalReference) {
             VM.addFinalRefCount(1);
         }
-        signal();
+        lock.notifyAll();
         return true;
     }
 
-    final boolean headIsNull() {
-        return head == null;
-    }
-
-    final Reference<? extends T> poll0() { // must hold lock
+    private Reference<? extends T> poll0() { // must hold lock
         Reference<? extends T> r = head;
         if (r != null) {
             r.queue = NULL;
             // Update r.queue *before* removing from list, to avoid
             // race with concurrent enqueued checks and fast-path
@@ -140,61 +114,74 @@
             return r;
         }
         return null;
     }
 
-    final Reference<? extends T> remove0(long timeout)
-            throws IllegalArgumentException, InterruptedException { // must hold lock
+    private Reference<? extends T> remove0(long timeout) throws InterruptedException { // must hold lock
         Reference<? extends T> r = poll0();
         if (r != null) return r;
         long start = System.nanoTime();
         for (;;) {
-            await(timeout);
+            lock.wait(timeout);
             r = poll0();
             if (r != null) return r;
-
             long end = System.nanoTime();
             timeout -= (end - start) / 1000_000;
             if (timeout <= 0) return null;
             start = end;
         }
     }
 
-    final Reference<? extends T> remove0() throws InterruptedException { // must hold lock
+    private Reference<? extends T> remove0() throws InterruptedException { // must hold lock
         for (;;) {
             var r = poll0();
             if (r != null) return r;
-            await();
+            lock.wait();
         }
     }
 
     boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */
-        lock.lock();
-        try {
+        synchronized (lock) {
             return enqueue0(r);
-        } finally {
-            lock.unlock();
         }
     }
 
+    private boolean tryDisablePreempt() {
+        if (Thread.currentThread().isVirtual() && ContinuationSupport.isSupported()) {
+            Continuation.pin();
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private void enablePreempt() {
+        Continuation.unpin();
+    }
+
     /**
      * Polls this queue to see if a reference object is available.  If one is
      * available without further delay then it is removed from the queue and
      * returned.  Otherwise this method immediately returns {@code null}.
      *
      * @return  A reference object, if one was immediately available,
      *          otherwise {@code null}
      * @see java.lang.ref.Reference#enqueue()
      */
     public Reference<? extends T> poll() {
-        if (headIsNull())
+        if (head == null)
             return null;
-        lock.lock();
+
+        // Prevent a virtual thread from being preempted as this could potentially
+        // deadlock with a carrier that is polling the same reference queue.
+        boolean disabled = tryDisablePreempt();
         try {
-            return poll0();
+            synchronized (lock) {
+                return poll0();
+            }
         } finally {
-            lock.unlock();
+            if (disabled) enablePreempt();
         }
     }
 
     /**
      * Removes the next reference object in this queue, blocking until either
@@ -222,15 +209,12 @@
         if (timeout < 0)
             throw new IllegalArgumentException("Negative timeout value");
         if (timeout == 0)
             return remove();
 
-        lock.lock();
-        try {
+        synchronized (lock) {
             return remove0(timeout);
-        } finally {
-            lock.unlock();
         }
     }
 
     /**
      * Removes the next reference object in this queue, blocking until one
@@ -239,15 +223,12 @@
      * @return A reference object, blocking until one becomes available
      * @throws  InterruptedException  If the wait is interrupted
      * @see java.lang.ref.Reference#enqueue()
      */
     public Reference<? extends T> remove() throws InterruptedException {
-        lock.lock();
-        try {
+        synchronized (lock) {
             return remove0();
-        } finally {
-            lock.unlock();
         }
     }
 
     /**
      * Iterate queue and invoke given action with each Reference.
diff a/src/java.base/share/classes/java/util/concurrent/ForkJoinPool.java b/src/java.base/share/classes/java/util/concurrent/ForkJoinPool.java
--- a/src/java.base/share/classes/java/util/concurrent/ForkJoinPool.java
+++ b/src/java.base/share/classes/java/util/concurrent/ForkJoinPool.java
@@ -49,10 +49,11 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.function.Predicate;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.locks.LockSupport;
+import jdk.internal.access.JavaLangAccess;
 import jdk.internal.access.JavaUtilConcurrentFJPAccess;
 import jdk.internal.access.SharedSecrets;
 import jdk.internal.misc.Unsafe;
 import jdk.internal.vm.SharedThreadContainer;
 
@@ -2630,21 +2631,22 @@
         throw new RejectedExecutionException();
     }
 
     private void poolSubmit(boolean signalIfEmpty, ForkJoinTask<?> task) {
         Thread t; ForkJoinWorkerThread wt; WorkQueue q; boolean internal;
-        if (((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) &&
+        if (((t = JLA.currentCarrierThread()) instanceof ForkJoinWorkerThread) &&
             (wt = (ForkJoinWorkerThread)t).pool == this) {
             internal = true;
             q = wt.workQueue;
         }
         else {                     // find and lock queue
             internal = false;
             q = submissionQueue(ThreadLocalRandom.getProbe());
         }
         q.push(task, signalIfEmpty ? this : null, internal);
     }
+    private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess();
 
     /**
      * Returns queue for an external submission, bypassing call to
      * submissionQueue if already established and unlocked.
      */
diff a/src/java.base/share/classes/java/util/concurrent/StructuredTaskScope.java b/src/java.base/share/classes/java/util/concurrent/StructuredTaskScope.java
--- a/src/java.base/share/classes/java/util/concurrent/StructuredTaskScope.java
+++ b/src/java.base/share/classes/java/util/concurrent/StructuredTaskScope.java
@@ -27,562 +27,1099 @@
 import java.lang.invoke.MethodHandles;
 import java.lang.invoke.VarHandle;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.time.Duration;
-import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
 import java.util.Objects;
-import java.util.Optional;
-import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
+import java.util.stream.Stream;
 import jdk.internal.javac.PreviewFeature;
+import jdk.internal.misc.InnocuousThread;
 import jdk.internal.misc.ThreadFlock;
 import jdk.internal.invoke.MhUtil;
 
 /**
- * A basic API for <em>structured concurrency</em>. {@code StructuredTaskScope} supports
- * cases where a task splits into several concurrent subtasks, and where the subtasks must
- * complete before the main task continues. A {@code StructuredTaskScope} can be used to
- * ensure that the lifetime of a concurrent operation is confined by a <em>syntax block</em>,
- * just like that of a sequential operation in structured programming.
+ * An API for <em>structured concurrency</em>. {@code StructuredTaskScope} supports cases
+ * where execution of a <em>task</em> (a unit of work) splits into several concurrent
+ * subtasks, and where the subtasks must complete before the task continues. A {@code
+ * StructuredTaskScope} can be used to ensure that the lifetime of a concurrent operation
+ * is confined by a <em>syntax block</em>, just like that of a sequential operation in
+ * structured programming.
  *
- * <h2>Basic operation</h2>
+ * <p> {@code StructuredTaskScope} defines the static method {@link #open() open} to open
+ * a new {@code StructuredTaskScope} and the {@link #close() close} method to close it.
+ * The API is designed to be used with the {@code try}-with-resources statement where
+ * the {@code StructuredTaskScope} is opened as a resource and then closed automatically.
+ * The code inside the block uses the {@link #fork(Callable) fork} method to fork subtasks.
+ * After forking, it uses the {@link #join() join} method to wait for all subtasks to
+ * finish (or some other outcome) as a single operation. Forking a subtask starts a new
+ * {@link Thread} to run the subtask. The thread executing the task does not continue
+ * beyond the {@code close} method until all threads started to execute subtasks have finished.
+ * To ensure correct usage, the {@code fork}, {@code join} and {@code close} methods may
+ * only be invoked by the <em>owner thread</em> (the thread that opened the {@code
+ * StructuredTaskScope}), the {@code fork} method may not be called after {@code join},
+ * the {@code join} method may only be invoked once, and the {@code close} method throws
+ * an exception after closing if the owner did not invoke the {@code join} method after
+ * forking subtasks.
  *
- * A {@code StructuredTaskScope} is created with one of its public constructors. It defines
- * the {@link #fork(Callable) fork} method to start a thread to execute a subtask, the {@link
- * #join() join} method to wait for all subtasks to finish, and the {@link #close() close}
- * method to close the task scope. The API is intended to be used with the {@code
- * try-with-resources} statement. The intention is that code in the try <em>block</em>
- * uses the {@code fork} method to fork threads to execute the subtasks, wait for the
- * subtasks to finish with the {@code join} method, and then <em>process the results</em>.
- * A call to the {@code fork} method returns a {@link Subtask Subtask} to representing
- * the <em>forked subtask</em>. Once {@code join} is called, the {@code Subtask} can be
- * used to get the result completed successfully, or the exception if the subtask failed.
+ * <p> As a first example, consider a task that splits into two subtasks to concurrently
+ * fetch resources from two URL locations "left" and "right". Both subtasks may complete
+ * successfully, one subtask may succeed and the other may fail, or both subtasks may
+ * fail. The task in this example is interested in the successful result from both
+ * subtasks. It waits in the {@link #join() join} method for both subtasks to complete
+ * successfully or for either subtask to fail.
  * {@snippet lang=java :
- *     Callable<String> task1 = ...
- *     Callable<Integer> task2 = ...
+ *    // @link substring="open" target="#open()" :
+ *    try (var scope = StructuredTaskScope.open()) {
  *
- *     try (var scope = new StructuredTaskScope<Object>()) {
+ *        // @link substring="fork" target="#fork(Callable)" :
+ *        Subtask<String> subtask1 = scope.fork(() -> query(left));
+ *        Subtask<Integer> subtask2 = scope.fork(() -> query(right));
  *
- *         Subtask<String> subtask1 = scope.fork(task1);   // @highlight substring="fork"
- *         Subtask<Integer> subtask2 = scope.fork(task2);  // @highlight substring="fork"
+ *        // throws if either subtask fails
+ *        scope.join();  // @link substring="join" target="#join()"
  *
- *         scope.join();                                   // @highlight substring="join"
+ *        // both subtasks completed successfully
+ *        // @link substring="get" target="Subtask#get()" :
+ *        return new MyResult(subtask1.get(), subtask2.get());
  *
- *         ... process results/exceptions ...
- *
- *     } // close                                          // @highlight substring="close"
+ *    // @link substring="close" target="#close()" :
+ *    } // close
  * }
- * <p> The following example forks a collection of homogeneous subtasks, waits for all of
- * them to complete with the {@code join} method, and uses the {@link Subtask.State
- * Subtask.State} to partition the subtasks into a set of the subtasks that completed
- * successfully and another for the subtasks that failed.
- * {@snippet lang=java :
- *     List<Callable<String>> callables = ...
  *
- *     try (var scope = new StructuredTaskScope<String>()) {
+ * <p> If both subtasks complete successfully then the {@code join} method completes
+ * normally and the task uses the {@link Subtask#get() Subtask.get()} method to get
+ * the result of each subtask. If one of the subtasks fails then the other subtask is
+ * cancelled (this will interrupt the thread executing the other subtask) and the {@code
+ * join} method throws {@link FailedException} with the exception from the failed subtask
+ * as the {@linkplain Throwable#getCause() cause}.
  *
- *         List<Subtask<String>> subtasks = callables.stream().map(scope::fork).toList();
+ * <p> In the example, the subtasks produce results of different types ({@code String} and
+ * {@code Integer}). In other cases the subtasks may all produce results of the same type.
+ * If the example had used {@code StructuredTaskScope.<String>open()} then it could
+ * only be used to fork subtasks that return a {@code String} result.
  *
- *         scope.join();
+ * <h2>Joiners</h2>
  *
- *         Map<Boolean, Set<Subtask<String>>> map = subtasks.stream()
- *                 .collect(Collectors.partitioningBy(h -> h.state() == Subtask.State.SUCCESS,
- *                                                    Collectors.toSet()));
+ * <p> In the example above, the task fails if any subtask fails. If all subtasks
+ * succeed then the {@code join} method completes normally. Other policy and outcome is
+ * supported by creating a {@code StructuredTaskScope} with a {@link Joiner} that
+ * implements the desired policy. A {@code Joiner} handles subtask completion and produces
+ * the outcome for the {@link #join() join} method. In the example above, {@code join}
+ * returns {@code null}. Depending on the {@code Joiner}, {@code join} may return a
+ * result, a stream of elements, or some other object. The {@code Joiner} interface defines
+ * factory methods to create {@code Joiner}s for some common cases.
  *
- *     } // close
- * }
+ * <p> A {@code Joiner} may <a id="Cancallation">cancel</a> the scope (sometimes called
+ * "short-circuiting") when some condition is reached that does not require the result of
+ * subtasks that are still executing. Cancelling the scope prevents new threads from being
+ * started to execute further subtasks, {@linkplain Thread#interrupt() interrupts} the
+ * threads executing subtasks that have not completed, and causes the {@code join} method
+ * to wakeup with the outcome (result or exception). In the above example, the outcome is
+ * that {@code join} completes with a result of {@code null} when all subtasks succeed.
+ * The scope is cancelled if any of the subtasks fail and {@code join} throws {@code
+ * FailedException} with the exception from the failed subtask as the cause. Other {@code
+ * Joiner} implementations may cancel the scope for other reasons.
+ *
+ * <p> To allow for cancellation, subtasks must be coded so that they finish as soon as
+ * possible when interrupted. Subtasks that do not respond to interrupt, e.g. block on
+ * methods that are not interruptible, may delay the closing of a scope indefinitely. The
+ * {@link #close() close} method always waits for threads executing subtasks to finish,
+ * even if the scope is cancelled, so execution cannot continue beyond the {@code close}
+ * method until the interrupted threads finish.
+ *
+ * <p> Now consider another example that splits into two subtasks. In this example,
+ * each subtask produces a {@code String} result and the task is only interested in
+ * the result from the first subtask to complete successfully. The example uses {@link
+ * Joiner#anySuccessfulResultOrThrow() Joiner.anySuccessfulResultOrThrow()} to
+ * create a {@code Joiner} that makes available the result of the first subtask to
+ * complete successfully. The type parameter in the example is "{@code String}" so that
+ * only subtasks that return a {@code String} can be forked.
+ * {@snippet lang=java :
+ *    // @link substring="open" target="#open(Policy)" :
+ *    try (var scope = StructuredTaskScope.open(Joiner.<String>anySuccessfulResultOrThrow())) {
+ *
+ *        scope.fork(callable1);
+ *        scope.fork(callable2);
  *
- * <p> To ensure correct usage, the {@code join} and {@code close} methods may only be
- * invoked by the <em>owner</em> (the thread that opened/created the task scope), and the
- * {@code close} method throws an exception after closing if the owner did not invoke the
- * {@code join} method after forking.
+ *        // throws if both subtasks fail
+ *        String firstResult = scope.join();
  *
- * <p> {@code StructuredTaskScope} defines the {@link #shutdown() shutdown} method to shut
- * down a task scope without closing it. The {@code shutdown()} method <em>cancels</em> all
- * unfinished subtasks by {@linkplain Thread#interrupt() interrupting} the threads. It
- * prevents new threads from starting in the task scope. If the owner is waiting in the
- * {@code join} method then it will wakeup.
+ *    }
+ * }
+ *
+ * <p> In the example, the task forks the two subtasks, then waits in the {@code
+ * join} method for either subtask to complete successfully or for both subtasks to fail.
+ * If one of the subtasks completes successfully then the {@code Joiner} causes the other
+ * subtask to be cancelled (this will interrupt the thread executing the subtask), and
+ * the {@code join} method returns the result from the successful subtask. Cancelling the
+ * other subtask avoids the task waiting for a result that it doesn't care about. If
+ * both subtasks fail then the {@code join} method throws {@code FailedException} with the
+ * exception from one of the subtasks as the {@linkplain Throwable#getCause() cause}.
  *
- * <p> Shutdown is used for <em>short-circuiting</em> and allow subclasses to implement
- * <em>policy</em> that does not require all subtasks to finish.
+ * <p> Whether code uses the {@code Subtask} returned from {@code fork} will depend on
+ * the {@code Joiner} and usage. Some {@code Joiner} implementations are suited to subtasks
+ * that return results of the same type and where the {@code join} method returns a result
+ * for the task to use. Code that forks subtasks that return results of different
+ * types, and uses a {@code Joiner} such as {@code Joiner.awaitAllSuccessfulOrThrow()} that
+ * does not return a result, will use {@link Subtask#get() Subtask.get()} after joining.
  *
- * <h2>Subclasses with policies for common cases</h2>
+ * <h2>Exception handling</h2>
  *
- * Two subclasses of {@code StructuredTaskScope} are defined to implement policy for
- * common cases:
- * <ol>
- *   <li> {@link ShutdownOnSuccess ShutdownOnSuccess} captures the result of the first
- *   subtask to complete successfully. Once captured, it shuts down the task scope to
- *   interrupt unfinished threads and wakeup the owner. This class is intended for cases
- *   where the result of any subtask will do ("invoke any") and where there is no need to
- *   wait for results of other unfinished subtasks. It defines methods to get the first
- *   result or throw an exception if all subtasks fail.
- *   <li> {@link ShutdownOnFailure ShutdownOnFailure} captures the exception of the first
- *   subtask to fail. Once captured, it shuts down the task scope to interrupt unfinished
- *   threads and wakeup the owner. This class is intended for cases where the results of all
- *   subtasks are required ("invoke all"); if any subtask fails then the results of other
- *   unfinished subtasks are no longer needed. If defines methods to throw an exception if
- *   any of the subtasks fail.
- * </ol>
+ * <p> A {@code StructuredTaskScope} is opened with a {@link Joiner Joiner} that
+ * handles subtask completion and produces the outcome for the {@link #join() join} method.
+ * In some cases, the outcome will be a result, in other cases it will be an exception.
+ * If the outcome is an exception then the {@code join} method throws {@link
+ * FailedException} with the exception as the {@linkplain Throwable#getCause()
+ * cause}. For many {@code Joiner} implementations, the exception will be an exception
+ * thrown by a subtask that failed. In the case of {@link Joiner#allSuccessfulOrThrow()
+ * allSuccessfulOrThrow} and {@link Joiner#awaitAllSuccessfulOrThrow() awaitAllSuccessfulOrThrow}
+ * for example, the exception is from the first subtask to fail.
  *
- * <p> The following are two examples that use the two classes. In both cases, a pair of
- * subtasks are forked to fetch resources from two URL locations "left" and "right". The
- * first example creates a ShutdownOnSuccess object to capture the result of the first
- * subtask to complete successfully, cancelling the other by way of shutting down the task
- * scope. The main task waits in {@code join} until either subtask completes with a result
- * or both subtasks fail. It invokes {@link ShutdownOnSuccess#result(Function)
- * result(Function)} method to get the captured result. If both subtasks fail then this
- * method throws a {@code WebApplicationException} with the exception from one of the
- * subtasks as the cause.
+ * <p> Many of the details for how exceptions are handled will depend on usage. In some
+ * cases it may be useful to add a {@code catch} block to the {@code try}-with-resources
+ * statement to catch {@code FailedException}. The exception handling may use {@code
+ * instanceof} with pattern matching to handle specific causes.
  * {@snippet lang=java :
- *     try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
+ *    try (var scope = StructuredTaskScope.open()) {
  *
- *         scope.fork(() -> fetch(left));
- *         scope.fork(() -> fetch(right));
+ *        ..
  *
- *         scope.join();
+ *    } catch (StructuredTaskScope.FailedException e) {
  *
- *         // @link regex="result(?=\()" target="ShutdownOnSuccess#result" :
- *         String result = scope.result(e -> new WebApplicationException(e));
+ *        Throwable cause = e.getCause();
+ *        switch (cause) {
+ *            case IOException ioe -> ..
+ *            default -> ..
+ *        }
  *
- *         ...
- *     }
+ *    }
  * }
- * The second example creates a ShutdownOnFailure object to capture the exception of the
- * first subtask to fail, cancelling the other by way of shutting down the task scope. The
- * main task waits in {@link #joinUntil(Instant)} until both subtasks complete with a
- * result, either fails, or a deadline is reached. It invokes {@link
- * ShutdownOnFailure#throwIfFailed(Function) throwIfFailed(Function)} to throw an exception
- * if either subtask fails. This method is a no-op if both subtasks complete successfully.
- * The example uses {@link Supplier#get()} to get the result of each subtask. Using
- * {@code Supplier} instead of {@code Subtask} is preferred for common cases where the
- * object returned by fork is only used to get the result of a subtask that completed
- * successfully.
- * {@snippet lang=java :
- *    Instant deadline = ...
+ * In other cases it may not be useful to catch {@code FailedException} but instead leave
+ * it to propagate to the configured {@linkplain Thread.UncaughtExceptionHandler uncaught
+ * exception handler} for logging purposes.
  *
- *    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
+ * <p> For cases where a specific exception triggers the use of a default result then it
+ * may be more appropriate to handle this in the subtask itself rather than the subtask
+ * failing and the scope owner handling the exception.
  *
- *         Supplier<String> supplier1 = scope.fork(() -> query(left));
- *         Supplier<String> supplier2 = scope.fork(() -> query(right));
+ * <h2>Configuration</h2>
  *
- *         scope.joinUntil(deadline);
+ * A {@code StructuredTaskScope} is opened with {@linkplain Config configuration} that
+ * consists of a {@link ThreadFactory} to create threads, an optional name for monitoring
+ * and management purposes, and an optional timeout.
  *
- *         // @link substring="throwIfFailed" target="ShutdownOnFailure#throwIfFailed" :
- *         scope.throwIfFailed(e -> new WebApplicationException(e));
+ * <p> The {@link #open()} and {@link #open(Joiner)} methods create a {@code StructuredTaskScope}
+ * with the <a id="DefaultConfiguration"> <em>default configuration</em></a>. The default
+ * configuration has a {@code ThreadFactory} that creates unnamed
+ * <a href="{@docRoot}/java.base/java/lang/Thread.html#virtual-threads">virtual threads</a>,
+ * is unnamed for monitoring and management purposes, and has no timeout.
  *
- *         // both subtasks completed successfully
- *         String result = Stream.of(supplier1, supplier2)
- *                 .map(Supplier::get)
- *                 .collect(Collectors.joining(", ", "{ ", " }"));
+ * <p> The 2-arg {@link #open(Joiner, Function) open} method can be used to create a
+ * {@code StructuredTaskScope} that uses a different {@code ThreadFactory}, has a name for
+ * the purposes of monitoring and management, or has a timeout that cancels the scope if
+ * the timeout expires before or while waiting for subtasks to complete. The {@code open}
+ * method is called with a {@linkplain Function function} that is applied to the default
+ * configuration and returns a {@link Config Config} for the {@code StructuredTaskScope}
+ * under construction.
  *
- *         ...
- *     }
- * }
+ * <p> The following example opens a new {@code StructuredTaskScope} with a {@code
+ * ThreadFactory} that creates virtual threads {@linkplain Thread#setName(String) named}
+ * "duke-0", "duke-1" ...
+ * {@snippet lang = java:
+ *    // @link substring="name" target="Thread.Builder#name(String, long)" :
+ *    ThreadFactory factory = Thread.ofVirtual().name("duke-", 0).factory();
+ *
+ *    // @link substring="withThreadFactory" target="Config#withThreadFactory(ThreadFactory)" :
+ *    try (var scope = StructuredTaskScope.open(joiner, cf -> cf.withThreadFactory(factory))) {
  *
- * <h2>Extending StructuredTaskScope</h2>
+ *        scope.fork( .. );   // runs in a virtual thread with name "duke-0"
+ *        scope.fork( .. );   // runs in a virtual thread with name "duke-1"
  *
- * {@code StructuredTaskScope} can be extended, and the {@link #handleComplete(Subtask)
- * handleComplete} method overridden, to implement policies other than those implemented
- * by {@code ShutdownOnSuccess} and {@code ShutdownOnFailure}. A subclass may, for example,
- * collect the results of subtasks that complete successfully and ignore subtasks that
- * fail. It may collect exceptions when subtasks fail. It may invoke the {@link #shutdown()
- * shutdown} method to shut down and cause {@link #join() join} to wakeup when some
- * condition arises.
+ *        scope.join();
  *
- * <p> A subclass will typically define methods to make available results, state, or other
- * outcome to code that executes after the {@code join} method. A subclass that collects
- * results and ignores subtasks that fail may define a method that returns the results.
- * A subclass that implements a policy to shut down when a subtask fails may define a
- * method to get the exception of the first subtask to fail.
+ *     }
+ *}
  *
- * <p> The following is an example of a simple {@code StructuredTaskScope} implementation
- * that collects homogenous subtasks that complete successfully. It defines the method
- * "{@code completedSuccessfully()}" that the main task can invoke after it joins.
+ * <p> A second example sets a timeout, represented by a {@link Duration}. The timeout
+ * starts when the new scope is opened. If the timeout expires before the {@code join}
+ * method has completed then the scope is <a href="#Cancallation">cancelled</a>. This
+ * interrupts the threads executing the two subtasks and causes the {@link #join() join}
+ * method to throw {@link TimeoutException}.
  * {@snippet lang=java :
- *     class CollectingScope<T> extends StructuredTaskScope<T> {
- *         private final Queue<Subtask<? extends T>> subtasks = new LinkedTransferQueue<>();
+ *    Duration timeout = Duration.ofSeconds(10);
  *
- *         @Override
- *         protected void handleComplete(Subtask<? extends T> subtask) {
- *             if (subtask.state() == Subtask.State.SUCCESS) {
- *                 subtasks.add(subtask);
- *             }
- *         }
+ *    // @link substring="allSuccessfulOrThrow" target="Joiner#allSuccessfulOrThrow()" :
+ *    try (var scope = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow(),
+ *    // @link substring="withTimeout" target="Config#withTimeout(Duration)" :
+ *                                              cf -> cf.withTimeout(timeout))) {
  *
- *         @Override
- *         public CollectingScope<T> join() throws InterruptedException {
- *             super.join();
- *             return this;
- *         }
+ *        scope.fork(callable1);
+ *        scope.fork(callable2);
  *
- *         public Stream<Subtask<? extends T>> completedSuccessfully() {
- *             // @link substring="ensureOwnerAndJoined" target="ensureOwnerAndJoined" :
- *             super.ensureOwnerAndJoined();
- *             return subtasks.stream();
- *         }
- *     }
+ *        List<String> result = scope.join()
+ *                                   .map(Subtask::get)
+ *                                   .toList();
+ *
+ *   }
  * }
- * <p> The implementations of the {@code completedSuccessfully()} method in the example
- * invokes {@link #ensureOwnerAndJoined()} to ensure that the method can only be invoked
- * by the owner thread and only after it has joined.
  *
- * <h2><a id="TreeStructure">Tree structure</a></h2>
+ * <h2>Inheritance of scoped value bindings</h2>
+ *
+ * {@link ScopedValue} supports the execution of a method with a {@code ScopedValue} bound
+ * to a value for the bounded period of execution of the method by the <em>current thread</em>.
+ * It allows a value to be safely and efficiently shared to methods without using method
+ * parameters.
  *
- * Task scopes form a tree where parent-child relations are established implicitly when
- * opening a new task scope:
- * <ul>
- *   <li> A parent-child relation is established when a thread started in a task scope
- *   opens its own task scope. A thread started in task scope "A" that opens task scope
- *   "B" establishes a parent-child relation where task scope "A" is the parent of task
- *   scope "B".
- *   <li> A parent-child relation is established with nesting. If a thread opens task
- *   scope "B", then opens task scope "C" (before it closes "B"), then the enclosing task
- *   scope "B" is the parent of the nested task scope "C".
- * </ul>
+ * <p> When used in conjunction with a {@code StructuredTaskScope}, a {@code ScopedValue}
+ * can also safely and efficiently share a value to methods executed by subtasks forked
+ * in the scope. When a {@code ScopedValue} object is bound to a value in the thread
+ * executing the task then that binding is inherited by the threads created to
+ * execute the subtasks. The thread executing the task does not continue beyond the
+ * {@link #close() close} method until all threads executing the subtasks have finished.
+ * This ensures that the {@code ScopedValue} is not reverted to being {@linkplain
+ * ScopedValue#isBound() unbound} (or its previous value) while subtasks are executing.
+ * In addition to providing a safe and efficient means to inherit a value into subtasks,
+ * the inheritance allows sequential code using {@code ScopedValue} be refactored to use
+ * structured concurrency.
  *
- * The <i>descendants</i> of a task scope are the child task scopes that it is a parent
- * of, plus the descendants of the child task scopes, recursively.
+ * <p> To ensure correctness, opening a new {@code StructuredTaskScope} captures the
+ * current thread's scoped value bindings. These are the scoped values bindings that are
+ * inherited by the threads created to execute subtasks in the scope. Forking a
+ * subtask checks that the bindings in effect at the time that the subtask is forked
+ * match the bindings when the {@code StructuredTaskScope} was created. This check ensures
+ * that a subtask does not inherit a binding that is reverted in the task before the
+ * subtask has completed.
  *
- * <p> The tree structure supports:
- * <ul>
- *   <li> Inheritance of {@linkplain ScopedValue scoped values} across threads.
- *   <li> Confinement checks. The phrase "threads contained in the task scope" in method
- *   descriptions means threads started in the task scope or descendant scopes.
- * </ul>
+ * <p> A {@code ScopedValue} that is shared across threads requires that the value be an
+ * immutable object or for all access to the value to be appropriately synchronized.
  *
- * <p> The following example demonstrates the inheritance of a scoped value. A scoped
- * value {@code USERNAME} is bound to the value "{@code duke}". A {@code StructuredTaskScope}
- * is created and its {@code fork} method invoked to start a thread to execute {@code
- * childTask}. The thread inherits the scoped value <em>bindings</em> captured when
- * creating the task scope. The code in {@code childTask} uses the value of the scoped
- * value and so reads the value "{@code duke}".
+ * <p> The following example demonstrates the inheritance of scoped value bindings. The
+ * scoped value USERNAME is bound to the value "duke" for the bounded period of a lambda
+ * expression by the thread executing it. The code in the block opens a {@code
+ * StructuredTaskScope} and forks two subtasks, it then waits in the {@code join} method
+ * and aggregates the results from both subtasks. If code executed by the threads
+ * running subtask1 and subtask2 uses {@link ScopedValue#get()}, to get the value of
+ * USERNAME, then value "duke" will be returned.
  * {@snippet lang=java :
+ *     // @link substring="newInstance" target="ScopedValue#newInstance()" :
  *     private static final ScopedValue<String> USERNAME = ScopedValue.newInstance();
  *
- *     // @link substring="runWhere" target="ScopedValue#runWhere(ScopedValue, Object, Runnable)" :
- *     ScopedValue.runWhere(USERNAME, "duke", () -> {
- *         try (var scope = new StructuredTaskScope<String>()) {
+ *     // @link substring="callWhere" target="ScopedValue#where" :
+ *     MyResult result = ScopedValue.where(USERNAME, "duke").call(() -> {
  *
- *             scope.fork(() -> childTask());           // @highlight substring="fork"
- *             ...
- *          }
- *     });
+ *         try (var scope = StructuredTaskScope.open()) {
  *
- *     ...
+ *             Subtask<String> subtask1 = scope.fork( .. );    // inherits binding
+ *             Subtask<Integer> subtask2 = scope.fork( .. );   // inherits binding
  *
- *     String childTask() {
- *         // @link substring="get" target="ScopedValue#get()" :
- *         String name = USERNAME.get();   // "duke"
- *         ...
- *     }
+ *             scope.join();
+ *             return new MyResult(subtask1.get(), subtask2.get());
+ *         }
+ *
+ *     });
  * }
  *
- * <p> {@code StructuredTaskScope} does not define APIs that exposes the tree structure
- * at this time.
+ * <p> A scoped value inherited into a subtask may be
+ * <a href="{@docRoot}/java.base/java/lang/ScopedValues.html#rebind">rebound</a> to a new
+ * value in the subtask for the bounded execution of some method executed in the subtask.
+ * When the method completes, the value of the {@code ScopedValue} reverts to its previous
+ * value, the value inherited from the thread executing the task.
  *
- * <p> Unless otherwise specified, passing a {@code null} argument to a constructor
- * or method in this class will cause a {@link NullPointerException} to be thrown.
+ * <p> A subtask may execute code that itself opens a new {@code StructuredTaskScope}.
+ * A task executing in thread T1 opens a {@code StructuredTaskScope} and forks a
+ * subtask that runs in thread T2. The scoped value bindings captured when T1 opens the
+ * scope are inherited into T2. The subtask (in thread T2) executes code that opens a
+ * new {@code StructuredTaskScope} and forks a subtask that runs in thread T3. The scoped
+ * value bindings captured when T2 opens the scope are inherited into T3. These
+ * include (or may be the same) as the bindings that were inherited from T1. In effect,
+ * scoped values are inherited into a tree of subtasks, not just one level of subtask.
  *
  * <h2>Memory consistency effects</h2>
  *
- * <p> Actions in the owner thread of, or a thread contained in, the task scope prior to
+ * <p> Actions in the owner thread of a {@code StructuredTaskScope} prior to
  * {@linkplain #fork forking} of a subtask
  * <a href="{@docRoot}/java.base/java/util/concurrent/package-summary.html#MemoryVisibility">
- * <i>happen-before</i></a> any actions taken by that subtask, which in turn <i>happen-before</i>
- * the subtask result is {@linkplain Subtask#get() retrieved} or <i>happen-before</i> any
- * actions taken in a thread after {@linkplain #join() joining} of the task scope.
+ * <i>happen-before</i></a> any actions taken by that subtask, which in turn
+ * <i>happen-before</i> the subtask result is {@linkplain Subtask#get() retrieved}.
  *
- * @jls 17.4.5 Happens-before Order
+ * <h2>General exceptions</h2>
+ *
+ * <p> Unless otherwise specified, passing a {@code null} argument to a method in this
+ * class will cause a {@link NullPointerException} to be thrown.
  *
- * @param <T> the result type of tasks executed in the task scope
+ * @param <T> the result type of subtasks executed in the scope
+ * @param <R> the result type of the scope
+ *
+ * @jls 17.4.5 Happens-before Order
  * @since 21
  */
 @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
-public class StructuredTaskScope<T> implements AutoCloseable {
-    private final ThreadFactory factory;
+public class StructuredTaskScope<T, R> implements AutoCloseable {
+    private static final VarHandle CANCELLED;
+    static {
+        MethodHandles.Lookup l = MethodHandles.lookup();
+        CANCELLED = MhUtil.findVarHandle(l, "cancelled", boolean.class);
+    }
+
+    private final Joiner<? super T, ? extends R> joiner;
+    private final ThreadFactory threadFactory;
     private final ThreadFlock flock;
-    private final ReentrantLock shutdownLock = new ReentrantLock();
 
-    // states: OPEN -> SHUTDOWN -> CLOSED
-    private static final int OPEN     = 0;   // initial state
-    private static final int SHUTDOWN = 1;
-    private static final int CLOSED   = 2;
+    // state, only accessed by owner thread
+    private static final int ST_NEW            = 0;
+    private static final int ST_FORKED         = 1;   // subtasks forked, need to join
+    private static final int ST_JOIN_STARTED   = 2;   // join started, can no longer fork
+    private static final int ST_JOIN_COMPLETED = 3;   // join completed
+    private static final int ST_CLOSED         = 4;   // closed
+    private int state;
+
+    // timer task, only accessed by owner thread
+    private Future<?> timerTask;
+
+    // set or read by any thread
+    private volatile boolean cancelled;
+
+    // set by the timer thread, read by the owner thread
+    private volatile boolean timeoutExpired;
+
+    /**
+     * Throws WrongThreadException if the current thread is not the owner thread.
+     */
+    private void ensureOwner() {
+        if (Thread.currentThread() != flock.owner()) {
+            throw new WrongThreadException("Current thread not owner");
+        }
+    }
+
+    /**
+     * Throws IllegalStateException if already joined or scope is closed.
+     */
+    private void ensureNotJoined() {
+        assert Thread.currentThread() == flock.owner();
+        if (state > ST_FORKED) {
+            throw new IllegalStateException("Already joined or scope is closed");
+        }
+    }
+
+    /**
+     * Throws IllegalStateException if invoked by the owner thread and the owner thread
+     * has not joined.
+     */
+    private void ensureJoinedIfOwner() {
+        if (Thread.currentThread() == flock.owner() && state <= ST_JOIN_STARTED) {
+            throw new IllegalStateException("join not called");
+        }
+    }
+
+    /**
+     * Interrupts all threads in this scope, except the current thread.
+     */
+    private void implInterruptAll() {
+        flock.threads()
+                .filter(t -> t != Thread.currentThread())
+                .forEach(t -> {
+                    try {
+                        t.interrupt();
+                    } catch (Throwable ignore) { }
+                });
+    }
+
+    @SuppressWarnings("removal")
+    private void interruptAll() {
+        if (System.getSecurityManager() == null) {
+            implInterruptAll();
+        } else {
+            PrivilegedAction<Void> pa = () -> {
+                implInterruptAll();
+                return null;
+            };
+            AccessController.doPrivileged(pa);
+        }
+    }
+
+    /**
+     * Cancel the scope if not already cancelled.
+     */
+    private void cancel() {
+        if (!cancelled && CANCELLED.compareAndSet(this, false, true)) {
+            // prevent new threads from starting
+            flock.shutdown();
+
+            // interrupt all unfinished threads
+            interruptAll();
 
-    // state: set to SHUTDOWN by any thread, set to CLOSED by owner, read by any thread
-    private volatile int state;
+            // wakeup join
+            flock.wakeup();
+        }
+    }
 
-    // Counters to support checking that the task scope owner joins before processing
-    // results and attempts join before closing the task scope. These counters are
-    // accessed only by the owner thread.
-    private int forkRound;         // incremented when the first subtask is forked after join
-    private int lastJoinAttempted; // set to the current fork round when join is attempted
-    private int lastJoinCompleted; // set to the current fork round when join completes
+    /**
+     * Schedules a task to cancel the scope on timeout.
+     */
+    private void scheduleTimeout(Duration timeout) {
+        assert Thread.currentThread() == flock.owner() && timerTask == null;
+        timerTask = TimerSupport.schedule(timeout, () -> {
+            if (!cancelled) {
+                timeoutExpired = true;
+                cancel();
+            }
+        });
+    }
 
     /**
-     * Represents a subtask forked with {@link #fork(Callable)}.
+     * Cancels the timer task if set.
+     */
+    private void cancelTimeout() {
+        assert Thread.currentThread() == flock.owner();
+        if (timerTask != null) {
+            timerTask.cancel(false);
+        }
+    }
+
+    /**
+     * Invoked by the thread for a subtask when the subtask completes before scope is cancelled.
+     */
+    private void onComplete(SubtaskImpl<? extends T> subtask) {
+        assert subtask.state() != Subtask.State.UNAVAILABLE;
+        if (joiner.onComplete(subtask)) {
+            cancel();
+        }
+    }
+
+    /**
+     * Initialize a new StructuredTaskScope.
+     */
+    @SuppressWarnings("this-escape")
+    private StructuredTaskScope(Joiner<? super T, ? extends R> joiner,
+                                ThreadFactory threadFactory,
+                                String name) {
+        this.joiner = joiner;
+        this.threadFactory = threadFactory;
+
+        if (name == null)
+            name = Objects.toIdentityString(this);
+        this.flock = ThreadFlock.open(name);
+    }
+
+    /**
+     * Represents a subtask forked with {@link #fork(Callable)} or {@link #fork(Runnable)}.
+     *
+     * <p> Code that forks subtasks can use the {@link #get() get()} method after {@linkplain
+     * #join() joining} to obtain the result of a subtask that completed successfully. It
+     * can use the {@link #exception()} method to obtain the exception thrown by a subtask
+     * that failed.
+     *
      * @param <T> the result type
      * @since 21
      */
     @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
     public sealed interface Subtask<T> extends Supplier<T> permits SubtaskImpl {
-        /**
-         * {@return the value returning task provided to the {@code fork} method}
-         *
-         * @apiNote Task objects with unique identity may be used for correlation by
-         * implementations of {@link #handleComplete(Subtask) handleComplete}.
-         */
-        Callable<? extends T> task();
-
         /**
          * Represents the state of a subtask.
          * @see Subtask#state()
          * @since 21
          */
         @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
         enum State {
             /**
              * The subtask result or exception is not available. This state indicates that
-             * the subtask was forked but has not completed, it completed after the task
-             * scope was {@linkplain #shutdown() shut down}, or it was forked after the
-             * task scope was shut down.
+             * the subtask was forked but has not completed, it completed after the scope
+             * was cancelled, or it was forked after the scoped was cancelled (in which
+             * case a thread was not created to execute the subtask).
              */
             UNAVAILABLE,
             /**
-             * The subtask completed successfully with a result. The {@link Subtask#get()
-             * Subtask.get()} method can be used to obtain the result. This is a terminal
-             * state.
+             * The subtask completed successfully. The {@link Subtask#get() Subtask.get()}
+             * method can be used to get the result. This is a terminal state.
              */
             SUCCESS,
             /**
              * The subtask failed with an exception. The {@link Subtask#exception()
-             * Subtask.exception()} method can be used to obtain the exception. This is a
+             * Subtask.exception()} method can be used to get the exception. This is a
              * terminal state.
              */
             FAILED,
         }
 
         /**
-         * {@return the state of the subtask}
+         * {@return the subtask state}
          */
         State state();
 
         /**
-         * Returns the result of the subtask.
+         * Returns the result of this subtask if it completed successfully. If
+         * {@linkplain #fork(Callable) forked} to execute a value-returning task then the
+         * result from the {@link Callable#call() call} method is returned. If
+         * {@linkplain #fork(Runnable) forked} to execute a task that does not return a
+         * result then {@code null} is returned.
          *
-         * <p> To ensure correct usage, if the scope owner {@linkplain #fork(Callable) forks}
-         * a subtask, then it must join (with {@link #join() join} or {@link #joinUntil(Instant)
-         * joinUntil}) before it can obtain the result of the subtask.
+         * <p> Code executing in the scope owner thread can use this method to get the
+         * result of a successful subtask only after it has {@linkplain #join() joined}.
+         *
+         * <p> Code executing in the {@code Joiner} {@link Joiner#onComplete(Subtask)
+         * onComplete} method should test that the {@linkplain #state() subtask state} is
+         * {@link State#SUCCESS SUCCESS} before using this method to get the result.
          *
          * @return the possibly-null result
          * @throws IllegalStateException if the subtask has not completed, did not complete
-         * successfully, or the current thread is the task scope owner and did not join
-         * after forking
+         * successfully, or the current thread is the scope owner invoking this
+         * method before {@linkplain #join() joining}
          * @see State#SUCCESS
          */
         T get();
 
         /**
-         * {@return the exception thrown by the subtask}
+         * {@return the exception thrown by this subtask if it failed} If
+         * {@linkplain #fork(Callable) forked} to execute a value-returning task then
+         * the exception thrown by the {@link Callable#call() call} method is returned.
+         * If {@linkplain #fork(Runnable) forked} to execute a task that does not return
+         * a result then the exception thrown by the {@link Runnable#run() run} method is
+         * returned.
+         *
+         * <p> Code executing in the scope owner thread can use this method to get the
+         * exception thrown by a failed subtask only after it has {@linkplain #join() joined}.
          *
-         * <p> To ensure correct usage, if the scope owner {@linkplain #fork(Callable) forks}
-         * a subtask, then it must join (with {@link #join() join} or {@link #joinUntil(Instant)
-         * joinUntil}) before it can obtain the exception thrown by the subtask.
+         * <p> Code executing in a {@code Joiner} {@link Joiner#onComplete(Subtask)
+         * onComplete} method should test that the {@linkplain #state() subtask state} is
+         * {@link State#FAILED FAILED} before using this method to get the exception.
          *
          * @throws IllegalStateException if the subtask has not completed, completed with
-         * a result, or the current thread is the task scope owner and did not join after
-         * forking
+         * a result, or the current thread is the scope owner invoking this method
+         * before {@linkplain #join() joining}
          * @see State#FAILED
          */
         Throwable exception();
     }
 
     /**
-     * Creates a structured task scope with the given name and thread factory. The task
-     * scope is optionally named for the purposes of monitoring and management. The thread
-     * factory is used to {@link ThreadFactory#newThread(Runnable) create} threads when
-     * subtasks are {@linkplain #fork(Callable) forked}. The task scope is owned by the
-     * current thread.
+     * An object used with a {@link StructuredTaskScope} to handle subtask completion and
+     * produce the result for the scope owner waiting in the {@link #join() join} method
+     * for subtasks to complete.
      *
-     * <p> Construction captures the current thread's {@linkplain ScopedValue scoped value}
-     * bindings for inheritance by threads started in the task scope. The
-     * <a href="#TreeStructure">Tree Structure</a> section in the class description details
-     * how parent-child relations are established implicitly for the purpose of inheritance
-     * of scoped value bindings.
+     * <p> Joiner defines static methods to create {@code Joiner} objects for common cases:
+     * <ul>
+     *   <li> {@link #allSuccessfulOrThrow() allSuccessfulOrThrow()} creates a {@code Joiner}
+     *   that yields a stream of the completed subtasks for {@code join} to return when
+     *   all subtasks complete successfully. It cancels the scope and causes {@code join}
+     *   to throw if any subtask fails.
+     *   <li> {@link #anySuccessfulResultOrThrow() anySuccessfulResultOrThrow()} creates a
+     *   {@code Joiner} that yields the result of the first subtask to succeed for {@code
+     *   join} to return. It causes {@code join} to throw if all subtasks fail.
+     *   <li> {@link #awaitAllSuccessfulOrThrow() awaitAllSuccessfulOrThrow()} creates a
+     *   {@code Joiner} that waits for all successful subtasks. It cancels the scope and
+     *   causes {@code join} to throw if any subtask fails.
+     *   <li> {@link #awaitAll() awaitAll()} creates a {@code Joiner} that waits for all
+     *   subtasks. It does not cancel the scope or cause {@code join} to throw.
+     * </ul>
+     *
+     * <p> In addition to the methods to create {@code Joiner} objects for common cases,
+     * the {@link #allUntil(Predicate) allUntil(Predicate)} method is defined to create a
+     * {@code Joiner} that yields a stream of all subtasks. It is created with a {@link
+     * Predicate Predicate} that determines if the scope should continue or be cancelled.
+     * This {@code Joiner} can be built upon to create custom policies that cancel the
+     * scope based on some condition.
+     *
+     * <p> More advanced policies can be developed by implementing the {@code Joiner}
+     * interface. The {@link #onFork(Subtask)} method is invoked when subtasks are forked.
+     * The {@link #onComplete(Subtask)} method is invoked when subtasks complete with a
+     * result or exception. These methods return a {@code boolean} to indicate if scope
+     * should be cancelled. These methods can be used to collect subtasks, results, or
+     * exceptions, and control when to cancel the scope. The {@link #result()} method
+     * must be implemented to produce the result (or exception) for the {@code join}
+     * method.
+     *
+     * <p> Unless otherwise specified, passing a {@code null} argument to a method
+     * in this class will cause a {@link NullPointerException} to be thrown.
+     *
+     * @implSpec Implementations of this interface must be thread safe. The {@link
+     * #onComplete(Subtask)} method defined by this interface may be invoked by several
+     * threads concurrently.
      *
-     * @param name the name of the task scope, can be null
-     * @param factory the thread factory
+     * @apiNote It is very important that a new {@code Joiner} object is created for each
+     * {@code StructuredTaskScope}. {@code Joiner} objects should never be shared with
+     * different scopes or re-used after a task is closed.
+     *
+     * <p> Designing a {@code Joiner} should take into account the code at the use-site
+     * where the results from the {@link StructuredTaskScope#join() join} method are
+     * processed. It should be clear what the {@code Joiner} does vs. the application
+     * code at the use-site. In general, the {@code Joiner} implementation is not the
+     * place to code "business logic". A {@code Joiner} should be designed to be as
+     * general purpose as possible.
+     *
+     * @param <T> the result type of subtasks executed in the scope
+     * @param <R> the result type of the scope
+     * @since 24
+     * @see #open(Joiner)
      */
-    @SuppressWarnings("this-escape")
-    public StructuredTaskScope(String name, ThreadFactory factory) {
-        this.factory = Objects.requireNonNull(factory, "'factory' is null");
-        if (name == null)
-            name = Objects.toIdentityString(this);
-        this.flock = ThreadFlock.open(name);
+    @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
+    @FunctionalInterface
+    public interface Joiner<T, R> {
+
+        /**
+         * Invoked by {@link #fork(Callable) fork(Callable)} and {@link #fork(Runnable)
+         * fork(Runnable)} when forking a subtask. The method is invoked from the task
+         * owner thread. The method is invoked before a thread is created to run the
+         * subtask.
+         *
+         * @implSpec The default implementation throws {@code NullPointerException} if the
+         * subtask is {@code null}. It throws {@code IllegalArgumentException} if the
+         * subtask is not in the {@link Subtask.State#UNAVAILABLE UNAVAILABLE} state, it
+         * otherwise returns {@code false}.
+         *
+         * @apiNote This method is invoked by the {@code fork} methods. It should not be
+         * invoked directly.
+         *
+         * @param subtask the subtask
+         * @return {@code true} to cancel the scope, otherwise {@code false}
+         */
+        default boolean onFork(Subtask<? extends T> subtask) {
+            if (subtask.state() != Subtask.State.UNAVAILABLE) {
+                throw new IllegalArgumentException();
+            }
+            return false;
+        }
+
+        /**
+         * Invoked by the thread started to execute a subtask after the subtask completes
+         * successfully or fails with an exception. This method is not invoked if a
+         * subtask completes after the scope is cancelled.
+         *
+         * @implSpec The default implementation throws {@code NullPointerException} if the
+         * subtask is {@code null}. It throws {@code IllegalArgumentException} if the
+         * subtask is not in the {@link Subtask.State#SUCCESS SUCCESS} or {@link
+         * Subtask.State#FAILED FAILED} state, it otherwise returns {@code false}.
+         *
+         * @apiNote This method is invoked by subtasks when they complete. It should not
+         * be invoked directly.
+         *
+         * @param subtask the subtask
+         * @return {@code true} to cancel the scope, otherwise {@code false}
+         */
+        default boolean onComplete(Subtask<? extends T> subtask) {
+            if (subtask.state() == Subtask.State.UNAVAILABLE) {
+                throw new IllegalArgumentException();
+            }
+            return false;
+        }
+
+        /**
+         * Invoked by the {@link #join() join()} method to produce the result (or exception)
+         * after waiting for all subtasks to complete or the scope cancelled. The result
+         * from this method is returned by the {@code join} method. If this method throws,
+         * then {@code join} throws {@link FailedException} with the exception thrown by
+         * this method as the cause.
+         *
+         * <p> In normal usage, this method will be called at most once by the {@code join}
+         * method to produce the result (or exception). The behavior of this method when
+         * invoked directly, and invoked more than once, is not specified. Where possible,
+         * an implementation should return an equal result (or throw the same exception)
+         * on second or subsequent calls to produce the outcome.
+         *
+         * @apiNote This method is invoked by the {@code join} method. It should not be
+         * invoked directly.
+         *
+         * @return the result
+         * @throws Throwable the exception
+         */
+        R result() throws Throwable;
+
+        /**
+         * {@return a new Joiner object that yields a stream of all subtasks when all
+         * subtasks complete successfully}
+         * The {@code Joiner} <a href="StructuredTaskScope.html#Cancallation">cancels</a>
+         * the scope and causes {@code join} to throw if any subtask fails.
+         *
+         * <p> If all subtasks complete successfully, the joiner's {@link Joiner#result()}
+         * method returns a stream of all subtasks in the order that they were forked.
+         * If any subtask failed then the {@code result} method throws the exception from
+         * the first subtask to fail.
+         *
+         * @apiNote Joiners returned by this method are suited to cases where all subtasks
+         * return a result of the same type. Joiners returned by {@link
+         * #awaitAllSuccessfulOrThrow()} are suited to cases where the subtasks return
+         * results of different types.
+         *
+         * @param <T> the result type of subtasks
+         */
+        static <T> Joiner<T, Stream<Subtask<T>>> allSuccessfulOrThrow() {
+            return new AllSuccessful<>();
+        }
+
+        /**
+         * {@return a new Joiner object that yields the result of any subtask that
+         * completed successfully}
+         * The {@code Joiner} causes {@code join} to throw if all subtasks fail.
+         *
+         * <p> The joiner's {@link Joiner#result()} method returns the result of a subtask
+         * that completed successfully. If all subtasks fail then the {@code result} method
+         * throws the exception from one of the failed subtasks. The {@code result} method
+         * throws {@code NoSuchElementException} if no subtasks were forked.
+         *
+         * @param <T> the result type of subtasks
+         */
+        static <T> Joiner<T, T> anySuccessfulResultOrThrow() {
+            return new AnySuccessful<>();
+        }
+
+        /**
+         * {@return a new Joiner object that waits for subtasks to complete successfully}
+         * The {@code Joiner} <a href="StructuredTaskScope.html#Cancallation">cancels</a>
+         * the scope and causes {@code join} to throw if any subtask fails.
+         *
+         * <p> The joiner's {@link Joiner#result() result} method returns {@code null}
+         * if all subtasks complete successfully, or throws the exception from the first
+         * subtask to fail.
+         *
+         * @apiNote Joiners returned by this method are suited to cases where subtasks
+         * return results of different types. Joiners returned by {@link #allSuccessfulOrThrow()}
+         * are suited to cases where the subtasks return a result of the same type.
+         *
+         * @param <T> the result type of subtasks
+         */
+        static <T> Joiner<T, Void> awaitAllSuccessfulOrThrow() {
+            return new AwaitSuccessful<>();
+        }
+
+        /**
+         * {@return a new Joiner object that waits for all subtasks to complete}
+         * The {@code Joiner} does not cancel the scope if a subtask fails.
+         *
+         * <p> The joiner's {@link Joiner#result() result} method returns {@code null}.
+         *
+         * @apiNote This Joiner can be useful for cases where subtasks make use of
+         * <em>side-effects</em> rather than return results or fail with exceptions.
+         * The {@link #fork(Runnable) fork(Runnable)} method can be used to fork subtasks
+         * that do not return a result.
+         *
+         * @param <T> the result type of subtasks
+         */
+        static <T> Joiner<T, Void> awaitAll() {
+            // ensure that new Joiner object is returned
+            return new Joiner<T, Void>() {
+                @Override
+                public Void result() {
+                    return null;
+                }
+            };
+        }
+
+        /**
+         * {@return a new Joiner object that yields a stream of all subtasks when all
+         * subtasks complete or a predicate returns {@code true} to cancel the scope}
+         *
+         * <p> The joiner's {@link Joiner#onComplete(Subtask)} method invokes the
+         * predicate's {@link Predicate#test(Object) test} method with the subtask that
+         * completed successfully or failed with an exception. If the {@code test} method
+         * returns {@code true} then <a href="StructuredTaskScope.html#Cancallation">
+         * the scope is cancelled</a>. The {@code test} method must be thread safe as it
+         * may be invoked concurrently from several threads.
+         *
+         * <p> The joiner's {@link #result()} method returns the stream of all subtasks,
+         * in fork order. The stream may contain subtasks that have completed
+         * (in {@link Subtask.State#SUCCESS SUCCESS} or {@link Subtask.State#FAILED FAILED}
+         * state) or subtasks in the {@link Subtask.State#UNAVAILABLE UNAVAILABLE} state
+         * if the scope was cancelled before all subtasks were forked or completed.
+         *
+         * <p> The following example uses this method to create a {@code Joiner} that
+         * <a href="StructuredTaskScope.html#Cancallation">cancels</a> the scope when
+         * two or more subtasks fail.
+         * {@snippet lang=java :
+         *    class CancelAfterTwoFailures<T> implements Predicate<Subtask<? extends T>> {
+         *         private final AtomicInteger failedCount = new AtomicInteger();
+         *         @Override
+         *         public boolean test(Subtask<? extends T> subtask) {
+         *             return subtask.state() == Subtask.State.FAILED
+         *                     && failedCount.incrementAndGet() >= 2;
+         *         }
+         *     }
+         *
+         *     var joiner = Joiner.all(new CancelAfterTwoFailures<String>());
+         * }
+         *
+         * @param isDone the predicate to evaluate completed subtasks
+         * @param <T> the result type of subtasks
+         */
+        static <T> Joiner<T, Stream<Subtask<T>>> allUntil(Predicate<Subtask<? extends T>> isDone) {
+            return new AllSubtasks<>(isDone);
+        }
     }
 
     /**
-     * Creates an unnamed structured task scope that creates virtual threads. The task
-     * scope is owned by the current thread.
+     * Represents the configuration for a {@code StructuredTaskScope}.
+     *
+     * <p> The configuration for a {@code StructuredTaskScope} consists of a {@link
+     * ThreadFactory} to create threads, an optional name for the purposes of monitoring
+     * and management, and an optional timeout.
+     *
+     * <p> Creating a {@code StructuredTaskScope} with {@link #open()} or {@link #open(Joiner)}
+     * uses the <a href="StructuredTaskScope.html#DefaultConfiguration">default
+     * configuration</a>. The default configuration consists of a thread factory that
+     * creates unnamed <a href="{@docRoot}/java.base/java/lang/Thread.html#virtual-threads">
+     * virtual threads</a>, no name for monitoring and management purposes, and no timeout.
      *
-     * @implSpec This constructor is equivalent to invoking the 2-arg constructor with a
-     * name of {@code null} and a thread factory that creates virtual threads.
+     * <p> Creating a {@code StructuredTaskScope} with its 2-arg {@link #open(Joiner, Function)
+     * open} method allows a different configuration to be used. The function specified
+     * to the {@code open} method is applied to the default configuration and returns the
+     * configuration for the {@code StructuredTaskScope} under construction. The function
+     * can use the {@code with-} prefixed methods defined here to specify the components
+     * of the configuration to use.
+     *
+     * <p> Unless otherwise specified, passing a {@code null} argument to a method
+     * in this class will cause a {@link NullPointerException} to be thrown.
+     *
+     * @since 24
      */
-    public StructuredTaskScope() {
-        this(null, Thread.ofVirtual().factory());
-    }
+    @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
+    public sealed interface Config permits ConfigImpl {
+        /**
+         * {@return a new {@code Config} object with the given thread factory}
+         * The other components are the same as this object. The thread factory is used by
+         * a scope to create threads when {@linkplain #fork(Callable) forking} subtasks.
+         * @param threadFactory the thread factory
+         *
+         * @apiNote The thread factory will typically create
+         * <a href="{@docRoot}/java.base/java/lang/Thread.html#virtual-threads">virtual threads</a>,
+         * maybe with names for monitoring purposes, an {@linkplain Thread.UncaughtExceptionHandler
+         * uncaught exception handler}, or other properties configured.
+         *
+         * @see #fork(Callable)
+         */
+        Config withThreadFactory(ThreadFactory threadFactory);
 
-    private IllegalStateException newIllegalStateExceptionScopeClosed() {
-        return new IllegalStateException("Task scope is closed");
-    }
+        /**
+         * {@return a new {@code Config} object with the given name}
+         * The other components are the same as this object. A scope is optionally
+         * named for the purposes of monitoring and management.
+         * @param name the name
+         * @see StructuredTaskScope#toString()
+         */
+        Config withName(String name);
 
-    private IllegalStateException newIllegalStateExceptionNoJoin() {
-        return new IllegalStateException("Owner did not join after forking subtasks");
+        /**
+         * {@return a new {@code Config} object with the given timeout}
+         * The other components are the same as this object.
+         * @param timeout the timeout
+         *
+         * @apiNote Applications using deadlines, expressed as an {@link java.time.Instant},
+         * can use {@link Duration#between Duration.between(Instant.now(), deadline)} to
+         * compute the timeout for this method.
+         *
+         * @see #join()
+         */
+        Config withTimeout(Duration timeout);
     }
 
     /**
-     * Throws IllegalStateException if the scope is closed, returning the state if not
-     * closed.
+     * Exception thrown by {@link #join()} when the outcome is an exception rather than a
+     * result.
+     *
+     * @since 24
      */
-    private int ensureOpen() {
-        int s = state;
-        if (s == CLOSED)
-            throw newIllegalStateExceptionScopeClosed();
-        return s;
-    }
+    @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
+    public static class FailedException extends RuntimeException {
+        @java.io.Serial
+        static final long serialVersionUID = -1533055100078459923L;
 
-    /**
-     * Throws WrongThreadException if the current thread is not the owner.
-     */
-    private void ensureOwner() {
-        if (Thread.currentThread() != flock.owner())
-            throw new WrongThreadException("Current thread not owner");
+        /**
+         * Constructs a {@code FailedException} with the specified cause.
+         *
+         * @param  cause the cause, can be {@code null}
+         */
+        public FailedException(Throwable cause) {
+            super(cause);
+        }
     }
 
     /**
-     * Throws WrongThreadException if the current thread is not the owner
-     * or a thread contained in the tree.
+     * Exception thrown by {@link #join()} if the scope was created with a timeout and
+     * the timeout expired before or while waiting in {@code join}.
+     *
+     * @since 24
+     * @see Config#withTimeout(Duration)
      */
-    private void ensureOwnerOrContainsThread() {
-        Thread currentThread = Thread.currentThread();
-        if (currentThread != flock.owner() && !flock.containsThread(currentThread))
-            throw new WrongThreadException("Current thread not owner or thread in the tree");
+    @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
+    public static class TimeoutException extends RuntimeException {
+        @java.io.Serial
+        static final long serialVersionUID = 705788143955048766L;
+
+        /**
+         * Constructs a {@code TimeoutException} with no detail message.
+         */
+        public TimeoutException() { }
     }
 
     /**
-     * Throws IllegalStateException if the current thread is the owner, and the owner did
-     * not join after forking a subtask in the given fork round.
+     * Opens a new {@code StructuredTaskScope} to use the given {@code Joiner} object and
+     * with configuration that is the result of applying the given function to the
+     * <a href="#DefaultConfiguration">default configuration</a>.
+     *
+     * <p> The {@code configFunction} is called with the default configuration and returns
+     * the configuration for the new scope. The function may, for example, set the
+     * {@linkplain Config#withThreadFactory(ThreadFactory) ThreadFactory} or set
+     * a {@linkplain Config#withTimeout(Duration) timeout}.
+     *
+     * <p> If a {@code ThreadFactory} is set then its {@link ThreadFactory#newThread(Runnable)
+     * newThread} method will be called to create threads when {@linkplain #fork(Callable)
+     * forking} subtasks in this scope. If a {@code ThreadFactory} is not set then
+     * forking subtasks will create an unnamed virtual thread for each subtask.
+     *
+     * <p> If a {@linkplain Config#withTimeout(Duration) timeout} is set then it starts
+     * when the scope is opened. If the timeout expires before the scope has
+     * {@linkplain #join() joined} then the scope is <a href="#Cancallation">cancelled</a>
+     * and the {@code join} method throws {@link TimeoutException}.
+     *
+     * <p> The new scope is owned by the current thread. Only code executing in this
+     * thread can {@linkplain #fork(Callable) fork}, {@linkplain #join() join}, or
+     * {@linkplain #close close} the scope.
+     *
+     * <p> Construction captures the current thread's {@linkplain ScopedValue scoped
+     * value} bindings for inheritance by threads started in the scope.
+     *
+     * @param joiner the joiner
+     * @param configFunction a function to produce the configuration
+     * @return a new scope
+     * @param <T> the result type of subtasks executed in the scope
+     * @param <R> the result type of the scope
+     * @since 24
      */
-    private void ensureJoinedIfOwner(int round) {
-        if (Thread.currentThread() == flock.owner() && (round > lastJoinCompleted)) {
-            throw newIllegalStateExceptionNoJoin();
+    public static <T, R> StructuredTaskScope<T, R> open(Joiner<? super T, ? extends R> joiner,
+                                                        Function<Config, Config> configFunction) {
+        Objects.requireNonNull(joiner);
+
+        var config = (ConfigImpl) configFunction.apply(ConfigImpl.defaultConfig());
+        var scope = new StructuredTaskScope<T, R>(joiner, config.threadFactory(), config.name());
+
+        // schedule timeout
+        Duration timeout = config.timeout();
+        if (timeout != null) {
+            boolean scheduled = false;
+            try {
+                scope.scheduleTimeout(timeout);
+                scheduled = true;
+            } finally {
+                if (!scheduled) {
+                    scope.close();  // pop if scheduling timeout failed
+                }
+            }
         }
+
+        return scope;
     }
 
     /**
-     * Ensures that the current thread is the owner of this task scope and that it joined
-     * (with {@link #join()} or {@link #joinUntil(Instant)}) after {@linkplain #fork(Callable)
-     * forking} subtasks.
+     * Opens a new {@code StructuredTaskScope}to use the given {@code Joiner} object. The
+     * scope is created with the <a href="#DefaultConfiguration">default configuration</a>.
+     * The default configuration has a {@code ThreadFactory} that creates unnamed
+     * <a href="{@docRoot}/java.base/java/lang/Thread.html#virtual-threads">virtual threads</a>,
+     * is unnamed for monitoring and management purposes, and has no timeout.
      *
-     * @apiNote This method can be used by subclasses that define methods to make available
-     * results, state, or other outcome to code intended to execute after the join method.
+     * @implSpec
+     * This factory method is equivalent to invoking the 2-arg open method with the given
+     * joiner and the {@linkplain Function#identity() identity function}.
      *
-     * @throws WrongThreadException if the current thread is not the task scope owner
-     * @throws IllegalStateException if the task scope is open and task scope owner did
-     * not join after forking
+     * @param joiner the joiner
+     * @return a new scope
+     * @param <T> the result type of subtasks executed in the scope
+     * @param <R> the result type of the scope
+     * @since 24
      */
-    protected final void ensureOwnerAndJoined() {
-        ensureOwner();
-        if (forkRound > lastJoinCompleted) {
-            throw newIllegalStateExceptionNoJoin();
-        }
+    public static <T, R> StructuredTaskScope<T, R> open(Joiner<? super T, ? extends R> joiner) {
+        return open(joiner, Function.identity());
     }
 
     /**
-     * Invoked by a subtask when it completes successfully or fails in this task scope.
-     * This method is not invoked if a subtask completes after the task scope is
-     * {@linkplain #shutdown() shut down}.
+     * Opens a new {@code StructuredTaskScope} that can be used to fork subtasks that return
+     * results of any type. The scope's {@link #join()} method waits for all subtasks to
+     * succeed or any subtask to fail.
      *
-     * @implSpec The default implementation throws {@code NullPointerException} if the
-     * subtask is {@code null}. It throws {@link IllegalArgumentException} if the subtask
-     * has not completed.
+     * <p> The {@code join} method returns {@code null} if all subtasks complete successfully.
+     * It throws {@link FailedException} if any subtask fails, with the exception from
+     * the first subtask to fail as the cause.
      *
-     * @apiNote The {@code handleComplete} method should be thread safe. It may be
-     * invoked by several threads concurrently.
+     * <p> The scope is created with the <a href="#DefaultConfiguration">default
+     * configuration</a>. The default configuration has a {@code ThreadFactory} that creates
+     * unnamed <a href="{@docRoot}/java.base/java/lang/Thread.html#virtual-threads">virtual
+     * threads</a>, is unnamed for monitoring and management purposes, and has no timeout.
      *
-     * @param subtask the subtask
+     * @implSpec
+     * This factory method is equivalent to invoking the 2-arg open method with a joiner
+     * created with {@link Joiner#awaitAllSuccessfulOrThrow() awaitAllSuccessfulOrThrow()}
+     * and the {@linkplain Function#identity() identity function}.
      *
-     * @throws IllegalArgumentException if called with a subtask that has not completed
+     * @param <T> the result type of subtasks
+     * @return a new scope
+     * @since 24
      */
-    protected void handleComplete(Subtask<? extends T> subtask) {
-        if (subtask.state() == Subtask.State.UNAVAILABLE)
-            throw new IllegalArgumentException();
+    public static <T> StructuredTaskScope<T, Void> open() {
+        return open(Joiner.awaitAllSuccessfulOrThrow(), Function.identity());
     }
 
     /**
-     * Starts a new thread in this task scope to execute a value-returning task, thus
-     * creating a <em>subtask</em> of this task scope.
+     * Starts a new thread in this scope to execute a value-returning task, thus
+     * creating a <em>subtask</em>. The value-returning task is provided to this method
+     * as a {@link Callable}, the thread executes the task's {@link Callable#call() call}
+     * method.
      *
-     * <p> The value-returning task is provided to this method as a {@link Callable}, the
-     * thread executes the task's {@link Callable#call() call} method. The thread is
-     * created with the task scope's {@link ThreadFactory}. It inherits the current thread's
+     * <p> This method first creates a {@link Subtask Subtask} to represent the <em>forked
+     * subtask</em>. It invokes the joiner's {@link Joiner#onFork(Subtask) onFork} method
+     * with the {@code Subtask} object. If the {@code onFork} completes with an exception
+     * or error then it is propagated by the {@code fork} method. If the scope is cancelled,
+     * or {@code onFork} returns {@code true} to cancel the scope, then this method returns
+     * the {@code Subtask}, in the {@link Subtask.State#UNAVAILABLE UNAVAILABLE} state,
+     * without creating a thread to execute the subtask. If the scope is not cancelled
+     * then a thread is created with the {@link ThreadFactory} configured when the scope
+     * was created, and the thread is started. Forking a subtask inherits the current thread's
      * {@linkplain ScopedValue scoped value} bindings. The bindings must match the bindings
-     * captured when the task scope was created.
-     *
-     * <p> This method returns a {@link Subtask Subtask} to represent the <em>forked
-     * subtask</em>. The {@code Subtask} object can be used to obtain the result when
-     * the subtask completes successfully, or the exception when the subtask fails. To
-     * ensure correct usage, the {@link Subtask#get() get()} and {@link Subtask#exception()
-     * exception()} methods may only be called by the task scope owner after it has waited
-     * for all threads to finish with the {@link #join() join} or {@link #joinUntil(Instant)}
-     * methods. When the subtask completes, the thread invokes the {@link
-     * #handleComplete(Subtask) handleComplete} method to consume the completed subtask.
-     * If the task scope is {@linkplain #shutdown() shut down} before the subtask completes
-     * then the {@code handleComplete} method will not be invoked.
+     * captured when the scope was opened. If the subtask completes (successfully or with
+     * an exception) before the scope is cancelled, then the thread invokes the joiner's
+     * {@link Joiner#onComplete(Subtask) onComplete} method with subtask in the
+     * {@link Subtask.State#SUCCESS SUCCESS} or {@link Subtask.State#FAILED FAILED} state.
      *
-     * <p> If this task scope is {@linkplain #shutdown() shutdown} (or in the process of
-     * shutting down) then the subtask will not run and the {@code handleComplete} method
-     * will not be invoked.
+     * <p> This method returns the {@link Subtask Subtask} object. In some usages, this
+     * object may be used to get its result. In other cases it may be used for correlation
+     * or just discarded. To ensure correct usage, the {@link Subtask#get() Subtask.get()}
+     * method may only be called by the scope owner to get the result after it has
+     * waited for subtasks to complete with the {@link #join() join} method and the subtask
+     * completed successfully. Similarly, the {@link Subtask#exception() Subtask.exception()}
+     * method may only be called by the scope owner after it has joined and the subtask
+     * failed. If the scope was cancelled before the subtask was forked, or before it
+     * completes, then neither method can be used to obtain the outcome.
      *
-     * <p> This method may only be invoked by the task scope owner or threads contained
-     * in the task scope.
-     *
-     * @implSpec This method may be overridden for customization purposes, wrapping tasks
-     * for example. If overridden, the subclass must invoke {@code super.fork} to start a
-     * new thread in this task scope.
+     * <p> This method may only be invoked by the scope owner.
      *
      * @param task the value-returning task for the thread to execute
      * @param <U> the result type
      * @return the subtask
-     * @throws IllegalStateException if this task scope is closed
-     * @throws WrongThreadException if the current thread is not the task scope owner or a
-     * thread contained in the task scope
+     * @throws WrongThreadException if the current thread is not the scope owner
+     * @throws IllegalStateException if the owner has already {@linkplain #join() joined}
+     * or the scope is closed
      * @throws StructureViolationException if the current scoped value bindings are not
-     * the same as when the task scope was created
+     * the same as when the scope was created
      * @throws RejectedExecutionException if the thread factory rejected creating a
      * thread to run the subtask
      */
     public <U extends T> Subtask<U> fork(Callable<? extends U> task) {
-        Objects.requireNonNull(task, "'task' is null");
-        int s = ensureOpen();   // throws ISE if closed
-
-        // when forked by the owner, the subtask is forked in the current or next round
-        int round = -1;
-        if (Thread.currentThread() == flock.owner()) {
-            round = forkRound;
-            if (forkRound == lastJoinCompleted) {
-                // new round if first fork after join
-                round++;
-            }
+        Objects.requireNonNull(task);
+        ensureOwner();
+        ensureNotJoined();
+
+        var subtask = new SubtaskImpl<U>(this, task);
+
+        // notify joiner, even if cancelled
+        if (joiner.onFork(subtask)) {
+            cancel();
         }
 
-        SubtaskImpl<U> subtask = new SubtaskImpl<>(this, task, round);
-        if (s < SHUTDOWN) {
+        if (!cancelled) {
             // create thread to run task
-            Thread thread = factory.newThread(subtask);
+            Thread thread = threadFactory.newThread(subtask);
             if (thread == null) {
                 throw new RejectedExecutionException("Rejected by thread factory");
             }
 
             // attempt to start the thread
@@ -592,275 +1129,180 @@
                 // shutdown by another thread, or underlying flock is shutdown due
                 // to unstructured use
             }
         }
 
-        // force owner to join if this is the first fork in the round
-        if (Thread.currentThread() == flock.owner() && round > forkRound) {
-            forkRound = round;
-        }
-
-        // return forked subtask or a subtask that did not run
+        // force owner to join
+        state = ST_FORKED;
         return subtask;
     }
 
     /**
-     * Wait for all threads to finish or the task scope to shut down.
-     */
-    private void implJoin(Duration timeout)
-        throws InterruptedException, TimeoutException
-    {
-        ensureOwner();
-        lastJoinAttempted = forkRound;
-        int s = ensureOpen();  // throws ISE if closed
-        if (s == OPEN) {
-            // wait for all threads, wakeup, interrupt, or timeout
-            if (timeout != null) {
-                flock.awaitAll(timeout);
-            } else {
-                flock.awaitAll();
-            }
-        }
-        lastJoinCompleted = forkRound;
-    }
-
-    /**
-     * Wait for all subtasks started in this task scope to finish or the task scope to
-     * shut down.
+     * Starts a new thread in this scope to execute a task that does not return a
+     * result, creating a <em>subtask</em>.
      *
-     * <p> This method waits for all subtasks by waiting for all threads {@linkplain
-     * #fork(Callable) started} in this task scope to finish execution. It stops waiting
-     * when all threads finish, the task scope is {@linkplain #shutdown() shut down}, or
-     * the current thread is {@linkplain Thread#interrupt() interrupted}.
+     * <p> This method works exactly the same as {@link #fork(Callable)} except that
+     * the task is provided to this method as a {@link Runnable}, the thread executes
+     * the task's {@link Runnable#run() run} method, and its result is {@code null}.
      *
-     * <p> This method may only be invoked by the task scope owner.
-     *
-     * @implSpec This method may be overridden for customization purposes or to return a
-     * more specific return type. If overridden, the subclass must invoke {@code
-     * super.join} to ensure that the method waits for threads in this task scope to
-     * finish.
-     *
-     * @return this task scope
-     * @throws IllegalStateException if this task scope is closed
-     * @throws WrongThreadException if the current thread is not the task scope owner
-     * @throws InterruptedException if interrupted while waiting
+     * @param task the task for the thread to execute
+     * @return the subtask
+     * @throws WrongThreadException if the current thread is not the scope owner
+     * @throws IllegalStateException if the owner has already {@linkplain #join() joined}
+     * or the scope is closed
+     * @throws StructureViolationException if the current scoped value bindings are not
+     * the same as when the scope was created
+     * @throws RejectedExecutionException if the thread factory rejected creating a
+     * thread to run the subtask
+     * @since 24
      */
-    public StructuredTaskScope<T> join() throws InterruptedException {
-        try {
-            implJoin(null);
-        } catch (TimeoutException e) {
-            throw new InternalError();
-        }
-        return this;
+    public Subtask<? extends T> fork(Runnable task) {
+        Objects.requireNonNull(task);
+        return fork(() -> { task.run(); return null; });
     }
 
     /**
-     * Wait for all subtasks started in this task scope to finish or the task scope to
-     * shut down, up to the given deadline.
+     * Waits for all subtasks started in this scope to complete or the scope is cancelled.
+     * If a {@linkplain Config#withTimeout(Duration) timeout} has been set then the scope
+     * is cancelled if the timeout expires before or while waiting.
+     * Once finished waiting, the {@code Joiner}'s {@link Joiner#result() result} method
+     * is invoked to get the result or throw an exception. If the {@code result} method
+     * throws then this method throws {@code FailedException} with the exception thrown
+     * by the {@code result()} method as the cause.
      *
      * <p> This method waits for all subtasks by waiting for all threads {@linkplain
-     * #fork(Callable) started} in this task scope to finish execution. It stops waiting
-     * when all threads finish, the task scope is {@linkplain #shutdown() shut down}, the
-     * deadline is reached, or the current thread is {@linkplain Thread#interrupt()
-     * interrupted}.
-     *
-     * <p> This method may only be invoked by the task scope owner.
+     * #fork(Callable) started} in this scope to finish execution. It stops waiting
+     * when all threads finish, the {@code Joiner}'s {@link Joiner#onFork(Subtask)
+     * onFork} or {@link Joiner#onComplete(Subtask) onComplete} returns {@code true}
+     * to cancel the scope, the timeout (if set) expires, or the current thread is
+     * {@linkplain Thread#interrupt() interrupted}.
      *
-     * @implSpec This method may be overridden for customization purposes or to return a
-     * more specific return type. If overridden, the subclass must invoke {@code
-     * super.joinUntil} to ensure that the method waits for threads in this task scope to
-     * finish.
+     * <p> This method may only be invoked by the scope owner, and only once.
      *
-     * @param deadline the deadline
-     * @return this task scope
-     * @throws IllegalStateException if this task scope is closed
-     * @throws WrongThreadException if the current thread is not the task scope owner
+     * @return the {@link Joiner#result() result}
+     * @throws WrongThreadException if the current thread is not the scope owner
+     * @throws IllegalStateException if already joined or this scope is closed
+     * @throws FailedException if the <i>outcome</i> is an exception, thrown with the
+     * exception from {@link Joiner#result() Joiner.result()} as the cause
+     * @throws TimeoutException if a timeout is set and the timeout expires before or
+     * while waiting
      * @throws InterruptedException if interrupted while waiting
-     * @throws TimeoutException if the deadline is reached while waiting
+     * @since 24
      */
-    public StructuredTaskScope<T> joinUntil(Instant deadline)
-        throws InterruptedException, TimeoutException
-    {
-        Duration timeout = Duration.between(Instant.now(), deadline);
-        implJoin(timeout);
-        return this;
-    }
-
-    /**
-     * Interrupt all unfinished threads.
-     */
-    private void implInterruptAll() {
-        flock.threads()
-            .filter(t -> t != Thread.currentThread())
-            .forEach(t -> {
-                try {
-                    t.interrupt();
-                } catch (Throwable ignore) { }
-            });
-    }
+    public R join() throws InterruptedException {
+        ensureOwner();
+        ensureNotJoined();
 
-    @SuppressWarnings("removal")
-    private void interruptAll() {
-        if (System.getSecurityManager() == null) {
-            implInterruptAll();
-        } else {
-            PrivilegedAction<Void> pa = () -> {
-                implInterruptAll();
-                return null;
-            };
-            AccessController.doPrivileged(pa);
-        }
-    }
+        // join started
+        state = ST_JOIN_STARTED;
 
-    /**
-     * Shutdown the task scope if not already shutdown. Return true if this method
-     * shutdowns the task scope, false if already shutdown.
-     */
-    private boolean implShutdown() {
-        shutdownLock.lock();
-        try {
-            if (state < SHUTDOWN) {
-                // prevent new threads from starting
-                flock.shutdown();
+        // wait for all subtasks, the scope to be cancelled, or interrupt
+        flock.awaitAll();
 
-                // set status before interrupting tasks
-                state = SHUTDOWN;
+        // throw if timeout expired
+        if (timeoutExpired) {
+            throw new TimeoutException();
+        }
+        cancelTimeout();
 
-                // interrupt all unfinished threads
-                interruptAll();
+        // all subtasks completed or cancelled
+        state = ST_JOIN_COMPLETED;
 
-                return true;
-            } else {
-                // already shutdown
-                return false;
-            }
-        } finally {
-            shutdownLock.unlock();
+        // invoke joiner to get result
+        try {
+            return joiner.result();
+        } catch (Throwable e) {
+            throw new FailedException(e);
         }
     }
 
     /**
-     * Shut down this task scope without closing it. Shutting down a task scope prevents
-     * new threads from starting, interrupts all unfinished threads, and causes the
-     * {@link #join() join} method to wakeup. Shutdown is useful for cases where the
-     * results of unfinished subtasks are no longer needed. It will typically be called
-     * by the {@link #handleComplete(Subtask)} implementation of a subclass that
-     * implements a policy to discard unfinished tasks once some outcome is reached.
-     *
-     * <p> More specifically, this method:
-     * <ul>
-     * <li> {@linkplain Thread#interrupt() Interrupts} all unfinished threads in the
-     * task scope (except the current thread).
-     * <li> Wakes up the task scope owner if it is waiting in {@link #join()} or {@link
-     * #joinUntil(Instant)}. If the task scope owner is not waiting then its next call to
-     * {@code join} or {@code joinUntil} will return immediately.
-     * </ul>
+     * {@return {@code true} if this scope is <a href="#Cancallation">cancelled</a> or in
+     * the process of being cancelled, otherwise {@code false}}
      *
-     * <p> The {@linkplain Subtask.State state} of unfinished subtasks that complete at
-     * around the time that the task scope is shutdown is not defined. A subtask that
-     * completes successfully with a result, or fails with an exception, at around
-     * the time that the task scope is shutdown may or may not <i>transition</i> to a
-     * terminal state.
+     * <p> Cancelling the scope prevents new threads from starting in the scope and
+     * {@linkplain Thread#interrupt() interrupts} threads executing unfinished subtasks.
+     * It may take some time before the interrupted threads finish execution; this
+     * method may return {@code true} before all threads have been interrupted or before
+     * all threads have finished.
      *
-     * <p> This method may only be invoked by the task scope owner or threads contained
-     * in the task scope.
+     * @apiNote A task with a lengthy "forking phase" (the code that executes before
+     * it invokes {@link #join() join}) may use this method to avoid doing work in cases
+     * where scope is cancelled by the completion of a previously forked subtask or timeout.
      *
-     * @implSpec This method may be overridden for customization purposes. If overridden,
-     * the subclass must invoke {@code super.shutdown} to ensure that the method shuts
-     * down the task scope.
-     *
-     * @apiNote
-     * There may be threads that have not finished because they are executing code that
-     * did not respond (or respond promptly) to thread interrupt. This method does not wait
-     * for these threads. When the owner invokes the {@link #close() close} method
-     * to close the task scope then it will wait for the remaining threads to finish.
-     *
-     * @throws IllegalStateException if this task scope is closed
-     * @throws WrongThreadException if the current thread is not the task scope owner or
-     * a thread contained in the task scope
-     * @see #isShutdown()
-     */
-    public void shutdown() {
-        ensureOwnerOrContainsThread();
-        int s = ensureOpen();  // throws ISE if closed
-        if (s < SHUTDOWN && implShutdown())
-            flock.wakeup();
-    }
-
-    /**
-     * {@return true if this task scope is shutdown, otherwise false}
-     * @see #shutdown()
+     * @since 24
      */
-    public final boolean isShutdown() {
-        return state >= SHUTDOWN;
+    public boolean isCancelled() {
+        return cancelled;
     }
 
     /**
-     * Closes this task scope.
+     * Closes this scope.
      *
-     * <p> This method first shuts down the task scope (as if by invoking the {@link
-     * #shutdown() shutdown} method). It then waits for the threads executing any
-     * unfinished tasks to finish. If interrupted, this method will continue to wait for
-     * the threads to finish before completing with the interrupt status set.
+     * <p> This method first <a href="#Cancallation">cancels</a> the scope, if not
+     * already cancelled. This interrupts the threads executing unfinished subtasks. This
+     * method then waits for all threads to finish. If interrupted while waiting then it
+     * will continue to wait until the threads finish, before completing with the interrupt
+     * status set.
      *
-     * <p> This method may only be invoked by the task scope owner. If the task scope
-     * is already closed then the task scope owner invoking this method has no effect.
+     * <p> This method may only be invoked by the scope owner. If the scope
+     * is already closed then the scope owner invoking this method has no effect.
      *
      * <p> A {@code StructuredTaskScope} is intended to be used in a <em>structured
-     * manner</em>. If this method is called to close a task scope before nested task
-     * scopes are closed then it closes the underlying construct of each nested task scope
-     * (in the reverse order that they were created in), closes this task scope, and then
+     * manner</em>. If this method is called to close a scope before nested task
+     * scopes are closed then it closes the underlying construct of each nested scope
+     * (in the reverse order that they were created in), closes this scope, and then
      * throws {@link StructureViolationException}.
-     * Similarly, if this method is called to close a task scope while executing with
-     * {@linkplain ScopedValue scoped value} bindings, and the task scope was created
+     * Similarly, if this method is called to close a scope while executing with
+     * {@linkplain ScopedValue scoped value} bindings, and the scope was created
      * before the scoped values were bound, then {@code StructureViolationException} is
-     * thrown after closing the task scope.
-     * If a thread terminates without first closing task scopes that it owns then
+     * thrown after closing the scope.
+     * If a thread terminates without first closing scopes that it owns then
      * termination will cause the underlying construct of each of its open tasks scopes to
-     * be closed. Closing is performed in the reverse order that the task scopes were
-     * created in. Thread termination may therefore be delayed when the task scope owner
-     * has to wait for threads forked in these task scopes to finish.
+     * be closed. Closing is performed in the reverse order that the scopes were
+     * created in. Thread termination may therefore be delayed when the scope owner
+     * has to wait for threads forked in these scopes to finish.
      *
-     * @implSpec This method may be overridden for customization purposes. If overridden,
-     * the subclass must invoke {@code super.close} to close the task scope.
-     *
-     * @throws IllegalStateException thrown after closing the task scope if the task scope
+     * @throws IllegalStateException thrown after closing the scope if the scope
      * owner did not attempt to join after forking
-     * @throws WrongThreadException if the current thread is not the task scope owner
+     * @throws WrongThreadException if the current thread is not the scope owner
      * @throws StructureViolationException if a structure violation was detected
      */
     @Override
     public void close() {
         ensureOwner();
         int s = state;
-        if (s == CLOSED)
+        if (s == ST_CLOSED) {
             return;
+        }
 
+        // cancel the scope if join did not complete
+        if (s < ST_JOIN_COMPLETED) {
+            cancel();
+            cancelTimeout();
+        }
+
+        // wait for stragglers
         try {
-            if (s < SHUTDOWN)
-                implShutdown();
             flock.close();
         } finally {
-            state = CLOSED;
+            state = ST_CLOSED;
         }
 
-        // throw ISE if the owner didn't attempt to join after forking
-        if (forkRound > lastJoinAttempted) {
-            lastJoinCompleted = forkRound;
-            throw newIllegalStateExceptionNoJoin();
+        // throw ISE if the owner didn't join after forking
+        if (s == ST_FORKED) {
+            throw new IllegalStateException("Owner did not join after forking");
         }
     }
 
+    /**
+     * {@inheritDoc}  If a {@link Config#withName(String) name} for monitoring and
+     * monitoring purposes has been set then the string representation includes the name.
+     */
     @Override
     public String toString() {
-        String name = flock.name();
-        return switch (state) {
-            case OPEN     -> name;
-            case SHUTDOWN -> name + "/shutdown";
-            case CLOSED   -> name + "/closed";
-            default -> throw new InternalError();
-        };
+        return flock.name();
     }
 
     /**
      * Subtask implementation, runs the task specified to the fork method.
      */
@@ -871,21 +1313,17 @@
             AltResult(Subtask.State state) {
                 this(state, null);
             }
         }
 
-        private final StructuredTaskScope<? super T> scope;
+        private final StructuredTaskScope<? super T, ?> scope;
         private final Callable<? extends T> task;
-        private final int round;
         private volatile Object result;
 
-        SubtaskImpl(StructuredTaskScope<? super T> scope,
-                    Callable<? extends T> task,
-                    int round) {
+        SubtaskImpl(StructuredTaskScope<? super T, ?> scope, Callable<? extends T> task) {
             this.scope = scope;
             this.task = task;
-            this.round = round;
         }
 
         @Override
         public void run() {
             T result = null;
@@ -894,26 +1332,21 @@
                 result = task.call();
             } catch (Throwable e) {
                 ex = e;
             }
 
-            // nothing to do if task scope is shutdown
-            if (scope.isShutdown())
+            // nothing to do if scope is cancelled
+            if (scope.isCancelled())
                 return;
 
-            // capture result or exception, invoke handleComplete
+            // set result/exception and invoke onComplete
             if (ex == null) {
                 this.result = (result != null) ? result : RESULT_NULL;
             } else {
                 this.result = new AltResult(State.FAILED, ex);
             }
-            scope.handleComplete(this);
-        }
-
-        @Override
-        public Callable<? extends T> task() {
-            return task;
+            scope.onComplete(this);
         }
 
         @Override
         public Subtask.State state() {
             Object result = this.result;
@@ -927,11 +1360,11 @@
             }
         }
 
         @Override
         public T get() {
-            scope.ensureJoinedIfOwner(round);
+            scope.ensureJoinedIfOwner();
             Object result = this.result;
             if (result instanceof AltResult) {
                 if (result == RESULT_NULL) return null;
             } else if (result != null) {
                 @SuppressWarnings("unchecked")
@@ -942,11 +1375,11 @@
                     "Result is unavailable or subtask did not complete successfully");
         }
 
         @Override
         public Throwable exception() {
-            scope.ensureJoinedIfOwner(round);
+            scope.ensureJoinedIfOwner();
             Object result = this.result;
             if (result instanceof AltResult alt && alt.state() == State.FAILED) {
                 return alt.exception();
             }
             throw new IllegalStateException(
@@ -966,355 +1399,199 @@
             return Objects.toIdentityString(this) + stateAsString;
         }
     }
 
     /**
-     * A {@code StructuredTaskScope} that captures the result of the first subtask to
-     * complete {@linkplain Subtask.State#SUCCESS successfully}. Once captured, it
-     * {@linkplain #shutdown() shuts down} the task scope to interrupt unfinished threads
-     * and wakeup the task scope owner. The policy implemented by this class is intended
-     * for cases where the result of any subtask will do ("invoke any") and where the
-     * results of other unfinished subtasks are no longer needed.
-     *
-     * <p> Unless otherwise specified, passing a {@code null} argument to a method
-     * in this class will cause a {@link NullPointerException} to be thrown.
-     *
-     * @apiNote This class implements a policy to shut down the task scope when a subtask
-     * completes successfully. There shouldn't be any need to directly shut down the task
-     * scope with the {@link #shutdown() shutdown} method.
-     *
-     * @param <T> the result type
-     * @since 21
+     * A joiner that returns a stream of all subtasks when all subtasks complete
+     * successfully. Cancels the scope if any subtask fails.
      */
-    @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
-    public static final class ShutdownOnSuccess<T> extends StructuredTaskScope<T> {
-        private static final Object RESULT_NULL = new Object();
-        private static final VarHandle FIRST_RESULT;
+    private static final class AllSuccessful<T> implements Joiner<T, Stream<Subtask<T>>> {
         private static final VarHandle FIRST_EXCEPTION;
         static {
             MethodHandles.Lookup l = MethodHandles.lookup();
-            FIRST_RESULT = MhUtil.findVarHandle(l, "firstResult", Object.class);
             FIRST_EXCEPTION = MhUtil.findVarHandle(l, "firstException", Throwable.class);
         }
-        private volatile Object firstResult;
         private volatile Throwable firstException;
 
-        /**
-         * Constructs a new {@code ShutdownOnSuccess} with the given name and thread factory.
-         * The task scope is optionally named for the purposes of monitoring and management.
-         * The thread factory is used to {@link ThreadFactory#newThread(Runnable) create}
-         * threads when subtasks are {@linkplain #fork(Callable) forked}. The task scope
-         * is owned by the current thread.
-         *
-         * <p> Construction captures the current thread's {@linkplain ScopedValue scoped
-         * value} bindings for inheritance by threads started in the task scope. The
-         * <a href="#TreeStructure">Tree Structure</a> section in the class description
-         * details how parent-child relations are established implicitly for the purpose
-         * of inheritance of scoped value bindings.
-         *
-         * @param name the name of the task scope, can be null
-         * @param factory the thread factory
-         */
-        public ShutdownOnSuccess(String name, ThreadFactory factory) {
-            super(name, factory);
+        // list of forked subtasks, only accessed by owner thread
+        private final List<Subtask<T>> subtasks = new ArrayList<>();
+
+        @Override
+        public boolean onFork(Subtask<? extends T> subtask) {
+            @SuppressWarnings("unchecked")
+            var tmp = (Subtask<T>) Objects.requireNonNull(subtask);
+            subtasks.add(tmp);
+            return false;
         }
 
-        /**
-         * Constructs a new unnamed {@code ShutdownOnSuccess} that creates virtual threads.
-         *
-         * @implSpec This constructor is equivalent to invoking the 2-arg constructor with
-         * a name of {@code null} and a thread factory that creates virtual threads.
-         */
-        public ShutdownOnSuccess() {
-            this(null, Thread.ofVirtual().factory());
+        @Override
+        public boolean onComplete(Subtask<? extends T> subtask) {
+            return (subtask.state() == Subtask.State.FAILED)
+                    && (firstException == null)
+                    && FIRST_EXCEPTION.compareAndSet(this, null, subtask.exception());
         }
 
         @Override
-        protected void handleComplete(Subtask<? extends T> subtask) {
-            if (firstResult != null) {
-                // already captured a result
-                return;
+        public Stream<Subtask<T>> result() throws Throwable {
+            Throwable ex = firstException;
+            if (ex != null) {
+                throw ex;
+            } else {
+                return subtasks.stream();
             }
+        }
+    }
 
-            if (subtask.state() == Subtask.State.SUCCESS) {
-                // task succeeded
-                T result = subtask.get();
-                Object r = (result != null) ? result : RESULT_NULL;
-                if (FIRST_RESULT.compareAndSet(this, null, r)) {
-                    super.shutdown();
-                }
-            } else if (firstException == null) {
-                // capture the exception thrown by the first subtask that failed
-                FIRST_EXCEPTION.compareAndSet(this, null, subtask.exception());
-            }
+    /**
+     * A joiner that returns the result of the first subtask to complete successfully.
+     * Cancels the scope if any subtasks succeeds.
+     */
+    private static final class AnySuccessful<T> implements Joiner<T, T> {
+        private static final VarHandle FIRST_SUCCESS;
+        private static final VarHandle FIRST_EXCEPTION;
+        static {
+            MethodHandles.Lookup l = MethodHandles.lookup();
+            FIRST_SUCCESS = MhUtil.findVarHandle(l, "firstSuccess", Subtask.class);
+            FIRST_EXCEPTION = MhUtil.findVarHandle(l, "firstException", Throwable.class);
         }
+        private volatile Subtask<T> firstSuccess;
+        private volatile Throwable firstException;
 
-        /**
-         * Wait for a subtask started in this task scope to complete {@linkplain
-         * Subtask.State#SUCCESS successfully} or all subtasks to complete.
-         *
-         * <p> This method waits for all subtasks by waiting for all threads {@linkplain
-         * #fork(Callable) started} in this task scope to finish execution. It stops waiting
-         * when all threads finish, a subtask completes successfully, or the current
-         * thread is {@linkplain Thread#interrupt() interrupted}. It also stops waiting
-         * if the {@link #shutdown() shutdown} method is invoked directly to shut down
-         * this task scope.
-         *
-         * <p> This method may only be invoked by the task scope owner.
-         *
-         * @throws IllegalStateException {@inheritDoc}
-         * @throws WrongThreadException {@inheritDoc}
-         */
         @Override
-        public ShutdownOnSuccess<T> join() throws InterruptedException {
-            super.join();
-            return this;
+        public boolean onComplete(Subtask<? extends T> subtask) {
+            Objects.requireNonNull(subtask);
+            if (firstSuccess == null) {
+                if (subtask.state() == Subtask.State.SUCCESS) {
+                    // capture the first subtask that completes successfully
+                    return FIRST_SUCCESS.compareAndSet(this, null, subtask);
+                } else if (firstException == null) {
+                    // capture the exception thrown by the first task to fail
+                    FIRST_EXCEPTION.compareAndSet(this, null, subtask.exception());
+                }
+            }
+            return false;
         }
 
-        /**
-         * Wait for a subtask started in this task scope to complete {@linkplain
-         * Subtask.State#SUCCESS successfully} or all subtasks to complete, up to the
-         * given deadline.
-         *
-         * <p> This method waits for all subtasks by waiting for all threads {@linkplain
-         * #fork(Callable) started} in this task scope to finish execution. It stops waiting
-         * when all threads finish, a subtask completes successfully, the deadline is
-         * reached, or the current thread is {@linkplain Thread#interrupt() interrupted}.
-         * It also stops waiting if the {@link #shutdown() shutdown} method is invoked
-         * directly to shut down this task scope.
-         *
-         * <p> This method may only be invoked by the task scope owner.
-         *
-         * @throws IllegalStateException {@inheritDoc}
-         * @throws WrongThreadException {@inheritDoc}
-         */
         @Override
-        public ShutdownOnSuccess<T> joinUntil(Instant deadline)
-            throws InterruptedException, TimeoutException
-        {
-            super.joinUntil(deadline);
-            return this;
+        public T result() throws Throwable {
+            Subtask<T> firstSuccess = this.firstSuccess;
+            if (firstSuccess != null) {
+                return firstSuccess.get();
+            }
+            Throwable firstException = this.firstException;
+            if (firstException != null) {
+                throw firstException;
+            } else {
+                throw new NoSuchElementException("No subtasks completed");
+            }
         }
+    }
 
-        /**
-         * {@return the result of the first subtask that completed {@linkplain
-         * Subtask.State#SUCCESS successfully}}
-         *
-         * <p> When no subtask completed successfully, but a subtask {@linkplain
-         * Subtask.State#FAILED failed} then {@code ExecutionException} is thrown with
-         * the subtask's exception as the {@linkplain Throwable#getCause() cause}.
-         *
-         * @throws ExecutionException if no subtasks completed successfully but at least
-         * one subtask failed
-         * @throws IllegalStateException if no subtasks completed or the task scope owner
-         * did not join after forking
-         * @throws WrongThreadException if the current thread is not the task scope owner
-         */
-        public T result() throws ExecutionException {
-            return result(ExecutionException::new);
+    /**
+     * A joiner that that waits for all successful subtasks. Cancels the scope if any
+     * subtask fails.
+     */
+    private static final class AwaitSuccessful<T> implements Joiner<T, Void> {
+        private static final VarHandle FIRST_EXCEPTION;
+        static {
+            MethodHandles.Lookup l = MethodHandles.lookup();
+            FIRST_EXCEPTION = MhUtil.findVarHandle(l, "firstException", Throwable.class);
         }
+        private volatile Throwable firstException;
 
-        /**
-         * Returns the result of the first subtask that completed {@linkplain
-         * Subtask.State#SUCCESS successfully}, otherwise throws an exception produced
-         * by the given exception supplying function.
-         *
-         * <p> When no subtask completed successfully, but a subtask {@linkplain
-         * Subtask.State#FAILED failed}, then the exception supplying function is invoked
-         * with subtask's exception.
-         *
-         * @param esf the exception supplying function
-         * @param <X> type of the exception to be thrown
-         * @return the result of the first subtask that completed with a result
-         *
-         * @throws X if no subtasks completed successfully but at least one subtask failed
-         * @throws IllegalStateException if no subtasks completed or the task scope owner
-         * did not join after forking
-         * @throws WrongThreadException if the current thread is not the task scope owner
-         */
-        public <X extends Throwable> T result(Function<Throwable, ? extends X> esf) throws X {
-            Objects.requireNonNull(esf);
-            ensureOwnerAndJoined();
-
-            Object result = firstResult;
-            if (result == RESULT_NULL) {
-                return null;
-            } else if (result != null) {
-                @SuppressWarnings("unchecked")
-                T r = (T) result;
-                return r;
-            }
+        @Override
+        public boolean onComplete(Subtask<? extends T> subtask) {
+            return (subtask.state() == Subtask.State.FAILED)
+                    && (firstException == null)
+                    && FIRST_EXCEPTION.compareAndSet(this, null, subtask.exception());
+        }
 
-            Throwable exception = firstException;
-            if (exception != null) {
-                X ex = esf.apply(exception);
-                Objects.requireNonNull(ex, "esf returned null");
+        @Override
+        public Void result() throws Throwable {
+            Throwable ex = firstException;
+            if (ex != null) {
                 throw ex;
+            } else {
+                return null;
             }
-
-            throw new IllegalStateException("No completed subtasks");
         }
     }
 
     /**
-     * A {@code StructuredTaskScope} that captures the exception of the first subtask to
-     * {@linkplain Subtask.State#FAILED fail}. Once captured, it {@linkplain #shutdown()
-     * shuts down} the task scope to interrupt unfinished threads and wakeup the task
-     * scope owner. The policy implemented by this class is intended for cases where the
-     * results for all subtasks are required ("invoke all"); if any subtask fails then the
-     * results of other unfinished subtasks are no longer needed.
-     *
-     * <p> Unless otherwise specified, passing a {@code null} argument to a method
-     * in this class will cause a {@link NullPointerException} to be thrown.
-     *
-     * @apiNote This class implements a policy to shut down the task scope when a subtask
-     * fails. There shouldn't be any need to directly shut down the task scope with the
-     * {@link #shutdown() shutdown} method.
-     *
-     * @since 21
+     * A joiner that returns a stream of all subtasks.
      */
-    @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
-    public static final class ShutdownOnFailure extends StructuredTaskScope<Object> {
-        private static final VarHandle FIRST_EXCEPTION =
-                MhUtil.findVarHandle(MethodHandles.lookup(), "firstException", Throwable.class);
-        private volatile Throwable firstException;
+    private static class AllSubtasks<T> implements Joiner<T, Stream<Subtask<T>>> {
+        private final Predicate<Subtask<? extends T>> isDone;
+        // list of forked subtasks, only accessed by owner thread
+        private final List<Subtask<T>> subtasks = new ArrayList<>();
 
-        /**
-         * Constructs a new {@code ShutdownOnFailure} with the given name and thread factory.
-         * The task scope is optionally named for the purposes of monitoring and management.
-         * The thread factory is used to {@link ThreadFactory#newThread(Runnable) create}
-         * threads when subtasks are {@linkplain #fork(Callable) forked}. The task scope
-         * is owned by the current thread.
-         *
-         * <p> Construction captures the current thread's {@linkplain ScopedValue scoped
-         * value} bindings for inheritance by threads started in the task scope. The
-         * <a href="#TreeStructure">Tree Structure</a> section in the class description
-         * details how parent-child relations are established implicitly for the purpose
-         * of inheritance of scoped value bindings.
-         *
-         * @param name the name of the task scope, can be null
-         * @param factory the thread factory
-         */
-        public ShutdownOnFailure(String name, ThreadFactory factory) {
-            super(name, factory);
+        AllSubtasks(Predicate<Subtask<? extends T>> isDone) {
+            this.isDone = Objects.requireNonNull(isDone);
         }
 
-        /**
-         * Constructs a new unnamed {@code ShutdownOnFailure} that creates virtual threads.
-         *
-         * @implSpec This constructor is equivalent to invoking the 2-arg constructor with
-         * a name of {@code null} and a thread factory that creates virtual threads.
-         */
-        public ShutdownOnFailure() {
-            this(null, Thread.ofVirtual().factory());
+        @Override
+        public boolean onFork(Subtask<? extends T> subtask) {
+            @SuppressWarnings("unchecked")
+            var tmp = (Subtask<T>) Objects.requireNonNull(subtask);
+            subtasks.add(tmp);
+            return false;
         }
 
         @Override
-        protected void handleComplete(Subtask<?> subtask) {
-            if (subtask.state() == Subtask.State.FAILED
-                    && firstException == null
-                    && FIRST_EXCEPTION.compareAndSet(this, null, subtask.exception())) {
-                super.shutdown();
-            }
+        public boolean onComplete(Subtask<? extends T> subtask) {
+            return isDone.test(Objects.requireNonNull(subtask));
         }
 
-        /**
-         * Wait for all subtasks started in this task scope to complete or for a subtask
-         * to {@linkplain Subtask.State#FAILED fail}.
-         *
-         * <p> This method waits for all subtasks by waiting for all threads {@linkplain
-         * #fork(Callable) started} in this task scope to finish execution. It stops waiting
-         * when all threads finish, a subtask fails, or the current thread is {@linkplain
-         * Thread#interrupt() interrupted}. It also stops waiting if the {@link #shutdown()
-         * shutdown} method is invoked directly to shut down this task scope.
-         *
-         * <p> This method may only be invoked by the task scope owner.
-         *
-         * @throws IllegalStateException {@inheritDoc}
-         * @throws WrongThreadException {@inheritDoc}
-         */
         @Override
-        public ShutdownOnFailure join() throws InterruptedException {
-            super.join();
-            return this;
+        public Stream<Subtask<T>> result() {
+            return subtasks.stream();
+        }
+    }
+
+    /**
+     * Implementation of Config.
+     */
+    private record ConfigImpl(ThreadFactory threadFactory,
+                              String name,
+                              Duration timeout) implements Config {
+        static Config defaultConfig() {
+            return new ConfigImpl(Thread.ofVirtual().factory(), null, null);
         }
 
-        /**
-         * Wait for all subtasks started in this task scope to complete or for a subtask
-         * to {@linkplain Subtask.State#FAILED fail}, up to the given deadline.
-         *
-         * <p> This method waits for all subtasks by waiting for all threads {@linkplain
-         * #fork(Callable) started} in this task scope to finish execution. It stops waiting
-         * when all threads finish, a subtask fails, the deadline is reached, or the current
-         * thread is {@linkplain Thread#interrupt() interrupted}. It also stops waiting
-         * if the {@link #shutdown() shutdown} method is invoked directly to shut down
-         * this task scope.
-         *
-         * <p> This method may only be invoked by the task scope owner.
-         *
-         * @throws IllegalStateException {@inheritDoc}
-         * @throws WrongThreadException {@inheritDoc}
-         */
         @Override
-        public ShutdownOnFailure joinUntil(Instant deadline)
-            throws InterruptedException, TimeoutException
-        {
-            super.joinUntil(deadline);
-            return this;
+        public Config withThreadFactory(ThreadFactory threadFactory) {
+            return new ConfigImpl(Objects.requireNonNull(threadFactory), name, timeout);
         }
 
-        /**
-         * Returns the exception of the first subtask that {@linkplain Subtask.State#FAILED
-         * failed}. If no subtasks failed then an empty {@code Optional} is returned.
-         *
-         * @return the exception for the first subtask to fail or an empty optional if no
-         * subtasks failed
-         *
-         * @throws WrongThreadException if the current thread is not the task scope owner
-         * @throws IllegalStateException if the task scope owner did not join after forking
-         */
-        public Optional<Throwable> exception() {
-            ensureOwnerAndJoined();
-            return Optional.ofNullable(firstException);
+        @Override
+        public Config withName(String name) {
+            return new ConfigImpl(threadFactory, Objects.requireNonNull(name), timeout);
         }
 
-        /**
-         * Throws if a subtask {@linkplain Subtask.State#FAILED failed}.
-         * If any subtask failed with an exception then {@code ExecutionException} is
-         * thrown with the exception of the first subtask to fail as the {@linkplain
-         * Throwable#getCause() cause}. This method does nothing if no subtasks failed.
-         *
-         * @throws ExecutionException if a subtask failed
-         * @throws WrongThreadException if the current thread is not the task scope owner
-         * @throws IllegalStateException if the task scope owner did not join after forking
-         */
-        public void throwIfFailed() throws ExecutionException {
-            throwIfFailed(ExecutionException::new);
+        @Override
+        public Config withTimeout(Duration timeout) {
+            return new ConfigImpl(threadFactory, name, Objects.requireNonNull(timeout));
         }
+    }
 
-        /**
-         * Throws the exception produced by the given exception supplying function if a
-         * subtask {@linkplain Subtask.State#FAILED failed}. If any subtask failed with
-         * an exception then the function is invoked with the exception of the first
-         * subtask to fail. The exception returned by the function is thrown. This method
-         * does nothing if no subtasks failed.
-         *
-         * @param esf the exception supplying function
-         * @param <X> type of the exception to be thrown
-         *
-         * @throws X produced by the exception supplying function
-         * @throws WrongThreadException if the current thread is not the task scope owner
-         * @throws IllegalStateException if the task scope owner did not join after forking
-         */
-        public <X extends Throwable>
-        void throwIfFailed(Function<Throwable, ? extends X> esf) throws X {
-            ensureOwnerAndJoined();
-            Objects.requireNonNull(esf);
-            Throwable exception = firstException;
-            if (exception != null) {
-                X ex = esf.apply(exception);
-                Objects.requireNonNull(ex, "esf returned null");
-                throw ex;
-            }
+    /**
+     * Used to schedule a task to cancel the scope when a timeout expires.
+     */
+    private static class TimerSupport {
+        private static final ScheduledExecutorService DELAYED_TASK_SCHEDULER;
+        static {
+            ScheduledThreadPoolExecutor stpe = (ScheduledThreadPoolExecutor)
+                Executors.newScheduledThreadPool(1, task -> {
+                    Thread t = InnocuousThread.newThread("StructuredTaskScope-Timer", task);
+                    t.setDaemon(true);
+                    return t;
+                });
+            stpe.setRemoveOnCancelPolicy(true);
+            DELAYED_TASK_SCHEDULER = stpe;
+        }
+
+        static Future<?> schedule(Duration timeout, Runnable task) {
+            long nanos = TimeUnit.NANOSECONDS.convert(timeout);
+            return DELAYED_TASK_SCHEDULER.schedule(task, nanos, TimeUnit.NANOSECONDS);
         }
     }
 }
diff a/src/java.base/share/classes/javax/security/auth/Subject.java b/src/java.base/share/classes/javax/security/auth/Subject.java
--- a/src/java.base/share/classes/javax/security/auth/Subject.java
+++ b/src/java.base/share/classes/javax/security/auth/Subject.java
@@ -434,11 +434,11 @@
     public static <T> T callAs(final Subject subject,
             final Callable<T> action) throws CompletionException {
         Objects.requireNonNull(action);
         if (!SharedSecrets.getJavaLangAccess().allowSecurityManager()) {
             try {
-                return ScopedValue.callWhere(SCOPED_SUBJECT, subject, action::call);
+                return ScopedValue.where(SCOPED_SUBJECT, subject).call(action::call);
             } catch (Exception e) {
                 throw new CompletionException(e);
             }
         } else {
             try {
diff a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java
--- a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java
+++ b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java
@@ -44,10 +44,11 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
 import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.stream.Stream;
 
 import jdk.internal.loader.NativeLibraries;
 import jdk.internal.misc.CarrierThreadLocal;
 import jdk.internal.module.ServicesCatalog;
@@ -610,10 +611,15 @@
     /**
      * Returns the virtual thread default scheduler.
      */
     Executor virtualThreadDefaultScheduler();
 
+    /**
+     * Returns a stream of the delayed task schedulers used for virtual threads.
+     */
+    Stream<ScheduledExecutorService> virtualThreadDelayedTaskSchedulers();
+
     /**
      * Creates a new StackWalker
      */
     StackWalker newStackWalkerInstance(Set<StackWalker.Option> options,
                                        ContinuationScope contScope,
diff a/src/java.base/share/classes/jdk/internal/access/JavaLangRefAccess.java b/src/java.base/share/classes/jdk/internal/access/JavaLangRefAccess.java
--- a/src/java.base/share/classes/jdk/internal/access/JavaLangRefAccess.java
+++ b/src/java.base/share/classes/jdk/internal/access/JavaLangRefAccess.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2014, 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.  Oracle designates this
@@ -48,13 +48,6 @@
      * Runs the finalization methods of any objects pending finalization.
      *
      * Invoked by Runtime.runFinalization()
      */
     void runFinalization();
-
-    /**
-     * Constructs a new NativeReferenceQueue.
-     *
-     * Invoked by jdk.internal.util.ReferencedKeyMap
-     */
-    <T> ReferenceQueue<T> newNativeReferenceQueue();
 }
diff a/src/java.base/share/classes/jdk/internal/event/VirtualThreadPinnedEvent.java b/src/java.base/share/classes/jdk/internal/event/VirtualThreadPinnedEvent.java
--- a/src/java.base/share/classes/jdk/internal/event/VirtualThreadPinnedEvent.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.  Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-package jdk.internal.event;
-
-/**
- * Event recording that a virtual thread has parked on its carrier thread.
- */
-public class VirtualThreadPinnedEvent extends Event {
-}
diff a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java
--- a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java
+++ b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java
@@ -69,11 +69,11 @@
         FOREIGN,
         @JEP(number=459, title="String Templates", status="Second Preview")
         STRING_TEMPLATES,
         @JEP(number=477, title="Implicitly Declared Classes and Instance Main Methods", status="Third Preview")
         IMPLICIT_CLASSES,
-        @JEP(number=481, title="Scoped Values", status="Third Preview")
+        @JEP(number=487, title="Scoped Values", status="Fourth Preview")
         SCOPED_VALUES,
         @JEP(number=480, title="Structured Concurrency", status="Third Preview")
         STRUCTURED_CONCURRENCY,
         @JEP(number=466, title="ClassFile API", status="Second Preview")
         CLASSFILE_API,
diff a/src/java.base/share/classes/jdk/internal/misc/InternalLock.java b/src/java.base/share/classes/jdk/internal/misc/InternalLock.java
--- a/src/java.base/share/classes/jdk/internal/misc/InternalLock.java
+++ b/src/java.base/share/classes/jdk/internal/misc/InternalLock.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2021, 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.  Oracle designates this
@@ -37,14 +37,14 @@
  */
 public class InternalLock {
     private static final boolean CAN_USE_INTERNAL_LOCK;
     static {
         String s = System.getProperty("jdk.io.useMonitors");
-        if (s != null && (s.isEmpty() || s.equals("true"))) {
-            CAN_USE_INTERNAL_LOCK = false;
-        } else {
+        if (s != null && s.equals("false")) {
             CAN_USE_INTERNAL_LOCK = true;
+        } else {
+            CAN_USE_INTERNAL_LOCK = false;
         }
     }
 
     private final ReentrantLock lock;
 
diff a/src/java.base/share/classes/jdk/internal/misc/ThreadFlock.java b/src/java.base/share/classes/jdk/internal/misc/ThreadFlock.java
--- a/src/java.base/share/classes/jdk/internal/misc/ThreadFlock.java
+++ b/src/java.base/share/classes/jdk/internal/misc/ThreadFlock.java
@@ -270,19 +270,12 @@
 
     /**
      * Shutdown this flock so that no new threads can be started, existing threads
      * in the flock will continue to run. This method is a no-op if the flock is
      * already shutdown or closed.
-     *
-     * <p> This method may only be invoked by the flock owner or threads {@linkplain
-     * #containsThread(Thread) contained} in the flock.
-     *
-     * @throws WrongThreadException if the current thread is not the owner or a thread
-     * contained in the flock
      */
     public void shutdown() {
-        ensureOwnerOrContainsThread();
         if (!shutdown) {
             shutdown = true;
         }
     }
 
@@ -368,16 +361,12 @@
      * {@linkplain #owner() owner} to return immediately.
      *
      * <p> If the owner is blocked in {@code awaitAll} then it will return immediately.
      * If the owner is not blocked in {@code awaitAll} then its next call to wait
      * will return immediately. The method does nothing when the flock is closed.
-     *
-     * @throws WrongThreadException if the current thread is not the owner or a thread
-     * contained in the flock
      */
     public void wakeup() {
-        ensureOwnerOrContainsThread();
         if (!getAndSetPermit(true) && Thread.currentThread() != owner()) {
             LockSupport.unpark(owner());
         }
     }
 
diff a/src/java.base/share/classes/jdk/internal/util/ReferencedKeyMap.java b/src/java.base/share/classes/jdk/internal/util/ReferencedKeyMap.java
--- a/src/java.base/share/classes/jdk/internal/util/ReferencedKeyMap.java
+++ b/src/java.base/share/classes/jdk/internal/util/ReferencedKeyMap.java
@@ -140,33 +140,11 @@
      * @param <K> the type of keys maintained by the new map
      * @param <V> the type of mapped values
      */
     public static <K, V> ReferencedKeyMap<K, V>
     create(boolean isSoft, Supplier<Map<ReferenceKey<K>, V>> supplier) {
-        return create(isSoft, false, supplier);
-    }
-
-    /**
-     * Create a new {@link ReferencedKeyMap} map.
-     *
-     * @param isSoft          true if {@link SoftReference} keys are to
-     *                        be used, {@link WeakReference} otherwise.
-     * @param useNativeQueue  true if uses NativeReferenceQueue
-     *                        otherwise use {@link ReferenceQueue}.
-     * @param supplier        {@link Supplier} of the backing map
-     *
-     * @return a new map with {@link Reference} keys
-     *
-     * @param <K> the type of keys maintained by the new map
-     * @param <V> the type of mapped values
-     */
-    public static <K, V> ReferencedKeyMap<K, V>
-    create(boolean isSoft, boolean useNativeQueue, Supplier<Map<ReferenceKey<K>, V>> supplier) {
-        return new ReferencedKeyMap<K, V>(isSoft, supplier.get(),
-                useNativeQueue ? SharedSecrets.getJavaLangRefAccess().newNativeReferenceQueue()
-                               : new ReferenceQueue<>()
-                );
+        return new ReferencedKeyMap<K, V>(isSoft, supplier.get(), new ReferenceQueue<>());
     }
 
     /**
      * {@return a key suitable for a map entry}
      *
diff a/src/java.base/share/classes/jdk/internal/util/ReferencedKeySet.java b/src/java.base/share/classes/jdk/internal/util/ReferencedKeySet.java
--- a/src/java.base/share/classes/jdk/internal/util/ReferencedKeySet.java
+++ b/src/java.base/share/classes/jdk/internal/util/ReferencedKeySet.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.  Oracle designates this
@@ -104,29 +104,11 @@
      *
      * @param <E> the type of elements maintained by this set
      */
     public static <E> ReferencedKeySet<E>
     create(boolean isSoft, Supplier<Map<ReferenceKey<E>, ReferenceKey<E>>> supplier) {
-        return create(isSoft, false, supplier);
-    }
-
-    /**
-     * Create a new {@link ReferencedKeySet} elements.
-     *
-     * @param isSoft          true if {@link SoftReference} elements are to
-     *                        be used, {@link WeakReference} otherwise.
-     * @param useNativeQueue  true if uses NativeReferenceQueue
-     *                        otherwise use {@link ReferenceQueue}.
-     * @param supplier        {@link Supplier} of the backing map
-     *
-     * @return a new set with {@link Reference} elements
-     *
-     * @param <E> the type of elements maintained by this set
-     */
-    public static <E> ReferencedKeySet<E>
-    create(boolean isSoft, boolean useNativeQueue, Supplier<Map<ReferenceKey<E>, ReferenceKey<E>>> supplier) {
-         return new ReferencedKeySet<>(ReferencedKeyMap.create(isSoft, useNativeQueue, supplier));
+        return new ReferencedKeySet<>(ReferencedKeyMap.create(isSoft, supplier));
     }
 
     /**
      * Removes enqueued weak references from set.
      */
diff a/src/java.base/share/classes/jdk/internal/vm/Continuation.java b/src/java.base/share/classes/jdk/internal/vm/Continuation.java
--- a/src/java.base/share/classes/jdk/internal/vm/Continuation.java
+++ b/src/java.base/share/classes/jdk/internal/vm/Continuation.java
@@ -54,13 +54,27 @@
         PRESERVE_SCOPED_VALUE_CACHE = (value == null) || Boolean.parseBoolean(value);
     }
 
     /** Reason for pinning */
     public enum Pinned {
-        /** Native frame on stack */ NATIVE,
-        /** Monitor held */          MONITOR,
-        /** In critical section */   CRITICAL_SECTION }
+        NATIVE(2, "Native frame or <clinit> on stack"),
+        MONITOR(3, "Monitor held"),
+        CRITICAL_SECTION(4, "In critical section");
+
+        private final int reasonCode;
+        private final String reasonString;
+        Pinned(int reasonCode, String reasonString) {
+            this.reasonCode = reasonCode;
+            this.reasonString = reasonString;
+        }
+        public int reasonCode() {
+            return reasonCode;
+        }
+        public String reasonString() {
+            return reasonString;
+        }
+    }
 
     /** Preemption attempt result */
     public enum PreemptStatus {
         /** Success */                                                      SUCCESS(null),
         /** Permanent failure */                                            PERM_FAIL_UNSUPPORTED(null),
@@ -351,12 +365,10 @@
         return cont.yield0(scope, null);
     }
 
     @Hidden
     private boolean yield0(ContinuationScope scope, Continuation child) {
-        preempted = false;
-
         if (scope != this.scope)
             this.yieldInfo = scope;
         int res = doYield();
         U.storeFence(); // needed to prevent certain transformations by the compiler
 
diff a/src/java.base/share/classes/jdk/internal/vm/VThreadSummary.java b/src/java.base/share/classes/jdk/internal/vm/VThreadSummary.java
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/vm/VThreadSummary.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package jdk.internal.vm;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import jdk.internal.access.JavaLangAccess;
+import jdk.internal.access.SharedSecrets;
+import jdk.internal.misc.Unsafe;
+import sun.nio.ch.Poller;
+
+/**
+ * The implementation for the jcmd Thread.vthread_summary diagnostic command.
+ */
+public class VThreadSummary {
+    private static final Unsafe U = Unsafe.getUnsafe();
+    private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess();
+
+    // maximum number of thread containers to print
+    private static final int MAX_THREAD_CONTAINERS = 256;
+
+    private VThreadSummary() { }
+
+    /**
+     * Invoked by the VM to print virtual thread summary information.
+     * @return the UTF-8 encoded information to print
+     */
+    private static byte[] print() {
+        StringBuilder sb = new StringBuilder();
+
+        // print virtual thread scheduler
+        printSchedulers(sb);
+        sb.append(System.lineSeparator());
+
+        // print I/O pollers if initialized
+        if (!U.shouldBeInitialized(Poller.class)) {
+            printPollers(sb);
+            sb.append(System.lineSeparator());
+        }
+
+        // print thread groupings/containers
+        printThreadContainers(sb);
+        sb.append(System.lineSeparator());
+
+        return sb.toString().getBytes(StandardCharsets.UTF_8);
+    }
+
+    /**
+     * Print information on the virtual thread schedulers to given string builder.
+     */
+    private static void printSchedulers(StringBuilder sb) {
+        sb.append("Virtual thread scheduler:")
+                .append(System.lineSeparator());
+        sb.append(JLA.virtualThreadDefaultScheduler())
+                .append(System.lineSeparator());
+
+        sb.append(System.lineSeparator());
+
+        sb.append("Timeout schedulers:")
+                .append(System.lineSeparator());
+        var schedulers = JLA.virtualThreadDelayedTaskSchedulers().toList();
+        for (int i = 0; i < schedulers.size(); i++) {
+            sb.append('[')
+                    .append(i)
+                    .append("] ")
+                    .append(schedulers.get(i))
+                    .append(System.lineSeparator());
+        }
+    }
+
+    /**
+     * Print information on threads registered for I/O to the given string builder.
+     */
+    private static void printPollers(StringBuilder sb) {
+        Poller masterPoller = Poller.masterPoller();
+        List<Poller> readPollers = Poller.readPollers();
+        List<Poller> writePollers = Poller.writePollers();
+
+        if (masterPoller != null) {
+            sb.append("Master I/O poller:")
+                    .append(System.lineSeparator())
+                    .append(masterPoller)
+                    .append(System.lineSeparator());
+            sb.append(System.lineSeparator());
+        }
+
+        sb.append("Read I/O pollers:");
+        sb.append(System.lineSeparator());
+        IntStream.range(0, readPollers.size())
+                .forEach(i -> sb.append('[')
+                        .append(i)
+                        .append("] ")
+                        .append(readPollers.get(i))
+                        .append(System.lineSeparator()));
+        sb.append(System.lineSeparator());
+
+        sb.append("Write I/O pollers:");
+        sb.append(System.lineSeparator());
+        IntStream.range(0, writePollers.size())
+                .forEach(i -> sb.append('[')
+                        .append(i)
+                        .append("] ")
+                        .append(writePollers.get(i))
+                        .append(System.lineSeparator()));
+    }
+
+    /**
+     * Print the thread containers that don't have an owner to the given string builder.
+     * The output will include the root container and all thread pools.
+     *
+     * In the future, this could be extended to support structured concurrency so that
+     * it prints a tree of owned thread containers.
+     */
+    private static void printThreadContainers(StringBuilder sb) {
+        sb.append("Thread groupings:")
+                .append(System.lineSeparator());
+
+        ThreadContainer root = ThreadContainers.root();
+        printThreadContainer(root, sb);
+
+        int printed = 1;
+        Iterator<ThreadContainer> iterator = root.children().iterator();
+        while (iterator.hasNext() && printed < MAX_THREAD_CONTAINERS) {
+            ThreadContainer container = iterator.next();
+            if (container.owner() == null) {
+                printThreadContainer(container, sb);
+                printed++;
+            }
+        }
+        if (iterator.hasNext()) {
+            sb.append("<truncated ...>")
+                    .append(System.lineSeparator());
+        }
+    }
+
+    /**
+     * Print a thread container to the given string builder.
+     */
+    private static void printThreadContainer(ThreadContainer container, StringBuilder sb) {
+        Map<Boolean, Long> threadCounts = container.threads()
+                .collect(Collectors.partitioningBy(Thread::isVirtual, Collectors.counting()));
+        long platformThreadCount = threadCounts.get(Boolean.FALSE);
+        long virtualThreadCount = threadCounts.get(Boolean.TRUE);
+        sb.append(container)
+                .append(" [platform threads = ")
+                .append(platformThreadCount)
+                .append(", virtual threads = ")
+                .append(virtualThreadCount)
+                .append("]")
+                .append(System.lineSeparator());
+    }
+}
diff a/src/java.base/share/classes/sun/nio/ch/Poller.java b/src/java.base/share/classes/sun/nio/ch/Poller.java
--- a/src/java.base/share/classes/sun/nio/ch/Poller.java
+++ b/src/java.base/share/classes/sun/nio/ch/Poller.java
@@ -24,11 +24,13 @@
  */
 package sun.nio.ch;
 
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.locks.LockSupport;
@@ -38,11 +40,11 @@
 
 /**
  * Polls file descriptors. Virtual threads invoke the poll method to park
  * until a given file descriptor is ready for I/O.
  */
-abstract class Poller {
+public abstract class Poller {
     private static final Pollers POLLERS;
     static {
         try {
             var pollers = new Pollers();
             pollers.start();
@@ -140,10 +142,24 @@
         } else {
             assert false;
         }
     }
 
+    /**
+     * Parks the current thread until a Selector's file descriptor is ready.
+     * @param fdVal the Selector's file descriptor
+     * @param nanos the waiting time or 0 to wait indefinitely
+     */
+    static void pollSelector(int fdVal, long nanos) throws IOException {
+        assert nanos >= 0L;
+        Poller poller = POLLERS.masterPoller();
+        if (poller == null) {
+            poller = POLLERS.readPoller(fdVal);
+        }
+        poller.poll(fdVal, nanos, () -> true);
+    }
+
     /**
      * If there is a thread polling the given file descriptor for the given event then
      * the thread is unparked.
      */
     static void stopPoll(int fdVal, int event) {
@@ -256,10 +272,22 @@
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
 
+    /**
+     * Returns the number I/O operations currently registered with this poller.
+     */
+    public int registered() {
+        return map.size();
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toIdentityString(this) + " [registered = " + registered() + "]";
+    }
+
     /**
      * The Pollers used for read and write events.
      */
     private static class Pollers {
         private final PollerProvider provider;
@@ -342,10 +370,17 @@
                     startPlatformThread("Write-Poller", p::pollerLoop);
                 });
             }
         }
 
+        /**
+         * Returns the master poller, or null if there is no master poller.
+         */
+        Poller masterPoller() {
+            return masterPoller;
+        }
+
         /**
          * Returns the read poller for the given file descriptor.
          */
         Poller readPoller(int fdVal) {
             int index = provider.fdValToIndex(fdVal, readPollers.length);
@@ -358,10 +393,25 @@
         Poller writePoller(int fdVal) {
             int index = provider.fdValToIndex(fdVal, writePollers.length);
             return writePollers[index];
         }
 
+        /**
+         * Return the list of read pollers.
+         */
+        List<Poller> readPollers() {
+            return List.of(readPollers);
+        }
+
+        /**
+         * Return the list of write pollers.
+         */
+        List<Poller> writePollers() {
+            return List.of(writePollers);
+        }
+
+
         /**
          * Reads the given property name to get the poller count. If the property is
          * set then the value must be a power of 2. Returns 1 if the property is not
          * set.
          * @throws IllegalArgumentException if the property is set to a value that
@@ -391,6 +441,27 @@
             } catch (Exception e) {
                 throw new InternalError(e);
             }
         }
     }
+
+    /**
+     * Return the master poller or null if there is no master poller.
+     */
+    public static Poller masterPoller() {
+        return POLLERS.masterPoller();
+    }
+
+    /**
+     * Return the list of read pollers.
+     */
+    public static List<Poller> readPollers() {
+        return POLLERS.readPollers();
+    }
+
+    /**
+     * Return the list of write pollers.
+     */
+    public static List<Poller> writePollers() {
+        return POLLERS.writePollers();
+    }
 }
diff a/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java b/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java
--- a/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java
+++ b/src/java.base/share/classes/sun/security/ssl/X509TrustManagerImpl.java
@@ -23,10 +23,11 @@
  * questions.
  */
 
 package sun.security.ssl;
 
+import java.lang.invoke.MethodHandles;
 import java.net.Socket;
 import java.security.*;
 import java.security.cert.*;
 import java.util.*;
 import java.util.concurrent.locks.ReentrantLock;
@@ -49,10 +50,18 @@
  * @author Andreas Sterbenz
  */
 final class X509TrustManagerImpl extends X509ExtendedTrustManager
         implements X509TrustManager {
 
+    static {
+        try {
+            MethodHandles.lookup().ensureInitialized(AnchorCertificates.class);
+        } catch (IllegalAccessException e) {
+            throw new ExceptionInInitializerError(e);
+        }
+    }
+
     private final String validatorType;
 
     /**
      * The Set of trusted X509Certificates.
      */
diff a/src/java.base/share/native/libjava/Thread.c b/src/java.base/share/native/libjava/Thread.c
--- a/src/java.base/share/native/libjava/Thread.c
+++ b/src/java.base/share/native/libjava/Thread.c
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 1994, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1994, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.  Oracle designates this
@@ -41,10 +41,11 @@
     {"yield0",           "()V",        (void *)&JVM_Yield},
     {"sleepNanos0",      "(J)V",       (void *)&JVM_SleepNanos},
     {"currentCarrierThread", "()" THD, (void *)&JVM_CurrentCarrierThread},
     {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
     {"setCurrentThread", "(" THD ")V", (void *)&JVM_SetCurrentThread},
+    {"setCurrentLockId", "(J)V",       (void *)&JVM_SetCurrentLockId},
     {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
     {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
     {"getThreads",       "()[" THD,    (void *)&JVM_GetAllThreads},
     {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
     {"getStackTrace0",   "()" OBJ,     (void *)&JVM_GetStackTrace},
diff a/src/java.base/share/native/libjava/VirtualThread.c b/src/java.base/share/native/libjava/VirtualThread.c
--- a/src/java.base/share/native/libjava/VirtualThread.c
+++ b/src/java.base/share/native/libjava/VirtualThread.c
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.  Oracle designates this
@@ -36,11 +36,19 @@
     { "notifyJvmtiEnd",            "()V",  (void *)&JVM_VirtualThreadEnd },
     { "notifyJvmtiMount",          "(Z)V", (void *)&JVM_VirtualThreadMount },
     { "notifyJvmtiUnmount",        "(Z)V", (void *)&JVM_VirtualThreadUnmount },
     { "notifyJvmtiHideFrames",     "(Z)V", (void *)&JVM_VirtualThreadHideFrames },
     { "notifyJvmtiDisableSuspend", "(Z)V", (void *)&JVM_VirtualThreadDisableSuspend },
+    { "takeVirtualThreadListToUnblock", "()" VIRTUAL_THREAD, (void *)&JVM_TakeVirtualThreadListToUnblock},
 };
 
 JNIEXPORT void JNICALL
 Java_java_lang_VirtualThread_registerNatives(JNIEnv *env, jclass clazz) {
     (*env)->RegisterNatives(env, clazz, methods, (sizeof(methods)/sizeof(methods[0])));
 }
+
+JNIEXPORT void JNICALL
+Java_java_lang_VirtualThread_virtualThreadPinnedEvent(JNIEnv *env, jclass ignored,
+                                                      jint reasonCode, jstring reasonString)
+{
+    JVM_VirtualThreadPinnedEvent(reasonCode, reasonString);
+}
diff a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/JavaThread.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/JavaThread.java
--- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/JavaThread.java
+++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/JavaThread.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2000, 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -53,10 +53,11 @@
   private static AddressField  osThreadField;
   private static AddressField  stackBaseField;
   private static CIntegerField stackSizeField;
   private static CIntegerField terminatedField;
   private static AddressField activeHandlesField;
+  private static CIntegerField lockIdField;
   private static long oopPtrSize;
 
   private static JavaThreadPDAccess access;
 
   // JavaThreadStates read from underlying process
@@ -99,10 +100,11 @@
     osThreadField     = type.getAddressField("_osthread");
     stackBaseField    = type.getAddressField("_stack_base");
     stackSizeField    = type.getCIntegerField("_stack_size");
     terminatedField   = type.getCIntegerField("_terminated");
     activeHandlesField = type.getAddressField("_active_handles");
+    lockIdField        = type.getCIntegerField("_lock_id");
 
     lockStackTopOffset = type.getField("_lock_stack").getOffset() + typeLockStack.getField("_top").getOffset();
     lockStackBaseOffset = type.getField("_lock_stack").getOffset() + typeLockStack.getField("_base[0]").getOffset();
     oopPtrSize = VM.getVM().getAddressSize();
 
@@ -374,10 +376,14 @@
         return "<null>";
     }
     return OopUtilities.threadOopGetName(threadObj);
   }
 
+  public Address getLockId() {
+    return lockIdField.getAddress(addr);
+  }
+
   //
   // Oop traversal
   //
 
   public void oopsDo(AddressVisitor oopVisitor) {
diff a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/ObjectMonitor.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/ObjectMonitor.java
--- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/ObjectMonitor.java
+++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/ObjectMonitor.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2001, 2021, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -49,10 +49,12 @@
     metadataFieldOffset = f.getOffset();
     f = type.getField("_object");
     objectFieldOffset = f.getOffset();
     f = type.getField("_owner");
     ownerFieldOffset = f.getOffset();
+    f = type.getField("_stack_locker");
+    stackLockerFieldOffset = f.getOffset();
     f = type.getField("_next_om");
     nextOMFieldOffset = f.getOffset();
     contentionsField  = new CIntField(type.getCIntegerField("_contentions"), 0);
     waitersField      = new CIntField(type.getCIntegerField("_waiters"), 0);
     recursionsField   = type.getCIntegerField("_recursions");
@@ -85,10 +87,11 @@
   public boolean isOwnedAnonymous() {
     return addr.getAddressAt(ownerFieldOffset).asLongValue() == ANONYMOUS_OWNER;
   }
 
   public Address owner() { return addr.getAddressAt(ownerFieldOffset); }
+  public Address stackLocker() { return addr.getAddressAt(stackLockerFieldOffset); }
   // FIXME
   //  void      set_owner(void* owner);
 
   public int    waiters() { return (int)waitersField.getValue(this); }
 
@@ -115,10 +118,11 @@
   // would confuse the SA's type system.
   private static ObjectHeap    heap;
   private static long          metadataFieldOffset;
   private static long          objectFieldOffset;
   private static long          ownerFieldOffset;
+  private static long          stackLockerFieldOffset;
   private static long          nextOMFieldOffset;
   private static CIntField     contentionsField;
   private static CIntField     waitersField;
   private static CIntegerField recursionsField;
   private static long          ANONYMOUS_OWNER;
diff a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/Threads.java b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/Threads.java
--- a/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/Threads.java
+++ b/src/jdk.hotspot.agent/share/classes/sun/jvm/hotspot/runtime/Threads.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2000, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -210,32 +210,24 @@
             JavaThread thread = threads.getJavaThreadAt(i);
             thread.oopsDo(oopVisitor);
         }
     }
 
-    // refer to Threads::owning_thread_from_monitor_owner
-    public JavaThread owningThreadFromMonitor(Address o) {
-        assert(VM.getVM().getCommandLineFlag("LockingMode").getInt() != LockingMode.getLightweight());
+    private JavaThread owningThreadFromMonitor(Address o) {
         if (o == null) return null;
         for (int i = 0; i < getNumberOfThreads(); i++) {
             JavaThread thread = getJavaThreadAt(i);
-            if (o.equals(thread.threadObjectAddress())) {
+            if (o.equals(thread.getLockId())) {
                 return thread;
             }
         }
-
-        for (int i = 0; i < getNumberOfThreads(); i++) {
-            JavaThread thread = getJavaThreadAt(i);
-            if (thread.isLockOwned(o))
-                return thread;
-        }
         return null;
     }
 
     public JavaThread owningThreadFromMonitor(ObjectMonitor monitor) {
-        if (VM.getVM().getCommandLineFlag("LockingMode").getInt() == LockingMode.getLightweight()) {
-            if (monitor.isOwnedAnonymous()) {
+        if (monitor.isOwnedAnonymous()) {
+            if (VM.getVM().getCommandLineFlag("LockingMode").getInt() == LockingMode.getLightweight()) {
                 OopHandle object = monitor.object();
                 for (int i = 0; i < getNumberOfThreads(); i++) {
                     JavaThread thread = getJavaThreadAt(i);
                     if (thread.isLockOwned(object)) {
                         return thread;
@@ -244,15 +236,20 @@
                 // We should have found the owner, however, as the VM could be in any state, including the middle
                 // of performing GC, it is not always possible to do so. Just return null if we can't locate it.
                 System.out.println("Warning: We failed to find a thread that owns an anonymous lock. This is likely");
                 System.out.println("due to the JVM currently running a GC. Locking information may not be accurate.");
                 return null;
+            } else {
+                assert(VM.getVM().getCommandLineFlag("LockingMode").getInt() == LockingMode.getLegacy());
+                Address o = (Address)monitor.stackLocker();
+                for (int i = 0; i < getNumberOfThreads(); i++) {
+                    JavaThread thread = getJavaThreadAt(i);
+                    if (thread.isLockOwned(o))
+                        return thread;
+                }
+                return null;
             }
-            // Owner can only be threads at this point.
-            Address o = monitor.owner();
-            if (o == null) return null;
-            return new JavaThread(o);
         } else {
             return owningThreadFromMonitor(monitor.owner());
         }
     }
 
diff a/src/jdk.jcmd/share/man/jcmd.1 b/src/jdk.jcmd/share/man/jcmd.1
--- a/src/jdk.jcmd/share/man/jcmd.1
+++ b/src/jdk.jcmd/share/man/jcmd.1
@@ -937,11 +937,11 @@
 \f[I]filepath\f[R]: The file path to the output file (STRING, no default
 value)
 .RE
 .TP
 \f[V]Thread.print\f[R] [\f[I]options\f[R]]
-Prints all threads with stacktraces.
+Prints all platform threads with stacktraces.
 .RS
 .PP
 Impact: Medium --- depends on the number of threads.
 .PP
 \f[B]Note:\f[R]
@@ -956,10 +956,18 @@
 .IP \[bu] 2
 \f[V]-l\f[R]: (Optional) Prints \f[V]java.util.concurrent\f[R] locks
 (BOOLEAN, false)
 .RE
 .TP
+\f[V]Thread.vthread_summary\f[R]
+Print the virtual thread scheduler, timeout schedulers, I/O pollers, and
+thread groupings.
+.RS
+.PP
+Impact: Low
+.RE
+.TP
 \f[V]VM.cds\f[R] [\f[I]arguments\f[R]]
 Dump a static or dynamic shared archive that includes all currently
 loaded classes.
 .RS
 .PP
diff a/src/jdk.jfr/share/classes/jdk/jfr/events/VirtualThreadPinnedEvent.java b/src/jdk.jfr/share/classes/jdk/jfr/events/VirtualThreadPinnedEvent.java
--- a/src/jdk.jfr/share/classes/jdk/jfr/events/VirtualThreadPinnedEvent.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.  Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-package jdk.jfr.events;
-
-import jdk.jfr.Category;
-import jdk.jfr.Label;
-import jdk.jfr.Name;
-import jdk.jfr.internal.MirrorEvent;
-
-@Category("Java Application")
-@Label("Virtual Thread Pinned")
-@Name("jdk.VirtualThreadPinned")
-public final class VirtualThreadPinnedEvent extends MirrorEvent {
-}
diff a/src/jdk.jfr/share/classes/jdk/jfr/internal/JDKEvents.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/JDKEvents.java
--- a/src/jdk.jfr/share/classes/jdk/jfr/internal/JDKEvents.java
+++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/JDKEvents.java
@@ -66,11 +66,10 @@
         jdk.internal.event.SocketWriteEvent.class,
         jdk.internal.event.ThreadSleepEvent.class,
         jdk.internal.event.TLSHandshakeEvent.class,
         jdk.internal.event.VirtualThreadStartEvent.class,
         jdk.internal.event.VirtualThreadEndEvent.class,
-        jdk.internal.event.VirtualThreadPinnedEvent.class,
         jdk.internal.event.VirtualThreadSubmitFailedEvent.class,
         jdk.internal.event.X509CertificateEvent.class,
         jdk.internal.event.X509ValidationEvent.class,
         DirectBufferStatisticsEvent.class,
         InitialSecurityPropertyEvent.class,
diff a/src/jdk.jfr/share/classes/jdk/jfr/internal/MirrorEvents.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/MirrorEvents.java
--- a/src/jdk.jfr/share/classes/jdk/jfr/internal/MirrorEvents.java
+++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/MirrorEvents.java
@@ -41,11 +41,10 @@
 import jdk.jfr.events.SocketReadEvent;
 import jdk.jfr.events.SocketWriteEvent;
 import jdk.jfr.events.TLSHandshakeEvent;
 import jdk.jfr.events.ThreadSleepEvent;
 import jdk.jfr.events.VirtualThreadEndEvent;
-import jdk.jfr.events.VirtualThreadPinnedEvent;
 import jdk.jfr.events.VirtualThreadStartEvent;
 import jdk.jfr.events.VirtualThreadSubmitFailedEvent;
 import jdk.jfr.events.X509CertificateEvent;
 import jdk.jfr.events.X509ValidationEvent;
 import jdk.jfr.internal.util.Utils;
@@ -70,11 +69,10 @@
         register("jdk.internal.event.SocketWriteEvent", SocketWriteEvent.class);
         register("jdk.internal.event.ThreadSleepEvent", ThreadSleepEvent.class);
         register("jdk.internal.event.TLSHandshakeEvent", TLSHandshakeEvent.class);
         register("jdk.internal.event.VirtualThreadStartEvent", VirtualThreadStartEvent.class);
         register("jdk.internal.event.VirtualThreadEndEvent", VirtualThreadEndEvent.class);
-        register("jdk.internal.event.VirtualThreadPinnedEvent", VirtualThreadPinnedEvent.class);
         register("jdk.internal.event.VirtualThreadSubmitFailedEvent", VirtualThreadSubmitFailedEvent.class);
         register("jdk.internal.event.X509CertificateEvent", X509CertificateEvent.class);
         register("jdk.internal.event.X509ValidationEvent", X509ValidationEvent.class);
         register("jdk.internal.event.ErrorThrownEvent", ErrorThrownEvent.class);
         register("jdk.internal.event.ExceptionStatisticsEvent", ExceptionStatisticsEvent.class);
diff a/src/jdk.jfr/share/conf/jfr/default.jfc b/src/jdk.jfr/share/conf/jfr/default.jfc
--- a/src/jdk.jfr/share/conf/jfr/default.jfc
+++ b/src/jdk.jfr/share/conf/jfr/default.jfc
@@ -73,11 +73,10 @@
     </event>
 
     <event name="jdk.VirtualThreadPinned">
       <setting name="enabled">true</setting>
       <setting name="stackTrace">true</setting>
-      <setting name="threshold">20 ms</setting>
     </event>
 
     <event name="jdk.VirtualThreadSubmitFailed">
       <setting name="enabled">true</setting>
       <setting name="stackTrace">true</setting>
diff a/src/jdk.jfr/share/conf/jfr/profile.jfc b/src/jdk.jfr/share/conf/jfr/profile.jfc
--- a/src/jdk.jfr/share/conf/jfr/profile.jfc
+++ b/src/jdk.jfr/share/conf/jfr/profile.jfc
@@ -73,11 +73,10 @@
     </event>
 
     <event name="jdk.VirtualThreadPinned">
       <setting name="enabled">true</setting>
       <setting name="stackTrace">true</setting>
-      <setting name="threshold">20 ms</setting>
     </event>
 
     <event name="jdk.VirtualThreadSubmitFailed">
       <setting name="enabled">true</setting>
       <setting name="stackTrace">true</setting>
diff a/src/jdk.management/share/classes/com/sun/management/internal/PlatformMBeanProviderImpl.java b/src/jdk.management/share/classes/com/sun/management/internal/PlatformMBeanProviderImpl.java
--- a/src/jdk.management/share/classes/com/sun/management/internal/PlatformMBeanProviderImpl.java
+++ b/src/jdk.management/share/classes/com/sun/management/internal/PlatformMBeanProviderImpl.java
@@ -163,43 +163,47 @@
                         threadMBean);
             }
         });
 
         /**
-         * VirtualThreadSchedulerMXBean.
+         * VirtualThreadSchedulerMXBean registered when using JDK's virtual thread scheduler.
          */
-        initMBeanList.add(new PlatformComponent<VirtualThreadSchedulerMXBean>() {
-            private final Set<Class<? extends VirtualThreadSchedulerMXBean>> mbeanInterfaces =
-                    Set.of(VirtualThreadSchedulerMXBean.class);
-            private final Set<String> mbeanInterfaceNames =
-                    Set.of(VirtualThreadSchedulerMXBean.class.getName());
-            private VirtualThreadSchedulerMXBean impl;
-
-            @Override
-            public Set<Class<? extends VirtualThreadSchedulerMXBean>> mbeanInterfaces() {
-                return mbeanInterfaces;
-            }
+        PrivilegedAction<String> pa = () -> System.getProperty("jdk.virtualThreadScheduler.implClass");
+        String value = AccessController.doPrivileged(pa);
+        if (value == null) {
+            initMBeanList.add(new PlatformComponent<VirtualThreadSchedulerMXBean>() {
+                private final Set<Class<? extends VirtualThreadSchedulerMXBean>> mbeanInterfaces =
+                        Set.of(VirtualThreadSchedulerMXBean.class);
+                private final Set<String> mbeanInterfaceNames =
+                        Set.of(VirtualThreadSchedulerMXBean.class.getName());
+                private VirtualThreadSchedulerMXBean impl;
+
+                @Override
+                public Set<Class<? extends VirtualThreadSchedulerMXBean>> mbeanInterfaces() {
+                    return mbeanInterfaces;
+                }
 
-            @Override
-            public Set<String> mbeanInterfaceNames() {
-                return mbeanInterfaceNames;
-            }
+                @Override
+                public Set<String> mbeanInterfaceNames() {
+                    return mbeanInterfaceNames;
+                }
 
-            @Override
-            public String getObjectNamePattern() {
-                return "jdk.management:type=VirtualThreadScheduler";
-            }
+                @Override
+                public String getObjectNamePattern() {
+                    return "jdk.management:type=VirtualThreadScheduler";
+                }
 
-            @Override
-            public Map<String, VirtualThreadSchedulerMXBean> nameToMBeanMap() {
-                VirtualThreadSchedulerMXBean impl = this.impl;
-                if (impl == null) {
-                    this.impl = impl = VirtualThreadSchedulerImpls.create();
+                @Override
+                public Map<String, VirtualThreadSchedulerMXBean> nameToMBeanMap() {
+                    VirtualThreadSchedulerMXBean impl = this.impl;
+                    if (impl == null) {
+                        this.impl = impl = VirtualThreadSchedulerImpls.create();
+                    }
+                    return Map.of("jdk.management:type=VirtualThreadScheduler", impl);
                 }
-                return Map.of("jdk.management:type=VirtualThreadScheduler", impl);
-            }
-        });
+            });
+        }
 
         /**
          * OperatingSystemMXBean
          */
         initMBeanList.add(new PlatformComponent<OperatingSystemMXBean>() {
diff a/test/failure_handler/src/share/conf/common.properties b/test/failure_handler/src/share/conf/common.properties
--- a/test/failure_handler/src/share/conf/common.properties
+++ b/test/failure_handler/src/share/conf/common.properties
@@ -1,7 +1,7 @@
 #
-# Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
 # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 #
 # This code is free software; you can redistribute it and/or modify it
 # under the terms of the GNU General Public License version 2 only, as
 # published by the Free Software Foundation.
@@ -32,11 +32,11 @@
   jcmd.compiler.codecache jcmd.compiler.codelist \
         jcmd.compiler.queue \
   jcmd.vm.classloader_stats jcmd.vm.stringtable \
         jcmd.vm.symboltable jcmd.vm.uptime jcmd.vm.dynlibs \
         jcmd.vm.system_properties jcmd.vm.info \
-  jcmd.gc.heap_info jcmd.gc.class_histogram jcmd.gc.finalizer_info jcmd.thread.dump_to_file \
+  jcmd.gc.heap_info jcmd.gc.class_histogram jcmd.gc.finalizer_info jcmd.thread.dump_to_file jcmd.thread.vthread_summary \
   jstack jhsdb.jstack.live.default jhsdb.jstack.live.mixed
 
 jinfo.app=jinfo
 
 jcmd.app=jcmd
@@ -59,10 +59,12 @@
 
 jcmd.thread.dump_to_file.args=%p Thread.dump_to_file -format=json JavaThread.dump.%p.%iterCount
 jcmd.thread.dump_to_file.params.repeat=6
 jcmd.thread.dump_to_file.params.successArtifacts=JavaThread.dump.%p.%iterCount
 
+jcmd.thread.vthread_summary.args=%p Thread.vthread_summary
+
 jstack.app=jstack
 jstack.args=-e -l %p
 jstack.params.repeat=6
 
 jhsdb.app=jhsdb
diff a/test/hotspot/gtest/nmt/test_vmatree.cpp b/test/hotspot/gtest/nmt/test_vmatree.cpp
--- a/test/hotspot/gtest/nmt/test_vmatree.cpp
+++ b/test/hotspot/gtest/nmt/test_vmatree.cpp
@@ -29,11 +29,11 @@
 #include "nmt/vmatree.hpp"
 #include "runtime/os.hpp"
 #include "unittest.hpp"
 
 using Tree = VMATree;
-using Node = Tree::TreapNode;
+using TNode = Tree::TreapNode;
 using NCS = NativeCallStackStorage;
 
 class NMTVMATreeTest : public testing::Test {
 public:
   NCS ncs;
@@ -75,11 +75,11 @@
     return x->val().out.type();
   }
 
   int count_nodes(Tree& tree) {
     int count = 0;
-    treap(tree).visit_in_order([&](Node* x) {
+    treap(tree).visit_in_order([&](TNode* x) {
       ++count;
     });
     return count;
   }
 
@@ -128,11 +128,11 @@
     Tree tree;
     tree.reserve_mapping(0, 100 * 10, rd);
     for (int i = 0; i < 10; i++) {
       tree.commit_mapping(i * 100, 100, rd);
     }
-    treap(tree).visit_in_order([&](Node* x) {
+    treap(tree).visit_in_order([&](TNode* x) {
       VMATree::StateType in = in_type_of(x);
       VMATree::StateType out = out_type_of(x);
       EXPECT_TRUE((in == VMATree::StateType::Released && out == VMATree::StateType::Committed) ||
                   (in == VMATree::StateType::Committed && out == VMATree::StateType::Released));
     });
@@ -153,11 +153,11 @@
       }
       return false;
     };
 
     int i = 0;
-    treap(tree).visit_in_order([&](Node* x) {
+    treap(tree).visit_in_order([&](TNode* x) {
       if (i < 16) {
         found[i] = x->key();
       }
       i++;
     });
@@ -185,11 +185,11 @@
   VMATree::RegionData rd1(si[0], mtTest);
   VMATree::RegionData rd2(si[1], mtNone);
   tree.reserve_mapping(0, 100, rd1);
   tree.commit_mapping(20, 50, rd2, true);
   tree.uncommit_mapping(30, 10, rd2);
-  tree.visit_in_order([&](Node* node) {
+  tree.visit_in_order([&](TNode* node) {
     if (node->key() != 100) {
       EXPECT_EQ(mtTest, node->val().out.mem_tag()) << "failed at: " << node->key();
       if (node->key() != 20 && node->key() != 40) {
         EXPECT_EQ(VMATree::StateType::Reserved, node->val().out.type());
       }
@@ -225,11 +225,11 @@
     Tree tree;
     VMATree::RegionData rd{si[0], mtTest };
     VMATree::RegionData rd2{si[1], mtNMT };
     tree.commit_mapping(50, 50, rd2);
     tree.reserve_mapping(0, 100, rd);
-    treap(tree).visit_in_order([&](Node* x) {
+    treap(tree).visit_in_order([&](TNode* x) {
       EXPECT_TRUE(x->key() == 0 || x->key() == 100);
       if (x->key() == 0) {
         EXPECT_EQ(x->val().out.regiondata().mem_tag, mtTest);
       }
     });
@@ -262,11 +262,11 @@
     Tree::RegionData rd{si[0], mtNMT};
     VMATree::RegionData rd2{si[1], mtTest};
     Tree tree;
     tree.reserve_mapping(0, 100, rd);
     tree.commit_mapping(0, 100, rd2);
-    treap(tree).visit_range_in_order(0, 99999, [&](Node* x) {
+    treap(tree).visit_range_in_order(0, 99999, [&](TNode* x) {
       if (x->key() == 0) {
         EXPECT_EQ(mtTest, x->val().out.regiondata().mem_tag);
       }
       if (x->key() == 100) {
         EXPECT_EQ(mtTest, x->val().in.regiondata().mem_tag);
diff a/test/hotspot/jtreg/ProblemList-Virtual.txt b/test/hotspot/jtreg/ProblemList-Virtual.txt
--- a/test/hotspot/jtreg/ProblemList-Virtual.txt
+++ b/test/hotspot/jtreg/ProblemList-Virtual.txt
@@ -85,10 +85,26 @@
 # of this no vthreads can complete their reads, and the test times out as a result.
 
 vmTestbase/nsk/jdi/VMOutOfMemoryException/VMOutOfMemoryException001/VMOutOfMemoryException001.java 8285417 generic-all
 
 ###
+# The test first suspends a vthread and all the carriers and then it resumes the vthread expecting it
+# to run to completion. In mainline it only works because the suspension step happens while the vthread is
+# pinned to the carrier blocked on synchronized. So the carrier only actually suspends on the next unmount
+# transition which in this test happens once the vthread has finished executing the expected code.
+
+vmTestbase/nsk/jdi/ThreadReference/isSuspended/issuspended002/TestDescription.java 8338713 generic-all
+
+###
+# The test sends a StopThread to a vthread expecting that is currently pinned to the carrier blocked on
+# synchronized. Since the vthread is now unmounted StopThread returns JVMTI_ERROR_OPAQUE_FRAME error.
+
+vmTestbase/nsk/jdb/kill/kill001/kill001.java 8338714 generic-all
+
+# Unknown reasons, should be investigated
+vmTestbase/gc/gctests/MemoryEaterMT/MemoryEaterMT.java 0000000 generic-all
+
 # Fails on Windows because of additional memory allocation.
 
 gc/g1/TestMixedGCLiveThreshold.java#25percent 8334759 windows-x64
 
 ##########
@@ -100,5 +116,22 @@
 gc/g1/TestSkipRebuildRemsetPhase.java 0000000 generic-all
 runtime/ErrorHandling/MachCodeFramesInErrorFile.java 0000000 generic-all
 runtime/Thread/AsyncExceptionOnMonitorEnter.java 0000000 generic-all
 runtime/Thread/StopAtExit.java 0000000 generic-all
 runtime/handshake/HandshakeWalkStackTest.java 0000000 generic-all
+
+# java.lang.ClassNotFoundException: jdk.test.lib.process.ProcessTools
+runtime/cds/appcds/jigsaw/classpathtests/EmptyClassInBootClassPath.java 0000000 generic-all
+
+#  java.lang.ClassNotFoundException: / in ProcessTools.main
+runtime/cds/appcds/jigsaw/modulepath/OptimizeModuleHandlingTest.java 0000000 generic-all
+
+# too fragile when main thread is a virtual thread
+runtime/Thread/ThreadCountLimit.java#id0 0000000 generic-all
+
+# Invalid code heap sizes
+compiler/arguments/TestC1Globals.java#id0 0000000 generic-all
+compiler/arguments/TestC1Globals.java#id1 0000000 generic-all
+compiler/arguments/TestC1Globals.java#id2 0000000 generic-all
+
+# OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
+compiler/codecache/CodeCacheFullCountTest.java 8332926 generic-all
diff a/test/hotspot/jtreg/ProblemList-Xcomp.txt b/test/hotspot/jtreg/ProblemList-Xcomp.txt
--- a/test/hotspot/jtreg/ProblemList-Xcomp.txt
+++ b/test/hotspot/jtreg/ProblemList-Xcomp.txt
@@ -55,5 +55,19 @@
 compiler/cha/TypeProfileFinalMethod.java 8341039 generic-all
 
 gc/arguments/TestNewSizeFlags.java 8299116 macosx-aarch64
 
 runtime/condy/escapeAnalysis/TestEscapeCondy.java 8339694 generic-all
+
+############################################################################
+
+# Loom, fibers branch
+
+gc/g1/plab/TestPLABPromotion.java                                       8278126 generic-all
+gc/g1/plab/TestPLABResize.java                                          8278126 generic-all
+
+gc/stringdedup/TestStringDeduplicationInterned.java#G1                  8278127 generic-all
+gc/stringdedup/TestStringDeduplicationInterned.java#Parallel            8278127 generic-all
+gc/stringdedup/TestStringDeduplicationInterned.java#Serial              8278127 generic-all
+gc/stringdedup/TestStringDeduplicationInterned.java#Z                   8278127 generic-all
+
+gc/z/TestGarbageCollectorMXBean.java                                    8278129 generic-all
diff a/test/hotspot/jtreg/ProblemList-vthread-Xcomp.txt b/test/hotspot/jtreg/ProblemList-vthread-Xcomp.txt
--- /dev/null
+++ b/test/hotspot/jtreg/ProblemList-vthread-Xcomp.txt
@@ -0,0 +1,34 @@
+#
+# Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+#
+# This code is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 2 only, as
+# published by the Free Software Foundation.
+#
+# This code is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# version 2 for more details (a copy is included in the LICENSE file that
+# accompanied this code).
+#
+# You should have received a copy of the GNU General Public License version
+# 2 along with this work; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+# or visit www.oracle.com if you need additional information or have any
+# questions.
+#
+
+#############################################################################
+#
+# List of quarantined tests for testing in Xcomp mode with wrapper.
+#
+#############################################################################
+
+####
+## Unexpected results
+compiler/uncommontrap/TestUnstableIfTrap.java
+gc/g1/plab/TestPLABPromotion.java
+gc/whitebox/TestWBGC.java
diff a/test/hotspot/jtreg/ProblemList.txt b/test/hotspot/jtreg/ProblemList.txt
--- a/test/hotspot/jtreg/ProblemList.txt
+++ b/test/hotspot/jtreg/ProblemList.txt
@@ -144,10 +144,18 @@
 serviceability/sa/ClhsdbPmap.java#core            8267433,8318754 macosx-x64,macosx-aarch64
 serviceability/sa/ClhsdbPstack.java#core          8267433,8318754 macosx-x64,macosx-aarch64
 serviceability/sa/TestJmapCore.java               8267433,8318754 macosx-x64,macosx-aarch64
 serviceability/sa/TestJmapCoreMetaspace.java      8267433,8318754 macosx-x64,macosx-aarch64
 
+serviceability/sa/ClhsdbFindPC.java#xcomp-core 8284045 macosx-aarch64
+serviceability/sa/ClhsdbCDSCore.java 8284045 macosx-aarch64
+serviceability/sa/TestJmapCore.java 8284045 macosx-aarch64
+
+# "waiting on ..." missing from stdout/stderr
+serviceability/sa/TestClhsdbJstackLock.java 0000000 generic-all
+serviceability/sa/TestJhsdbJstackLock.java 0000000 generic-all
+
 serviceability/attach/ConcAttachTest.java 8290043 linux-all
 
 serviceability/jvmti/stress/StackTrace/NotSuspended/GetStackTraceNotSuspendedStressTest.java 8315980 linux-all,windows-x64
 
 #############################################################################
diff a/test/hotspot/jtreg/TEST.groups b/test/hotspot/jtreg/TEST.groups
--- a/test/hotspot/jtreg/TEST.groups
+++ b/test/hotspot/jtreg/TEST.groups
@@ -774,5 +774,10 @@
 # JDB tests
 vmTestbase_nsk_jdb = \
   vmTestbase/nsk/jdb
 
 # vmTestbase_*_quick groups are defined in TEST.quick-groups
+
+hotspot_remaining = \
+ :hotspot_all_no_apps \
+  -vmTestbase \
+  -:tier1
diff a/test/hotspot/jtreg/compiler/codecache/stress/OverloadCompileQueueTest.java b/test/hotspot/jtreg/compiler/codecache/stress/OverloadCompileQueueTest.java
--- a/test/hotspot/jtreg/compiler/codecache/stress/OverloadCompileQueueTest.java
+++ b/test/hotspot/jtreg/compiler/codecache/stress/OverloadCompileQueueTest.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2014, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -37,10 +37,11 @@
  *                   -XX:CompileCommand=dontinline,compiler.codecache.stress.Helper$TestCase::method
  *                   -XX:CompileCommand=exclude,java.lang.Thread::sleep
  *                   -XX:CompileCommand=exclude,java.lang.Thread::sleepNanos
  *                   -XX:CompileCommand=exclude,java.lang.Thread::beforeSleep
  *                   -XX:CompileCommand=exclude,java.lang.Thread::afterSleep
+ *                   -XX:CompileCommand=exclude,java.util.concurrent.TimeUnit::toNanos
  *                   -XX:CompileCommand=exclude,java.lang.Math::min
  *                   -XX:CompileCommand=exclude,jdk.internal.event.ThreadSleepEvent::*
  *                   -XX:CompileCommand=exclude,jdk.internal.event.SleepEvent::*
  *                   -XX:CompileCommand=exclude,jdk.internal.event.Event::*
  *                   -XX:-SegmentedCodeCache
@@ -50,10 +51,11 @@
  *                   -XX:CompileCommand=dontinline,compiler.codecache.stress.Helper$TestCase::method
  *                   -XX:CompileCommand=exclude,java.lang.Thread::sleep
  *                   -XX:CompileCommand=exclude,java.lang.Thread::sleepNanos
  *                   -XX:CompileCommand=exclude,java.lang.Thread::beforeSleep
  *                   -XX:CompileCommand=exclude,java.lang.Thread::afterSleep
+ *                   -XX:CompileCommand=exclude,java.util.concurrent.TimeUnit::toNanos
  *                   -XX:CompileCommand=exclude,java.lang.Math::min
  *                   -XX:CompileCommand=exclude,jdk.internal.event.ThreadSleepEvent::*
  *                   -XX:CompileCommand=exclude,jdk.internal.event.SleepEvent::*
  *                   -XX:CompileCommand=exclude,jdk.internal.event.Event::*
  *                   -XX:+SegmentedCodeCache
diff a/test/hotspot/jtreg/runtime/vthread/JNIMonitor/JNIMonitor.java b/test/hotspot/jtreg/runtime/vthread/JNIMonitor/JNIMonitor.java
--- a/test/hotspot/jtreg/runtime/vthread/JNIMonitor/JNIMonitor.java
+++ b/test/hotspot/jtreg/runtime/vthread/JNIMonitor/JNIMonitor.java
@@ -117,10 +117,12 @@
             // Enable the JNI warning
             "-Xcheck:jni",
             "-Xlog:jni=debug",
             // Enable thread termination logging as a visual cross-check
             "-Xlog:thread+os=info",
+            // We only count monitors in LM_LEGACY mode
+            "-XX:LockingMode=1",
             "JNIMonitor$" + test,
         };
         OutputAnalyzer oa = ProcessTools.executeTestJava(cmdArgs);
         oa.shouldHaveExitValue(0);
         oa.stdoutShouldMatch(terminated);
@@ -217,16 +219,20 @@
                 throw new RuntimeException(e);
             }
         }
 
         static void runTest(int nThreads, boolean skipUnlock, boolean throwOnExit) throws Throwable {
-            final Object monitor = new Object();
+            final Object[] monitors = new Object[nThreads];
+            for (int i = 0; i < nThreads; i++) {
+                monitors[i] = new Object();
+            }
             final AtomicReference<Throwable> exception = new AtomicReference();
             // Ensure all our VT's operate of the same carrier, sequentially.
             ExecutorService scheduler = Executors.newSingleThreadExecutor();
             ThreadFactory factory = virtualThreadBuilder(scheduler).factory();
             for (int i = 0 ; i < nThreads; i++) {
+                Object monitor = skipUnlock ? monitors[i] : monitors[0];
                 Thread th = factory.newThread(() -> {
                         try {
                             int res = monitorEnter(monitor);
                             Asserts.assertTrue(res == 0, "monitorEnter should return 0.");
                             Asserts.assertTrue(Thread.holdsLock(monitor), "monitor should be owned");
diff a/test/hotspot/jtreg/serviceability/dcmd/thread/ThreadDumpToFileTest.java b/test/hotspot/jtreg/serviceability/dcmd/thread/ThreadDumpToFileTest.java
--- a/test/hotspot/jtreg/serviceability/dcmd/thread/ThreadDumpToFileTest.java
+++ b/test/hotspot/jtreg/serviceability/dcmd/thread/ThreadDumpToFileTest.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -23,10 +23,11 @@
 
 /*
  * @test
  * @bug 8284161 8287008
  * @summary Basic test for jcmd Thread.dump_to_file
+ * @modules jdk.jcmd
  * @library /test/lib
  * @run junit/othervm ThreadDumpToFileTest
  */
 
 import java.io.IOException;
diff a/test/hotspot/jtreg/serviceability/dcmd/thread/VThreadSummaryTest.java b/test/hotspot/jtreg/serviceability/dcmd/thread/VThreadSummaryTest.java
--- /dev/null
+++ b/test/hotspot/jtreg/serviceability/dcmd/thread/VThreadSummaryTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @bug 8337199
+ * @summary Basic test for jcmd Thread.vthread_summary
+ * @requires vm.continuations
+ * @modules jdk.jcmd
+ * @library /test/lib
+ * @run junit/othervm VThreadSummaryTest
+ */
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.ForkJoinWorkerThread;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.IntStream;
+import java.lang.management.ManagementFactory;
+import jdk.management.VirtualThreadSchedulerMXBean;
+
+import jdk.test.lib.dcmd.PidJcmdExecutor;
+import jdk.test.lib.process.OutputAnalyzer;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class VThreadSummaryTest {
+
+    private OutputAnalyzer jcmd() {
+        return new PidJcmdExecutor().execute("Thread.vthread_summary");
+    }
+
+    /**
+     * Test that output includes the default scheduler and timeout schedulers.
+     */
+    @Test
+    void testSchedulers() {
+        // ensure default scheduler are timeout schedulers are initialized
+        Thread.startVirtualThread(() -> { });
+
+        jcmd().shouldContain("Virtual thread scheduler:")
+                .shouldContain(Objects.toIdentityString(defaultScheduler()))
+                .shouldContain("Timeout schedulers:")
+                .shouldContain("[0] " + ScheduledThreadPoolExecutor.class.getName());
+    }
+
+    /**
+     * Test that the output includes the read and writer I/O pollers.
+     */
+    @Test
+    void testPollers() throws Exception {
+        // do blocking I/O op on a virtual thread to ensure poller mechanism is initialized
+        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
+            executor.submit(() -> {
+                try (var listener = new ServerSocket()) {
+                    InetAddress lb = InetAddress.getLoopbackAddress();
+                    listener.bind(new InetSocketAddress(lb, 0));
+                    listener.setSoTimeout(1000);
+                    try (Socket s = listener.accept()) {
+                        fail("Connection from " + s.getRemoteSocketAddress());
+                    } catch (SocketTimeoutException e) {
+                        // expected
+                    }
+                }
+                return null;
+            }).get();
+        }
+
+        jcmd().shouldContain("Read I/O pollers:")
+                .shouldContain("Write I/O pollers:")
+                .shouldContain("[0] sun.nio.ch");
+    }
+
+    /**
+     * Test that the output includes thread groupings.
+     */
+    @Test
+    void testThreadGroupings() {
+        // ensure common pool is initialized
+        CompletableFuture.runAsync(() -> { });
+
+        try (var pool = Executors.newFixedThreadPool(1);
+             var executor = Executors.newVirtualThreadPerTaskExecutor()) {
+
+            jcmd().shouldContain("<root>")
+                    .shouldContain("ForkJoinPool.commonPool")
+                    .shouldContain(Objects.toIdentityString(pool))
+                    .shouldContain(Objects.toIdentityString(executor));
+        }
+    }
+
+    /**
+     * Test that output is truncated when there are too many thread groupings.
+     */
+    @Test
+    void testTooManyThreadGroupings() {
+        List<ExecutorService> executors = IntStream.range(0, 1000)
+                .mapToObj(_ -> Executors.newCachedThreadPool())
+                .toList();
+        try {
+            jcmd().shouldContain("<root>")
+                    .shouldContain("<truncated ...>");
+        } finally {
+            executors.forEach(ExecutorService::close);
+        }
+    }
+
+    /**
+     * Returns the virtual thread default scheduler. This implementation works by finding
+     * all FJ worker threads and mapping them to their pool. VirtualThreadSchedulerMXBean
+     * is used to temporarily changing target parallelism to an "unique" value, make it
+     * possbile to find the right pool.
+     */
+    private ForkJoinPool defaultScheduler() {
+        var done = new AtomicBoolean();
+        Thread vthread = Thread.startVirtualThread(() -> {
+            while (!done.get()) {
+                Thread.onSpinWait();
+            }
+        });
+        var bean = ManagementFactory.getPlatformMXBean(VirtualThreadSchedulerMXBean.class);
+        int parallelism = bean.getParallelism();
+        try {
+            bean.setParallelism(133);
+            return Thread.getAllStackTraces()
+                    .keySet()
+                    .stream()
+                    .filter(ForkJoinWorkerThread.class::isInstance)
+                    .map(t -> ((ForkJoinWorkerThread) t).getPool())
+                    .filter(p -> p.getParallelism() == 133)
+                    .findAny()
+                    .orElseThrow();
+        } finally {
+            bean.setParallelism(parallelism);
+            done.set(true);
+        }
+    }
+}
diff a/test/hotspot/jtreg/serviceability/jvmti/events/MonitorContendedEnter/mcontenter01/libmcontenter01.cpp b/test/hotspot/jtreg/serviceability/jvmti/events/MonitorContendedEnter/mcontenter01/libmcontenter01.cpp
--- a/test/hotspot/jtreg/serviceability/jvmti/events/MonitorContendedEnter/mcontenter01/libmcontenter01.cpp
+++ b/test/hotspot/jtreg/serviceability/jvmti/events/MonitorContendedEnter/mcontenter01/libmcontenter01.cpp
@@ -33,11 +33,10 @@
 extern "C" {
 
 /* ========================================================================== */
 
 /* scaffold objects */
-static JNIEnv *jni = nullptr;
 static jvmtiEnv *jvmti = nullptr;
 static jlong timeout = 0;
 
 /* test objects */
 static jthread expected_thread = nullptr;
@@ -69,11 +68,11 @@
   }
 }
 
 /* ========================================================================== */
 
-static int prepare() {
+static int prepare(JNIEnv* jni) {
   jvmtiError err = jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_MONITOR_CONTENDED_ENTER, nullptr);
   if (err != JVMTI_ERROR_NONE) {
     jni->FatalError("Error enabling JVMTI_EVENT_MONITOR_CONTENDED_ENTER.");
   }
   return JNI_TRUE;
@@ -90,19 +89,18 @@
 }
 
 /* agent algorithm
  */
 static void JNICALL
-agentProc(jvmtiEnv *jvmti, JNIEnv *agentJNI, void *arg) {
-  jni = agentJNI;
+agentProc(jvmtiEnv *jvmti, JNIEnv *jni, void *arg) {
 
   /* wait for initial sync */
   if (!agent_wait_for_sync(timeout)) {
     return;
   }
 
-  if (!prepare()) {
+  if (!prepare(jni)) {
     set_agent_fail_status();
     return;
   }
 
   /* clear events count */
diff a/test/hotspot/jtreg/serviceability/jvmti/events/MonitorContendedEntered/mcontentered01/libmcontentered01.cpp b/test/hotspot/jtreg/serviceability/jvmti/events/MonitorContendedEntered/mcontentered01/libmcontentered01.cpp
--- a/test/hotspot/jtreg/serviceability/jvmti/events/MonitorContendedEntered/mcontentered01/libmcontentered01.cpp
+++ b/test/hotspot/jtreg/serviceability/jvmti/events/MonitorContendedEntered/mcontentered01/libmcontentered01.cpp
@@ -30,11 +30,10 @@
 extern "C" {
 
 /* ========================================================================== */
 
 /* scaffold objects */
-static JNIEnv *jni = nullptr;
 static jvmtiEnv *jvmti = nullptr;
 static jlong timeout = 0;
 
 /* test objects */
 static jthread expected_thread = nullptr;
@@ -125,11 +124,10 @@
 
 /* agent algorithm
  */
 static void JNICALL
 agentProc(jvmtiEnv *jvmti, JNIEnv *agentJNI, void *arg) {
-  jni = agentJNI;
 
 /* wait for initial sync */
   if (!agent_wait_for_sync(timeout))
     return;
 
diff a/test/hotspot/jtreg/serviceability/jvmti/events/MonitorWaited/monitorwaited01/libmonitorwaited01.cpp b/test/hotspot/jtreg/serviceability/jvmti/events/MonitorWaited/monitorwaited01/libmonitorwaited01.cpp
--- a/test/hotspot/jtreg/serviceability/jvmti/events/MonitorWaited/monitorwaited01/libmonitorwaited01.cpp
+++ b/test/hotspot/jtreg/serviceability/jvmti/events/MonitorWaited/monitorwaited01/libmonitorwaited01.cpp
@@ -28,20 +28,23 @@
 #include "jvmti_thread.hpp"
 
 
 extern "C" {
 
+const int MAX_COUNT = 50;
+
 /* scaffold objects */
-static JNIEnv *jni = nullptr;
 static jvmtiEnv *jvmti = nullptr;
 static jlong timeout = 0;
 
 /* test objects */
 static jthread expected_thread = nullptr;
 static jobject expected_object = nullptr;
 static volatile int eventsCount = 0;
 
+static void check_stack_trace(JNIEnv* env, jthread thr);
+
 void JNICALL
 MonitorWaited(jvmtiEnv *jvmti, JNIEnv *jni, jthread thr, jobject obj, jboolean timed_out) {
 
   LOG("MonitorWaited event:\n\tthread: %p, object: %p, timed_out: %s\n",
       thr, obj, (timed_out == JNI_TRUE) ? "true" : "false");
@@ -63,14 +66,53 @@
     if (timed_out == JNI_TRUE) {
       COMPLAIN("Unexpected timed_out value: true\n");
       set_agent_fail_status();
     }
   }
+
+  if (jni->IsVirtualThread(thr)) {
+    check_stack_trace(jni, thr);
+  }
 }
 
 /* ========================================================================== */
 
+static void check_stack_trace(JNIEnv* jni, jthread thr) {
+  jvmtiError err;
+  jint count = 0;
+  jint skipped = 0;
+
+  print_stack_trace(jvmti, jni, nullptr);
+
+  jvmtiFrameInfo frameInfo[MAX_COUNT];
+
+  err = jvmti->GetStackTrace(thr, 0, MAX_COUNT, frameInfo, &count);
+  check_jvmti_status(jni, err, "event handler: error in JVMTI GetStackTrace call");
+
+  const int expected_count = 8;
+  const char* expected_methods[expected_count] = {"wait0", "wait", "run", "runWith", "run", "run", "enter0", "enter"};
+
+  if (count != expected_count) {
+    LOG("Expected 8 methods in the stack but found %d", count);
+    jni->FatalError("Unexpected method count");
+  }
+
+  for (int idx = 0; idx < count; idx++) {
+    jclass declaringClass = nullptr;
+    char *clasSignature = nullptr;
+    char *methodName = nullptr;
+
+    err = jvmti->GetMethodName(frameInfo[idx].method, &methodName, nullptr, nullptr);
+    check_jvmti_status(jni, err, "event handler: error in JVMTI GetMethodName call");
+
+    if (strcmp(methodName, expected_methods[idx]) != 0) {
+      LOG("Expected method %s but found %s", expected_methods[idx], methodName);
+      jni->FatalError("Unexpected method found");
+    }
+  }
+}
+
 static int prepare() {
   /* enable MonitorWait event */
   jvmtiError err = jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_MONITOR_WAITED, nullptr);
   if (err != JVMTI_ERROR_NONE) {
     LOG("Prepare: 11\n");
@@ -88,11 +130,10 @@
   return JNI_TRUE;
 }
 
 static void JNICALL
 agentProc(jvmtiEnv *jvmti, JNIEnv *agentJNI, void *arg) {
-  jni = agentJNI;
 
   /* wait for initial sync */
   if (!agent_wait_for_sync(timeout))
     return;
 
diff a/test/hotspot/jtreg/serviceability/jvmti/events/MonitorWaited/monitorwaited01/monitorwaited01.java b/test/hotspot/jtreg/serviceability/jvmti/events/MonitorWaited/monitorwaited01/monitorwaited01.java
--- a/test/hotspot/jtreg/serviceability/jvmti/events/MonitorWaited/monitorwaited01/monitorwaited01.java
+++ b/test/hotspot/jtreg/serviceability/jvmti/events/MonitorWaited/monitorwaited01/monitorwaited01.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2003, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2003, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -38,10 +38,11 @@
  * COMMENTS
  *     The test updated to match new JVMTI spec 0.2.90:
  *     - change signature of agentProc function
  *       and save JNIEnv pointer now passed as argument.
  *
+ * @requires vm.continuations
  * @library /test/lib
  * @compile monitorwaited01.java
  * @run main/othervm/native -agentlib:monitorwaited01 monitorwaited01 platform
  * @run main/othervm/native -agentlib:monitorwaited01 monitorwaited01 virtual
  */
diff a/test/hotspot/jtreg/serviceability/jvmti/vthread/GetThreadStateMountedTest/GetThreadStateMountedTest.java b/test/hotspot/jtreg/serviceability/jvmti/vthread/GetThreadStateMountedTest/GetThreadStateMountedTest.java
--- a/test/hotspot/jtreg/serviceability/jvmti/vthread/GetThreadStateMountedTest/GetThreadStateMountedTest.java
+++ b/test/hotspot/jtreg/serviceability/jvmti/vthread/GetThreadStateMountedTest/GetThreadStateMountedTest.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -249,17 +249,18 @@
         if (errCount > 0) {
             throw new RuntimeException("Test failed, " + errCount + " errors");
         }
     }
 
+    private static native void runFromNative(Runnable runnable);
+
     private static Thread createPinnedVThread(Runnable runnable) {
-        final Object syncObj = new Object();
-        return Thread.ofVirtual().unstarted(() -> {
-            synchronized (syncObj) {
-                runnable.run();
-            }
-        });
+        return Thread.ofVirtual().unstarted(() -> runFromNative(runnable));
+    }
+
+    private static void runUpcall(Runnable runnable) {
+        runnable.run();
     }
 
     // Native implementation of suspendWaiting.
     // Returns false if the method is not able to reach the desired state in several tries.
     private static native boolean trySuspendInWaitingState(Thread vthread);
diff a/test/hotspot/jtreg/serviceability/jvmti/vthread/GetThreadStateMountedTest/libGetThreadStateMountedTest.cpp b/test/hotspot/jtreg/serviceability/jvmti/vthread/GetThreadStateMountedTest/libGetThreadStateMountedTest.cpp
--- a/test/hotspot/jtreg/serviceability/jvmti/vthread/GetThreadStateMountedTest/libGetThreadStateMountedTest.cpp
+++ b/test/hotspot/jtreg/serviceability/jvmti/vthread/GetThreadStateMountedTest/libGetThreadStateMountedTest.cpp
@@ -189,10 +189,20 @@
 }
 
 
 static std::atomic<bool> time_to_exit(false);
 
+extern "C" JNIEXPORT void JNICALL
+Java_GetThreadStateMountedTest_runFromNative(JNIEnv* jni, jclass clazz, jobject runnable) {
+  jmethodID mid = jni->GetStaticMethodID(clazz, "runUpcall", "(Ljava/lang/Runnable;)V");
+  if (mid == nullptr) {
+    jni->FatalError("failed to get runUpcall method");
+    return;
+  }
+  jni->CallStaticVoidMethod(clazz, mid, runnable);
+}
+
 extern "C" JNIEXPORT void JNICALL
 Java_GetThreadStateMountedTest_waitInNative(JNIEnv* jni, jclass clazz) {
   // Notify main thread that we are ready
   jfieldID fid = jni->GetStaticFieldID(clazz, "waitInNativeReady", "Z");
   if (fid == nullptr) {
diff a/test/hotspot/jtreg/serviceability/jvmti/vthread/StopThreadTest/StopThreadTest.java b/test/hotspot/jtreg/serviceability/jvmti/vthread/StopThreadTest/StopThreadTest.java
--- a/test/hotspot/jtreg/serviceability/jvmti/vthread/StopThreadTest/StopThreadTest.java
+++ b/test/hotspot/jtreg/serviceability/jvmti/vthread/StopThreadTest/StopThreadTest.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -23,27 +23,34 @@
 
 /*
  * @test id=default
  * @summary Verifies JVMTI StopThread support for virtual threads.
  * @requires vm.continuations
+ * @library /test/lib
  * @run main/othervm/native -agentlib:StopThreadTest StopThreadTest
  */
 
 /*
  * @test id=no-vmcontinuations
  * @summary Verifies JVMTI StopThread support for bound virtual threads.
- * @run main/othervm/native -agentlib:StopThreadTest -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations StopThreadTest
+ * @library /test/lib
+ * @run main/othervm/native -agentlib:StopThreadTest -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations -DboundVThread=true StopThreadTest
  */
 
 /*
  * @test id=platform
  * @summary Verifies JVMTI StopThread support for platform threads.
+ * @library /test/lib
  * @run main/othervm/native -agentlib:StopThreadTest StopThreadTest platform
  */
 
+import jdk.test.lib.Platform;
 import java.lang.AssertionError;
 
+import com.sun.management.HotSpotDiagnosticMXBean;
+import java.lang.management.ManagementFactory;
+
 /*
  *     The test exercises the JVMTI function: StopThread(jthread).
  *     The test creates a new virtual or platform thread.
  *     Its method run() invokes the following methods:
  *      - method A() that is blocked on a monitor
@@ -51,11 +58,13 @@
  *      - method C() that forces agent to send AssertionError exception to its own thread
  *     All cases are using JVMTI StopThread to send an AssertionError object.
  */
 public class StopThreadTest {
     private static final String agentLib = "StopThreadTest";
+    static final boolean isBoundVThread = Boolean.getBoolean("boundVThread");
     static final int JVMTI_ERROR_NONE = 0;
+    static final int JVMTI_ERROR_OPAQUE_FRAME = 32;
     static final int THREAD_NOT_SUSPENDED = 13;
     static final int PASSED = 0;
     static final int FAILED = 2;
 
     static void log(String str) { System.out.println(str); }
@@ -102,11 +111,11 @@
             if (is_virtual) {
                 testTaskThread = Thread.ofVirtual().name("TestTaskThread").start(testTask);
             } else {
                 testTaskThread = Thread.ofPlatform().name("TestTaskThread").start(testTask);
             }
-            TestTask.ensureAtPointA();
+            TestTask.ensureAtPointA(testTaskThread);
 
             if (is_virtual) { // this check is for virtual target thread only
                 log("\nMain #A.1: unsuspended");
                 retCode = stopThread(testTaskThread);
                 if (retCode != THREAD_NOT_SUSPENDED) {
@@ -117,14 +126,16 @@
             }
 
             log("\nMain #A.2: suspended");
             suspendThread(testTaskThread);
             retCode = stopThread(testTaskThread);
-            if (retCode != JVMTI_ERROR_NONE) {
-                throwFailed("Main #A.2: expected JVMTI_ERROR_NONE instead of: " + retCode);
+            int expectedRetCode = preemptableVirtualThread() ? JVMTI_ERROR_OPAQUE_FRAME : JVMTI_ERROR_NONE;
+            String expectedRetCodeName = preemptableVirtualThread() ? "JVMTI_ERROR_OPAQUE_FRAME" : "JVMTI_ERROR_NONE";
+            if (retCode != expectedRetCode) {
+                throwFailed("Main #A.2: expected " + expectedRetCodeName + " instead of: " + retCode);
             } else {
-                log("Main #A.2: got expected JVMTI_ERROR_NONE");
+                log("Main #A.2: got expected " + expectedRetCodeName);
             }
             resumeThread(testTaskThread);
         }
         log("\nMain #B: method B() must be blocked in a breakpoint event handler");
         {
@@ -177,12 +188,13 @@
             } catch (InterruptedException e) {
                 throw new RuntimeException("Interruption in TestTask.sleep: \n\t" + e);
             }
         }
 
-        static void ensureAtPointA() {
-            while (!atPointA) {
+        static void ensureAtPointA(Thread vt) {
+            // wait while the thread state is not the expected one
+            while (vt.getState() != Thread.State.BLOCKED) {
                 sleep(1);
             }
         }
 
         // Ensure thread is finished.
@@ -201,11 +213,11 @@
             } catch (AssertionError ex) {
                 log("TestTask.run: caught expected AssertionError from method A()");
                 seenExceptionFromA = true;
             }
             Thread.interrupted();
-            if (!seenExceptionFromA) {
+            if (!seenExceptionFromA && !preemptableVirtualThread()) {
                 StopThreadTest.setFailed("TestTask.run: expected AssertionError from method A()");
             }
             sleep(1); // to cause yield
 
             boolean seenExceptionFromB = false;
@@ -239,11 +251,10 @@
         // StopThread is used to send an AssertionError object two times:
         //  - when not suspended: THREAD_NOT_SUSPENDED is expected
         //  - when suspended: JVMTI_ERROR_NONE is expected
         static void A() {
             log("TestTask.A: started");
-            atPointA = true;
             synchronized (lock) {
             }
             log("TestTask.A: finished");
         }
 
@@ -261,6 +272,12 @@
             log("TestTask.C: started");
             StopThreadTest.stopThread(Thread.currentThread());
             log("TestTask.C: finished");
         }
     }
+
+    static boolean preemptableVirtualThread() {
+        boolean legacyLockingMode = ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class)
+                                        .getVMOption("LockingMode").getValue().equals("1");
+        return is_virtual && !isBoundVThread && !legacyLockingMode;
+    }
 }
diff a/test/hotspot/jtreg/serviceability/jvmti/vthread/SuspendResume1/SuspendResume1.java b/test/hotspot/jtreg/serviceability/jvmti/vthread/SuspendResume1/SuspendResume1.java
--- a/test/hotspot/jtreg/serviceability/jvmti/vthread/SuspendResume1/SuspendResume1.java
+++ b/test/hotspot/jtreg/serviceability/jvmti/vthread/SuspendResume1/SuspendResume1.java
@@ -23,19 +23,33 @@
 
 /*
  * @test id=default
  * @summary Test SuspendThread/ResumeThread, SuspendThreadList/ResumeThreadList
  *          for virtual threads.
+ * @requires vm.debug != true
  * @library /test/lib
  * @compile SuspendResume1.java
  * @run driver jdk.test.lib.FileInstaller . .
  * @run main/othervm/native/timeout=600
  *      -Djdk.virtualThreadScheduler.maxPoolSize=1
  *      -agentlib:SuspendResume1
  *      SuspendResume1
  */
 
+/*
+ * @test id=debug
+ * @requires vm.debug == true
+ * @library /test/lib
+ * @compile SuspendResume1.java
+ * @run driver jdk.test.lib.FileInstaller . .
+ * @run main/othervm/native/timeout=600
+ *      -Djdk.virtualThreadScheduler.maxPoolSize=1
+ *      -agentlib:SuspendResume1
+ *      -XX:-VerifyContinuations
+ *      SuspendResume1
+ */
+
  /*
  * @test id=no-vmcontinuations
  * @requires vm.continuations
  * @library /test/lib
  * @compile SuspendResume1.java
@@ -166,11 +180,11 @@
 
     // ensure thread is ready
     public void ensureReady() {
         try {
             while (!threadReady) {
-                sleep(1000);
+                sleep(100);
             }
         } catch (InterruptedException e) {
             throw new RuntimeException("Interruption while preparing tested thread: \n\t" + e);
         }
     }
diff a/test/hotspot/jtreg/serviceability/jvmti/vthread/SuspendResume2/SuspendResume2.java b/test/hotspot/jtreg/serviceability/jvmti/vthread/SuspendResume2/SuspendResume2.java
--- a/test/hotspot/jtreg/serviceability/jvmti/vthread/SuspendResume2/SuspendResume2.java
+++ b/test/hotspot/jtreg/serviceability/jvmti/vthread/SuspendResume2/SuspendResume2.java
@@ -22,19 +22,33 @@
  */
 
 /*
  * @test id=default
  * @summary Test SuspendAllVirtualThreads/ResumeAllVirtualThreads
+ * @requires vm.debug != true
  * @library /test/lib
  * @compile SuspendResume2.java
  * @run driver jdk.test.lib.FileInstaller . .
  * @run main/othervm/native
  *      -Djdk.virtualThreadScheduler.maxPoolSize=1
  *      -agentlib:SuspendResume2
  *      SuspendResume2
  */
 
+/*
+ * @test id=debug
+ * @requires vm.debug == true
+ * @library /test/lib
+ * @compile SuspendResume2.java
+ * @run driver jdk.test.lib.FileInstaller . .
+ * @run main/othervm/native
+ *      -Djdk.virtualThreadScheduler.maxPoolSize=1
+ *      -agentlib:SuspendResume2
+ *      -XX:-VerifyContinuations
+ *      SuspendResume2
+ */
+
 /*
  * @test id=no-vmcontinuations
  * @requires vm.continuations
  * @library /test/lib
  * @compile SuspendResume2.java
@@ -146,11 +160,11 @@
         int i = 0;
         int n = 1000;
         while (!shouldFinish) {
             if (n <= 0) {
                 n = 1000;
-                SuspendResume2.sleep(1);
+                SuspendResume2.sleep(10);
             }
             if (i > n) {
                 i = 0;
                 n = n - 1;
             }
@@ -160,11 +174,11 @@
 
     // ensure thread is ready
     public void ensureReady() {
         try {
             while (!threadReady) {
-                sleep(1);
+                sleep(100);
             }
         } catch (InterruptedException e) {
             throw new RuntimeException("Interruption while preparing tested thread: \n\t" + e);
         }
     }
diff a/test/hotspot/jtreg/serviceability/jvmti/vthread/SuspendWithInterruptLock/SuspendWithInterruptLock.java b/test/hotspot/jtreg/serviceability/jvmti/vthread/SuspendWithInterruptLock/SuspendWithInterruptLock.java
--- a/test/hotspot/jtreg/serviceability/jvmti/vthread/SuspendWithInterruptLock/SuspendWithInterruptLock.java
+++ b/test/hotspot/jtreg/serviceability/jvmti/vthread/SuspendWithInterruptLock/SuspendWithInterruptLock.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -30,17 +30,24 @@
  * @run main/othervm SuspendWithInterruptLock
  */
 
 /**
  * @test id=xint
- * @summary Do not suspend virtual threads in a critical section.
- * @bug 8311218
+ * @requires vm.debug != true
  * @requires vm.continuations
  * @library /testlibrary
  * @run main/othervm -Xint SuspendWithInterruptLock
  */
 
+/**
+ * @test id=xint-debug
+ * @requires vm.debug == true
+ * @requires vm.continuations
+ * @library /testlibrary
+ * @run main/othervm -Xint -XX:-VerifyContinuations SuspendWithInterruptLock
+ */
+
 import jvmti.JVMTIUtils;
 
 public class SuspendWithInterruptLock {
     static volatile boolean done;
 
diff a/test/hotspot/jtreg/serviceability/sa/ClhsdbInspect.java b/test/hotspot/jtreg/serviceability/sa/ClhsdbInspect.java
--- a/test/hotspot/jtreg/serviceability/sa/ClhsdbInspect.java
+++ b/test/hotspot/jtreg/serviceability/sa/ClhsdbInspect.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -60,12 +60,12 @@
             // the key is a literal, searched in the jstack output; the value is a regex searched in the clhsdb output
             Map<String, String> tokensMap = new HashMap<>();
             tokensMap.put("(a java.lang.Class for LingeredAppWithLock)",
                           "instance of Oop for java/lang/Class");
             tokensMap.put("Method*=", "Type is Method");
-            tokensMap.put("(a java/util/concurrent/locks/AbstractQueuedSynchronizer$ConditionObject)",
-                          "instance of Oop for java/util/concurrent/locks/AbstractQueuedSynchronizer\\$ConditionObject");
+            tokensMap.put("(a java.lang.ref.ReferenceQueue$Lock)",
+                          "instance of Oop for java/lang/ref/ReferenceQueue\\$Lock");
 
             String[] lines = jstackOutput.split("\\R");
 
             for (String key: tokensMap.keySet()) {
                 cmds = new ArrayList<String>();
diff a/test/hotspot/jtreg/vmTestbase/nsk/jvmti/scenarios/bcinstr/BI04/bi04t002/newclass02/java.base/java/lang/Object.java b/test/hotspot/jtreg/vmTestbase/nsk/jvmti/scenarios/bcinstr/BI04/bi04t002/newclass02/java.base/java/lang/Object.java
--- a/test/hotspot/jtreg/vmTestbase/nsk/jvmti/scenarios/bcinstr/BI04/bi04t002/newclass02/java.base/java/lang/Object.java
+++ b/test/hotspot/jtreg/vmTestbase/nsk/jvmti/scenarios/bcinstr/BI04/bi04t002/newclass02/java.base/java/lang/Object.java
@@ -21,206 +21,251 @@
  * questions.
  */
 
 package java.lang;
 
-import jdk.internal.misc.Blocker;
+import jdk.internal.vm.annotation.IntrinsicCandidate;
 import nsk.jvmti.scenarios.bcinstr.BI04.bi04t002a;
 
 /**
- * Class <code>Object</code> is the root of the class hierarchy.
- * Every class has <code>Object</code> as a superclass. All objects,
+ * Class {@code Object} is the root of the class hierarchy.
+ * Every class has {@code Object} as a superclass. All objects,
  * including arrays, implement the methods of this class.
  *
- * @author  unascribed
- * @version 1.67, 02/03/04
  * @see     java.lang.Class
- * @since   JDK1.0
+ * @since   1.0
  */
 public class Object {
 
     /**
-     * Returns the runtime class of an object. That <tt>Class</tt>
-     * object is the object that is locked by <tt>static synchronized</tt>
-     * methods of the represented class.
+     * Constructs a new object.
+     */
+    @IntrinsicCandidate
+    public Object() {}
+
+    /**
+     * Returns the runtime class of this {@code Object}. The returned
+     * {@code Class} object is the object that is locked by {@code
+     * static synchronized} methods of the represented class.
+     *
+     * <p><b>The actual result type is {@code Class<? extends |X|>}
+     * where {@code |X|} is the erasure of the static type of the
+     * expression on which {@code getClass} is called.</b> For
+     * example, no cast is required in this code fragment:</p>
      *
-     * @return The <code>java.lang.Class</code> object that represents
-     *         the runtime class of the object.  The result is of type
-     *         {@code Class<? extends X>} where X is the
-     *         static type of the expression on which
-     *         <code>getClass</code> is called.
+     * <p>
+     * {@code Number n = 0;                             }<br>
+     * {@code Class<? extends Number> c = n.getClass(); }
+     * </p>
+     *
+     * @return The {@code Class} object that represents the runtime
+     *         class of this object.
+     * @jls 15.8.2 Class Literals
      */
-    public final native Class<? extends Object> getClass();
+    @IntrinsicCandidate
+    public final native Class<?> getClass();
 
     /**
-     * Returns a hash code value for the object. This method is
-     * supported for the benefit of hashtables such as those provided by
-     * <code>java.util.Hashtable</code>.
+     * {@return a hash code value for this object} This method is
+     * supported for the benefit of hash tables such as those provided by
+     * {@link java.util.HashMap}.
      * <p>
-     * The general contract of <code>hashCode</code> is:
+     * The general contract of {@code hashCode} is:
      * <ul>
      * <li>Whenever it is invoked on the same object more than once during
-     *     an execution of a Java application, the <tt>hashCode</tt> method
+     *     an execution of a Java application, the {@code hashCode} method
      *     must consistently return the same integer, provided no information
-     *     used in <tt>equals</tt> comparisons on the object is modified.
+     *     used in {@code equals} comparisons on the object is modified.
      *     This integer need not remain consistent from one execution of an
      *     application to another execution of the same application.
-     * <li>If two objects are equal according to the <tt>equals(Object)</tt>
-     *     method, then calling the <code>hashCode</code> method on each of
-     *     the two objects must produce the same integer result.
+     * <li>If two objects are equal according to the {@link
+     *     #equals(Object) equals} method, then calling the {@code
+     *     hashCode} method on each of the two objects must produce the
+     *     same integer result.
      * <li>It is <em>not</em> required that if two objects are unequal
-     *     according to the {@link java.lang.Object#equals(java.lang.Object)}
-     *     method, then calling the <tt>hashCode</tt> method on each of the
-     *     two objects must produce distinct integer results.  However, the
-     *     programmer should be aware that producing distinct integer results
-     *     for unequal objects may improve the performance of hashtables.
+     *     according to the {@link #equals(Object) equals} method, then
+     *     calling the {@code hashCode} method on each of the two objects
+     *     must produce distinct integer results.  However, the programmer
+     *     should be aware that producing distinct integer results for
+     *     unequal objects may improve the performance of hash tables.
      * </ul>
-     * <p>
-     * As much as is reasonably practical, the hashCode method defined by
-     * class <tt>Object</tt> does return distinct integers for distinct
-     * objects. (This is typically implemented by converting the internal
-     * address of the object into an integer, but this implementation
-     * technique is not required by the
-     * Java<font size="-2"><sup>TM</sup></font> programming language.)
      *
-     * @return  a hash code value for this object.
+     * @implSpec
+     * As far as is reasonably practical, the {@code hashCode} method defined
+     * by class {@code Object} returns distinct integers for distinct objects.
+     *
+     * @apiNote
+     * The {@link java.util.Objects#hash(Object...) hash} and {@link
+     * java.util.Objects#hashCode(Object) hashCode} methods of {@link
+     * java.util.Objects} can be used to help construct simple hash codes.
+     *
      * @see     java.lang.Object#equals(java.lang.Object)
-     * @see     java.util.Hashtable
+     * @see     java.lang.System#identityHashCode
      */
+    @IntrinsicCandidate
     public native int hashCode();
 
     /**
      * Indicates whether some other object is "equal to" this one.
      * <p>
-     * The <code>equals</code> method implements an equivalence relation
+     * The {@code equals} method implements an equivalence relation
      * on non-null object references:
      * <ul>
      * <li>It is <i>reflexive</i>: for any non-null reference value
-     *     <code>x</code>, <code>x.equals(x)</code> should return
-     *     <code>true</code>.
+     *     {@code x}, {@code x.equals(x)} should return
+     *     {@code true}.
      * <li>It is <i>symmetric</i>: for any non-null reference values
-     *     <code>x</code> and <code>y</code>, <code>x.equals(y)</code>
-     *     should return <code>true</code> if and only if
-     *     <code>y.equals(x)</code> returns <code>true</code>.
+     *     {@code x} and {@code y}, {@code x.equals(y)}
+     *     should return {@code true} if and only if
+     *     {@code y.equals(x)} returns {@code true}.
      * <li>It is <i>transitive</i>: for any non-null reference values
-     *     <code>x</code>, <code>y</code>, and <code>z</code>, if
-     *     <code>x.equals(y)</code> returns <code>true</code> and
-     *     <code>y.equals(z)</code> returns <code>true</code>, then
-     *     <code>x.equals(z)</code> should return <code>true</code>.
+     *     {@code x}, {@code y}, and {@code z}, if
+     *     {@code x.equals(y)} returns {@code true} and
+     *     {@code y.equals(z)} returns {@code true}, then
+     *     {@code x.equals(z)} should return {@code true}.
      * <li>It is <i>consistent</i>: for any non-null reference values
-     *     <code>x</code> and <code>y</code>, multiple invocations of
-     *     <tt>x.equals(y)</tt> consistently return <code>true</code>
-     *     or consistently return <code>false</code>, provided no
-     *     information used in <code>equals</code> comparisons on the
+     *     {@code x} and {@code y}, multiple invocations of
+     *     {@code x.equals(y)} consistently return {@code true}
+     *     or consistently return {@code false}, provided no
+     *     information used in {@code equals} comparisons on the
      *     objects is modified.
-     * <li>For any non-null reference value <code>x</code>,
-     *     <code>x.equals(null)</code> should return <code>false</code>.
+     * <li>For any non-null reference value {@code x},
+     *     {@code x.equals(null)} should return {@code false}.
      * </ul>
+     *
      * <p>
-     * The <tt>equals</tt> method for class <code>Object</code> implements
+     * An equivalence relation partitions the elements it operates on
+     * into <i>equivalence classes</i>; all the members of an
+     * equivalence class are equal to each other. Members of an
+     * equivalence class are substitutable for each other, at least
+     * for some purposes.
+     *
+     * @implSpec
+     * The {@code equals} method for class {@code Object} implements
      * the most discriminating possible equivalence relation on objects;
-     * that is, for any non-null reference values <code>x</code> and
-     * <code>y</code>, this method returns <code>true</code> if and only
-     * if <code>x</code> and <code>y</code> refer to the same object
-     * (<code>x == y</code> has the value <code>true</code>).
-     * <p>
-     * Note that it is generally necessary to override the <tt>hashCode</tt>
+     * that is, for any non-null reference values {@code x} and
+     * {@code y}, this method returns {@code true} if and only
+     * if {@code x} and {@code y} refer to the same object
+     * ({@code x == y} has the value {@code true}).
+     *
+     * In other words, under the reference equality equivalence
+     * relation, each equivalence class only has a single element.
+     *
+     * @apiNote
+     * It is generally necessary to override the {@link #hashCode() hashCode}
      * method whenever this method is overridden, so as to maintain the
-     * general contract for the <tt>hashCode</tt> method, which states
+     * general contract for the {@code hashCode} method, which states
      * that equal objects must have equal hash codes.
+     * <p>The two-argument {@link java.util.Objects#equals(Object,
+     * Object) Objects.equals} method implements an equivalence relation
+     * on two possibly-null object references.
      *
      * @param   obj   the reference object with which to compare.
-     * @return  <code>true</code> if this object is the same as the obj
-     *          argument; <code>false</code> otherwise.
+     * @return  {@code true} if this object is the same as the obj
+     *          argument; {@code false} otherwise.
      * @see     #hashCode()
-     * @see     java.util.Hashtable
+     * @see     java.util.HashMap
      */
     public boolean equals(Object obj) {
         bi04t002a.instrInvoke(bi04t002a.INSTR_EQUALS);
         return (this == obj);
     }
 
     /**
      * Creates and returns a copy of this object.  The precise meaning
      * of "copy" may depend on the class of the object. The general
-     * intent is that, for any object <tt>x</tt>, the expression:
+     * intent is that, for any object {@code x}, the expression:
      * <blockquote>
      * <pre>
      * x.clone() != x</pre></blockquote>
      * will be true, and that the expression:
      * <blockquote>
      * <pre>
      * x.clone().getClass() == x.getClass()</pre></blockquote>
-     * will be <tt>true</tt>, but these are not absolute requirements.
+     * will be {@code true}, but these are not absolute requirements.
      * While it is typically the case that:
      * <blockquote>
      * <pre>
      * x.clone().equals(x)</pre></blockquote>
-     * will be <tt>true</tt>, this is not an absolute requirement.
+     * will be {@code true}, this is not an absolute requirement.
      * <p>
      * By convention, the returned object should be obtained by calling
-     * <tt>super.clone</tt>.  If a class and all of its superclasses (except
-     * <tt>Object</tt>) obey this convention, it will be the case that
-     * <tt>x.clone().getClass() == x.getClass()</tt>.
+     * {@code super.clone}.  If a class and all of its superclasses (except
+     * {@code Object}) obey this convention, it will be the case that
+     * {@code x.clone().getClass() == x.getClass()}.
      * <p>
      * By convention, the object returned by this method should be independent
      * of this object (which is being cloned).  To achieve this independence,
      * it may be necessary to modify one or more fields of the object returned
-     * by <tt>super.clone</tt> before returning it.  Typically, this means
+     * by {@code super.clone} before returning it.  Typically, this means
      * copying any mutable objects that comprise the internal "deep structure"
      * of the object being cloned and replacing the references to these
      * objects with references to the copies.  If a class contains only
      * primitive fields or references to immutable objects, then it is usually
-     * the case that no fields in the object returned by <tt>super.clone</tt>
+     * the case that no fields in the object returned by {@code super.clone}
      * need to be modified.
-     * <p>
-     * The method <tt>clone</tt> for class <tt>Object</tt> performs a
+     *
+     * @implSpec
+     * The method {@code clone} for class {@code Object} performs a
      * specific cloning operation. First, if the class of this object does
-     * not implement the interface <tt>Cloneable</tt>, then a
-     * <tt>CloneNotSupportedException</tt> is thrown. Note that all arrays
-     * are considered to implement the interface <tt>Cloneable</tt>.
+     * not implement the interface {@code Cloneable}, then a
+     * {@code CloneNotSupportedException} is thrown. Note that all arrays
+     * are considered to implement the interface {@code Cloneable} and that
+     * the return type of the {@code clone} method of an array type {@code T[]}
+     * is {@code T[]} where T is any reference or primitive type.
      * Otherwise, this method creates a new instance of the class of this
      * object and initializes all its fields with exactly the contents of
      * the corresponding fields of this object, as if by assignment; the
      * contents of the fields are not themselves cloned. Thus, this method
      * performs a "shallow copy" of this object, not a "deep copy" operation.
      * <p>
-     * The class <tt>Object</tt> does not itself implement the interface
-     * <tt>Cloneable</tt>, so calling the <tt>clone</tt> method on an object
-     * whose class is <tt>Object</tt> will result in throwing an
+     * The class {@code Object} does not itself implement the interface
+     * {@code Cloneable}, so calling the {@code clone} method on an object
+     * whose class is {@code Object} will result in throwing an
      * exception at run time.
      *
      * @return     a clone of this instance.
-     * @exception  CloneNotSupportedException  if the object's class does not
-     *               support the <code>Cloneable</code> interface. Subclasses
-     *               that override the <code>clone</code> method can also
+     * @throws  CloneNotSupportedException  if the object's class does not
+     *               support the {@code Cloneable} interface. Subclasses
+     *               that override the {@code clone} method can also
      *               throw this exception to indicate that an instance cannot
      *               be cloned.
      * @see java.lang.Cloneable
      */
+    @IntrinsicCandidate
     protected native Object clone() throws CloneNotSupportedException;
 
     /**
-     * Returns a string representation of the object. In general, the
-     * <code>toString</code> method returns a string that
+     * {@return a string representation of the object}
+     *
+     * Satisfying this method's contract implies a non-{@code null}
+     * result must be returned.
+     *
+     * @apiNote
+     * In general, the
+     * {@code toString} method returns a string that
      * "textually represents" this object. The result should
      * be a concise but informative representation that is easy for a
      * person to read.
      * It is recommended that all subclasses override this method.
-     * <p>
-     * The <code>toString</code> method for class <code>Object</code>
+     * The string output is not necessarily stable over time or across
+     * JVM invocations.
+     * @implSpec
+     * The {@code toString} method for class {@code Object}
      * returns a string consisting of the name of the class of which the
-     * object is an instance, the at-sign character `<code>@</code>', and
+     * object is an instance, the at-sign character `{@code @}', and
      * the unsigned hexadecimal representation of the hash code of the
      * object. In other words, this method returns a string equal to the
      * value of:
-     * <blockquote>
-     * <pre>
+     * {@snippet lang=java :
      * getClass().getName() + '@' + Integer.toHexString(hashCode())
-     * </pre></blockquote>
-     *
-     * @return  a string representation of the object.
+     * }
+     * The {@link java.util.Objects#toIdentityString(Object)
+     * Objects.toIdentityString} method returns the string for an
+     * object equal to the string that would be returned if neither
+     * the {@code toString} nor {@code hashCode} methods were
+     * overridden by the object's class.
      */
     public String toString() {
         bi04t002a.instrInvoke(bi04t002a.INSTR_TOSTRING);
         return getClass().getName() + "@" + Integer.toHexString(hashCode());
     }
@@ -228,11 +273,11 @@
     /**
      * Wakes up a single thread that is waiting on this object's
      * monitor. If any threads are waiting on this object, one of them
      * is chosen to be awakened. The choice is arbitrary and occurs at
      * the discretion of the implementation. A thread waits on an object's
-     * monitor by calling one of the <code>wait</code> methods.
+     * monitor by calling one of the {@code wait} methods.
      * <p>
      * The awakened thread will not be able to proceed until the current
      * thread relinquishes the lock on this object. The awakened thread will
      * compete in the usual manner with any other threads that might be
      * actively competing to synchronize on this object; for example, the
@@ -242,325 +287,333 @@
      * This method should only be called by a thread that is the owner
      * of this object's monitor. A thread becomes the owner of the
      * object's monitor in one of three ways:
      * <ul>
      * <li>By executing a synchronized instance method of that object.
-     * <li>By executing the body of a <code>synchronized</code> statement
+     * <li>By executing the body of a {@code synchronized} statement
      *     that synchronizes on the object.
-     * <li>For objects of type <code>Class,</code> by executing a
-     *     synchronized static method of that class.
+     * <li>For objects of type {@code Class,} by executing a
+     *     static synchronized method of that class.
      * </ul>
      * <p>
      * Only one thread at a time can own an object's monitor.
      *
-     * @exception  IllegalMonitorStateException  if the current thread is not
+     * @throws  IllegalMonitorStateException  if the current thread is not
      *               the owner of this object's monitor.
      * @see        java.lang.Object#notifyAll()
      * @see        java.lang.Object#wait()
      */
+    @IntrinsicCandidate
     public final native void notify();
 
     /**
      * Wakes up all threads that are waiting on this object's monitor. A
      * thread waits on an object's monitor by calling one of the
-     * <code>wait</code> methods.
+     * {@code wait} methods.
      * <p>
      * The awakened threads will not be able to proceed until the current
      * thread relinquishes the lock on this object. The awakened threads
      * will compete in the usual manner with any other threads that might
      * be actively competing to synchronize on this object; for example,
      * the awakened threads enjoy no reliable privilege or disadvantage in
      * being the next thread to lock this object.
      * <p>
      * This method should only be called by a thread that is the owner
-     * of this object's monitor. See the <code>notify</code> method for a
+     * of this object's monitor. See the {@code notify} method for a
      * description of the ways in which a thread can become the owner of
      * a monitor.
      *
-     * @exception  IllegalMonitorStateException  if the current thread is not
+     * @throws  IllegalMonitorStateException  if the current thread is not
      *               the owner of this object's monitor.
      * @see        java.lang.Object#notify()
      * @see        java.lang.Object#wait()
      */
+    @IntrinsicCandidate
     public final native void notifyAll();
 
     /**
-     * Causes current thread to wait until either another thread invokes the
-     * {@link java.lang.Object#notify()} method or the
-     * {@link java.lang.Object#notifyAll()} method for this object, or a
-     * specified amount of time has elapsed.
-     * <p>
-     * The current thread must own this object's monitor.
-     * <p>
-     * This method causes the current thread (call it <var>T</var>) to
-     * place itself in the wait set for this object and then to relinquish
-     * any and all synchronization claims on this object. Thread <var>T</var>
-     * becomes disabled for thread scheduling purposes and lies dormant
-     * until one of four things happens:
-     * <ul>
-     * <li>Some other thread invokes the <tt>notify</tt> method for this
-     * object and thread <var>T</var> happens to be arbitrarily chosen as
-     * the thread to be awakened.
-     * <li>Some other thread invokes the <tt>notifyAll</tt> method for this
-     * object.
-     * <li>Some other thread {@link java.lang.Thread#interrupt() interrupts}
-     * thread <var>T</var>.
-     * <li>The specified amount of real time has elapsed, more or less.  If
-     * <tt>timeout</tt> is zero, however, then real time is not taken into
-     * consideration and the thread simply waits until notified.
-     * </ul>
-     * The thread <var>T</var> is then removed from the wait set for this
-     * object and re-enabled for thread scheduling. It then competes in the
-     * usual manner with other threads for the right to synchronize on the
-     * object; once it has gained control of the object, all its
-     * synchronization claims on the object are restored to the status quo
-     * ante - that is, to the situation as of the time that the <tt>wait</tt>
-     * method was invoked. Thread <var>T</var> then returns from the
-     * invocation of the <tt>wait</tt> method. Thus, on return from the
-     * <tt>wait</tt> method, the synchronization state of the object and of
-     * thread <tt>T</tt> is exactly as it was when the <tt>wait</tt> method
-     * was invoked.
+     * Causes the current thread to wait until it is awakened, typically
+     * by being <em>notified</em> or <em>interrupted</em>.
      * <p>
-     * A thread can also wake up without being notified, interrupted, or
-     * timing out, a so-called <i>spurious wakeup</i>.  While this will rarely
-     * occur in practice, applications must guard against it by testing for
-     * the condition that should have caused the thread to be awakened, and
-     * continuing to wait if the condition is not satisfied.  In other words,
-     * waits should always occur in loops, like this one:
-     * <pre>
-     *     synchronized (obj) {
-     *         while (&lt;condition does not hold&gt;)
-     *             obj.wait(timeout);
-     *         ... // Perform action appropriate to condition
-     *     }
-     * </pre>
-     * (For more information on this topic, see Section 3.2.3 in Doug Lea's
-     * "Concurrent Programming in Java (Second Edition)" (Addison-Wesley,
-     * 2000), or Item 50 in Joshua Bloch's "Effective Java Programming
-     * Language Guide" (Addison-Wesley, 2001).
-     * <p>
-     * If the current thread is
-     * {@link java.lang.Thread#interrupt() interrupted} by another thread
-     * while it is waiting, then an <tt>InterruptedException</tt> is thrown.
-     * This exception is not thrown until the lock status of this object has
-     * been restored as described above.
-     * <p>
-     * Note that the <tt>wait</tt> method, as it places the current thread
-     * into the wait set for this object, unlocks only this object; any
-     * other objects on which the current thread may be synchronized remain
-     * locked while the thread waits.
+     * In all respects, this method behaves as if {@code wait(0L, 0)}
+     * had been called. See the specification of the {@link #wait(long, int)} method
+     * for details.
+     *
+     * @throws IllegalMonitorStateException if the current thread is not
+     *         the owner of the object's monitor
+     * @throws InterruptedException if any thread interrupted the current thread before or
+     *         while the current thread was waiting. The <em>interrupted status</em> of the
+     *         current thread is cleared when this exception is thrown.
+     * @see    #notify()
+     * @see    #notifyAll()
+     * @see    #wait(long)
+     * @see    #wait(long, int)
+     */
+    public final void wait() throws InterruptedException {
+        bi04t002a.instrInvoke(bi04t002a.INSTR_WAIT);
+        wait(0L);
+    }
+
+    /**
+     * Causes the current thread to wait until it is awakened, typically
+     * by being <em>notified</em> or <em>interrupted</em>, or until a
+     * certain amount of real time has elapsed.
      * <p>
-     * This method should only be called by a thread that is the owner
-     * of this object's monitor. See the <code>notify</code> method for a
-     * description of the ways in which a thread can become the owner of
-     * a monitor.
+     * In all respects, this method behaves as if {@code wait(timeoutMillis, 0)}
+     * had been called. See the specification of the {@link #wait(long, int)} method
+     * for details.
      *
-     * @param      timeout   the maximum time to wait in milliseconds.
-     * @exception  IllegalArgumentException      if the value of timeout is
-     *               negative.
-     * @exception  IllegalMonitorStateException  if the current thread is not
-     *               the owner of the object's monitor.
-     * @exception  InterruptedException if another thread interrupted the
-     *             current thread before or while the current thread
-     *             was waiting for a notification.  The <i>interrupted
-     *             status</i> of the current thread is cleared when
-     *             this exception is thrown.
-     * @see        java.lang.Object#notify()
-     * @see        java.lang.Object#notifyAll()
+     * @param  timeoutMillis the maximum time to wait, in milliseconds
+     * @throws IllegalArgumentException if {@code timeoutMillis} is negative
+     * @throws IllegalMonitorStateException if the current thread is not
+     *         the owner of the object's monitor
+     * @throws InterruptedException if any thread interrupted the current thread before or
+     *         while the current thread was waiting. The <em>interrupted status</em> of the
+     *         current thread is cleared when this exception is thrown.
+     * @see    #notify()
+     * @see    #notifyAll()
+     * @see    #wait()
+     * @see    #wait(long, int)
      */
     public final void wait(long timeoutMillis) throws InterruptedException {
-        if (!Thread.currentThread().isVirtual()) {
-            wait0(timeoutMillis);
-            return;
+        if (timeoutMillis < 0) {
+            throw new IllegalArgumentException("timeout value is negative");
         }
 
-        // virtual thread waiting
-        boolean attempted = Blocker.begin();
-        try {
+        if (Thread.currentThread() instanceof VirtualThread vthread) {
+            try {
+                wait0(timeoutMillis);
+            } catch (InterruptedException e) {
+                // virtual thread's interrupt status needs to be cleared
+                vthread.getAndClearInterrupt();
+                throw e;
+            } finally {
+                if (timeoutMillis > 0) {
+                    vthread.cancelWaitTimeout();
+                }
+            }
+        } else {
             wait0(timeoutMillis);
-        } catch (InterruptedException e) {
-            // virtual thread's interrupt status needs to be cleared
-            Thread.currentThread().getAndClearInterrupt();
-            throw e;
-        } finally {
-            Blocker.end(attempted);
         }
     }
 
+    // final modifier so method not in vtable
+    private final native void wait0(long timeoutMillis) throws InterruptedException;
+
     /**
-     * Causes current thread to wait until another thread invokes the
-     * {@link java.lang.Object#notify()} method or the
-     * {@link java.lang.Object#notifyAll()} method for this object, or
-     * some other thread interrupts the current thread, or a certain
-     * amount of real time has elapsed.
-     * <p>
-     * This method is similar to the <code>wait</code> method of one
-     * argument, but it allows finer control over the amount of time to
-     * wait for a notification before giving up. The amount of real time,
-     * measured in nanoseconds, is given by:
-     * <blockquote>
-     * <pre>
-     * 1000000*timeout+nanos</pre></blockquote>
-     * <p>
-     * In all other respects, this method does the same thing as the
-     * method {@link #wait(long)} of one argument. In particular,
-     * <tt>wait(0, 0)</tt> means the same thing as <tt>wait(0)</tt>.
-     * <p>
-     * The current thread must own this object's monitor. The thread
-     * releases ownership of this monitor and waits until either of the
-     * following two conditions has occurred:
+     * Causes the current thread to wait until it is awakened, typically
+     * by being <em>notified</em> or <em>interrupted</em>, or until a
+     * certain amount of real time has elapsed.
+     * <p>
+     * The current thread must own this object's monitor lock. See the
+     * {@link #notify notify} method for a description of the ways in which
+     * a thread can become the owner of a monitor lock.
+     * <p>
+     * This method causes the current thread (referred to here as <var>T</var>) to
+     * place itself in the wait set for this object and then to relinquish any
+     * and all synchronization claims on this object. Note that only the locks
+     * on this object are relinquished; any other objects on which the current
+     * thread may be synchronized remain locked while the thread waits.
+     * <p>
+     * Thread <var>T</var> then becomes disabled for thread scheduling purposes
+     * and lies dormant until one of the following occurs:
      * <ul>
-     * <li>Another thread notifies threads waiting on this object's monitor
-     *     to wake up either through a call to the <code>notify</code> method
-     *     or the <code>notifyAll</code> method.
-     * <li>The timeout period, specified by <code>timeout</code>
-     *     milliseconds plus <code>nanos</code> nanoseconds arguments, has
-     *     elapsed.
+     * <li>Some other thread invokes the {@code notify} method for this
+     * object and thread <var>T</var> happens to be arbitrarily chosen as
+     * the thread to be awakened.
+     * <li>Some other thread invokes the {@code notifyAll} method for this
+     * object.
+     * <li>Some other thread {@linkplain Thread#interrupt() interrupts}
+     * thread <var>T</var>.
+     * <li>The specified amount of real time has elapsed, more or less.
+     * The amount of real time, in nanoseconds, is given by the expression
+     * {@code 1000000 * timeoutMillis + nanos}. If {@code timeoutMillis} and {@code nanos}
+     * are both zero, then real time is not taken into consideration and the
+     * thread waits until awakened by one of the other causes.
+     * <li>Thread <var>T</var> is awakened spuriously. (See below.)
      * </ul>
      * <p>
-     * The thread then waits until it can re-obtain ownership of the
-     * monitor and resumes execution.
+     * The thread <var>T</var> is then removed from the wait set for this
+     * object and re-enabled for thread scheduling. It competes in the
+     * usual manner with other threads for the right to synchronize on the
+     * object; once it has regained control of the object, all its
+     * synchronization claims on the object are restored to the status quo
+     * ante - that is, to the situation as of the time that the {@code wait}
+     * method was invoked. Thread <var>T</var> then returns from the
+     * invocation of the {@code wait} method. Thus, on return from the
+     * {@code wait} method, the synchronization state of the object and of
+     * thread {@code T} is exactly as it was when the {@code wait} method
+     * was invoked.
      * <p>
-     * As in the one argument version, interrupts and spurious wakeups are
-     * possible, and this method should always be used in a loop:
-     * <pre>
+     * A thread can wake up without being notified, interrupted, or timing out, a
+     * so-called <em>spurious wakeup</em>.  While this will rarely occur in practice,
+     * applications must guard against it by testing for the condition that should
+     * have caused the thread to be awakened, and continuing to wait if the condition
+     * is not satisfied. See the example below.
+     * <p>
+     * For more information on this topic, see section 14.2,
+     * "Condition Queues," in Brian Goetz and others' <cite>Java Concurrency
+     * in Practice</cite> (Addison-Wesley, 2006) or Item 81 in Joshua
+     * Bloch's <cite>Effective Java, Third Edition</cite> (Addison-Wesley,
+     * 2018).
+     * <p>
+     * If the current thread is {@linkplain java.lang.Thread#interrupt() interrupted}
+     * by any thread before or while it is waiting, then an {@code InterruptedException}
+     * is thrown.  The <em>interrupted status</em> of the current thread is cleared when
+     * this exception is thrown. This exception is not thrown until the lock status of
+     * this object has been restored as described above.
+     *
+     * @apiNote
+     * The recommended approach to waiting is to check the condition being awaited in
+     * a {@code while} loop around the call to {@code wait}, as shown in the example
+     * below. Among other things, this approach avoids problems that can be caused
+     * by spurious wakeups.
+     *
+     * {@snippet lang=java :
      *     synchronized (obj) {
-     *         while (&lt;condition does not hold&gt;)
-     *             obj.wait(timeout, nanos);
-     *         ... // Perform action appropriate to condition
+     *         while ( <condition does not hold and timeout not exceeded> ) {
+     *             long timeoutMillis = ... ; // recompute timeout values
+     *             int nanos = ... ;
+     *             obj.wait(timeoutMillis, nanos);
+     *         }
+     *         ... // Perform action appropriate to condition or timeout
      *     }
-     * </pre>
-     * This method should only be called by a thread that is the owner
-     * of this object's monitor. See the <code>notify</code> method for a
-     * description of the ways in which a thread can become the owner of
-     * a monitor.
+     * }
      *
-     * @param      timeout   the maximum time to wait in milliseconds.
-     * @param      nanos      additional time, in nanoseconds range
-     *                       0-999999.
-     * @exception  IllegalArgumentException      if the value of timeout is
-     *                      negative or the value of nanos is
-     *                      not in the range 0-999999.
-     * @exception  IllegalMonitorStateException  if the current thread is not
-     *               the owner of this object's monitor.
-     * @exception  InterruptedException if another thread interrupted the
-     *             current thread before or while the current thread
-     *             was waiting for a notification.  The <i>interrupted
-     *             status</i> of the current thread is cleared when
-     *             this exception is thrown.
+     * @param  timeoutMillis the maximum time to wait, in milliseconds
+     * @param  nanos   additional time, in nanoseconds, in the range 0-999999 inclusive
+     * @throws IllegalArgumentException if {@code timeoutMillis} is negative,
+     *         or if the value of {@code nanos} is out of range
+     * @throws IllegalMonitorStateException if the current thread is not
+     *         the owner of the object's monitor
+     * @throws InterruptedException if any thread interrupted the current thread before or
+     *         while the current thread was waiting. The <em>interrupted status</em> of the
+     *         current thread is cleared when this exception is thrown.
+     * @see    #notify()
+     * @see    #notifyAll()
+     * @see    #wait()
+     * @see    #wait(long)
      */
-    public final void wait(long timeout, int nanos) throws InterruptedException {
+    public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
 
         bi04t002a.instrInvoke(bi04t002a.INSTR_WAIT_JI);
 
-        if (timeout < 0) {
-            throw new IllegalArgumentException("timeout value is negative");
+        if (timeoutMillis < 0) {
+            throw new IllegalArgumentException("timeoutMillis value is negative");
         }
 
         if (nanos < 0 || nanos > 999999) {
             throw new IllegalArgumentException(
                                 "nanosecond timeout value out of range");
         }
 
-            if (nanos >= 500000 || (nanos != 0 && timeout == 0)) {
-                timeout++;
-            }
-
-            wait(timeout);
-    }
-
-    // final modifier so method not in vtable
-    private final native void wait0(long timeoutMillis) throws InterruptedException;
+        if (nanos > 0 && timeoutMillis < Long.MAX_VALUE) {
+            timeoutMillis++;
+        }
 
-    /**
-     * Causes current thread to wait until another thread invokes the
-     * {@link java.lang.Object#notify()} method or the
-     * {@link java.lang.Object#notifyAll()} method for this object.
-     * In other words, this method behaves exactly as if it simply
-     * performs the call <tt>wait(0)</tt>.
-     * <p>
-     * The current thread must own this object's monitor. The thread
-     * releases ownership of this monitor and waits until another thread
-     * notifies threads waiting on this object's monitor to wake up
-     * either through a call to the <code>notify</code> method or the
-     * <code>notifyAll</code> method. The thread then waits until it can
-     * re-obtain ownership of the monitor and resumes execution.
-     * <p>
-     * As in the one argument version, interrupts and spurious wakeups are
-     * possible, and this method should always be used in a loop:
-     * <pre>
-     *     synchronized (obj) {
-     *         while (&lt;condition does not hold&gt;)
-     *             obj.wait();
-     *         ... // Perform action appropriate to condition
-     *     }
-     * </pre>
-     * This method should only be called by a thread that is the owner
-     * of this object's monitor. See the <code>notify</code> method for a
-     * description of the ways in which a thread can become the owner of
-     * a monitor.
-     *
-     * @exception  IllegalMonitorStateException  if the current thread is not
-     *               the owner of the object's monitor.
-     * @exception  InterruptedException if another thread interrupted the
-     *             current thread before or while the current thread
-     *             was waiting for a notification.  The <i>interrupted
-     *             status</i> of the current thread is cleared when
-     *             this exception is thrown.
-     * @see        java.lang.Object#notify()
-     * @see        java.lang.Object#notifyAll()
-     */
-    public final void wait() throws InterruptedException {
-        bi04t002a.instrInvoke(bi04t002a.INSTR_WAIT);
-        wait(0);
+        wait(timeoutMillis);
     }
 
     /**
      * Called by the garbage collector on an object when garbage collection
      * determines that there are no more references to the object.
-     * A subclass overrides the <code>finalize</code> method to dispose of
+     * A subclass overrides the {@code finalize} method to dispose of
      * system resources or to perform other cleanup.
      * <p>
-     * The general contract of <tt>finalize</tt> is that it is invoked
-     * if and when the Java<font size="-2"><sup>TM</sup></font> virtual
+     * <b>When running in a Java virtual machine in which finalization has been
+     * disabled or removed, the garbage collector will never call
+     * {@code finalize()}. In a Java virtual machine in which finalization is
+     * enabled, the garbage collector might call {@code finalize} only after an
+     * indefinite delay.</b>
+     * <p>
+     * The general contract of {@code finalize} is that it is invoked
+     * if and when the Java virtual
      * machine has determined that there is no longer any
      * means by which this object can be accessed by any thread that has
      * not yet died, except as a result of an action taken by the
      * finalization of some other object or class which is ready to be
-     * finalized. The <tt>finalize</tt> method may take any action, including
+     * finalized. The {@code finalize} method may take any action, including
      * making this object available again to other threads; the usual purpose
-     * of <tt>finalize</tt>, however, is to perform cleanup actions before
+     * of {@code finalize}, however, is to perform cleanup actions before
      * the object is irrevocably discarded. For example, the finalize method
      * for an object that represents an input/output connection might perform
      * explicit I/O transactions to break the connection before the object is
      * permanently discarded.
      * <p>
-     * The <tt>finalize</tt> method of class <tt>Object</tt> performs no
+     * The {@code finalize} method of class {@code Object} performs no
      * special action; it simply returns normally. Subclasses of
-     * <tt>Object</tt> may override this definition.
+     * {@code Object} may override this definition.
      * <p>
      * The Java programming language does not guarantee which thread will
-     * invoke the <tt>finalize</tt> method for any given object. It is
+     * invoke the {@code finalize} method for any given object. It is
      * guaranteed, however, that the thread that invokes finalize will not
      * be holding any user-visible synchronization locks when finalize is
      * invoked. If an uncaught exception is thrown by the finalize method,
      * the exception is ignored and finalization of that object terminates.
      * <p>
-     * After the <tt>finalize</tt> method has been invoked for an object, no
+     * After the {@code finalize} method has been invoked for an object, no
      * further action is taken until the Java virtual machine has again
      * determined that there is no longer any means by which this object can
      * be accessed by any thread that has not yet died, including possible
      * actions by other objects or classes which are ready to be finalized,
      * at which point the object may be discarded.
      * <p>
-     * The <tt>finalize</tt> method is never invoked more than once by a Java
+     * The {@code finalize} method is never invoked more than once by a Java
      * virtual machine for any given object.
      * <p>
-     * Any exception thrown by the <code>finalize</code> method causes
+     * Any exception thrown by the {@code finalize} method causes
      * the finalization of this object to be halted, but is otherwise
      * ignored.
      *
-     * @throws Throwable the <code>Exception</code> raised by this method
+     * @apiNote
+     * Classes that embed non-heap resources have many options
+     * for cleanup of those resources. The class must ensure that the
+     * lifetime of each instance is longer than that of any resource it embeds.
+     * {@link java.lang.ref.Reference#reachabilityFence} can be used to ensure that
+     * objects remain reachable while resources embedded in the object are in use.
+     * <p>
+     * A subclass should avoid overriding the {@code finalize} method
+     * unless the subclass embeds non-heap resources that must be cleaned up
+     * before the instance is collected.
+     * Finalizer invocations are not automatically chained, unlike constructors.
+     * If a subclass overrides {@code finalize} it must invoke the superclass
+     * finalizer explicitly.
+     * To guard against exceptions prematurely terminating the finalize chain,
+     * the subclass should use a {@code try-finally} block to ensure
+     * {@code super.finalize()} is always invoked. For example,
+     * {@snippet lang="java":
+     *     @Override
+     *     protected void finalize() throws Throwable {
+     *         try {
+     *             ... // cleanup subclass state
+     *         } finally {
+     *             super.finalize();
+     *         }
+     *     }
+     * }
+     *
+     * @deprecated Finalization is deprecated and subject to removal in a future
+     * release. The use of finalization can lead to problems with security,
+     * performance, and reliability.
+     * See <a href="https://openjdk.org/jeps/421">JEP 421</a> for
+     * discussion and alternatives.
+     * <p>
+     * Subclasses that override {@code finalize} to perform cleanup should use
+     * alternative cleanup mechanisms and remove the {@code finalize} method.
+     * Use {@link java.lang.ref.Cleaner} and
+     * {@link java.lang.ref.PhantomReference} as safer ways to release resources
+     * when an object becomes unreachable. Alternatively, add a {@code close}
+     * method to explicitly release resources, and implement
+     * {@code AutoCloseable} to enable use of the {@code try}-with-resources
+     * statement.
+     * <p>
+     * This method will remain in place until finalizers have been removed from
+     * most existing code.
+     *
+     * @throws Throwable the {@code Exception} raised by this method
+     * @see java.lang.ref.WeakReference
+     * @see java.lang.ref.PhantomReference
+     * @jls 12.6 Finalization of Class Instances
      */
+    @Deprecated(since="9", forRemoval=true)
     protected void finalize() throws Throwable { }
-
 }
diff a/test/jaxp/ProblemList-Virtual.txt b/test/jaxp/ProblemList-Virtual.txt
--- /dev/null
+++ b/test/jaxp/ProblemList-Virtual.txt
@@ -0,0 +1,4 @@
+####
+## Tests using custom path and con't be executed with virtual test thread factory
+
+javax/xml/jaxp/module/ServiceProviderTest/BasicModularXMLParserTest.java 000000 generic-all
diff a/test/jdk/ProblemList-Virtual.txt b/test/jdk/ProblemList-Virtual.txt
--- a/test/jdk/ProblemList-Virtual.txt
+++ b/test/jdk/ProblemList-Virtual.txt
@@ -42,30 +42,78 @@
 
 javax/management/remote/mandatory/connection/DeadLockTest.java 8309069 windows-x64
 
 javax/management/remote/mandatory/connection/ConnectionTest.java 8308352 windows-x64
 
+## DatagramSocket blocks ops are interruptible when invoked on virtual threads
+java/nio/channels/DatagramChannel/InterruptibleOrNot.java 0000000 generic-all
+java/net/DatagramSocket/InterruptibleDatagramSocket.java 0000000 generic-all
+
+# Underlying implementation is non-blocking
+java/nio/channels/SocketChannel/ShortWrite.java 0000000 generic-all
+
+jdk/jfr/event/compiler/TestCompilerCompile.java 8325392 generic-all
+
+
 ##########
 ## Tests incompatible with virtual test thread factory.
 ## There is no goal to run all test with virtual test thread factory.
 ## So any test might be added as incompatible, the bug id is not required.
 
 # Incorrect stack/threadgroup/exception expectations for main thread
+com/sun/net/httpserver/bugs/B6431193.java 0000000 generic-all
 java/lang/StackWalker/DumpStackTest.java 0000000 generic-all
 java/lang/StackWalker/StackWalkTest.java 0000000 generic-all
 java/lang/StackWalker/CallerFromMain.java 0000000 generic-all
 java/lang/Thread/MainThreadTest.java 0000000 generic-all
 java/lang/Thread/UncaughtExceptionsTest.java 0000000 generic-all
-java/lang/Thread/virtual/GetStackTraceWhenRunnable.java 0000000 generic-all
 java/lang/invoke/condy/CondyNestedResolutionTest.java 0000000 generic-all
 java/lang/ref/OOMEInReferenceHandler.java 0000000 generic-all
 java/util/concurrent/locks/Lock/OOMEInAQS.java 0000000 generic-all
 jdk/internal/vm/Continuation/Scoped.java 0000000 generic-all
+jdk/jfr/event/runtime/TestThreadDumpEvent.java 0000000 generic-all
+jdk/jfr/event/runtime/TestThreadStartEvent.java 0000000 generic-all
+jdk/jfr/jvm/TestGetAllEventClasses.java 0000000 generic-all
 
 # The problems with permissions
-jdk/jfr/startupargs/TestDumpOnExit.java 0000000 generic-all
+java/io/File/GetXSpace.java 0000000 generic-all
+java/io/FilePermission/MergeName.java 0000000 generic-all
 java/lang/SecurityManager/modules/CustomSecurityManagerTest.java 0000000 generic-all
+java/net/httpclient/security/Security.java 0000000 generic-all
+java/security/AccessController/LimitedDoPrivileged.java	0000000 generic-all
 java/util/PluggableLocale/PermissionTest.java 0000000 generic-all
 java/util/Properties/StoreReproducibilityTest.java 0000000 generic-all
 java/util/Properties/StoreReproducibilityTest.java 0000000 generic-all
 javax/management/ImplementationVersion/ImplVersionTest.java 0000000 generic-all
 javax/management/remote/mandatory/version/ImplVersionTest.java 0000000 generic-all
+
+
+jdk/jfr/api/consumer/security/TestStreamingRemote.java 0000000 generic-all
+jdk/jfr/api/consumer/TestRecordedEventGetThread.java 0000000 generic-all
+jdk/jfr/api/consumer/TestRecordedObject.java 0000000 generic-all
+jdk/jfr/startupargs/TestDumpOnExit.java 0000000 generic-all
+
+# AccessControlException: access denied ("java.lang.reflect.ReflectPermission" "suppressAccessChecks")
+sun/security/util/Resources/early/EarlyResources.java 0000000 generic-all
+
+# ProcessTools fails with InaccessibleObjectException
+tools/jimage/JImageToolTest.java 0000000 generic-all
+
+com/sun/management/DiagnosticCommandMBean/DcmdMBeanPermissionsTest.java 0000000 generic-all
+
+# Not supported on virtual threads
+com/sun/management/ThreadMXBean/ThreadAllocatedMemory.java#G1 0000000 generic-all
+com/sun/management/ThreadMXBean/ThreadAllocatedMemory.java#Serial 0000000 generic-all
+java/lang/management/ManagementFactory/ProxyTypeMapping.java 0000000 generic-all
+java/lang/management/ManagementFactory/ValidateOpenTypes.java 0000000 generic-all
+java/lang/management/ThreadMXBean/Locks.java 0000000 generic-all
+java/lang/management/ThreadMXBean/LockedMonitorInNative.java 0000000 generic-all
+java/lang/management/ThreadMXBean/ThreadCpuTime.java 0000000 generic-all
+java/lang/management/ThreadMXBean/ThreadUserTime.java 0000000 generic-all
+
+# Too sensitive to thread counts
+java/lang/management/ThreadMXBean/ResetPeakThreadCount.java 0000000 generic-all
+
+# need to be reexamined
+sun/management/jmxremote/startstop/JMXStatusTest.java 0000000 generic-all
+sun/management/jmxremote/startstop/JMXStatusPerfCountersTest.java 0000000 generic-all
+sun/management/jmxremote/startstop/JMXStartStopTest.java 0000000 generic-all
diff a/test/jdk/ProblemList-vthread-Xcomp.txt b/test/jdk/ProblemList-vthread-Xcomp.txt
--- /dev/null
+++ b/test/jdk/ProblemList-vthread-Xcomp.txt
@@ -0,0 +1,29 @@
+#
+# Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+#
+# This code is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 2 only, as
+# published by the Free Software Foundation.
+#
+# This code is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# version 2 for more details (a copy is included in the LICENSE file that
+# accompanied this code).
+#
+# You should have received a copy of the GNU General Public License version
+# 2 along with this work; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+# or visit www.oracle.com if you need additional information or have any
+# questions.
+#
+
+#############################################################################
+#
+# List of quarantined tests for testing in Xcomp mode with wrapper.
+#
+#############################################################################
+
diff a/test/jdk/java/lang/ScopedValue/ScopedValueAPI.java b/test/jdk/java/lang/ScopedValue/ScopedValueAPI.java
--- a/test/jdk/java/lang/ScopedValue/ScopedValueAPI.java
+++ b/test/jdk/java/lang/ScopedValue/ScopedValueAPI.java
@@ -46,34 +46,34 @@
     private static Stream<ThreadFactory> factories() {
         return Stream.of(Thread.ofPlatform().factory(), Thread.ofVirtual().factory());
     }
 
     /**
-     * Test that runWhere invokes the Runnable's run method.
+     * Test that where invokes the Runnable's run method.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testRunWhere(ThreadFactory factory) throws Exception {
         test(factory, () -> {
             class Box { static boolean executed; }
             ScopedValue<String> name = ScopedValue.newInstance();
-            ScopedValue.runWhere(name, "duke", () -> { Box.executed = true; });
+            ScopedValue.where(name, "duke").run(() -> { Box.executed = true; });
             assertTrue(Box.executed);
         });
     }
 
     /**
-     * Test runWhere when the run method throws an exception.
+     * Test where when the run method throws an exception.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testRunWhereThrows(ThreadFactory factory) throws Exception {
         test(factory, () -> {
             class FooException extends RuntimeException {  }
             ScopedValue<String> name = ScopedValue.newInstance();
             Runnable op = () -> { throw new FooException(); };
-            assertThrows(FooException.class, () -> ScopedValue.runWhere(name, "duke", op));
+            assertThrows(FooException.class, () -> ScopedValue.where(name, "duke").run(op));
             assertFalse(name.isBound());
         });
     }
 
     /**
@@ -82,11 +82,11 @@
     @ParameterizedTest
     @MethodSource("factories")
     void testCallWhere(ThreadFactory factory) throws Exception {
         test(factory, () -> {
             ScopedValue<String> name = ScopedValue.newInstance();
-            String result = ScopedValue.callWhere(name, "duke", name::get);
+            String result = ScopedValue.where(name, "duke").call(name::get);
             assertEquals("duke", result);
         });
     }
 
     /**
@@ -97,11 +97,11 @@
     void testCallWhereThrows(ThreadFactory factory) throws Exception {
         test(factory, () -> {
             class FooException extends RuntimeException {  }
             ScopedValue<String> name = ScopedValue.newInstance();
             CallableOp<Void, RuntimeException> op = () -> { throw new FooException(); };
-            assertThrows(FooException.class, () -> ScopedValue.callWhere(name, "duke", op));
+            assertThrows(FooException.class, () -> ScopedValue.where(name, "duke").call(op));
             assertFalse(name.isBound());
         });
     }
 
     /**
@@ -114,21 +114,21 @@
             ScopedValue<String> name1 = ScopedValue.newInstance();
             ScopedValue<String> name2 = ScopedValue.newInstance();
             assertThrows(NoSuchElementException.class, name1::get);
             assertThrows(NoSuchElementException.class, name2::get);
 
-            // runWhere
-            ScopedValue.runWhere(name1, "duke", () -> {
+            // where
+            ScopedValue.where(name1, "duke").run(() -> {
                 assertEquals("duke", name1.get());
                 assertThrows(NoSuchElementException.class, name2::get);
 
             });
             assertThrows(NoSuchElementException.class, name1::get);
             assertThrows(NoSuchElementException.class, name2::get);
 
             // callWhere
-            ScopedValue.callWhere(name1, "duke", () -> {
+            ScopedValue.where(name1, "duke").call(() -> {
                 assertEquals("duke", name1.get());
                 assertThrows(NoSuchElementException.class, name2::get);
                 return null;
             });
             assertThrows(NoSuchElementException.class, name1::get);
@@ -146,20 +146,20 @@
             ScopedValue<String> name1 = ScopedValue.newInstance();
             ScopedValue<String> name2 = ScopedValue.newInstance();
             assertFalse(name1.isBound());
             assertFalse(name2.isBound());
 
-            // runWhere
-            ScopedValue.runWhere(name1, "duke", () -> {
+            // where
+            ScopedValue.where(name1, "duke").run(() -> {
                 assertTrue(name1.isBound());
                 assertFalse(name2.isBound());
             });
             assertFalse(name1.isBound());
             assertFalse(name2.isBound());
 
             // callWhere
-            ScopedValue.callWhere(name1, "duke", () -> {
+            ScopedValue.where(name1, "duke").call(() -> {
                 assertTrue(name1.isBound());
                 assertFalse(name2.isBound());
                 return null;
             });
             assertFalse(name1.isBound());
@@ -176,18 +176,18 @@
         test(factory, () -> {
             ScopedValue<String> name = ScopedValue.newInstance();
             assertNull(name.orElse(null));
             assertEquals("default", name.orElse("default"));
 
-            // runWhere
-            ScopedValue.runWhere(name, "duke", () -> {
+            // where
+            ScopedValue.where(name, "duke").run(() -> {
                 assertEquals("duke", name.orElse(null));
                 assertEquals("duke", name.orElse("default"));
             });
 
             // callWhere
-            ScopedValue.callWhere(name, "duke", () -> {
+            ScopedValue.where(name, "duke").call(() -> {
                 assertEquals("duke", name.orElse(null));
                 assertEquals("duke", name.orElse("default"));
                 return null;
             });
         });
@@ -202,17 +202,17 @@
         test(factory, () -> {
             class FooException extends RuntimeException { }
             ScopedValue<String> name = ScopedValue.newInstance();
             assertThrows(FooException.class, () -> name.orElseThrow(FooException::new));
 
-            // runWhere
-            ScopedValue.runWhere(name, "duke", () -> {
+            // where
+            ScopedValue.where(name, "duke").run(() -> {
                 assertEquals("duke", name.orElseThrow(FooException::new));
             });
 
             // callWhere
-            ScopedValue.callWhere(name, "duke", () -> {
+            ScopedValue.where(name, "duke").call(() -> {
                 assertEquals("duke", name.orElseThrow(FooException::new));
                 return null;
             });
         });
     }
@@ -257,31 +257,31 @@
     @MethodSource("factories")
     void testRebinding(ThreadFactory factory) throws Exception {
         test(factory, () -> {
             ScopedValue<String> name = ScopedValue.newInstance();
 
-            // runWhere
-            ScopedValue.runWhere(name, "duke", () -> {
+            // where
+            ScopedValue.where(name, "duke").run(() -> {
                 assertTrue(name.isBound());
                 assertEquals("duke", name.get());
 
-                ScopedValue.runWhere(name, "duchess", () -> {
+                ScopedValue.where(name, "duchess").run(() -> {
                     assertTrue(name.isBound());
                     assertEquals("duchess", name.get());
                 });
 
                 assertTrue(name.isBound());
                 assertEquals("duke", name.get());
             });
             assertFalse(name.isBound());
 
             // callWhere
-            ScopedValue.callWhere(name, "duke", () -> {
+            ScopedValue.where(name, "duke").call(() -> {
                 assertTrue(name.isBound());
                 assertEquals("duke", name.get());
 
-                ScopedValue.callWhere(name, "duchess", () -> {
+                ScopedValue.where(name, "duchess").call(() -> {
                     assertTrue(name.isBound());
                     assertEquals("duchess", name.get());
                     return null;
                 });
 
@@ -300,31 +300,31 @@
     @MethodSource("factories")
     void testRebindingFromNull(ThreadFactory factory) throws Exception {
         test(factory, () -> {
             ScopedValue<String> name = ScopedValue.newInstance();
 
-            // runWhere
-            ScopedValue.runWhere(name, null, () -> {
+            // where
+            ScopedValue.where(name, null).run(() -> {
                 assertTrue(name.isBound());
                 assertNull(name.get());
 
-                ScopedValue.runWhere(name, "duchess", () -> {
+                ScopedValue.where(name, "duchess").run(() -> {
                     assertTrue(name.isBound());
                     assertTrue("duchess".equals(name.get()));
                 });
 
                 assertTrue(name.isBound());
                 assertNull(name.get());
             });
             assertFalse(name.isBound());
 
             // callWhere
-            ScopedValue.callWhere(name, null, () -> {
+            ScopedValue.where(name, null).call(() -> {
                 assertTrue(name.isBound());
                 assertNull(name.get());
 
-                ScopedValue.callWhere(name, "duchess", () -> {
+                ScopedValue.where(name, "duchess").call(() -> {
                     assertTrue(name.isBound());
                     assertTrue("duchess".equals(name.get()));
                     return null;
                 });
 
@@ -343,31 +343,31 @@
     @MethodSource("factories")
     void testRebindingToNull(ThreadFactory factory) throws Exception {
         test(factory, () -> {
             ScopedValue<String> name = ScopedValue.newInstance();
 
-            // runWhere
-            ScopedValue.runWhere(name, "duke", () -> {
+            // where
+            ScopedValue.where(name, "duke").run(() -> {
                 assertTrue(name.isBound());
                 assertEquals("duke", name.get());
 
-                ScopedValue.runWhere(name, null, () -> {
+                ScopedValue.where(name, null).run(() -> {
                     assertTrue(name.isBound());
                     assertNull(name.get());
                 });
 
                 assertTrue(name.isBound());
                 assertEquals("duke", name.get());
             });
             assertFalse(name.isBound());
 
             // callWhere
-            ScopedValue.callWhere(name, "duke", () -> {
+            ScopedValue.where(name, "duke").call(() -> {
                 assertTrue(name.isBound());
                 assertEquals("duke", name.get());
 
-                ScopedValue.callWhere(name, null, () -> {
+                ScopedValue.where(name, null).call(() -> {
                     assertTrue(name.isBound());
                     assertNull(name.get());
                     return null;
                 });
 
@@ -408,15 +408,15 @@
     void testNullPointerException() {
         ScopedValue<String> name = ScopedValue.newInstance();
 
         assertThrows(NullPointerException.class, () -> ScopedValue.where(null, "duke"));
 
-        assertThrows(NullPointerException.class, () -> ScopedValue.runWhere(null, "duke", () -> { }));
-        assertThrows(NullPointerException.class, () -> ScopedValue.runWhere(name, "duke", null));
+        assertThrows(NullPointerException.class, () -> ScopedValue.where(null, "duke").run(() -> { }));
+        assertThrows(NullPointerException.class, () -> ScopedValue.where(name, "duke").run(null));
 
-        assertThrows(NullPointerException.class, () -> ScopedValue.callWhere(null, "duke", () -> ""));
-        assertThrows(NullPointerException.class, () -> ScopedValue.callWhere(name, "duke", null));
+        assertThrows(NullPointerException.class, () -> ScopedValue.where(null, "duke").call(() -> ""));
+        assertThrows(NullPointerException.class, () -> ScopedValue.where(name, "duke").call(null));
 
         assertThrows(NullPointerException.class, () -> name.orElseThrow(null));
 
         var carrier = ScopedValue.where(name, "duke");
         assertThrows(NullPointerException.class, () -> carrier.where(null, "duke"));
diff a/test/jdk/java/lang/ScopedValue/StressStackOverflow.java b/test/jdk/java/lang/ScopedValue/StressStackOverflow.java
--- a/test/jdk/java/lang/ScopedValue/StressStackOverflow.java
+++ b/test/jdk/java/lang/ScopedValue/StressStackOverflow.java
@@ -50,10 +50,11 @@
 import java.lang.ScopedValue.CallableOp;
 import java.time.Duration;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.StructureViolationException;
 import java.util.concurrent.StructuredTaskScope;
+import java.util.concurrent.StructuredTaskScope.Joiner;
 import java.util.function.Supplier;
 
 public class StressStackOverflow {
     public static final ScopedValue<Integer> el = ScopedValue.newInstance();
 
@@ -167,11 +168,11 @@
 
     // Run op in a new thread. Platform or virtual threads are chosen at random.
     void runInNewThread(Runnable op) {
         var threadFactory
                 = (ThreadLocalRandom.current().nextBoolean() ? Thread.ofPlatform() : Thread.ofVirtual()).factory();
-        try (var scope = new StructuredTaskScope<>("", threadFactory)) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withThreadFactory(threadFactory))) {
             var handle = scope.fork(() -> {
                 op.run();
                 return null;
             });
             scope.join();
@@ -184,11 +185,11 @@
     }
 
     public void run() {
         try {
             ScopedValue.where(inheritedValue, 42).where(el, 0).run(() -> {
-                try (var scope = new StructuredTaskScope<>()) {
+                try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
                     try {
                         if (ThreadLocalRandom.current().nextBoolean()) {
                             // Repeatedly test Scoped Values set by ScopedValue::call(), get(), and run()
                             final var deepRecursion
                                 = new DeepRecursion(DeepRecursion.Behaviour.choose(ThreadLocalRandom.current()));
diff a/test/jdk/java/lang/Thread/virtual/ActivateSpareCarrier.java b/test/jdk/java/lang/Thread/virtual/ActivateSpareCarrier.java
--- a/test/jdk/java/lang/Thread/virtual/ActivateSpareCarrier.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-/**
- * @test
- * @bug 8292240
- * @summary Test the scenario where a blocking operation pins a virtual thread to its
- *   carrier thread (cT1) and doesn't activate a spare. Subsequent blocking operations
- *   that pin a virtual thread to cT1 should attempt to activate a spare.
- * @requires vm.continuations
- * @run main/othervm
- *     -Djdk.virtualThreadScheduler.parallelism=1
- *     -Djdk.virtualThreadScheduler.maxPoolSize=2 ActivateSpareCarrier 100
- */
-
-import java.time.Duration;
-import java.util.Comparator;
-import java.util.List;
-import java.util.concurrent.ForkJoinWorkerThread;
-import java.util.stream.Collectors;
-
-public class ActivateSpareCarrier {
-
-    private static final int DEFAULT_ITERTAIONS = 10_000;
-
-    private static final Object LOCK = new Object();
-
-    public static void main(String[] args) throws Exception {
-        int iterations;
-        if (args.length == 0) {
-            iterations = DEFAULT_ITERTAIONS;
-        } else {
-            iterations = Integer.parseInt(args[0]);
-        }
-        for (int i = 0; i < iterations; i++) {
-            test(i);
-        }
-    }
-
-    /**
-     * This method creates 3 virtual threads:
-     * - thread1 blocks in Object.wait, activating a spare carrier thread
-     * - thread2 is started and runs on the spare carrier thread
-     * - thread1 is notified causing it to re-adjust the release count and terminate
-     * - thread3 is started and should run on the one active thread
-     *
-     * This method need invoked at least twice in the same VM.
-     */
-    private static void test(int i) throws Exception {
-        System.out.printf("---- %d ----%n", i);
-
-        // thread1 blocks in wait, this triggers a tryCompensate to activate a spare thread
-        Thread thread1 = Thread.ofVirtual().unstarted(() -> {
-            System.out.println(Thread.currentThread());
-            synchronized (LOCK) {
-                try {
-                    LOCK.wait();
-                } catch (InterruptedException e) { }
-            }
-        });
-        System.out.printf("starting waiter thread #%d%n", thread1.threadId());
-        thread1.start();
-
-        // wait for thread1 to block in Object.wait
-        while (thread1.getState() != Thread.State.WAITING) {
-            Thread.sleep(10);
-        }
-
-        // start another virtual thread, it should run on the spare carrier thread
-        startAndJoinVirtualThread();
-
-        // notify thread1, this releases the blocker
-        synchronized (LOCK) {
-            LOCK.notifyAll();
-        }
-        joinThread(thread1);
-
-        // start another virtual thread after counts have been re-adjusted
-        startAndJoinVirtualThread();
-    }
-
-    /**
-     * Start a virtual thread and wait for it to terminate.
-     */
-    private static void startAndJoinVirtualThread() throws InterruptedException {
-        Thread thread = Thread.ofVirtual().unstarted(() -> {
-            System.out.println(Thread.currentThread());
-        });
-        System.out.format("starting #%d%n", thread.threadId());
-        thread.start();
-        joinThread(thread);
-    }
-
-    /**
-     * Wait for the give thread to terminate with diagnostic output if the thread does
-     * not terminate quickly.
-     */
-    private static void joinThread(Thread thread) throws InterruptedException {
-        long tid = thread.threadId();
-        System.out.printf("Waiting for #%d to terminate%n", tid);
-        boolean terminated = thread.join(Duration.ofSeconds(2));
-        if (!terminated) {
-            System.out.printf("#%d did not terminate quickly, continue to wait...%n", tid);
-            printForkJoinWorkerThreads();
-            thread.join();
-        }
-        System.out.printf("#%d terminated%n", tid);
-    }
-
-    /**
-     * Print the list of ForkJoinWorkerThreads and their stack traces.
-     */
-    private static void printForkJoinWorkerThreads() {
-        List<Thread> threads = Thread.getAllStackTraces().keySet().stream()
-                .filter(t -> t instanceof ForkJoinWorkerThread)
-                .sorted(Comparator.comparingLong(Thread::threadId))
-                .collect(Collectors.toList());
-        System.out.println("ForkJoinWorkerThreads:");
-        for (Thread t : threads) {
-            System.out.printf("    %s%n", t);
-            StackTraceElement[] stack = t.getStackTrace();
-            for (StackTraceElement e : stack) {
-                System.out.printf("      %s%n", e);
-            }
-        }
-    }
-}
diff a/test/jdk/java/lang/Thread/virtual/CancelTimerWithContention.java b/test/jdk/java/lang/Thread/virtual/CancelTimerWithContention.java
--- /dev/null
+++ b/test/jdk/java/lang/Thread/virtual/CancelTimerWithContention.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Test cancelling a timeout task for Object.wait(millis) when there is
+ *     contention on the timer queue
+ * @key randomness
+ * @run main/othervm
+ *     -Djdk.virtualThreadScheduler.parallelism=2
+ *     -Djdk.virtualThreadScheduler.timerQueues=1
+ *     CancelTimerWithContention
+ */
+
+import java.time.Instant;
+import java.util.Random;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class CancelTimerWithContention {
+
+    // number of threads
+    private static final int MIN_THREADS = 100;
+    private static final int MAX_THREADS = 10_000;
+
+    // number of monitors to enter
+    private static final int MIN_MONITORS = 2;
+    private static final int MAX_MONITORS = 8;
+
+    private static final Random RAND = new Random();
+
+    public static void main(String[] args) {
+        for (int threadCount = MIN_THREADS; threadCount <= MAX_THREADS; threadCount += 100) {
+            System.out.format("%s #threads = %d%n", Instant.now(), threadCount);
+            for (int lockCount = MIN_MONITORS; lockCount <= MAX_MONITORS;  lockCount += 2) {
+                test(threadCount, lockCount);
+            }
+        }
+    }
+
+    /**
+     * Test threads entering monitors and using Object.wait(millis) to wait. This
+     * testing scenario leads to a mix of contention (with responsible threads using
+     * short timeouts), timed-wait, and cancellation of timer tasks. This scenario
+     * can result in contention of the timer queue.
+     */
+    static void test(int threadCount, int monitorCount) {
+        var locks = new Object[monitorCount];
+        for (int i = 0; i < monitorCount; i++) {
+            locks[i] = new Object();
+        }
+
+        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
+            var finished = new AtomicInteger();
+
+            for (int i = 0; i < threadCount; i++) {
+                Object lock = locks[RAND.nextInt(monitorCount)];    // random lock
+
+                executor.submit(() -> {
+                    synchronized (lock) {
+                        lock.wait(Long.MAX_VALUE);
+                    }
+                    finished.incrementAndGet();
+                    return null;
+                });
+
+                synchronized (lock) {
+                    lock.notify();
+                }
+            }
+
+            // notify at most one thread until all threads are finished
+            while (finished.get() < threadCount) {
+                for (Object lock : locks) {
+                    synchronized (lock) {
+                        lock.notify();
+                    }
+                }
+            }
+        }
+    }
+}
diff a/test/jdk/java/lang/Thread/virtual/JfrEvents.java b/test/jdk/java/lang/Thread/virtual/JfrEvents.java
--- a/test/jdk/java/lang/Thread/virtual/JfrEvents.java
+++ b/test/jdk/java/lang/Thread/virtual/JfrEvents.java
@@ -60,15 +60,12 @@
 
 class JfrEvents {
 
     @BeforeAll
     static void setup() {
-        int minParallelism = 2;
-        if (Thread.currentThread().isVirtual()) {
-            minParallelism++;
-        }
-        VThreadRunner.ensureParallelism(minParallelism);
+        // need at least two carriers to test pinning
+        VThreadRunner.ensureParallelism(2);
     }
 
     /**
      * Test jdk.VirtualThreadStart and jdk.VirtualThreadEnd events.
      */
@@ -138,10 +135,227 @@
 
             assertContainsPinnedEvent(recording, vthread);
         }
     }
 
+    /**
+     * Test jdk.VirtualThreadPinned event when blocking on monitor while pinned.
+     */
+    @Test
+    void testBlockWhenPinned() throws Exception {
+        try (Recording recording = new Recording()) {
+            recording.enable("jdk.VirtualThreadPinned");
+            recording.start();
+
+            Object lock = new Object();
+
+            var started = new AtomicBoolean();
+            var vthread = Thread.ofVirtual().unstarted(() -> {
+                VThreadPinner.runPinned(() -> {
+                    started.set(true);
+                    synchronized (lock) { }
+                });
+            });
+
+            try {
+                synchronized (lock) {
+                    vthread.start();
+                    // wait for thread to start and block
+                    awaitTrue(started);
+                    await(vthread, Thread.State.BLOCKED);
+                }
+            } finally {
+                vthread.join();
+                recording.stop();
+            }
+
+            assertContainsPinnedEvent(recording, vthread);
+        }
+    }
+
+    /**
+     * Test jdk.VirtualThreadPinned event when waiting with Object.wait while pinned.
+     */
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    void testObjectWaitWhenPinned(boolean timed) throws Exception {
+        try (Recording recording = new Recording()) {
+            recording.enable("jdk.VirtualThreadPinned");
+            recording.start();
+
+            Object lock = new Object();
+
+            var started = new AtomicBoolean();
+            var vthread = Thread.startVirtualThread(() -> {
+                VThreadPinner.runPinned(() -> {
+                    started.set(true);
+                    synchronized (lock) {
+                        try {
+                            if (timed) {
+                                lock.wait(Long.MAX_VALUE);
+                            } else {
+                                lock.wait();
+                            }
+                        } catch (InterruptedException e) {
+                            fail();
+                        }
+                    }
+                });
+            });
+
+            try {
+                // wait for thread to start and wait
+                awaitTrue(started);
+                await(vthread, timed ? Thread.State.TIMED_WAITING : Thread.State.WAITING);
+            } finally {
+                synchronized (lock) {
+                    lock.notifyAll();
+                }
+                vthread.join();
+                recording.stop();
+            }
+
+            assertContainsPinnedEvent(recording, vthread);
+        }
+    }
+
+    /**
+     * Test jdk.VirtualThreadPinned event when parking in a class initializer.
+     */
+    @Test
+    void testParkInClassInitializer() throws Exception {
+        class TestClass {
+            static {
+                LockSupport.park();
+            }
+            static void m() {
+                // do nothing
+            }
+        }
+
+        try (Recording recording = new Recording()) {
+            recording.enable("jdk.VirtualThreadPinned");
+            recording.start();
+
+            var started = new AtomicBoolean();
+            Thread vthread = Thread.startVirtualThread(() -> {
+                started.set(true);
+                TestClass.m();
+            });
+
+            try {
+                // wait for it to start and park
+                awaitTrue(started);
+                await(vthread, Thread.State.WAITING);
+            } finally {
+                LockSupport.unpark(vthread);
+                vthread.join();
+                recording.stop();
+            }
+
+            assertContainsPinnedEvent(recording, vthread);
+        }
+    }
+
+    /**
+     * Test jdk.VirtualThreadPinned event when blocking on monitor in a class initializer.
+     */
+    @Test
+    void testBlockInClassInitializer() throws Exception {
+        class LockHolder {
+            static final Object lock = new Object();
+        }
+        class TestClass {
+            static {
+                synchronized (LockHolder.lock) { }
+            }
+            static void m() {
+                // no nothing
+            }
+        }
+
+        try (Recording recording = new Recording()) {
+            recording.enable("jdk.VirtualThreadPinned");
+            recording.start();
+
+            var started = new AtomicBoolean();
+            Thread vthread = Thread.ofVirtual().unstarted(() -> {
+                started.set(true);
+                TestClass.m();
+            });
+
+            try {
+                synchronized (LockHolder.lock) {
+                    vthread.start();
+                    // wait for thread to start and block
+                    awaitTrue(started);
+                    await(vthread, Thread.State.BLOCKED);
+                }
+            } finally {
+                vthread.join();
+                recording.stop();
+            }
+
+            assertContainsPinnedEvent(recording, vthread);
+        }
+    }
+
+    /**
+     * Test jdk.VirtualThreadPinned event when waiting for a class initializer.
+     */
+    @Test
+    void testWaitingForClassInitializer() throws Exception {
+        class TestClass {
+            static {
+                LockSupport.park();
+            }
+            static void m() {
+                // do nothing
+            }
+        }
+
+        try (Recording recording = new Recording()) {
+            recording.enable("jdk.VirtualThreadPinned");
+            recording.start();
+
+            var started1 = new AtomicBoolean();
+            var started2 = new AtomicBoolean();
+
+            Thread vthread1 =  Thread.ofVirtual().unstarted(() -> {
+                started1.set(true);
+                TestClass.m();
+            });
+            Thread vthread2 = Thread.ofVirtual().unstarted(() -> {
+                started2.set(true);
+                TestClass.m();
+            });
+
+            try {
+                // start first virtual thread and wait for it to start + park
+                vthread1.start();
+                awaitTrue(started1);
+                await(vthread1, Thread.State.WAITING);
+
+                // start second virtual thread and wait for it to start
+                vthread2.start();
+                awaitTrue(started2);
+
+                // give time for second virtual thread to wait on the MutexLocker
+                Thread.sleep(3000);
+
+            } finally {
+                LockSupport.unpark(vthread1);
+                vthread1.join();
+                vthread2.join();
+                recording.stop();
+            }
+
+            // the recording should have a pinned event for vthread2
+            assertContainsPinnedEvent(recording, vthread2);
+        }
+    }
+
     /**
      * Test jdk.VirtualThreadSubmitFailed event.
      */
     @Test
     void testVirtualThreadSubmitFailed() throws Exception {
diff a/test/jdk/java/lang/Thread/virtual/LockingMode.java b/test/jdk/java/lang/Thread/virtual/LockingMode.java
--- /dev/null
+++ b/test/jdk/java/lang/Thread/virtual/LockingMode.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.lang.management.ManagementFactory;
+import com.sun.management.HotSpotDiagnosticMXBean;
+
+class LockingMode {
+    private LockingMode() { }
+
+    /**
+     * Returns true if using legacy locking mode.
+     */
+    static boolean isLegacy() {
+        return ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class)
+                .getVMOption("LockingMode")
+                .getValue()
+                .equals("1");
+    }
+}
diff a/test/jdk/java/lang/Thread/virtual/MiscMonitorTests.java b/test/jdk/java/lang/Thread/virtual/MiscMonitorTests.java
--- /dev/null
+++ b/test/jdk/java/lang/Thread/virtual/MiscMonitorTests.java
@@ -0,0 +1,509 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test id=default
+ * @summary Tests for object monitors that have been useful to find bugs
+ * @library /test/lib
+ * @requires vm.continuations & vm.opt.LockingMode != 1
+ * @modules java.base/java.lang:+open
+ * @run junit/othervm MiscMonitorTests
+ */
+
+/*
+ * @test id=Xint
+ * @library /test/lib
+ * @requires vm.continuations & vm.opt.LockingMode != 1
+ * @modules java.base/java.lang:+open
+ * @run junit/othervm -Xint MiscMonitorTests
+ */
+
+/*
+ * @test id=Xcomp
+ * @library /test/lib
+ * @requires vm.continuations & vm.opt.LockingMode != 1
+ * @modules java.base/java.lang:+open
+ * @run junit/othervm -Xcomp MiscMonitorTests
+ */
+
+/*
+ * @test id=Xcomp-TieredStopAtLevel3
+ * @library /test/lib
+ * @requires vm.continuations & vm.opt.LockingMode != 1
+ * @modules java.base/java.lang:+open
+ * @run junit/othervm -Xcomp -XX:TieredStopAtLevel=3 MiscMonitorTests
+ */
+
+/*
+ * @test id=Xcomp-noTieredCompilation
+ * @summary Test virtual threads using synchronized
+ * @library /test/lib
+ * @requires vm.continuations & vm.opt.LockingMode != 1
+ * @modules java.base/java.lang:+open
+ * @run junit/othervm -Xcomp -XX:-TieredCompilation MiscMonitorTests
+ */
+
+/*
+ * @test id=gc
+ * @requires vm.debug == true & vm.continuations & vm.opt.LockingMode != 1
+ * @library /test/lib
+ * @modules java.base/java.lang:+open
+ * @run junit/othervm -XX:+UnlockDiagnosticVMOptions -XX:+FullGCALot -XX:FullGCALotInterval=1000 MiscMonitorTests
+ */
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.*;
+import java.util.ArrayList;
+import java.util.List;
+
+import jdk.test.lib.thread.VThreadScheduler;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class MiscMonitorTests {
+    static final int CARRIER_COUNT = Runtime.getRuntime().availableProcessors();
+
+    /**
+     * Test that yielding while holding monitors releases carrier.
+     */
+    @Test
+    void testReleaseOnYield() throws Exception {
+        try (var test = new TestReleaseOnYield()) {
+            test.runTest();
+        }
+    }
+
+    private static class TestReleaseOnYield extends TestBase {
+        final Object lock = new Object();
+        volatile boolean finish;
+        volatile int counter;
+
+        @Override
+        void runTest() throws Exception {
+            int vthreadCount = CARRIER_COUNT;
+
+            startVThreads(() -> foo(), vthreadCount, "Batch1");
+            sleep(500);  // Give time for threads to reach Thread.yield
+            startVThreads(() -> bar(), vthreadCount, "Batch2");
+
+            while (counter != vthreadCount) {
+                Thread.onSpinWait();
+            }
+            finish = true;
+            joinVThreads();
+        }
+
+        void foo() {
+            Object lock = new Object();
+            synchronized (lock) {
+                while (!finish) {
+                    Thread.yield();
+                }
+            }
+            System.err.println("Exiting foo from thread " + Thread.currentThread().getName());
+        }
+
+        void bar() {
+            synchronized (lock) {
+                counter++;
+            }
+            System.err.println("Exiting bar from thread " + Thread.currentThread().getName());
+        }
+    }
+
+    /**
+     * Test yielding while holding monitors with recursive locking releases carrier.
+     */
+    @Test
+    void testReleaseOnYieldRecursive() throws Exception {
+        try (var test = new TestReleaseOnYieldRecursive()) {
+            test.runTest();
+        }
+    }
+
+    private static class TestReleaseOnYieldRecursive extends TestBase {
+        final Object lock = new Object();
+        volatile boolean finish;
+        volatile int counter;
+
+        @Override
+        void runTest() throws Exception {
+            int vthreadCount = CARRIER_COUNT;
+
+            startVThreads(() -> foo(), vthreadCount, "Batch1");
+            sleep(500);  // Give time for threads to reach Thread.yield
+            startVThreads(() -> bar(), vthreadCount, "Batch2");
+
+            while (counter != 2 * vthreadCount) {
+                Thread.onSpinWait();
+            }
+            finish = true;
+            joinVThreads();
+        }
+
+        void foo() {
+           Object lock = new Object();
+            synchronized (lock) {
+                while (!finish) {
+                    Thread.yield();
+                }
+            }
+            System.err.println("Exiting foo from thread " + Thread.currentThread().getName());
+        }
+
+        void bar() {
+            synchronized (lock) {
+                counter++;
+            }
+            recursive(10);
+            System.err.println("Exiting bar from thread " + Thread.currentThread().getName());
+        };
+
+        void recursive(int count) {
+            synchronized (Thread.currentThread()) {
+                if (count > 0) {
+                    recursive(count - 1);
+                } else {
+                    synchronized (lock) {
+                        counter++;
+                        Thread.yield();
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Test that contention on monitorenter releases carrier.
+     */
+    @Test
+    void testReleaseOnContention() throws Exception {
+        try (var test = new TestReleaseOnContention()) {
+            test.runTest();
+        }
+    }
+
+    private static class TestReleaseOnContention extends TestBase {
+        final Object lock = new Object();
+        volatile boolean finish;
+        volatile int counter;
+
+        @Override
+        void runTest() throws Exception {
+            int vthreadCount = CARRIER_COUNT * 8;
+
+            startVThreads(() -> foo(), vthreadCount, "VThread");
+            sleep(500);  // Give time for threads to reach synchronized (lock)
+
+            finish = true;
+            joinVThreads();
+        }
+
+        void foo() {
+            synchronized (lock) {
+                while (!finish) {
+                    Thread.yield();
+                }
+            }
+            System.err.println("Exiting foo from thread " + Thread.currentThread().getName());
+        }
+    }
+
+    /**
+     * Test contention on monitorenter with extra monitors on stack shared by all threads.
+     */
+    @Test
+    void testContentionMultipleMonitors() throws Exception {
+        try (var test = new TestContentionMultipleMonitors()) {
+            test.runTest();
+        }
+    }
+
+    private static class TestContentionMultipleMonitors extends TestBase {
+        static int MONITOR_COUNT = 12;
+        final Object[] lockArray = new Object[MONITOR_COUNT];
+        final AtomicInteger workerCount = new AtomicInteger(0);
+        volatile boolean finish;
+
+        @Override
+        void runTest() throws Exception {
+            int vthreadCount = CARRIER_COUNT * 8;
+            for (int i = 0; i < MONITOR_COUNT; i++) {
+                lockArray[i] = new Object();
+            }
+
+            startVThreads(() -> foo(), vthreadCount, "VThread");
+
+            sleep(5000);
+            finish = true;
+            joinVThreads();
+            assertEquals(vthreadCount, workerCount.get());
+        }
+
+        void foo() {
+            while (!finish) {
+                int lockNumber = ThreadLocalRandom.current().nextInt(0, MONITOR_COUNT - 1);
+                synchronized (lockArray[lockNumber]) {
+                    recursive1(lockNumber, lockNumber);
+                }
+            }
+            workerCount.getAndIncrement();
+            System.err.println("Exiting foo from thread " + Thread.currentThread().getName());
+        }
+
+        public void recursive1(int depth, int lockNumber) {
+            if (depth > 0) {
+                recursive1(depth - 1, lockNumber);
+            } else {
+                if (Math.random() < 0.5) {
+                    Thread.yield();
+                }
+                recursive2(lockNumber);
+            }
+        }
+
+        public void recursive2(int lockNumber) {
+            if (lockNumber + 2 <= MONITOR_COUNT - 1) {
+                lockNumber += 2;
+                synchronized (lockArray[lockNumber]) {
+                    Thread.yield();
+                    recursive2(lockNumber);
+                }
+            }
+        }
+    }
+
+    /**
+     * Test contention on monitorenter with extra monitors on stack both local only and shared by all threads.
+     */
+    @Test
+    void testContentionMultipleMonitors2() throws Exception {
+        try (var test = new TestContentionMultipleMonitors2()) {
+            test.runTest();
+        }
+    }
+
+    private static class TestContentionMultipleMonitors2 extends TestBase {
+        int MONITOR_COUNT = 12;
+        final Object[] lockArray = new Object[MONITOR_COUNT];
+        final AtomicInteger workerCount = new AtomicInteger(0);
+        volatile boolean finish;
+
+        @Override
+        void runTest() throws Exception {
+            int vthreadCount = CARRIER_COUNT * 8;
+            for (int i = 0; i < MONITOR_COUNT; i++) {
+                lockArray[i] = new Object();
+            }
+
+            startVThreads(() -> foo(), vthreadCount, "VThread");
+
+            sleep(5000);
+            finish = true;
+            joinVThreads();
+            assertEquals(vthreadCount, workerCount.get());
+        }
+
+        void foo() {
+            Object[] myLockArray = new Object[MONITOR_COUNT];
+            for (int i = 0; i < MONITOR_COUNT; i++) {
+                myLockArray[i] = new Object();
+            }
+
+            while (!finish) {
+                int lockNumber = ThreadLocalRandom.current().nextInt(0, MONITOR_COUNT - 1);
+                synchronized (myLockArray[lockNumber]) {
+                    synchronized (lockArray[lockNumber]) {
+                        recursive1(lockNumber, lockNumber, myLockArray);
+                    }
+                }
+            }
+            workerCount.getAndIncrement();
+            System.err.println("Exiting foo from thread " + Thread.currentThread().getName());
+        }
+
+        public void recursive1(int depth, int lockNumber, Object[] myLockArray) {
+            if (depth > 0) {
+                recursive1(depth - 1, lockNumber, myLockArray);
+            } else {
+                if (Math.random() < 0.5) {
+                    Thread.yield();
+                }
+                recursive2(lockNumber, myLockArray);
+            }
+        }
+
+        public void recursive2(int lockNumber, Object[] myLockArray) {
+            if (lockNumber + 2 <= MONITOR_COUNT - 1) {
+                lockNumber += 2;
+                synchronized (myLockArray[lockNumber]) {
+                    if (Math.random() < 0.5) {
+                        Thread.yield();
+                    }
+                    synchronized (lockArray[lockNumber]) {
+                        Thread.yield();
+                        recursive2(lockNumber, myLockArray);
+                    }
+                }
+            }
+        }
+    }
+
+
+    /**
+     * Test contention on monitorenter with synchronized methods.
+     */
+    @Test
+    void testContentionWithSyncMethods() throws Exception {
+        try (var test = new TestContentionWithSyncMethods()) {
+            test.runTest();
+        }
+    }
+
+    private static class TestContentionWithSyncMethods extends TestBase {
+        static final int MONITOR_COUNT = 12;
+        final Object[] lockArray = new Object[MONITOR_COUNT];
+        final AtomicInteger workerCount = new AtomicInteger(0);
+        volatile boolean finish;
+
+        @Override
+        void runTest() throws Exception {
+            int vthreadCount = CARRIER_COUNT * 8;
+            for (int i = 0; i < MONITOR_COUNT; i++) {
+                lockArray[i] = new Object();
+            }
+
+            startVThreads(() -> foo(), vthreadCount, "VThread");
+
+            sleep(5000);
+            finish = true;
+            joinVThreads();
+            assertEquals(vthreadCount, workerCount.get());
+        }
+
+        void foo() {
+            Object myLock = new Object();
+
+            while (!finish) {
+                int lockNumber = ThreadLocalRandom.current().nextInt(0, MONITOR_COUNT - 1);
+                synchronized (myLock) {
+                    synchronized (lockArray[lockNumber]) {
+                        recursive(lockNumber, myLock);
+                    }
+                }
+            }
+            workerCount.getAndIncrement();
+            System.err.println("Exiting foo from thread " + Thread.currentThread().getName());
+        };
+
+        synchronized void recursive(int depth, Object myLock) {
+            if (depth > 0) {
+                recursive(depth - 1, myLock);
+            } else {
+                if (Math.random() < 0.5) {
+                    Thread.yield();
+                } else {
+                    synchronized (myLock) {
+                        Thread.yield();
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Test wait/notify mechanism.
+     */
+    @Test
+    void waitNotifyTest() throws Exception {
+        int threadCount = 1000;
+        int waitTime = 50;
+        Thread[] vthread = new Thread[threadCount];
+        long start = System.currentTimeMillis();
+
+        while (System.currentTimeMillis() - start < 5000) {
+            CountDownLatch latchStart = new CountDownLatch(threadCount);
+            CountDownLatch latchFinish = new CountDownLatch(threadCount);
+            Object object = new Object();
+
+            for (int i = 0; i < threadCount; i++) {
+                vthread[i] = Thread.ofVirtual().start(() -> {
+                    synchronized (object) {
+                        try {
+                            latchStart.countDown();
+                            object.wait(waitTime);
+                        } catch (InterruptedException e) {
+                            //do nothing;
+                        }
+                    }
+                    latchFinish.countDown();
+                });
+            }
+
+            try {
+                latchStart.await();
+                synchronized (object) {
+                    object.notifyAll();
+                }
+                latchFinish.await();
+                for (int i = 0; i < threadCount; i++) {
+                    vthread[i].join();
+                }
+            } catch (InterruptedException e) {
+                //do nothing;
+            }
+        }
+    }
+
+    private static abstract class TestBase implements AutoCloseable {
+        final ExecutorService scheduler = Executors.newFixedThreadPool(CARRIER_COUNT);
+        final List<Thread[]> vthreadList = new ArrayList<>();
+
+        abstract void runTest() throws Exception;
+
+        void startVThreads(Runnable r, int count, String name) {
+            Thread vthreads[] = new Thread[count];
+            for (int i = 0; i < count; i++) {
+                vthreads[i] = VThreadScheduler.virtualThreadBuilder(scheduler).name(name + "-" + i).start(r);
+            }
+            vthreadList.add(vthreads);
+        }
+
+        void joinVThreads() throws Exception {
+            for (Thread[] vthreads : vthreadList) {
+                for (Thread vthread : vthreads) {
+                    vthread.join();
+                }
+            }
+        }
+
+        void sleep(long ms) throws Exception {
+            Thread.sleep(ms);
+        }
+
+        @Override
+        public void close() {
+            scheduler.close();
+        }
+    }
+}
diff a/test/jdk/java/lang/Thread/virtual/MonitorEnterExit.java b/test/jdk/java/lang/Thread/virtual/MonitorEnterExit.java
--- a/test/jdk/java/lang/Thread/virtual/MonitorEnterExit.java
+++ b/test/jdk/java/lang/Thread/virtual/MonitorEnterExit.java
@@ -24,13 +24,94 @@
 /*
  * @test id=default
  * @summary Test virtual thread with monitor enter/exit
  * @modules java.base/java.lang:+open jdk.management
  * @library /test/lib
+ * @build LockingMode
  * @run junit/othervm --enable-native-access=ALL-UNNAMED MonitorEnterExit
  */
 
+/*
+ * @test id=LM_LEGACY
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=LM_LIGHTWEIGHT
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xint-LM_LEGACY
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xint -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xint-LM_LIGHTWEIGHT
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xint -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xcomp-LM_LEGACY
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xcomp -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xcomp-LM_LIGHTWEIGHT
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xcomp -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xcomp-TieredStopAtLevel1-LM_LEGACY
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xcomp -XX:TieredStopAtLevel=1 -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xcomp-TieredStopAtLevel1-LM_LIGHTWEIGHT
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xcomp -XX:TieredStopAtLevel=1 -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xcomp-noTieredCompilation-LM_LEGACY
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xcomp -XX:-TieredCompilation -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xcomp-noTieredCompilation-LM_LIGHTWEIGHT
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xcomp -XX:-TieredCompilation -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ThreadFactory;
@@ -46,27 +127,26 @@
 import jdk.test.lib.thread.VThreadRunner;   // ensureParallelism requires jdk.management
 import jdk.test.lib.thread.VThreadScheduler;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.RepeatedTest;
+import org.junit.jupiter.api.condition.DisabledIf;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.ValueSource;
 import org.junit.jupiter.params.provider.MethodSource;
-import org.junit.jupiter.api.condition.*;
 import static org.junit.jupiter.api.Assertions.*;
 import static org.junit.jupiter.api.Assumptions.*;
 
 class MonitorEnterExit {
+    static final int MAX_VTHREAD_COUNT = 4 * Runtime.getRuntime().availableProcessors();
     static final int MAX_ENTER_DEPTH = 256;
 
     @BeforeAll
     static void setup() {
-        // need >=2 carriers for testing pinning when main thread is a virtual thread
-        if (Thread.currentThread().isVirtual()) {
-            VThreadRunner.ensureParallelism(2);
-        }
+        // need >=2 carriers for tests that pin
+        VThreadRunner.ensureParallelism(2);
     }
 
     /**
      * Test monitor enter with no contention.
      */
@@ -148,10 +228,57 @@
                 assertTrue(Thread.holdsLock(lock));
             }
         }
     }
 
+    /**
+     * Test monitor reenter when there are other threads blocked trying to enter.
+     */
+    @Test
+    @DisabledIf("LockingMode#isLegacy")
+    void testReenterWithContention() throws Exception {
+        var lock = new Object();
+        VThreadRunner.run(() -> {
+            List<Thread> threads = new ArrayList<>();
+            testReenter(lock, 0, threads);
+
+            // wait for threads to terminate
+            for (Thread vthread : threads) {
+                vthread.join();
+            }
+        });
+    }
+
+    private void testReenter(Object lock, int depth, List<Thread> threads) throws Exception {
+        if (depth < MAX_ENTER_DEPTH) {
+            synchronized (lock) {
+                assertTrue(Thread.holdsLock(lock));
+
+                // start platform or virtual thread that blocks waiting to enter
+                var started = new CountDownLatch(1);
+                ThreadFactory factory = ThreadLocalRandom.current().nextBoolean()
+                        ? Thread.ofPlatform().factory()
+                        : Thread.ofVirtual().factory();
+                var thread = factory.newThread(() -> {
+                    started.countDown();
+                    synchronized (lock) {
+                        /* do nothing */
+                    }
+                });
+                thread.start();
+
+                // wait for thread to start and block
+                started.await();
+                await(thread, Thread.State.BLOCKED);
+                threads.add(thread);
+
+                // test reenter
+                testReenter(lock, depth + 1, threads);
+            }
+        }
+    }
+
     /**
      * Test monitor enter when pinned.
      */
     @Test
     void testEnterWhenPinned() throws Exception {
@@ -195,17 +322,11 @@
     /**
      * Test contended monitor enter when pinned. Monitor is held by virtual thread.
      */
     @Test
     void testContendedEnterWhenPinnedHeldByVirtualThread() throws Exception {
-        // need at least two carrier threads
-        int previousParallelism = VThreadRunner.ensureParallelism(2);
-        try {
-            VThreadRunner.run(this::testEnterWithContentionWhenPinned);
-        } finally {
-            VThreadRunner.setParallelism(previousParallelism);
-        }
+        VThreadRunner.run(this::testEnterWithContentionWhenPinned);
     }
 
     /**
      * Test contended monitor enter when pinned, monitor will be held by caller thread.
      */
@@ -232,10 +353,83 @@
 
         // check thread entered monitor
         assertTrue(entered.get());
     }
 
+    /**
+     * Test that blocking waiting to enter a monitor releases the carrier.
+     */
+    @Test
+    @DisabledIf("LockingMode#isLegacy")
+    void testReleaseWhenBlocked() throws Exception {
+        assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers");
+        try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) {
+            ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler);
+
+            var lock = new Object();
+
+            // thread enters monitor
+            var started = new CountDownLatch(1);
+            var vthread1 = factory.newThread(() -> {
+                started.countDown();
+                synchronized (lock) {
+                }
+            });
+
+            try {
+                synchronized (lock) {
+                    // start thread and wait for it to block
+                    vthread1.start();
+                    started.await();
+                    await(vthread1, Thread.State.BLOCKED);
+
+                    // carrier should be released, use it for another thread
+                    var executed = new AtomicBoolean();
+                    var vthread2 = factory.newThread(() -> {
+                        executed.set(true);
+                    });
+                    vthread2.start();
+                    vthread2.join();
+                    assertTrue(executed.get());
+                }
+            } finally {
+                vthread1.join();
+            }
+        }
+    }
+
+    /**
+     * Test lots of virtual threads blocked waiting to enter a monitor. If the number
+     * of virtual threads exceeds the number of carrier threads this test will hang if
+     * carriers aren't released.
+     */
+    @Test
+    @DisabledIf("LockingMode#isLegacy")
+    void testManyBlockedThreads() throws Exception {
+        Thread[] vthreads = new Thread[MAX_VTHREAD_COUNT];
+        var lock = new Object();
+        synchronized (lock) {
+            for (int i = 0; i < MAX_VTHREAD_COUNT; i++) {
+                var started = new CountDownLatch(1);
+                var vthread = Thread.ofVirtual().start(() -> {
+                    started.countDown();
+                    synchronized (lock) {
+                    }
+                });
+                // wait for thread to start and block
+                started.await();
+                await(vthread, Thread.State.BLOCKED);
+                vthreads[i] = vthread;
+            }
+        }
+
+        // cleanup
+        for (int i = 0; i < MAX_VTHREAD_COUNT; i++) {
+            vthreads[i].join();
+        }
+    }
+
     /**
      * Returns a stream of elements that are ordered pairs of platform and virtual thread
      * counts. 0,2,4,..16 platform threads. 2,4,6,..32 virtual threads.
      */
     static Stream<Arguments> threadCounts() {
diff a/test/jdk/java/lang/Thread/virtual/MonitorWaitNotify.java b/test/jdk/java/lang/Thread/virtual/MonitorWaitNotify.java
--- a/test/jdk/java/lang/Thread/virtual/MonitorWaitNotify.java
+++ b/test/jdk/java/lang/Thread/virtual/MonitorWaitNotify.java
@@ -24,13 +24,94 @@
 /*
  * @test id=default
  * @summary Test virtual threads using Object.wait/notifyAll
  * @modules java.base/java.lang:+open jdk.management
  * @library /test/lib
+ * @build LockingMode
  * @run junit/othervm --enable-native-access=ALL-UNNAMED MonitorWaitNotify
  */
 
+/*
+ * @test id=LM_LEGACY
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
+/*
+ * @test id=LM_LIGHTWEIGHT
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
+/*
+ * @test id=Xint-LM_LEGACY
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xint -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
+/*
+ * @test id=Xint-LM_LIGHTWEIGHT
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xint -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
+/*
+ * @test id=Xcomp-LM_LEGACY
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xcomp -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
+/*
+ * @test id=Xcomp-LM_LIGHTWEIGHT
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xcomp -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
+/*
+ * @test id=Xcomp-TieredStopAtLevel1-LM_LEGACY
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xcomp -XX:TieredStopAtLevel=1 -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
+/*
+ * @test id=Xcomp-TieredStopAtLevel1-LM_LIGHTWEIGHT
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xcomp -XX:TieredStopAtLevel=1 -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
+/*
+ * @test id=Xcomp-noTieredCompilation-LM_LEGACY
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xcomp -XX:-TieredCompilation -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
+/*
+ * @test id=Xcomp-noTieredCompilation-LM_LIGHTWEIGHT
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xcomp -XX:-TieredCompilation -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executors;
@@ -48,11 +129,14 @@
 import jdk.test.lib.thread.VThreadScheduler;
 import jdk.test.lib.thread.VThreadRunner;   // ensureParallelism requires jdk.management
 import jdk.test.lib.thread.VThreadPinner;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.condition.DisabledIf;
+import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.junit.jupiter.params.provider.ValueSource;
 import static org.junit.jupiter.api.Assertions.*;
 import static org.junit.jupiter.api.Assumptions.*;
 
 class MonitorWaitNotify {
@@ -201,10 +285,133 @@
                 lock.notifyAll();
             }
             thread.join();
         }
     }
+
+    /**
+     * Returns a stream of elements that are ordered pairs of platform and virtual thread
+     * counts. 0,2,4,..8 platform threads. 2,4,6,..16 virtual threads.
+     */
+    static Stream<Arguments> threadCounts() {
+        return IntStream.range(0, 9)
+                .filter(i -> i % 2 == 0)
+                .mapToObj(i -> i)
+                .flatMap(np -> IntStream.range(2, 17)
+                        .filter(i -> i % 2 == 0)
+                        .mapToObj(vp -> Arguments.of(np, vp)));
+    }
+
+    /**
+     * Test notify wakes only one thread when platform and virtual threads are waiting.
+     */
+    @ParameterizedTest
+    @MethodSource("threadCounts")
+    @DisabledIf("LockingMode#isLegacy")
+    void testNotifyOneThread(int nPlatformThreads, int nVirtualThreads) throws Exception {
+        int nThreads = nPlatformThreads + nVirtualThreads;
+
+        var lock = new Object();
+        var ready = new CountDownLatch(nThreads);
+        var notified = new AtomicInteger();
+
+        Runnable waitTask = () -> {
+            synchronized (lock) {
+                try {
+                    ready.countDown();
+                    lock.wait();
+                    notified.incrementAndGet();
+                } catch (InterruptedException e) { }
+            }
+        };
+
+        var threads = new ArrayList<Thread>();
+        try {
+            for (int i = 0; i < nPlatformThreads; i++) {
+                threads.add(Thread.ofPlatform().start(waitTask));
+            }
+            for (int i = 0; i < nVirtualThreads; i++) {
+                threads.add(Thread.ofVirtual().start(waitTask));
+            }
+
+            // wait for all threads to wait
+            ready.await();
+
+            // wake threads, one by one
+            for (int i = 0; i < threads.size(); i++) {
+
+                // wake one thread
+                synchronized (lock) {
+                    lock.notify();
+                }
+
+                // one thread should have awoken
+                int expectedWakeups = i + 1;
+                while (notified.get() < expectedWakeups) {
+                    Thread.sleep(10);
+                }
+                assertEquals(expectedWakeups, notified.get());
+            }
+        } finally {
+            for (Thread t : threads) {
+                t.interrupt();
+                t.join();
+            }
+        }
+    }
+
+    /**
+     * Test notifyAll wakes all threads.
+     */
+    @ParameterizedTest
+    @MethodSource("threadCounts")
+    @DisabledIf("LockingMode#isLegacy")
+    void testNotifyAllThreads(int nPlatformThreads, int nVirtualThreads) throws Exception {
+        int nThreads = nPlatformThreads + nVirtualThreads;
+
+        var lock = new Object();
+        var ready = new CountDownLatch(nThreads);
+        var notified = new CountDownLatch(nThreads);
+
+        Runnable waitTask = () -> {
+            synchronized (lock) {
+                try {
+                    ready.countDown();
+                    lock.wait();
+                    notified.countDown();
+                } catch (InterruptedException e) { }
+            }
+        };
+
+        var threads = new ArrayList<Thread>();
+        try {
+            for (int i = 0; i < nPlatformThreads; i++) {
+                threads.add(Thread.ofPlatform().start(waitTask));
+            }
+            for (int i = 0; i < nVirtualThreads; i++) {
+                threads.add(Thread.ofVirtual().start(waitTask));
+            }
+
+            // wait for all threads to wait
+            ready.await();
+
+            // wakeup all threads
+            synchronized (lock) {
+                lock.notifyAll();
+            }
+
+            // wait for all threads to have awoken
+            notified.await();
+
+        } finally {
+            for (Thread t : threads) {
+                t.interrupt();
+                t.join();
+            }
+        }
+    }
+
     /**
      * Test duration of timed Object.wait.
      */
     @Test
     void testTimedWaitDuration1() throws Exception {
@@ -487,10 +694,101 @@
         // thread should terminate
         vthread.join();
         assertTrue(completed.get());
     }
 
+    /**
+     * Test that Object.wait releases the carrier. This test uses a custom scheduler
+     * with one carrier thread.
+     */
+    @ParameterizedTest
+    @ValueSource(ints = { 0, 30000, Integer.MAX_VALUE })
+    @DisabledIf("LockingMode#isLegacy")
+    void testReleaseWhenWaiting1(int timeout) throws Exception {
+        assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers");
+        try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) {
+            ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler);
+
+            var lock = new Object();
+            var ready = new AtomicBoolean();
+            var completed = new AtomicBoolean();
+
+            var vthread1 = factory.newThread(() -> {
+                synchronized (lock) {
+                    try {
+                        ready.set(true);
+                        if (timeout > 0) {
+                            lock.wait(timeout);
+                        } else {
+                            lock.wait();
+                        }
+                    } catch (InterruptedException e) {
+                        fail("wait interrupted");
+                    }
+                }
+                completed.set(true);
+            });
+            vthread1.start();
+
+            // wait for vthread1 to start and wait
+            awaitTrue(ready);
+            await(vthread1, timeout > 0 ? Thread.State.TIMED_WAITING : Thread.State.WAITING);
+
+            // carrier should be released, use it for another thread
+            var executed = new AtomicBoolean();
+            var vthread2 = factory.newThread(() -> {
+                executed.set(true);
+            });
+            vthread2.start();
+            vthread2.join();
+            assertTrue(executed.get());
+
+            // wakeup vthread1
+            synchronized (lock) {
+                lock.notifyAll();
+            }
+
+            vthread1.join();
+            assertTrue(completed.get());
+        }
+    }
+
+    /**
+     * Test that Object.wait releases the carrier. This test arranges for 4*ncores - 1
+     * virtual threads to wait. For long timeout and no timeout cases, all virtual threads
+     * will wait until they are notified.
+     */
+    @ParameterizedTest
+    @ValueSource(ints = { 0, 10, 20, 100, 500, 30000, Integer.MAX_VALUE })
+    @DisabledIf("LockingMode#isLegacy")
+    void testReleaseWhenWaiting2(int timeout) throws Exception {
+        int VTHREAD_COUNT = 4 * Runtime.getRuntime().availableProcessors();
+        CountDownLatch latch = new CountDownLatch(VTHREAD_COUNT);
+        Object lock = new Object();
+        AtomicInteger counter = new AtomicInteger(0);
+
+        for (int i = 0; i < VTHREAD_COUNT; i++) {
+            Thread.ofVirtual().name("vthread-" + i).start(() -> {
+                synchronized (lock) {
+                    if (counter.incrementAndGet() == VTHREAD_COUNT) {
+                        lock.notifyAll();
+                    } else {
+                        try {
+                            if (timeout > 0) {
+                                lock.wait(timeout);
+                            } else {
+                                lock.wait();
+                            }
+                        } catch (InterruptedException e) {}
+                    }
+                }
+                latch.countDown();
+            });
+        }
+        latch.await();
+    }
+
     /**
      * Test that wait(long) throws IAE when timeout is negative.
      */
     @Test
     void testIllegalArgumentException() throws Exception {
diff a/test/jdk/java/lang/Thread/virtual/ParkWithFixedThreadPool.java b/test/jdk/java/lang/Thread/virtual/ParkWithFixedThreadPool.java
--- a/test/jdk/java/lang/Thread/virtual/ParkWithFixedThreadPool.java
+++ b/test/jdk/java/lang/Thread/virtual/ParkWithFixedThreadPool.java
@@ -28,18 +28,20 @@
  * @modules java.base/java.lang:+open
  * @library /test/lib
  * @run main ParkWithFixedThreadPool
  */
 
-import java.util.concurrent.*;
-import java.util.concurrent.atomic.*;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.locks.LockSupport;
 import jdk.test.lib.thread.VThreadScheduler;
 
 public class ParkWithFixedThreadPool {
     public static void main(String[] args) throws Exception {
-        try (ExecutorService scheduler = Executors.newFixedThreadPool(8)) {
+        try (var scheduler = new Scheduler(8)) {
             int vthreadCount = 300;
             Thread[] vthreads = new Thread[vthreadCount];
             Runnable target = new Runnable() {
                 public void run() {
                     int myIndex = -1;
@@ -72,6 +74,29 @@
             for (int i = 0; i < vthreadCount; i++) {
                 vthreads[i].join();
             }
         }
     }
+
+    static class Scheduler implements Executor, AutoCloseable {
+        private final ExecutorService pool;
+
+        Scheduler(int poolSize) {
+            pool = Executors.newFixedThreadPool(poolSize);
+        }
+
+        @Override
+        public void execute(Runnable task) {
+            try {
+                pool.execute(task);
+            } finally {
+                // ExecutorService::execute may consume parking permit
+                LockSupport.unpark(Thread.currentThread());
+            }
+        }
+
+        @Override
+        public void close() {
+            pool.close();
+        }
+    }
 }
diff a/test/jdk/java/lang/Thread/virtual/Parking.java b/test/jdk/java/lang/Thread/virtual/Parking.java
--- a/test/jdk/java/lang/Thread/virtual/Parking.java
+++ b/test/jdk/java/lang/Thread/virtual/Parking.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -19,27 +19,64 @@
  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  * or visit www.oracle.com if you need additional information or have any
  * questions.
  */
 
-/**
- * @test
+/*
+ * @test id=default
  * @summary Test virtual threads using park/unpark
+ * @modules java.base/java.lang:+open jdk.management
  * @library /test/lib
+ * @build LockingMode
  * @run junit Parking
  */
 
+/*
+ * @test id=Xint
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xint Parking
+ */
+
+/*
+ * @test id=Xcomp
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xcomp Parking
+ */
+
+/*
+ * @test id=Xcomp-noTieredCompilation
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @build LockingMode
+ * @run junit/othervm -Xcomp -XX:-TieredCompilation Parking
+ */
+
 import java.time.Duration;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.locks.LockSupport;
 
 import jdk.test.lib.thread.VThreadRunner;
+import jdk.test.lib.thread.VThreadScheduler;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIf;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assumptions.*;
 
 class Parking {
-    private static final Object lock = new Object();
+    static final int MAX_VTHREAD_COUNT = 4 * Runtime.getRuntime().availableProcessors();
+    static final Object lock = new Object();
 
     /**
      * Park, unparked by platform thread.
      */
     @Test
@@ -341,10 +378,95 @@
                 }
             }
         });
     }
 
+    /**
+     * Test that parking while holding a monitor releases the carrier.
+     */
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    @DisabledIf("LockingMode#isLegacy")
+    void testParkWhenHoldingMonitor(boolean reenter) throws Exception {
+        assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers");
+        try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) {
+            ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler);
+
+            var lock = new Object();
+
+            // thread enters (and maybe reenters) a monitor and parks
+            var started = new CountDownLatch(1);
+            var vthread1 = factory.newThread(() -> {
+                started.countDown();
+                synchronized (lock) {
+                    if (reenter) {
+                        synchronized (lock) {
+                            LockSupport.park();
+                        }
+                    } else {
+                        LockSupport.park();
+                    }
+                }
+            });
+
+            vthread1.start();
+            try {
+                // wait for thread to start and park
+                started.await();
+                await(vthread1, Thread.State.WAITING);
+
+                // carrier should be released, use it for another thread
+                var executed = new AtomicBoolean();
+                var vthread2 = factory.newThread(() -> {
+                    executed.set(true);
+                });
+                vthread2.start();
+                vthread2.join();
+                assertTrue(executed.get());
+            } finally {
+                LockSupport.unpark(vthread1);
+                vthread1.join();
+            }
+        }
+    }
+
+    /**
+     * Test lots of virtual threads parked while holding a monitor. If the number of
+     * virtual threads exceeds the number of carrier threads then this test will hang if
+     * parking doesn't release the carrier.
+     */
+    @Test
+    @DisabledIf("LockingMode#isLegacy")
+    void testManyParkedWhenHoldingMonitor() throws Exception {
+        Thread[] vthreads = new Thread[MAX_VTHREAD_COUNT];
+        var done = new AtomicBoolean();
+        for (int i = 0; i < MAX_VTHREAD_COUNT; i++) {
+            var lock = new Object();
+            var started = new CountDownLatch(1);
+            var vthread = Thread.ofVirtual().start(() -> {
+                started.countDown();
+                synchronized (lock) {
+                    while (!done.get()) {
+                        LockSupport.park();
+                    }
+                }
+            });
+            // wait for thread to start and park
+            started.await();
+            await(vthread, Thread.State.WAITING);
+            vthreads[i] = vthread;
+        }
+
+        // cleanup
+        done.set(true);
+        for (int i = 0; i < MAX_VTHREAD_COUNT; i++) {
+            var vthread = vthreads[i];
+            LockSupport.unpark(vthread);
+            vthread.join();
+        }
+    }
+
     /**
      * Schedule a thread to be interrupted after a delay.
      */
     private static void scheduleInterrupt(Thread thread, long delay) {
         Runnable interruptTask = () -> {
@@ -355,6 +477,18 @@
                 e.printStackTrace();
             }
         };
         new Thread(interruptTask).start();
     }
+
+    /**
+     * Waits for the given thread to reach a given state.
+     */
+    private void await(Thread thread, Thread.State expectedState) throws InterruptedException {
+        Thread.State state = thread.getState();
+        while (state != expectedState) {
+            assertTrue(state != Thread.State.TERMINATED, "Thread has terminated");
+            Thread.sleep(10);
+            state = thread.getState();
+        }
+    }
 }
diff a/test/jdk/java/lang/Thread/virtual/RetryMonitorEnterWhenPinned.java b/test/jdk/java/lang/Thread/virtual/RetryMonitorEnterWhenPinned.java
--- /dev/null
+++ b/test/jdk/java/lang/Thread/virtual/RetryMonitorEnterWhenPinned.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Test that a virtual thread waiting to enter a monitor, while pinning its
+ *   carrier, will retry until it enters the monitor. This avoids starvation when the
+ *   monitor is exited, an unmounted thread is the chosen successor, and the successor
+ *   can't continue because there are no carriers available.
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @requires vm.opt.LockingMode != 1
+ * @run main/othervm --enable-native-access=ALL-UNNAMED RetryMonitorEnterWhenPinned
+ */
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import jdk.test.lib.thread.VThreadPinner;
+
+public class RetryMonitorEnterWhenPinned {
+    public static void main(String[] args) throws Exception {
+        int iterations = (args.length > 0) ? Integer.parseInt(args[0]) : 10;
+        for (int i = 1; i <= iterations; i++) {
+            System.out.printf("%s -- iteration %d --%n", Instant.now(), i);
+            run();
+            System.out.println();
+        }
+    }
+
+    static void run() throws Exception {
+        var threads = new ArrayList<Thread>();
+
+        Object lock = new Object();
+        synchronized (lock) {
+
+            // start virtual threads that block on monitorenter
+            for (int i = 0; i < 100; i++) {
+                var started = new CountDownLatch(1);
+                Thread thread = Thread.startVirtualThread(() -> {
+                    started.countDown();
+                    synchronized (lock) {
+                        spin(20);
+                    }
+                });
+
+                // wait for thread to start and block
+                started.await();
+                await(thread, Thread.State.BLOCKED);
+                threads.add(thread);
+            }
+
+            // start virtual threads that block on monitorenter while pinned
+            int carriersAvailable = Runtime.getRuntime().availableProcessors();
+            if (Thread.currentThread().isVirtual()) {
+                carriersAvailable--;
+            }
+            for (int i = 0; i < 100; i++) {
+                var started = new CountDownLatch(1);
+                Thread thread = Thread.startVirtualThread(() -> {
+                    started.countDown();
+                    VThreadPinner.runPinned(() -> {
+                        synchronized (lock) {
+                            spin(20);
+                        }
+                    });
+                });
+
+                // if there are carriers available when wait until the thread blocks.
+                if (carriersAvailable > 0) {
+                    System.out.printf("%s waiting for thread #%d to block%n",
+                            Instant.now(), thread.threadId());
+                    started.await();
+                    await(thread, Thread.State.BLOCKED);
+                    carriersAvailable--;
+                }
+                threads.add(thread);
+            }
+
+        } // exit monitor
+
+        // wait for all threads to terminate
+        int threadsRemaining = threads.size();
+        while (threadsRemaining > 0) {
+            System.out.printf("%s waiting for %d threads to terminate%n",
+                    Instant.now(), threadsRemaining);
+            int terminated = 0;
+            for (Thread t : threads) {
+                if (t.join(Duration.ofSeconds(1))) {
+                    terminated++;
+                }
+            }
+            threadsRemaining = threads.size() - terminated;
+        }
+        System.out.printf("%s done%n", Instant.now());
+    }
+
+    /**
+     * Spin for the given number of milliseconds.
+     */
+    static void spin(long millis) {
+        long nanos = TimeUnit.MILLISECONDS.toNanos(millis);
+        long start = System.nanoTime();
+        while ((System.nanoTime() - start) < nanos) {
+            Thread.onSpinWait();
+        }
+    }
+
+    /**
+     * Wait for a thread to reach the expected state.
+     */
+    static void await(Thread thread, Thread.State expectedState) throws InterruptedException {
+        Thread.State state = thread.getState();
+        while (state != expectedState) {
+            assert state != Thread.State.TERMINATED : "Thread has terminated";
+            Thread.sleep(10);
+            state = thread.getState();
+        }
+    }
+}
diff a/test/jdk/java/lang/Thread/virtual/SynchronizedNative.java b/test/jdk/java/lang/Thread/virtual/SynchronizedNative.java
--- /dev/null
+++ b/test/jdk/java/lang/Thread/virtual/SynchronizedNative.java
@@ -0,0 +1,511 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test id=default
+ * @summary Test virtual threads with a synchronized native method and a native method
+ *      that enter/exits a monitor with JNI MonitorEnter/MonitorExit
+ * @requires vm.continuations
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @run junit/othervm SynchronizedNative
+ */
+
+/*
+ * @test id=Xint
+ * @requires vm.continuations
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @run junit/othervm -Xint SynchronizedNative
+ */
+
+/*
+ * @test id=Xcomp-TieredStopAtLevel1
+ * @requires vm.continuations
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @run junit/othervm -Xcomp -XX:TieredStopAtLevel=1 SynchronizedNative
+ */
+
+/*
+ * @test id=Xcomp-noTieredCompilation
+ * @requires vm.continuations
+ * @modules java.base/java.lang:+open jdk.management
+ * @library /test/lib
+ * @run junit/othervm -Xcomp -XX:-TieredCompilation SynchronizedNative
+ */
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Phaser;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.LockSupport;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import jdk.test.lib.thread.VThreadPinner;
+import jdk.test.lib.thread.VThreadRunner;   // ensureParallelism requires jdk.management
+import jdk.test.lib.thread.VThreadScheduler;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import static org.junit.jupiter.api.Assertions.*;
+
+class SynchronizedNative {
+
+    @BeforeAll
+    static void setup() throws Exception {
+        // need at least two carriers to test pinning
+        VThreadRunner.ensureParallelism(2);
+        System.loadLibrary("SynchronizedNative");
+    }
+
+    /**
+     * Test entering a monitor with a synchronized native method, no contention.
+     */
+    @Test
+    void testEnter() throws Exception {
+        Object lock = this;
+        VThreadRunner.run(() -> {
+            runWithSynchronizedNative(() -> {
+                assertTrue(Thread.holdsLock(lock));
+            });
+            assertFalse(Thread.holdsLock(lock));
+        });
+    }
+
+    /**
+     * Test reentering a monitor with synchronized native method, no contention.
+     */
+    @Test
+    void testReenter() throws Exception {
+        Object lock = this;
+        VThreadRunner.run(() -> {
+
+            // enter, reenter with a synchronized native method
+            synchronized (lock) {
+                runWithSynchronizedNative(() -> {
+                    assertTrue(Thread.holdsLock(lock));
+                });
+                assertTrue(Thread.holdsLock(lock));
+            }
+            assertFalse(Thread.holdsLock(lock));
+
+            // enter with synchronized native method, renter with synchronized statement
+            runWithSynchronizedNative(() -> {
+                assertTrue(Thread.holdsLock(lock));
+                synchronized (lock) {
+                    assertTrue(Thread.holdsLock(lock));
+                }
+                assertTrue(Thread.holdsLock(lock));
+            });
+            assertFalse(Thread.holdsLock(lock));
+
+            // enter with synchronized native method, reenter with synchronized native method
+            runWithSynchronizedNative(() -> {
+                assertTrue(Thread.holdsLock(lock));
+                runWithSynchronizedNative(() -> {
+                    assertTrue(Thread.holdsLock(lock));
+                });
+                assertTrue(Thread.holdsLock(lock));
+            });
+            assertFalse(Thread.holdsLock(lock));
+        });
+    }
+
+    /**
+     * Test entering a monitor with a synchronized native method and with contention.
+     */
+    @Test
+    void testEnterWithContention() throws Exception {
+        var lock = this;
+        var started = new CountDownLatch(1);
+        var entered = new AtomicBoolean();
+        var vthread = Thread.ofVirtual().unstarted(() -> {
+            started.countDown();
+            runWithSynchronizedNative(() -> {
+                assertTrue(Thread.holdsLock(lock));
+                entered.set(true);
+            });
+        });
+        try {
+            synchronized (lock) {
+                vthread.start();
+
+                // wait for thread to start and block
+                started.await();
+                await(vthread, Thread.State.BLOCKED);
+
+                assertFalse(entered.get());
+            }
+        } finally {
+            vthread.join();
+        }
+        assertTrue(entered.get());
+    }
+
+    /**
+     * Returns a stream of elements that are ordered pairs of platform and virtual thread
+     * counts. 0,2,4 platform threads. 2,4,6,8 virtual threads.
+     */
+    static Stream<Arguments> threadCounts() {
+        return IntStream.range(0, 5)
+                .filter(i -> i % 2 == 0)
+                .mapToObj(i -> i)
+                .flatMap(np -> IntStream.range(2, 9)
+                        .filter(i -> i % 2 == 0)
+                        .mapToObj(vp -> Arguments.of(np, vp)));
+    }
+
+    /**
+     * Execute a task concurrently from both platform and virtual threads.
+     */
+    private void executeConcurrently(int nPlatformThreads,
+                                     int nVirtualThreads,
+                                     Runnable task) throws Exception {
+        int parallism = nVirtualThreads;
+        if (Thread.currentThread().isVirtual()) {
+            parallism++;
+        }
+        int previousParallelism = VThreadRunner.ensureParallelism(parallism);
+        try {
+            int nthreads = nPlatformThreads + nVirtualThreads;
+            var phaser = new Phaser(nthreads + 1);
+
+            // start all threads
+            var threads = new Thread[nthreads];
+            int index = 0;
+            for (int i = 0; i < nPlatformThreads; i++) {
+                threads[index++] = Thread.ofPlatform().start(() -> {
+                    phaser.arriveAndAwaitAdvance();
+                    task.run();
+                });
+            }
+            for (int i = 0; i < nVirtualThreads; i++) {
+                threads[index++] = Thread.ofVirtual().start(() -> {
+                    phaser.arriveAndAwaitAdvance();
+                    task.run();
+                });
+            }
+
+            // wait for all threads to start
+            phaser.arriveAndAwaitAdvance();
+            System.err.printf("  %d threads started%n", nthreads);
+
+            // wait for all threads to terminate
+            for (Thread thread : threads) {
+                if (thread != null) {
+                    System.err.printf("  join %s ...%n", thread);
+                    thread.join();
+                }
+            }
+        } finally {
+            // reset parallelism
+            VThreadRunner.setParallelism(previousParallelism);
+        }
+    }
+
+
+    /**
+     * Test entering a monitor with a synchronized native method from many threads
+     * at the same time.
+     */
+    @ParameterizedTest
+    @MethodSource("threadCounts")
+    void testEnterConcurrently(int nPlatformThreads, int nVirtualThreads) throws Exception {
+        var counter = new Object() {
+            int value;
+            int value() { return value; }
+            void increment() { value++; }
+        };
+        var lock = this;
+        executeConcurrently(nPlatformThreads, nVirtualThreads, () -> {
+            runWithSynchronizedNative(() -> {
+                assertTrue(Thread.holdsLock(lock));
+                counter.increment();
+                LockSupport.parkNanos(100_000_000);  // 100ms
+            });
+        });
+        synchronized (lock) {
+            assertEquals(nPlatformThreads + nVirtualThreads, counter.value());
+        }
+    }
+
+    /**
+     * Test entering a monitor with JNI MonitorEnter.
+     */
+    @Test
+    void testEnterInNative() throws Exception {
+        Object lock = new Object();
+        VThreadRunner.run(() -> {
+            runWithMonitorEnteredInNative(lock, () -> {
+                assertTrue(Thread.holdsLock(lock));
+            });
+            assertFalse(Thread.holdsLock(lock));
+        });
+    }
+
+    /**
+     * Test reentering a monitor with JNI MonitorEnter.
+     */
+    @Test
+    void testReenterInNative() throws Exception {
+        Object lock = new Object();
+        VThreadRunner.run(() -> {
+
+            // enter, reenter with JNI MonitorEnter
+            synchronized (lock) {
+                runWithMonitorEnteredInNative(lock, () -> {
+                    assertTrue(Thread.holdsLock(lock));
+                });
+                assertTrue(Thread.holdsLock(lock));
+            }
+            assertFalse(Thread.holdsLock(lock));
+
+            // enter with JNI MonitorEnter, renter with synchronized statement
+            runWithMonitorEnteredInNative(lock, () -> {
+                assertTrue(Thread.holdsLock(lock));
+                synchronized (lock) {
+                    assertTrue(Thread.holdsLock(lock));
+                }
+                assertTrue(Thread.holdsLock(lock));
+            });
+            assertFalse(Thread.holdsLock(lock));
+
+            // enter with JNI MonitorEnter, renter with JNI MonitorEnter
+            runWithMonitorEnteredInNative(lock, () -> {
+                assertTrue(Thread.holdsLock(lock));
+                runWithMonitorEnteredInNative(lock, () -> {
+                    assertTrue(Thread.holdsLock(lock));
+                });
+                assertTrue(Thread.holdsLock(lock));
+            });
+            assertFalse(Thread.holdsLock(lock));
+        });
+    }
+
+    /**
+     * Test entering a monitor with JNI MonitorEnter and with contention.
+     */
+    @Test
+    void testEnterInNativeWithContention() throws Exception {
+        var lock = new Object();
+        var started = new CountDownLatch(1);
+        var entered = new AtomicBoolean();
+        var vthread = Thread.ofVirtual().unstarted(() -> {
+            started.countDown();
+            runWithMonitorEnteredInNative(lock, () -> {
+                assertTrue(Thread.holdsLock(lock));
+                entered.set(true);
+            });
+        });
+        try {
+            synchronized (lock) {
+                vthread.start();
+
+                // wait for thread to start and block
+                started.await();
+                await(vthread, Thread.State.BLOCKED);
+
+                assertFalse(entered.get());
+            }
+        } finally {
+            vthread.join();
+        }
+        assertTrue(entered.get());
+    }
+
+    /**
+     * Test entering a monitor with JNI MonitorEnter from many threads at the same time.
+     */
+    @ParameterizedTest
+    @MethodSource("threadCounts")
+    void testEnterInNativeConcurrently(int nPlatformThreads, int nVirtualThreads) throws Exception {
+        var counter = new Object() {
+            int value;
+            int value() { return value; }
+            void increment() { value++; }
+        };
+        var lock = counter;
+        executeConcurrently(nPlatformThreads, nVirtualThreads, () -> {
+            runWithMonitorEnteredInNative(lock, () -> {
+                assertTrue(Thread.holdsLock(lock));
+                counter.increment();
+                LockSupport.parkNanos(100_000_000);  // 100ms
+            });
+        });
+        synchronized (lock) {
+            assertEquals(nPlatformThreads + nVirtualThreads, counter.value());
+        }
+    }
+
+    /**
+     * Test parking with synchronized native method on stack.
+     */
+    @Test
+    void testParkingWhenPinned() throws Exception {
+        var lock = this;
+        var started = new CountDownLatch(1);
+        var entered = new AtomicBoolean();
+        var done = new AtomicBoolean();
+        var vthread = Thread.ofVirtual().start(() -> {
+            started.countDown();
+            runWithSynchronizedNative(() -> {
+                assertTrue(Thread.holdsLock(lock));
+                entered.set(true);
+                while (!done.get()) {
+                    LockSupport.park();
+                }
+            });
+        });
+        try {
+            // wait for thread to start and block
+            started.await();
+            await(vthread, Thread.State.WAITING);
+        } finally {
+            done.set(true);
+            LockSupport.unpark(vthread);
+            vthread.join();
+        }
+        assertTrue(entered.get());
+    }
+
+    /**
+     * Test blocking with synchronized native method on stack.
+     */
+    @Test
+    void testBlockingWhenPinned() throws Exception {
+        var lock1 = this;
+        var lock2 = new Object();
+
+        var started = new CountDownLatch(1);
+        var entered1 = new AtomicBoolean();   // set to true when vthread enters lock1
+        var entered2 = new AtomicBoolean();   // set to true when vthread enters lock2
+
+        var vthread = Thread.ofVirtual().unstarted(() -> {
+            started.countDown();
+            runWithSynchronizedNative(() -> {
+                assertTrue(Thread.holdsLock(lock1));
+                entered1.set(true);
+                synchronized (lock2) {   // should block
+                    assertTrue(Thread.holdsLock(lock2));
+                    entered2.set(true);
+                }
+            });
+        });
+        try {
+            synchronized (lock2) {
+                // start thread and wait for it to block trying to enter lock2
+                vthread.start();
+                started.await();
+                await(vthread, Thread.State.BLOCKED);
+
+                assertTrue(entered1.get());
+                assertFalse(entered2.get());
+            }
+        } finally {
+            vthread.join();
+        }
+        assertTrue(entered2.get());
+    }
+
+    /**
+     * Test that blocking on synchronized native method releases the carrier.
+     */
+    //@Test
+    void testReleaseWhenBlocked() throws Exception {
+        assertTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers");
+        try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) {
+            ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler);
+
+            var lock = this;
+            var started = new CountDownLatch(1);
+            var entered = new AtomicBoolean();   // set to true when vthread enters lock
+
+            var vthread1 = factory.newThread(() -> {
+                started.countDown();
+                runWithSynchronizedNative(() -> {
+                    assertTrue(Thread.holdsLock(lock));
+                    entered.set(true);
+                });
+                assertFalse(Thread.holdsLock(lock));
+            });
+
+            vthread1.start();
+            try {
+                synchronized (this) {
+                    // start thread and wait for it to block
+                    vthread1.start();
+                    started.await();
+                    await(vthread1, Thread.State.BLOCKED);
+
+                    // carrier should be released, use it for another thread
+                    var executed = new AtomicBoolean();
+                    var vthread2 = factory.newThread(() -> {
+                        executed.set(true);
+                    });
+                    vthread2.start();
+                    vthread2.join();
+                    assertTrue(executed.get());
+                }
+            } finally {
+                vthread1.join();
+            }
+        }
+    }
+
+    /**
+     * Invokes the given task's run method while holding the monitor for "this".
+     */
+    private synchronized native void runWithSynchronizedNative(Runnable task);
+
+    /**
+     * Invokes the given task's run method while holding the monitor for the given
+     * object. The monitor is entered with JNI MonitorEnter, and exited with JNI MonitorExit.
+     */
+    private native void runWithMonitorEnteredInNative(Object lock, Runnable task);
+
+    /**
+     * Called from native methods to run the given task.
+     */
+    private void run(Runnable task) {
+        task.run();
+    }
+
+    /**
+     * Waits for the given thread to reach a given state.
+     */
+    private void await(Thread thread, Thread.State expectedState) throws InterruptedException {
+        Thread.State state = thread.getState();
+        while (state != expectedState) {
+            assertTrue(state != Thread.State.TERMINATED, "Thread has terminated");
+            Thread.sleep(10);
+            state = thread.getState();
+        }
+    }
+}
diff a/test/jdk/java/lang/Thread/virtual/ThreadAPI.java b/test/jdk/java/lang/Thread/virtual/ThreadAPI.java
--- a/test/jdk/java/lang/Thread/virtual/ThreadAPI.java
+++ b/test/jdk/java/lang/Thread/virtual/ThreadAPI.java
@@ -25,18 +25,20 @@
  * @test id=default
  * @bug 8284161 8286788 8321270
  * @summary Test Thread API with virtual threads
  * @modules java.base/java.lang:+open jdk.management
  * @library /test/lib
+ * @build LockingMode
  * @run junit/othervm --enable-native-access=ALL-UNNAMED ThreadAPI
  */
 
 /*
  * @test id=no-vmcontinuations
  * @requires vm.continuations
  * @modules java.base/java.lang:+open jdk.management
  * @library /test/lib
+ * @build LockingMode
  * @run junit/othervm -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations
  *     --enable-native-access=ALL-UNNAMED ThreadAPI
  */
 
 import java.time.Duration;
@@ -66,10 +68,11 @@
 import jdk.test.lib.thread.VThreadScheduler;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.condition.DisabledIf;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.junit.jupiter.params.provider.ValueSource;
 import static org.junit.jupiter.api.Assertions.*;
 import static org.junit.jupiter.api.Assumptions.*;
@@ -1080,11 +1083,11 @@
 
     /**
      * Test Thread.yield releases carrier thread.
      */
     @Test
-    void testYield1() throws Exception {
+    void testYieldReleasesCarrier() throws Exception {
         assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers");
         var list = new CopyOnWriteArrayList<String>();
         try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) {
             ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler);
             var thread = factory.newThread(() -> {
@@ -1104,14 +1107,45 @@
         }
         assertEquals(List.of("A", "B", "A", "B"), list);
     }
 
     /**
-     * Test Thread.yield when thread is pinned by native frame.
+     * Test Thread.yield releases carrier thread when virtual thread holds a monitor.
      */
     @Test
-    void testYield2() throws Exception {
+    @DisabledIf("LockingMode#isLegacy")
+    void testYieldReleasesCarrierWhenHoldingMonitor() throws Exception {
+        assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers");
+        var list = new CopyOnWriteArrayList<String>();
+        try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) {
+            ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler);
+            var lock = new Object();
+            var thread = factory.newThread(() -> {
+                list.add("A");
+                var child = factory.newThread(() -> {
+                    list.add("B");
+                    synchronized (lock) {
+                        Thread.yield();
+                    }
+                    list.add("B");
+                });
+                child.start();
+                Thread.yield();
+                list.add("A");
+                try { child.join(); } catch (InterruptedException e) { }
+            });
+            thread.start();
+            thread.join();
+        }
+        assertEquals(List.of("A", "B", "A", "B"), list);
+    }
+
+    /**
+     * Test Thread.yield when thread is pinned.
+     */
+    @Test
+    void testYieldWhenPinned() throws Exception {
         assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers");
         var list = new CopyOnWriteArrayList<String>();
         try (ExecutorService scheduler = Executors.newFixedThreadPool(1)) {
             ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler);
             var thread = factory.newThread(() -> {
@@ -1134,11 +1168,11 @@
 
     /**
      * Test Thread.yield does not consume the thread's parking permit.
      */
     @Test
-    void testYield3() throws Exception {
+    void testYieldDoesNotConsumParkingPermit() throws Exception {
         var thread = Thread.ofVirtual().start(() -> {
             LockSupport.unpark(Thread.currentThread());
             Thread.yield();
             LockSupport.park();  // should not park
         });
@@ -1147,11 +1181,11 @@
 
     /**
      * Test Thread.yield does not make available the thread's parking permit.
      */
     @Test
-    void testYield4() throws Exception {
+    void testYieldDoesNotOfferParkingPermit() throws Exception {
         var thread = Thread.ofVirtual().start(() -> {
             Thread.yield();
             LockSupport.park();  // should park
         });
         try {
@@ -1969,10 +2003,42 @@
                 assertTrue(Thread.holdsLock(lock));
             }
         });
     }
 
+    /**
+     * Test Thread.holdsLock when lock held by carrier thread.
+     */
+    @Disabled
+    @Test
+    void testHoldsLock3() throws Exception {
+        assumeTrue(VThreadScheduler.supportsCustomScheduler(), "No support for custom schedulers");
+
+        Object lock = new Object();
+
+        // carrier thread runs all tasks while holding the lock
+        ThreadFactory carrierThreadFactory = task -> Thread.ofPlatform().unstarted(() -> {
+            synchronized (lock) {
+                task.run();
+            }
+        });
+        try (ExecutorService pool = Executors.newSingleThreadExecutor(carrierThreadFactory)) {
+            Executor scheduler = task -> pool.submit(task::run);
+            ThreadFactory factory = VThreadScheduler.virtualThreadFactory(scheduler);
+
+            // start virtual that tests if it holds the lock
+            var result = new AtomicReference<Boolean>();
+            Thread vthread = factory.newThread(() -> {
+                result.set(Thread.holdsLock(lock));
+            });
+            vthread.start();
+            vthread.join();
+            boolean holdsLock = result.get();
+            assertFalse(holdsLock, "Thread.holdsLock should return false");
+        }
+    }
+
     /**
      * Test Thread::getStackTrace on unstarted thread.
      */
     @Test
     void testGetStackTraceUnstarted() {
diff a/test/jdk/java/lang/Thread/virtual/TracePinnedThreads.java b/test/jdk/java/lang/Thread/virtual/TracePinnedThreads.java
--- a/test/jdk/java/lang/Thread/virtual/TracePinnedThreads.java
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-/**
- * @test
- * @bug 8284161 8289284 8322846
- * @summary Basic test of debugging option to trace pinned threads
- * @requires vm.continuations
- * @library /test/lib
- * @run junit/othervm -Djdk.tracePinnedThreads=full TracePinnedThreads
- * @run junit/othervm -Djdk.tracePinnedThreads=short TracePinnedThreads
- */
-
-import java.io.ByteArrayOutputStream;
-import java.io.PrintStream;
-import java.time.Duration;
-import java.util.concurrent.Executors;
-import java.util.concurrent.locks.LockSupport;
-
-import jdk.test.lib.thread.VThreadRunner;
-import org.junit.jupiter.api.Test;
-import static org.junit.jupiter.api.Assertions.*;
-
-class TracePinnedThreads {
-    static final Object lock = new Object();
-
-    /**
-     * Parks current thread for 1 second.
-     */
-    private static void park() {
-        long nanos = Duration.ofSeconds(1).toNanos();
-        LockSupport.parkNanos(nanos);
-    }
-
-    /**
-     * Invokes the park method through a native frame to park the current
-     * thread for 1 second.
-     */
-    private static native void invokePark();
-
-    /**
-     * Test parking inside synchronized block.
-     */
-    @Test
-    void testPinnedCausedBySynchronizedBlock() throws Exception {
-        String output = run(() -> {
-            synchronized (lock) {
-                park();
-            }
-        });
-        assertContains(output, "reason:MONITOR");
-        assertContains(output, "<== monitors:1");
-    }
-
-    /**
-     * Test parking with native frame on stack.
-     */
-    @Test
-    void testPinnedCausedByNativeMethod() throws Exception {
-        System.loadLibrary("TracePinnedThreads");
-        String output = run(() -> invokePark());
-        assertContains(output, "reason:NATIVE");
-        assertContains(output, "(Native Method)");
-    }
-
-    /**
-     * Test parking in class initializer.
-     */
-    @Test
-    void testPinnedCausedByClassInitializer() throws Exception {
-        class C {
-            static {
-                park();
-            }
-        }
-        String output = run(C::new);
-        assertContains(output, "reason:NATIVE");
-        assertContains(output, "<clinit>");
-    }
-
-    /**
-     * Test contention writing to System.out when pinned. The test creates four threads
-     * that write to System.out when pinned, this is enough to potentially deadlock
-     * without the changes in JDK-8322846.
-     */
-    @Test
-    void testContention() throws Exception {
-        // use several classes to avoid duplicate stack traces
-        class C1 {
-            synchronized void print() {
-                System.out.println("hello");
-            }
-        }
-        class C2 {
-            synchronized void print() {
-                System.out.println("hello");
-            }
-        }
-        class C3 {
-            synchronized void print() {
-                System.out.println("hello");
-            }
-        }
-        class C4 {
-            synchronized void print() {
-                System.out.println("hello");
-            }
-        }
-
-        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
-            executor.submit(() -> {
-                new C1().print();
-            });
-            executor.submit(() -> {
-                new C2().print();
-            });
-            executor.submit(() -> {
-                new C3().print();
-            });
-            executor.submit(() -> {
-                new C4().print();
-            });
-        }
-    }
-
-    /**
-     * Run a task in a virtual thread, returning a String with any output printed
-     * to standard output.
-     */
-    private static String run(Runnable task) throws Exception {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        PrintStream original = System.out;
-        System.setOut(new PrintStream(baos, true));
-        try {
-            VThreadRunner.run(task::run);
-        } finally {
-            System.setOut(original);
-        }
-        String output = new String(baos.toByteArray());
-        System.out.println(output);
-        return output;
-    }
-
-    /**
-     * Tests that s1 contains s2.
-     */
-    private static void assertContains(String s1, String s2) {
-        assertTrue(s1.contains(s2), s2 + " not found!!!");
-    }
-
-    /**
-     * Tests that s1 does not contain s2.
-     */
-    private static void assertDoesNotContain(String s1, String s2) {
-        assertFalse(s1.contains(s2), s2 + " found!!");
-    }
-}
diff a/test/jdk/java/lang/Thread/virtual/VirtualThreadPinnedEventThrows.java b/test/jdk/java/lang/Thread/virtual/VirtualThreadPinnedEventThrows.java
--- a/test/jdk/java/lang/Thread/virtual/VirtualThreadPinnedEventThrows.java
+++ b/test/jdk/java/lang/Thread/virtual/VirtualThreadPinnedEventThrows.java
@@ -22,11 +22,11 @@
  */
 
 /**
  * @test
  * @summary Test parking when pinned and emitting the JFR VirtualThreadPinnedEvent throws
- * @modules java.base/java.lang:+open java.base/jdk.internal.event jdk.management
+ * @modules java.base/jdk.internal.event jdk.management
  * @library /test/lib
  * @compile/module=java.base jdk/internal/event/VirtualThreadPinnedEvent.java
  * @run junit/othervm --enable-native-access=ALL-UNNAMED VirtualThreadPinnedEventThrows
  */
 
diff a/test/jdk/java/lang/Thread/virtual/libSynchronizedNative.c b/test/jdk/java/lang/Thread/virtual/libSynchronizedNative.c
--- /dev/null
+++ b/test/jdk/java/lang/Thread/virtual/libSynchronizedNative.c
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+#include "jni.h"
+
+JNIEXPORT void JNICALL
+Java_SynchronizedNative_runWithSynchronizedNative(JNIEnv *env, jobject obj, jobject task) {
+    jclass clazz = (*env)->GetObjectClass(env, obj);
+    jmethodID mid = (*env)->GetMethodID(env, clazz, "run", "(Ljava/lang/Runnable;)V");
+    if (mid != NULL) {
+        (*env)->CallVoidMethod(env, obj, mid, task);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_SynchronizedNative_runWithMonitorEnteredInNative(JNIEnv *env, jobject obj, jobject lock, jobject task) {
+    jclass clazz = (*env)->GetObjectClass(env, obj);
+    jmethodID mid = (*env)->GetMethodID(env, clazz, "run", "(Ljava/lang/Runnable;)V");
+    if (mid != NULL && (*env)->MonitorEnter(env, lock) == 0) {
+        (*env)->CallVoidMethod(env, obj, mid, task);
+        (*env)->MonitorExit(env, lock);  // can be called with pending exception
+    }
+}
diff a/test/jdk/java/lang/Thread/virtual/libTracePinnedThreads.c b/test/jdk/java/lang/Thread/virtual/libTracePinnedThreads.c
--- a/test/jdk/java/lang/Thread/virtual/libTracePinnedThreads.c
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-#include "jni.h"
-
-JNIEXPORT void JNICALL Java_TracePinnedThreads_invokePark(JNIEnv *env, jclass clazz) {
-    jmethodID mid = (*env)->GetStaticMethodID(env, clazz, "park", "()V");
-    if (mid != NULL) {
-        (*env)->CallStaticVoidMethod(env, clazz, mid);
-    }
-}
diff a/test/jdk/java/lang/Thread/virtual/stress/GetStackTraceALotWithTimedWait.java b/test/jdk/java/lang/Thread/virtual/stress/GetStackTraceALotWithTimedWait.java
--- /dev/null
+++ b/test/jdk/java/lang/Thread/virtual/stress/GetStackTraceALotWithTimedWait.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Stress test Thread.getStackTrace on a virtual thread in timed-Object.wait
+ * @requires vm.debug != true
+ * @run main/othervm GetStackTraceALotWithTimedWait 100000
+ */
+
+/*
+ * @test
+ * @requires vm.debug == true
+ * @run main/othervm GetStackTraceALotWithTimedWait 50000
+ */
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class GetStackTraceALotWithTimedWait {
+
+    public static void main(String[] args) throws Exception {
+        int iterations = args.length > 0 ? Integer.parseInt(args[0]) : 100_000;
+
+        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
+            var done = new AtomicBoolean();
+            var threads = new ArrayList<Thread>();
+
+            // start threads that invoke Object.wait with a short timeout
+            int nthreads = Runtime.getRuntime().availableProcessors();
+            for (int i = 0; i < nthreads; i++) {
+                var ref = new AtomicReference<Thread>();
+                var lock = new Object();
+                executor.submit(() -> {
+                    ref.set(Thread.currentThread());
+                    while (!done.get()) {
+                        synchronized (lock) {
+                            int delay = 1 + ThreadLocalRandom.current().nextInt(20);
+                            lock.wait(delay);
+                        }
+                    }
+                    return null;
+                });
+                Thread thread;
+                while ((thread = ref.get()) == null) {
+                    Thread.sleep(20);
+                }
+                threads.add(thread);
+            }
+
+            // hammer on Thread.getStackTrace
+            try {
+                for (int i = 1; i <= iterations; i++) {
+                    if ((i % 1000) == 0) {
+                        System.out.println(Instant.now() + " => " + i + " of " + iterations);
+                    }
+                    for (Thread thread : threads) {
+                        thread.getStackTrace();
+                    }
+                }
+            } finally {
+                done.set(true);
+            }
+        }
+    }
+}
+
diff a/test/jdk/java/lang/Thread/virtual/stress/LotsOfContendedMonitorEnter.java b/test/jdk/java/lang/Thread/virtual/stress/LotsOfContendedMonitorEnter.java
--- /dev/null
+++ b/test/jdk/java/lang/Thread/virtual/stress/LotsOfContendedMonitorEnter.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test id=default
+ * @summary Test virtual threads entering a lot of monitors with contention
+ * @requires vm.opt.LockingMode != 1
+ * @library /test/lib
+ * @run main/othervm LotsOfContendedMonitorEnter
+ */
+
+/*
+ * @test id=LM_LIGHTWEIGHT
+ * @requires vm.opt.LockingMode != 1
+ * @library /test/lib
+ * @run main/othervm -XX:LockingMode=2 LotsOfContendedMonitorEnter
+ */
+
+import java.util.concurrent.CountDownLatch;
+import jdk.test.lib.thread.VThreadRunner;
+
+public class LotsOfContendedMonitorEnter {
+
+    public static void main(String[] args) throws Exception {
+        int depth;
+        if (args.length > 0) {
+            depth = Integer.parseInt(args[0]);
+        } else {
+            depth = 1024;
+        }
+        VThreadRunner.run(() -> testContendedEnter(depth));
+    }
+
+    /**
+     * Enter the monitor for a new object, racing with another virtual thread that
+     * attempts to enter around the same time, then repeat to the given depth.
+     */
+    private static void testContendedEnter(int depthRemaining) throws Exception {
+        if (depthRemaining > 0) {
+            var lock = new Object();
+
+            // start thread to enter monitor for brief period, then enters again when signalled
+            var started = new CountDownLatch(1);
+            var signal = new CountDownLatch(1);
+            var thread = Thread.ofVirtual().start(() -> {
+                started.countDown();
+
+                // enter, may be contended
+                synchronized (lock) {
+                    Thread.onSpinWait();
+                }
+
+                // wait to be signalled
+                try {
+                    signal.await();
+                } catch (InterruptedException e) { }
+
+                // enter again, this will block until the main thread releases
+                synchronized (lock) {
+                    // do nothing
+                }
+            });
+            try {
+                // wait for thread to start
+                started.await();
+
+                // enter, may be contended
+                synchronized (lock) {
+                    // signal thread to enter monitor again, it should block
+                    signal.countDown();
+                    await(thread, Thread.State.BLOCKED);
+                    testContendedEnter(depthRemaining - 1);
+                }
+            } finally {
+                thread.join();
+            }
+        }
+    }
+
+    /**
+     * Waits for the given thread to reach a given state.
+     */
+    private static void await(Thread thread, Thread.State expectedState) {
+        Thread.State state = thread.getState();
+        while (state != expectedState) {
+            assert state != Thread.State.TERMINATED : "Thread has terminated";
+            Thread.yield();
+            state = thread.getState();
+        }
+    }
+}
diff a/test/jdk/java/lang/Thread/virtual/stress/LotsOfUncontendedMonitorEnter.java b/test/jdk/java/lang/Thread/virtual/stress/LotsOfUncontendedMonitorEnter.java
--- /dev/null
+++ b/test/jdk/java/lang/Thread/virtual/stress/LotsOfUncontendedMonitorEnter.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test id=default
+ * @summary Test virtual thread entering (and reentering) a lot of monitors with no contention
+ * @library /test/lib
+ * @run main/othervm LotsOfUncontendedMonitorEnter
+ */
+
+/*
+ * @test id=LM_LEGACY
+ * @library /test/lib
+ * @run main/othervm -XX:LockingMode=1 LotsOfUncontendedMonitorEnter
+ */
+
+/*
+ * @test id=LM_LIGHTWEIGHT
+ * @library /test/lib
+ * @run main/othervm -XX:LockingMode=2 LotsOfUncontendedMonitorEnter
+ */
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+import jdk.test.lib.thread.VThreadRunner;
+
+public class LotsOfUncontendedMonitorEnter {
+
+    public static void main(String[] args) throws Exception {
+        int depth;
+        if (args.length > 0) {
+            depth = Integer.parseInt(args[0]);
+        } else {
+            depth = 24; // 33554430 enters
+        }
+        VThreadRunner.run(() -> {
+            testEnter(List.of(), depth);
+        });
+    }
+
+    /**
+     * Enter the monitor for a new object, reenter a monitor that is already held, and
+     * repeat to the given depth.
+     */
+    private static void testEnter(List<Object> ownedMonitors, int depthRemaining) {
+        if (depthRemaining > 0) {
+            var lock = new Object();
+            synchronized (lock) {
+                // new list of owned monitors
+                var monitors = concat(ownedMonitors, lock);
+                testEnter(monitors, depthRemaining - 1);
+
+                // reenter a monitor that is already owned
+                int index = ThreadLocalRandom.current().nextInt(monitors.size());
+                var otherLock = monitors.get(index);
+
+                synchronized (otherLock) {
+                    testEnter(monitors, depthRemaining - 1);
+                }
+            }
+        }
+    }
+
+    /**
+     * Adds an element to a list, returning a new list.
+     */
+    private static <T> List<T> concat(List<T> list, T object) {
+        var newList = new ArrayList<>(list);
+        newList.add(object);
+        return newList;
+    }
+}
diff a/test/jdk/java/lang/Thread/virtual/stress/Skynet100kWithMonitors.java b/test/jdk/java/lang/Thread/virtual/stress/Skynet100kWithMonitors.java
--- /dev/null
+++ b/test/jdk/java/lang/Thread/virtual/stress/Skynet100kWithMonitors.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Stress test virtual threads with a variation of the Skynet 1M benchmark that uses
+ *   a channel implementation based on object monitors. This variant uses a reduced number of
+ *   100k virtual threads at the final level.
+ * @requires vm.debug != true & vm.continuations & vm.opt.LockingMode != 1
+ * @run main/othervm/timeout=300 Skynet100kWithMonitors 50
+ */
+
+/*
+ * @test
+ * @requires vm.debug == true & vm.continuations & vm.opt.LockingMode != 1
+ * @run main/othervm/timeout=300 Skynet100kWithMonitors 10
+ */
+
+public class Skynet100kWithMonitors {
+
+    public static void main(String[] args) {
+        int iterations = (args.length) > 0 ? Integer.parseInt(args[0]) : 10;
+        for (int i = 0; i < iterations; i++) {
+            skynet(100_000, 4999950000L);
+        }
+    }
+
+    static void skynet(int num, long expected) {
+        long start = System.currentTimeMillis();
+        var chan = new Channel<Long>();
+
+        Thread.startVirtualThread(() -> skynet(chan, 0, num, 10));
+
+        long sum = chan.receive();
+        long end = System.currentTimeMillis();
+        System.out.format("Result: %d in %s ms%n", sum, (end-start));
+        if (sum != expected)
+            throw new RuntimeException("Expected " + expected);
+    }
+
+    static void skynet(Channel<Long> result, int num, int size, int div) {
+        if (size == 1) {
+            result.send((long)num);
+        } else {
+            var chan = new Channel<Long>();
+            for (int i = 0; i < div; i++) {
+                int subNum = num + i * (size / div);
+                Thread.startVirtualThread(() -> skynet(chan, subNum, size / div, div));
+            }
+            long sum = 0;
+            for (int i = 0; i < div; i++) {
+                sum += chan.receive();
+            }
+            result.send(sum);
+        }
+    }
+
+    static class Channel<T> {
+        private final Object lock = new Object();
+        private T element;
+
+        Channel() {
+        }
+
+        void send(T e) {
+            boolean interrupted = false;
+            synchronized (lock) {
+                while (element != null) {
+                    try {
+                        lock.wait();
+                    } catch (InterruptedException x) {
+                        interrupted = true;
+                    }
+                }
+                element = e;
+                lock.notifyAll();
+            }
+            if (interrupted)
+                Thread.currentThread().interrupt();
+        }
+
+        T receive() {
+            T e;
+            boolean interrupted = false;
+            synchronized (lock) {
+                while ((e = element) == null) {
+                    try {
+                        lock.wait();
+                    } catch (InterruptedException x) {
+                        interrupted = true;
+                    }
+                }
+                element = null;
+                lock.notifyAll();
+            }
+            if (interrupted)
+                Thread.currentThread().interrupt();
+            return e;
+        }
+    }
+}
diff a/test/jdk/java/lang/Thread/virtual/stress/TEST.properties b/test/jdk/java/lang/Thread/virtual/stress/TEST.properties
--- a/test/jdk/java/lang/Thread/virtual/stress/TEST.properties
+++ /dev/null
@@ -1,24 +0,0 @@
-#
-# Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
-# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
-#
-# This code is free software; you can redistribute it and/or modify it
-# under the terms of the GNU General Public License version 2 only, as
-# published by the Free Software Foundation.
-#
-# This code is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
-# version 2 for more details (a copy is included in the LICENSE file that
-# accompanied this code).
-#
-# You should have received a copy of the GNU General Public License version
-# 2 along with this work; if not, write to the Free Software Foundation,
-# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
-#
-# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
-# or visit www.oracle.com if you need additional information or have any
-# questions.
-#
-
-exclusiveAccess.dirs=.
diff a/test/jdk/java/lang/reflect/callerCache/ReflectionCallerCacheTest.java b/test/jdk/java/lang/reflect/callerCache/ReflectionCallerCacheTest.java
--- a/test/jdk/java/lang/reflect/callerCache/ReflectionCallerCacheTest.java
+++ b/test/jdk/java/lang/reflect/callerCache/ReflectionCallerCacheTest.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -25,10 +25,11 @@
  * @test
  * @bug 8202113
  * @summary Test the caller class loader is not kept strongly reachable
  *         by reflection API
  * @library /test/lib/
+ * @requires vm.compMode != "Xcomp"
  * @modules jdk.compiler
  * @build ReflectionCallerCacheTest Members jdk.test.lib.compiler.CompilerUtils
  * @run testng/othervm ReflectionCallerCacheTest
  */
 
diff a/test/jdk/java/nio/channels/vthread/SelectorOps.java b/test/jdk/java/nio/channels/vthread/SelectorOps.java
--- a/test/jdk/java/nio/channels/vthread/SelectorOps.java
+++ b/test/jdk/java/nio/channels/vthread/SelectorOps.java
@@ -26,10 +26,18 @@
  * @summary Test virtual threads doing selection operations
  * @library /test/lib
  * @run junit/othervm --enable-native-access=ALL-UNNAMED SelectorOps
  */
 
+/*
+ * @test id=poller-modes
+ * @requires (os.family == "linux") | (os.family == "mac")
+ * @library /test/lib
+ * @run junit/othervm -Djdk.pollerMode=1 --enable-native-access=ALL-UNNAMED SelectorOps
+ * @run junit/othervm -Djdk.pollerMode=2 --enable-native-access=ALL-UNNAMED SelectorOps
+ */
+
 /*
  * @test id=no-vmcontinuations
  * @requires vm.continuations
  * @library /test/lib
  * @run junit/othervm -XX:+UnlockExperimentalVMOptions -XX:-VMContinuations
diff a/test/jdk/java/util/concurrent/StructuredTaskScope/StressCancellation.java b/test/jdk/java/util/concurrent/StructuredTaskScope/StressCancellation.java
--- /dev/null
+++ b/test/jdk/java/util/concurrent/StructuredTaskScope/StressCancellation.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Stress test of StructuredTaskScope cancellation with running and starting threads
+ * @enablePreview
+ * @run junit StressCancellation
+ */
+
+import java.time.Duration;
+import java.util.concurrent.StructuredTaskScope;
+import java.util.concurrent.StructuredTaskScope.Joiner;
+import java.util.concurrent.StructuredTaskScope.Subtask;
+import java.util.concurrent.ThreadFactory;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import static org.junit.jupiter.api.Assertions.*;
+
+class StressCancellation {
+
+    static Stream<Arguments> testCases() {
+        Stream<ThreadFactory> factories = Stream.of(
+                Thread.ofPlatform().factory(),
+                Thread.ofVirtual().factory()
+        );
+        // 0..15 forks before shutdown, 0..15 forks after shutdown
+        return factories.flatMap(f -> IntStream.range(0, 256)
+                .mapToObj(x -> Arguments.of(f, x & 0x0F, (x & 0xF0) >> 4)));
+    }
+
+    /**
+     * Test StructuredTaskScope cancellation with running threads and concurrently with
+     * threads that are starting. The cancellation should interrupt all running threads,
+     * join should wakeup, and close would complete quickly.
+     *
+     * @param factory the ThreadFactory to use
+     * @param beforeCancel the number of subtasks to fork before cancel
+     * @param afterCancel the number of subtasks to fork after cancel
+     */
+    @ParameterizedTest
+    @MethodSource("testCases")
+    void test(ThreadFactory factory, int beforeCancel, int afterCancel) throws Exception {
+        var joiner = new Joiner<Boolean, Void>() {
+            @Override
+            public boolean onComplete(Subtask<? extends Boolean> subtask) {
+                boolean cancel = subtask.get();
+                return cancel;
+            }
+            @Override
+            public Void result() {
+                return null;
+            }
+        };
+
+        try (var scope = StructuredTaskScope.open(joiner, cf -> cf.withThreadFactory(factory))) {
+            // fork subtasks
+            for (int i = 0; i < beforeCancel; i++) {
+                scope.fork(() -> {
+                    Thread.sleep(Duration.ofDays(1));
+                    return false;
+                });
+            }
+
+            // fork subtask to cancel
+            scope.fork(() -> true);
+
+            // fork after forking subtask to cancel
+            for (int i = 0; i < afterCancel; i++) {
+                scope.fork(() -> {
+                    Thread.sleep(Duration.ofDays(1));
+                    return false;
+                });
+            }
+
+            scope.join();
+        }
+    }
+}
diff a/test/jdk/java/util/concurrent/StructuredTaskScope/StressShutdown.java b/test/jdk/java/util/concurrent/StructuredTaskScope/StressShutdown.java
--- a/test/jdk/java/util/concurrent/StructuredTaskScope/StressShutdown.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-
-/*
- * @test
- * @bug 8311867
- * @summary Stress test of StructuredTaskScope.shutdown with running and starting threads
- * @enablePreview
- * @run junit StressShutdown
- */
-
-import java.time.Duration;
-import java.util.concurrent.Callable;
-import java.util.concurrent.StructuredTaskScope;
-import java.util.concurrent.ThreadFactory;
-import java.util.stream.IntStream;
-import java.util.stream.Stream;
-
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
-import static org.junit.jupiter.api.Assertions.*;
-
-class StressShutdown {
-
-    static final Callable<Void> SLEEP_FOR_A_DAY = () -> {
-        Thread.sleep(Duration.ofDays(1));
-        return null;
-    };
-
-    static Stream<Arguments> testCases() {
-        Stream<ThreadFactory> factories = Stream.of(
-                Thread.ofPlatform().factory(),
-                Thread.ofVirtual().factory()
-        );
-        // 0..15 forks before shutdown, 0..15 forks after shutdown
-        return factories.flatMap(f -> IntStream.range(0, 256)
-                .mapToObj(x -> Arguments.of(f, x & 0x0F, (x & 0xF0) >> 4)));
-    }
-
-    /**
-     * Test StructuredTaskScope.shutdown with running threads and concurrently with
-     * threads that are starting. The shutdown should interrupt all threads so that
-     * join wakes up.
-     *
-     * @param factory the ThreadFactory to use
-     * @param beforeShutdown the number of subtasks to fork before shutdown
-     * @param afterShutdown the number of subtasks to fork after shutdown
-     */
-    @ParameterizedTest
-    @MethodSource("testCases")
-    void testShutdown(ThreadFactory factory, int beforeShutdown, int afterShutdown)
-        throws InterruptedException
-    {
-        try (var scope = new StructuredTaskScope<>(null, factory)) {
-            // fork subtasks
-            for (int i = 0; i < beforeShutdown; i++) {
-                scope.fork(SLEEP_FOR_A_DAY);
-            }
-
-            // fork subtask to shutdown
-            scope.fork(() -> {
-                scope.shutdown();
-                return null;
-            });
-
-            // fork after forking subtask to shutdown
-            for (int i = 0; i < afterShutdown; i++) {
-                scope.fork(SLEEP_FOR_A_DAY);
-            }
-
-            scope.join();
-        }
-    }
-}
diff a/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredTaskScopeTest.java b/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredTaskScopeTest.java
--- a/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredTaskScopeTest.java
+++ b/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredTaskScopeTest.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -34,36 +34,36 @@
  * @enablePreview
  * @run junit/othervm -DthreadFactory=virtual StructuredTaskScopeTest
  */
 
 import java.time.Duration;
-import java.io.IOException;
-import java.time.Instant;
 import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
+import java.util.NoSuchElementException;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executors;
-import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
+import java.util.concurrent.LinkedTransferQueue;
 import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.TimeoutException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.StructuredTaskScope;
+import java.util.concurrent.StructuredTaskScope.TimeoutException;
+import java.util.concurrent.StructuredTaskScope.Config;
+import java.util.concurrent.StructuredTaskScope.FailedException;
+import java.util.concurrent.StructuredTaskScope.Joiner;
 import java.util.concurrent.StructuredTaskScope.Subtask;
-import java.util.concurrent.StructuredTaskScope.ShutdownOnSuccess;
-import java.util.concurrent.StructuredTaskScope.ShutdownOnFailure;
 import java.util.concurrent.StructureViolationException;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Supplier;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 import static java.lang.Thread.State.*;
 
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.BeforeAll;
@@ -99,36 +99,23 @@
     private static Stream<ThreadFactory> factories() {
         return threadFactories.stream();
     }
 
     /**
-     * Test that fork creates a new thread for each task.
+     * Test that fork creates virtual threads when no ThreadFactory is configured.
      */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testForkCreatesThread(ThreadFactory factory) throws Exception {
-        Set<Long> tids = ConcurrentHashMap.newKeySet();
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            for (int i = 0; i < 100; i++) {
+    @Test
+    void testForkCreatesVirtualThread() throws Exception {
+        Set<Thread> threads = ConcurrentHashMap.newKeySet();
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            for (int i = 0; i < 50; i++) {
+                // runnable
                 scope.fork(() -> {
-                    tids.add(Thread.currentThread().threadId());
-                    return null;
+                    threads.add(Thread.currentThread());
                 });
-            }
-            scope.join();
-        }
-        assertEquals(100, tids.size());
-    }
 
-    /**
-     * Test that fork creates a new virtual thread for each task.
-     */
-    @Test
-    void testForkCreateVirtualThread() throws Exception {
-        Set<Thread> threads = ConcurrentHashMap.newKeySet();
-        try (var scope = new StructuredTaskScope<Object>()) {
-            for (int i = 0; i < 100; i++) {
+                // callable
                 scope.fork(() -> {
                     threads.add(Thread.currentThread());
                     return null;
                 });
             }
@@ -137,745 +124,580 @@
         assertEquals(100, threads.size());
         threads.forEach(t -> assertTrue(t.isVirtual()));
     }
 
     /**
-     * Test that fork creates a new thread with the given thread factory.
+     * Test that fork create threads with the configured ThreadFactory.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testForkUsesFactory(ThreadFactory factory) throws Exception {
-        var count = new AtomicInteger();
-        ThreadFactory countingFactory = task -> {
-            count.incrementAndGet();
-            return factory.newThread(task);
-        };
-        try (var scope = new StructuredTaskScope<Object>(null, countingFactory)) {
-            for (int i = 0; i < 100; i++) {
-                scope.fork(() -> null);
+    void testForkUsesThreadFactory(ThreadFactory factory) throws Exception {
+        // TheadFactory that keeps reference to all threads it creates
+        class RecordingThreadFactory implements ThreadFactory {
+            final ThreadFactory delegate;
+            final Set<Thread> threads = ConcurrentHashMap.newKeySet();
+            RecordingThreadFactory(ThreadFactory delegate) {
+                this.delegate = delegate;
+            }
+            @Override
+            public Thread newThread(Runnable task) {
+                Thread thread = delegate.newThread(task);
+                threads.add(thread);
+                return thread;
+            }
+            Set<Thread> threads() {
+                return threads;
+            }
+        }
+        var recordingThreadFactory = new RecordingThreadFactory(factory);
+        Set<Thread> threads = ConcurrentHashMap.newKeySet();
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(recordingThreadFactory))) {
+
+            for (int i = 0; i < 50; i++) {
+                // runnable
+                scope.fork(() -> {
+                    threads.add(Thread.currentThread());
+                });
+
+                // callable
+                scope.fork(() -> {
+                    threads.add(Thread.currentThread());
+                    return null;
+                });
             }
             scope.join();
         }
-        assertEquals(100, count.get());
+        assertEquals(100, threads.size());
+        assertEquals(recordingThreadFactory.threads(), threads);
     }
 
     /**
-     * Test fork is confined to threads in the scope "tree".
+     * Test fork method is owner confined.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testForkConfined(ThreadFactory factory) throws Exception {
-        try (var scope1 = new StructuredTaskScope<Boolean>();
-             var scope2 = new StructuredTaskScope<Boolean>()) {
-
-            // thread in scope1 cannot fork thread in scope2
-            Subtask<Boolean> subtask1 = scope1.fork(() -> {
-                assertThrows(WrongThreadException.class, () -> {
-                    scope2.fork(() -> null);
-                });
-                return true;
-            });
-
-            // thread in scope2 can fork thread in scope1
-            Subtask<Boolean> subtask2 = scope2.fork(() -> {
-                scope1.fork(() -> null);
-                return true;
-            });
-
-            scope2.join();
-            scope1.join();
-
-            assertTrue(subtask1.get());
-            assertTrue(subtask2.get());
+        try (var scope = StructuredTaskScope.open(Joiner.<Boolean>awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
 
             // random thread cannot fork
             try (var pool = Executors.newSingleThreadExecutor()) {
                 Future<Void> future = pool.submit(() -> {
                     assertThrows(WrongThreadException.class, () -> {
-                        scope1.fork(() -> null);
-                    });
-                    assertThrows(WrongThreadException.class, () -> {
-                        scope2.fork(() -> null);
+                        scope.fork(() -> null);
                     });
                     return null;
                 });
                 future.get();
             }
+
+            // subtask cannot fork
+            Subtask<Boolean> subtask = scope.fork(() -> {
+                assertThrows(WrongThreadException.class, () -> {
+                    scope.fork(() -> null);
+                });
+                return true;
+            });
+            scope.join();
+            assertTrue(subtask.get());
         }
     }
 
     /**
-     * Test fork after join completes.
+     * Test fork after join, no subtasks forked before join.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testForkAfterJoin(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
-            // round 1
-            var subtask1 = scope.fork(() -> "foo");
-            assertThrows(IllegalStateException.class, subtask1::get);
-            scope.join();
-            assertEquals("foo", subtask1.get());
-
-            // round 2
-            var subtask2 = scope.fork(() -> "bar");
-            assertEquals("foo", subtask1.get());
-            assertThrows(IllegalStateException.class, subtask2::get);
+    void testForkAfterJoin1(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
             scope.join();
-            assertEquals("foo", subtask1.get());
-            assertEquals("bar", subtask2.get());
+            assertThrows(IllegalStateException.class, () -> scope.fork(() -> "bar"));
+        }
+    }
 
-            // round 3
-            var subtask3 = scope.fork(() -> "baz");
-            assertEquals("foo", subtask1.get());
-            assertEquals("bar", subtask2.get());
-            assertThrows(IllegalStateException.class, subtask3::get);
+    /**
+     * Test fork after join, subtasks forked before join.
+     */
+    @ParameterizedTest
+    @MethodSource("factories")
+    void testForkAfterJoin2(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
+            scope.fork(() -> "foo");
             scope.join();
-            assertEquals("foo", subtask1.get());
-            assertEquals("bar", subtask2.get());
-            assertEquals("baz", subtask3.get());
+            assertThrows(IllegalStateException.class, () -> scope.fork(() -> "bar"));
         }
     }
 
     /**
      * Test fork after join throws.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testForkAfterJoinThrows(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
             var latch = new CountDownLatch(1);
             var subtask1 = scope.fork(() -> {
                 latch.await();
                 return "foo";
             });
 
             // join throws
             Thread.currentThread().interrupt();
             assertThrows(InterruptedException.class, scope::join);
 
-            // allow subtask1 to finish
-            latch.countDown();
-
-            // continue to fork
-            var subtask2 = scope.fork(() -> "bar");
-            assertThrows(IllegalStateException.class, subtask1::get);
-            assertThrows(IllegalStateException.class, subtask2::get);
-            scope.join();
-            assertEquals("foo", subtask1.get());
-            assertEquals("bar", subtask2.get());
+            // fork should throw
+            assertThrows(IllegalStateException.class, () -> scope.fork(() -> "bar"));
         }
     }
 
     /**
-     * Test fork after scope is shutdown.
+     * Test fork after task scope is cancelled. This test uses a custom Joiner to
+     * cancel execution.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testForkAfterShutdown(ThreadFactory factory) throws Exception {
-        var executed = new AtomicBoolean();
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            scope.shutdown();
-            Subtask<String> subtask = scope.fork(() -> {
-                executed.set(true);
-                return null;
-            });
+    void testForkAfterCancel2(ThreadFactory factory) throws Exception {
+        var countingThreadFactory = new CountingThreadFactory(factory);
+        var testJoiner = new CancelAfterOneJoiner<String>();
+
+        try (var scope = StructuredTaskScope.open(testJoiner,
+                cf -> cf.withThreadFactory(countingThreadFactory))) {
+
+            // fork subtask, the scope should be cancelled when the subtask completes
+            var subtask1 = scope.fork(() -> "foo");
+            while (!scope.isCancelled()) {
+                Thread.sleep(20);
+            }
+
+            assertEquals(1, countingThreadFactory.threadCount());
+            assertEquals(1, testJoiner.onForkCount());
+            assertEquals(1, testJoiner.onCompleteCount());
+
+            // fork second subtask, it should not run
+            var subtask2 = scope.fork(() -> "bar");
+
+            // onFork should be invoked, newThread and onComplete should not be invoked
+            assertEquals(1, countingThreadFactory.threadCount());
+            assertEquals(2, testJoiner.onForkCount());
+            assertEquals(1, testJoiner.onCompleteCount());
+
             scope.join();
-            assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
-            assertThrows(IllegalStateException.class, subtask::get);
-            assertThrows(IllegalStateException.class, subtask::exception);
+
+            assertEquals(1, countingThreadFactory.threadCount());
+            assertEquals(2, testJoiner.onForkCount());
+            assertEquals(1, testJoiner.onCompleteCount());
+            assertEquals("foo", subtask1.get());
+            assertEquals(Subtask.State.UNAVAILABLE, subtask2.state());
         }
-        assertFalse(executed.get());
     }
 
     /**
-     * Test fork after scope is closed.
+     * Test fork after task scope is closed.
      */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testForkAfterClose(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
+    @Test
+    void testForkAfterClose() {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
             scope.close();
             assertThrows(IllegalStateException.class, () -> scope.fork(() -> null));
         }
     }
 
     /**
-     * Test fork when the thread factory rejects creating a thread.
+     * Test fork with a ThreadFactory that rejects creating a thread.
      */
     @Test
-    void testForkRejectedExecutionException() throws Exception {
+    void testForkRejectedExecutionException() {
         ThreadFactory factory = task -> null;
-        try (var scope = new StructuredTaskScope(null, factory)) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
             assertThrows(RejectedExecutionException.class, () -> scope.fork(() -> null));
-            scope.join();
         }
     }
 
     /**
      * Test join with no subtasks.
      */
     @Test
     void testJoinWithNoSubtasks() throws Exception {
-        try (var scope = new StructuredTaskScope()) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
             scope.join();
         }
     }
 
     /**
-     * Test join with unfinished subtasks.
+     * Test join with a remaining subtask.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testJoinWithSubtasks(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope(null, factory)) {
+    void testJoinWithRemainingSubtasks(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
             Subtask<String> subtask = scope.fork(() -> {
-                Thread.sleep(Duration.ofMillis(50));
+                Thread.sleep(Duration.ofMillis(100));
                 return "foo";
             });
             scope.join();
             assertEquals("foo", subtask.get());
         }
     }
 
     /**
-     * Test join is owner confined.
+     * Test join after join completed with a result.
      */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testJoinConfined(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Boolean>()) {
+    @Test
+    void testJoinAfterJoin1() throws Exception {
+        var results = new LinkedTransferQueue<>(List.of("foo", "bar", "baz"));
+        Joiner<Object, String> joiner = results::take;
+        try (var scope = StructuredTaskScope.open(joiner)) {
+            scope.fork(() -> "foo");
+            assertEquals("foo", scope.join());
 
-            // thread in scope cannot join
-            Subtask<Boolean> subtask = scope.fork(() -> {
-                assertThrows(WrongThreadException.class, () -> { scope.join(); });
-                return true;
-            });
+            // join already called
+            for (int i = 0 ; i < 3; i++) {
+                assertThrows(IllegalStateException.class, scope::join);
+            }
+        }
+    }
 
-            scope.join();
+    /**
+     * Test join after join completed with an exception.
+     */
+    @Test
+    void testJoinAfterJoin2() throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulResultOrThrow())) {
+            scope.fork(() -> { throw new FooException(); });
+            Throwable ex = assertThrows(FailedException.class, scope::join);
+            assertTrue(ex.getCause() instanceof FooException);
 
-            assertTrue(subtask.get());
+            // join already called
+            for (int i = 0 ; i < 3; i++) {
+                assertThrows(IllegalStateException.class, scope::join);
+            }
+        }
+    }
+
+    /**
+     * Test join after join completed with a timeout.
+     */
+    @Test
+    void testJoinAfterJoin3() throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulResultOrThrow(),
+                cf -> cf.withTimeout(Duration.ofMillis(100)))) {
+            // wait for scope to be cancelled by timeout
+            while (!scope.isCancelled()) {
+                Thread.sleep(20);
+            }
+            assertThrows(TimeoutException.class, scope::join);
+
+            // join already called
+            for (int i = 0 ; i < 3; i++) {
+                assertThrows(IllegalStateException.class, scope::join);
+            }
+        }
+    }
+
+    /**
+     * Test join method is owner confined.
+     */
+    @ParameterizedTest
+    @MethodSource("factories")
+    void testJoinConfined(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.<Boolean>awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
 
             // random thread cannot join
             try (var pool = Executors.newSingleThreadExecutor()) {
                 Future<Void> future = pool.submit(() -> {
                     assertThrows(WrongThreadException.class, scope::join);
                     return null;
                 });
                 future.get();
             }
+
+            // subtask cannot join
+            Subtask<Boolean> subtask = scope.fork(() -> {
+                assertThrows(WrongThreadException.class, () -> { scope.join(); });
+                return true;
+            });
+            scope.join();
+            assertTrue(subtask.get());
         }
     }
 
     /**
      * Test join with interrupt status set.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testInterruptJoin1(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope(null, factory)) {
-            var latch = new CountDownLatch(1);
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
 
             Subtask<String> subtask = scope.fork(() -> {
-                latch.await();
+                Thread.sleep(60_000);
                 return "foo";
             });
 
             // join should throw
             Thread.currentThread().interrupt();
             try {
                 scope.join();
                 fail("join did not throw");
             } catch (InterruptedException expected) {
-                assertFalse(Thread.interrupted());   // interrupt status should be clear
-            } finally {
-                // let task continue
-                latch.countDown();
+                assertFalse(Thread.interrupted());   // interrupt status should be cleared
             }
-
-            // join should complete
-            scope.join();
-            assertEquals("foo", subtask.get());
         }
     }
 
     /**
      * Test interrupt of thread blocked in join.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testInterruptJoin2(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope(null, factory)) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
+
             var latch = new CountDownLatch(1);
             Subtask<String> subtask = scope.fork(() -> {
-                latch.await();
+                Thread.sleep(60_000);
                 return "foo";
             });
 
             // join should throw
             scheduleInterruptAt("java.util.concurrent.StructuredTaskScope.join");
             try {
                 scope.join();
                 fail("join did not throw");
             } catch (InterruptedException expected) {
                 assertFalse(Thread.interrupted());   // interrupt status should be clear
-            } finally {
-                // let task continue
-                latch.countDown();
             }
-
-            // join should complete
-            scope.join();
-            assertEquals("foo", subtask.get());
         }
     }
 
     /**
-     * Test join when scope is shutdown.
+     * Test join when scope is cancelled.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testJoinWithShutdown1(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
-            var interrupted = new CountDownLatch(1);
-            var finish = new CountDownLatch(1);
-
-            Subtask<String> subtask = scope.fork(() -> {
-                try {
-                    Thread.sleep(Duration.ofDays(1));
-                } catch (InterruptedException e) {
-                    interrupted.countDown();
-                }
-                finish.await();
-                return "foo";
-            });
-
-            scope.shutdown();      // should interrupt task
-
-            interrupted.await();
-
-            scope.join();
+    void testJoinWhenCancelled(ThreadFactory factory) throws Exception {
+        var countingThreadFactory = new CountingThreadFactory(factory);
+        var testJoiner = new CancelAfterOneJoiner<String>();
 
-            // signal task to finish
-            finish.countDown();
-        }
-    }
+        try (var scope = StructuredTaskScope.open(testJoiner,
+                    cf -> cf.withThreadFactory(countingThreadFactory))) {
 
-    /**
-     * Test shutdown when owner is blocked in join.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testJoinWithShutdown2(ThreadFactory factory) throws Exception {
-        class MyScope<T> extends StructuredTaskScope<T> {
-            MyScope(ThreadFactory factory) {
-                super(null, factory);
-            }
-            @Override
-            protected void handleComplete(Subtask<? extends T> subtask) {
-                shutdown();
+            // fork subtask, the scope should be cancelled when the subtask completes
+            var subtask1 = scope.fork(() -> "foo");
+            while (!scope.isCancelled()) {
+                Thread.sleep(20);
             }
-        }
 
-        try (var scope = new MyScope<String>(factory)) {
-            Subtask<String> subtask1 = scope.fork(() -> {
-                Thread.sleep(Duration.ofMillis(50));
-                return "foo";
-            });
-            Subtask<String> subtask2 = scope.fork(() -> {
+            // fork second subtask, it should not run
+            var subtask2 = scope.fork(() -> {
                 Thread.sleep(Duration.ofDays(1));
                 return "bar";
             });
 
-            // join should wakeup when shutdown is called
             scope.join();
 
-            // task1 should have completed successfully
-            assertEquals(Subtask.State.SUCCESS, subtask1.state());
             assertEquals("foo", subtask1.get());
-            assertThrows(IllegalStateException.class, subtask1::exception);
-
-            // task2 result/exception not available
             assertEquals(Subtask.State.UNAVAILABLE, subtask2.state());
-            assertThrows(IllegalStateException.class, subtask2::get);
-            assertThrows(IllegalStateException.class, subtask2::exception);
         }
     }
 
     /**
      * Test join after scope is closed.
      */
     @Test
     void testJoinAfterClose() throws Exception {
-        try (var scope = new StructuredTaskScope()) {
-            scope.join();
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
             scope.close();
             assertThrows(IllegalStateException.class, () -> scope.join());
-            assertThrows(IllegalStateException.class, () -> scope.joinUntil(Instant.now()));
         }
     }
 
     /**
-     * Test joinUntil, subtasks finish before deadline expires.
+     * Test join with timeout, subtasks finish before timeout expires.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testJoinUntil1(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
+    void testJoinWithTimeout1(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory)
+                        .withTimeout(Duration.ofDays(1)))) {
+
             Subtask<String> subtask = scope.fork(() -> {
-                try {
-                    Thread.sleep(Duration.ofSeconds(2));
-                } catch (InterruptedException e) { }
+                Thread.sleep(Duration.ofSeconds(1));
                 return "foo";
             });
 
-            long startMillis = millisTime();
-            scope.joinUntil(Instant.now().plusSeconds(30));
-            expectDuration(startMillis, /*min*/1900, /*max*/20_000);
+            scope.join();
+
+            assertFalse(scope.isCancelled());
             assertEquals("foo", subtask.get());
         }
     }
 
     /**
-     * Test joinUntil, deadline expires before subtasks finish.
+     * Test join with timeout, timeout expires before subtasks finish.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testJoinUntil2(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
+    void testJoinWithTimeout2(ThreadFactory factory) throws Exception {
+        long startMillis = millisTime();
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory)
+                        .withTimeout(Duration.ofSeconds(2)))) {
+
             Subtask<Void> subtask = scope.fork(() -> {
                 Thread.sleep(Duration.ofDays(1));
                 return null;
             });
 
-            long startMillis = millisTime();
-            try {
-                scope.joinUntil(Instant.now().plusSeconds(2));
-            } catch (TimeoutException e) {
-                expectDuration(startMillis, /*min*/1900, /*max*/20_000);
-            }
+            assertThrows(TimeoutException.class, scope::join);
+            expectDuration(startMillis, /*min*/1900, /*max*/20_000);
+
+            assertTrue(scope.isCancelled());
             assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
         }
     }
 
     /**
-     * Test joinUntil many times.
+     * Test join with timeout that has already expired.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testJoinUntil3(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
-            Subtask<String> subtask = scope.fork(() -> {
+    void testJoinWithTimeout3(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory)
+                        .withTimeout(Duration.ofSeconds(-1)))) {
+
+            Subtask<Void> subtask = scope.fork(() -> {
                 Thread.sleep(Duration.ofDays(1));
                 return null;
             });
 
-            for (int i = 0; i < 3; i++) {
-                try {
-                    scope.joinUntil(Instant.now().plusMillis(50));
-                    fail("joinUntil did not throw");
-                } catch (TimeoutException expected) {
-                    assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
-                }
-            }
+            assertThrows(TimeoutException.class, scope::join);
+
+            assertTrue(scope.isCancelled());
+            assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
         }
     }
 
     /**
-     * Test joinUntil with a deadline that has already expired.
+     * Test that cancelling execution interrupts unfinished threads. This test uses
+     * a custom Joiner to cancel execution.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testJoinUntil4(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            Subtask<Void> subtask = scope.fork(() -> {
-                Thread.sleep(Duration.ofDays(1));
-                return null;
+    void testCancelInterruptsThreads2(ThreadFactory factory) throws Exception {
+        var testJoiner = new CancelAfterOneJoiner<String>();
+
+        try (var scope = StructuredTaskScope.open(testJoiner,
+                cf -> cf.withThreadFactory(factory))) {
+
+            // fork subtask1 that runs for a long time
+            var started = new CountDownLatch(1);
+            var interrupted = new CountDownLatch(1);
+            var subtask1 = scope.fork(() -> {
+                started.countDown();
+                try {
+                    Thread.sleep(Duration.ofDays(1));
+                } catch (InterruptedException e) {
+                    interrupted.countDown();
+                }
             });
+            started.await();
 
-            // now
-            try {
-                scope.joinUntil(Instant.now());
-                fail("joinUntil did not throw");
-            } catch (TimeoutException expected) {
-                assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
+            // fork subtask2, the scope should be cancelled when the subtask completes
+            var subtask2 = scope.fork(() -> "bar");
+            while (!scope.isCancelled()) {
+                Thread.sleep(20);
             }
 
-            // in the past
-            try {
-                scope.joinUntil(Instant.now().minusSeconds(1));
-                fail("joinUntil did not throw");
-            } catch (TimeoutException expected) {
-                assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
-            }
+            // subtask1 should be interrupted
+            interrupted.await();
+
+            scope.join();
+            assertEquals(Subtask.State.UNAVAILABLE, subtask1.state());
+            assertEquals("bar", subtask2.get());
         }
     }
 
     /**
-     * Test joinUntil with interrupt status set.
+     * Test that timeout interrupts unfinished threads.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testInterruptJoinUntil1(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
-            var latch = new CountDownLatch(1);
+    void testTimeoutInterruptsThreads(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory)
+                        .withTimeout(Duration.ofSeconds(2)))) {
 
-            Subtask<String> subtask = scope.fork(() -> {
-                latch.await();
-                return "foo";
+            var started = new AtomicBoolean();
+            var interrupted = new CountDownLatch(1);
+            Subtask<Void> subtask = scope.fork(() -> {
+                started.set(true);
+                try {
+                    Thread.sleep(Duration.ofDays(1));
+                } catch (InterruptedException e) {
+                    interrupted.countDown();
+                }
+                return null;
             });
 
-            // joinUntil should throw
-            Thread.currentThread().interrupt();
-            try {
-                scope.joinUntil(Instant.now().plusSeconds(30));
-                fail("joinUntil did not throw");
-            } catch (InterruptedException expected) {
-                assertFalse(Thread.interrupted());   // interrupt status should be clear
-            } finally {
-                // let task continue
-                latch.countDown();
+            while (!scope.isCancelled()) {
+                Thread.sleep(50);
             }
 
-            // join should complete
-            scope.join();
-            assertEquals("foo", subtask.get());
+            // if subtask started then it should be interrupted
+            if (started.get()) {
+                interrupted.await();
+            }
+
+            assertThrows(TimeoutException.class, scope::join);
+
+            assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
         }
     }
 
     /**
-     * Test interrupt of thread blocked in joinUntil.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testInterruptJoinUntil2(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope(null, factory)) {
-            var latch = new CountDownLatch(1);
-
-            Subtask<String> subtask = scope.fork(() -> {
-                latch.await();
-                return "foo";
-            });
-
-            // joinUntil should throw
-            scheduleInterruptAt("java.util.concurrent.StructuredTaskScope.joinUntil");
-            try {
-                scope.joinUntil(Instant.now().plusSeconds(30));
-                fail("joinUntil did not throw");
-            } catch (InterruptedException expected) {
-                assertFalse(Thread.interrupted());   // interrupt status should be clear
-            } finally {
-                // let task continue
-                latch.countDown();
-            }
-
-            // join should complete
-            scope.join();
-            assertEquals("foo", subtask.get());
-        }
-    }
-
-    /**
-     * Test that shutdown interrupts unfinished subtasks.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testShutdownInterruptsThreads1(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            var interrupted = new AtomicBoolean();
-            var latch = new CountDownLatch(1);
-            var subtask = scope.fork(() -> {
-                try {
-                    Thread.sleep(Duration.ofDays(1));
-                } catch (InterruptedException e) {
-                    interrupted.set(true);
-                } finally {
-                    latch.countDown();
-                }
-                return null;
-            });
-
-            scope.shutdown();
-
-            // wait for task to complete
-            latch.await();
-            assertTrue(interrupted.get());
-
-            scope.join();
-
-            // subtask result/exception not available
-            assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
-            assertThrows(IllegalStateException.class, subtask::get);
-            assertThrows(IllegalStateException.class, subtask::exception);
-        }
-    }
-
-    /**
-     * Test that shutdown does not interrupt current thread.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testShutdownInterruptsThreads2(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            var interrupted = new AtomicBoolean();
-            var latch = new CountDownLatch(1);
-            var subtask = scope.fork(() -> {
-                try {
-                    scope.shutdown();
-                    interrupted.set(Thread.currentThread().isInterrupted());
-                } finally {
-                    latch.countDown();
-                }
-                return null;
-            });
-
-            // wait for task to complete
-            latch.await();
-            assertFalse(interrupted.get());
-
-            scope.join();
-        }
-    }
-
-    /**
-     * Test shutdown wakes join.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testShutdownWakesJoin(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            var latch = new CountDownLatch(1);
-            scope.fork(() -> {
-                Thread.sleep(Duration.ofMillis(100));  // give time for join to block
-                scope.shutdown();
-                latch.await();
-                return null;
-            });
-
-            scope.join();
-
-            // join woke up, allow task to complete
-            latch.countDown();
-        }
-    }
-
-    /**
-     * Test shutdown after scope is closed.
-     */
-    @Test
-    void testShutdownAfterClose() throws Exception {
-        try (var scope = new StructuredTaskScope<Object>()) {
-            scope.join();
-            scope.close();
-            assertThrows(IllegalStateException.class, scope::shutdown);
-        }
-    }
-
-    /**
-     * Test shutdown is confined to threads in the scope "tree".
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testShutdownConfined(ThreadFactory factory) throws Exception {
-        try (var scope1 = new StructuredTaskScope<Boolean>();
-             var scope2 = new StructuredTaskScope<Boolean>()) {
-
-            // thread in scope1 cannot shutdown scope2
-            Subtask<Boolean> subtask1 = scope1.fork(() -> {
-                assertThrows(WrongThreadException.class, scope2::shutdown);
-                return true;
-            });
-
-            // wait for task in scope1 to complete to avoid racing with task in scope2
-            while (subtask1.state() == Subtask.State.UNAVAILABLE) {
-                Thread.sleep(10);
-            }
-
-            // thread in scope2 shutdown scope1
-            Subtask<Boolean> subtask2 = scope2.fork(() -> {
-                scope1.shutdown();
-                return true;
-            });
-
-            scope2.join();
-            scope1.join();
-
-            assertTrue(subtask1.get());
-            assertTrue(subtask1.get());
-
-            // random thread cannot shutdown
-            try (var pool = Executors.newSingleThreadExecutor()) {
-                Future<Void> future = pool.submit(() -> {
-                    assertThrows(WrongThreadException.class, scope1::shutdown);
-                    assertThrows(WrongThreadException.class, scope2::shutdown);
-                    return null;
-                });
-                future.get();
-            }
-        }
-    }
-
-    /**
-     * Test isShutdown.
-     */
-    @Test
-    void testIsShutdown() {
-        try (var scope = new StructuredTaskScope<Object>()) {
-            assertFalse(scope.isShutdown());   // before shutdown
-            scope.shutdown();
-            assertTrue(scope.isShutdown());    // after shutdown
-            scope.close();
-            assertTrue(scope.isShutdown());    // after cose
-        }
-    }
-
-    /**
-     * Test close without join, no subtasks forked.
+     * Test close without join, no subtasks forked.
      */
     @Test
     void testCloseWithoutJoin1() {
-        try (var scope = new StructuredTaskScope<Object>()) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
             // do nothing
         }
     }
 
     /**
-     * Test close without join, unfinished subtasks.
+     * Test close without join, subtasks forked.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testCloseWithoutJoin2(ThreadFactory factory) {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
             Subtask<String> subtask = scope.fork(() -> {
                 Thread.sleep(Duration.ofDays(1));
                 return null;
             });
-            assertThrows(IllegalStateException.class, scope::close);
-
-            // subtask result/exception not available
-            assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
-            assertThrows(IllegalStateException.class, subtask::get);
-            assertThrows(IllegalStateException.class, subtask::exception);
-        }
-    }
 
-    /**
-     * Test close without join, unfinished subtasks forked after join.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testCloseWithoutJoin3(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope(null, factory)) {
-            scope.fork(() -> "foo");
-            scope.join();
-
-            Subtask<String> subtask = scope.fork(() -> {
-                Thread.sleep(Duration.ofDays(1));
-                return null;
-            });
+            // first call to close should throw
             assertThrows(IllegalStateException.class, scope::close);
 
+            // subsequent calls to close should not throw
+            for (int i = 0; i < 3; i++) {
+                scope.close();
+            }
+
             // subtask result/exception not available
             assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
             assertThrows(IllegalStateException.class, subtask::get);
             assertThrows(IllegalStateException.class, subtask::exception);
         }
@@ -885,89 +707,85 @@
      * Test close after join throws. Close should not throw as join attempted.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testCloseAfterJoinThrows(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>()) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
             var subtask = scope.fork(() -> {
                 Thread.sleep(Duration.ofDays(1));
                 return null;
             });
 
             // join throws
             Thread.currentThread().interrupt();
             assertThrows(InterruptedException.class, scope::join);
             assertThrows(IllegalStateException.class, subtask::get);
-        }
-    }
-
-    /**
-     * Test close after joinUntil throws. Close should not throw as join attempted.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testCloseAfterJoinUntilThrows(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>()) {
-            var subtask = scope.fork(() -> {
-                Thread.sleep(Duration.ofDays(1));
-                return null;
-            });
 
-            // joinUntil throws
-            assertThrows(TimeoutException.class, () -> scope.joinUntil(Instant.now()));
-            assertThrows(IllegalStateException.class, subtask::get);
-        }
+        }  // close should not throw
     }
 
     /**
-     * Test close is owner confined.
+     * Test close method is owner confined.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testCloseConfined(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Boolean>()) {
-
-            // attempt to close from thread in scope
-            Subtask<Boolean> subtask = scope.fork(() -> {
-                assertThrows(WrongThreadException.class, scope::close);
-                return true;
-            });
-
-            scope.join();
-            assertTrue(subtask.get());
+        try (var scope = StructuredTaskScope.open(Joiner.<Boolean>awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
 
             // random thread cannot close scope
             try (var pool = Executors.newCachedThreadPool(factory)) {
                 Future<Boolean> future = pool.submit(() -> {
                     assertThrows(WrongThreadException.class, scope::close);
                     return null;
                 });
                 future.get();
             }
+
+            // subtask cannot close
+            Subtask<Boolean> subtask = scope.fork(() -> {
+                assertThrows(WrongThreadException.class, scope::close);
+                return true;
+            });
+            scope.join();
+            assertTrue(subtask.get());
         }
     }
 
     /**
      * Test close with interrupt status set.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testInterruptClose1(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
+        var testJoiner = new CancelAfterOneJoiner<String>();
+        try (var scope = StructuredTaskScope.open(testJoiner,
+                cf -> cf.withThreadFactory(factory))) {
+
+            // fork first subtask, a straggler as it continues after being interrupted
+            var started = new CountDownLatch(1);
             var done = new AtomicBoolean();
             scope.fork(() -> {
+                started.countDown();
                 try {
                     Thread.sleep(Duration.ofDays(1));
                 } catch (InterruptedException e) {
-                    // interrupted by shutdown, expected
+                    // interrupted by cancel, expected
                 }
                 Thread.sleep(Duration.ofMillis(100)); // force close to wait
                 done.set(true);
                 return null;
             });
+            started.await();
+
+            // fork second subtask, the scope should be cancelled when this subtask completes
+            scope.fork(() -> "bar");
+            while (!scope.isCancelled()) {
+                Thread.sleep(20);
+            }
 
-            scope.shutdown();
             scope.join();
 
             // invoke close with interrupt status set
             Thread.currentThread().interrupt();
             try {
@@ -983,285 +801,171 @@
      * Test interrupting thread waiting in close.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testInterruptClose2(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            var done = new AtomicBoolean();
+        var testJoiner = new CancelAfterOneJoiner<String>();
+        try (var scope = StructuredTaskScope.open(testJoiner,
+                cf -> cf.withThreadFactory(factory))) {
+
             Thread mainThread = Thread.currentThread();
+
+            // fork first subtask, a straggler as it continues after being interrupted
+            var started = new CountDownLatch(1);
+            var done = new AtomicBoolean();
             scope.fork(() -> {
+                started.countDown();
                 try {
                     Thread.sleep(Duration.ofDays(1));
                 } catch (InterruptedException e) {
-                    // interrupted by shutdown, expected
+                    // interrupted by cancel, expected
                 }
 
                 // interrupt main thread when it blocks in close
                 interruptThreadAt(mainThread, "java.util.concurrent.StructuredTaskScope.close");
 
                 Thread.sleep(Duration.ofMillis(100)); // force close to wait
                 done.set(true);
                 return null;
             });
+            started.await();
+
+            // fork second subtask, the scope should be cancelled when this subtask completes
+            scope.fork(() -> "bar");
+            while (!scope.isCancelled()) {
+                Thread.sleep(20);
+            }
 
-            scope.shutdown();   // interrupts task
             scope.join();
+
+            // main thread will be interrupted while blocked in close
             try {
                 scope.close();
             } finally {
-                assertTrue(Thread.interrupted()); // clear interrupt status
+                assertTrue(Thread.interrupted());   // clear interrupt status
                 assertTrue(done.get());
             }
         }
     }
 
     /**
      * Test that closing an enclosing scope closes the thread flock of a nested scope.
      */
     @Test
     void testCloseThrowsStructureViolation() throws Exception {
-        try (var scope1 = new StructuredTaskScope<Object>()) {
-            try (var scope2 = new StructuredTaskScope<Object>()) {
+        try (var scope1 = StructuredTaskScope.open(Joiner.awaitAll())) {
+            try (var scope2 = StructuredTaskScope.open(Joiner.awaitAll())) {
 
-                // join + close enclosing scope
-                scope1.join();
+                // close enclosing scope
                 try {
                     scope1.close();
                     fail("close did not throw");
                 } catch (StructureViolationException expected) { }
 
-                // underlying flock should be closed, fork should return a cancelled task
+                // underlying flock should be closed
                 var executed = new AtomicBoolean();
-                Subtask<Void> subtask = scope2.fork(() -> {
-                    executed.set(true);
-                    return null;
-                });
+                Subtask<?> subtask = scope2.fork(() -> executed.set(true));
                 assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
                 scope2.join();
                 assertFalse(executed.get());
+                assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
             }
         }
     }
 
     /**
-     * A StructuredTaskScope that collects the subtasks notified to the handleComplete method.
+     * Test that isCancelled returns true after close.
      */
-    private static class CollectAll<T> extends StructuredTaskScope<T> {
-        private final Set<Subtask<? extends T>> subtasks = ConcurrentHashMap.newKeySet();
-
-        CollectAll(ThreadFactory factory) {
-            super(null, factory);
-        }
-
-        @Override
-        protected void handleComplete(Subtask<? extends T> subtask) {
-            subtasks.add(subtask);
-        }
-
-        Set<Subtask<? extends T>> subtasks() {
-            return subtasks;
-        }
-
-        Subtask<? extends T> find(Callable<T> task) {
-            return subtasks.stream()
-                    .filter(h -> task.equals(h.task()))
-                    .findAny()
-                    .orElseThrow();
-        }
-    }
-
-    /**
-     * Test that handleComplete method is invoked for tasks that complete before shutdown.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testHandleCompleteBeforeShutdown(ThreadFactory factory) throws Exception {
-        try (var scope = new CollectAll<String>(factory)) {
-            Callable<String> task1 = () -> "foo";
-            Callable<String> task2 = () -> { throw new FooException(); };
-            scope.fork(task1);
-            scope.fork(task2);
-            scope.join();
-
-            var subtask1 = scope.find(task1);
-            assertEquals("foo", subtask1.get());
-
-            var subtask2 = scope.find(task2);
-            assertTrue(subtask2.exception() instanceof FooException);
-        }
-    }
-
-    /**
-     * Test that handleComplete method is not invoked for tasks that finish after shutdown
-     * or are forked after shutdown.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testHandleCompleteAfterShutdown(ThreadFactory factory) throws Exception {
-        try (var scope = new CollectAll<String>(factory)) {
-            Callable<String> task1 = () -> {
-                try {
-                    Thread.sleep(Duration.ofDays(1));
-                } catch (InterruptedException ignore) { }
-                return "foo";
-            };
-            Callable<String> task2 = () -> {
-                Thread.sleep(Duration.ofDays(1));
-                return "bar";
-            };
-            Callable<String> task3 = () -> "baz";
-
-            // forked before shutdown, will complete after shutdown
-            var subtask1 = scope.fork(task1);
-            var subtask2 = scope.fork(task2);
-
-            scope.shutdown();
-
-            // forked after shutdown
-            var subtask3 = scope.fork(task3);
-
-            scope.join();
-
-            // handleComplete should not be called
-            for (int i = 0; i < 3; i++) {
-                assertEquals(0, scope.subtasks().size());
-                Thread.sleep(20);
-            }
-
-            assertEquals(Subtask.State.UNAVAILABLE, subtask1.state());
-            assertEquals(Subtask.State.UNAVAILABLE, subtask2.state());
-            assertEquals(Subtask.State.UNAVAILABLE, subtask3.state());
+    @Test
+    void testIsCancelledAfterClose() throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            assertFalse(scope.isCancelled());
+            scope.close();
+            assertTrue(scope.isCancelled());
         }
     }
 
     /**
-     * Test that the default handleComplete throws IllegalArgumentException if called
-     * with a running task.
+     * Test Joiner.onFork throwing exception.
      */
     @Test
-    void testHandleCompleteThrows() throws Exception {
-        class TestScope<T> extends StructuredTaskScope<T> {
-            protected void handleComplete(Subtask<? extends T> subtask) {
-                super.handleComplete(subtask);
+    void testOnForkThrows() throws Exception {
+        var joiner = new Joiner<String, Void>() {
+            @Override
+            public boolean onFork(Subtask<? extends String> subtask) {
+                throw new FooException();
             }
-        }
-
-        try (var scope = new TestScope<String>()) {
-            var subtask = scope.fork(() -> {
-                Thread.sleep(Duration.ofDays(1));
-                return "foo";
-            });
-
-            // running task
-            assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
-            assertThrows(IllegalArgumentException.class, () -> scope.handleComplete(subtask));
-            scope.shutdown();
-
-            // null task
-            assertThrows(NullPointerException.class, () -> scope.handleComplete(null));
-
-            scope.join();
+            @Override
+            public Void result() {
+                return null;
+            }
+        };
+        try (var scope = StructuredTaskScope.open(joiner)) {
+            assertThrows(FooException.class, () -> scope.fork(() -> "foo"));
         }
     }
 
     /**
-     * Test ensureOwnerAndJoined.
+     * Test Joiner.onFork returning true to cancel execution.
      */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testEnsureOwnerAndJoined(ThreadFactory factory) throws Exception {
-        class MyScope<T> extends StructuredTaskScope<T> {
-            MyScope(ThreadFactory factory) {
-                super(null, factory);
+    @Test
+    void testOnForkCancelsExecution() throws Exception {
+        var joiner = new Joiner<String, Void>() {
+            @Override
+            public boolean onFork(Subtask<? extends String> subtask) {
+                return true;
             }
-            void invokeEnsureOwnerAndJoined() {
-                super.ensureOwnerAndJoined();
+            @Override
+            public Void result() {
+                return null;
             }
-        }
-
-        try (var scope = new MyScope<Boolean>(factory)) {
-            // owner thread, before join
-            scope.fork(() -> true);
-            assertThrows(IllegalStateException.class, () -> {
-                scope.invokeEnsureOwnerAndJoined();
-            });
-
-            // owner thread, after join
-            scope.join();
-            scope.invokeEnsureOwnerAndJoined();
-
-            // thread in scope cannot invoke ensureOwnerAndJoined
-            Subtask<Boolean> subtask = scope.fork(() -> {
-                assertThrows(WrongThreadException.class, () -> {
-                    scope.invokeEnsureOwnerAndJoined();
-                });
-                return true;
-            });
+        };
+        try (var scope = StructuredTaskScope.open(joiner)) {
+            assertFalse(scope.isCancelled());
+            scope.fork(() -> "foo");
+            assertTrue(scope.isCancelled());
             scope.join();
-            assertTrue(subtask.get());
-
-            // random thread cannot invoke ensureOwnerAndJoined
-            try (var pool = Executors.newSingleThreadExecutor()) {
-                Future<Void> future = pool.submit(() -> {
-                    assertThrows(WrongThreadException.class, () -> {
-                        scope.invokeEnsureOwnerAndJoined();
-                    });
-                    return null;
-                });
-                future.get();
-            }
         }
     }
 
     /**
-     * Test ensureOwnerAndJoined after the task scope has been closed.
+     * Test Joiner.onComplete returning true to cancel execution.
      */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testEnsureOwnerAndJoinedAfterClose(ThreadFactory factory) throws Exception {
-        class MyScope<T> extends StructuredTaskScope<T> {
-            MyScope(ThreadFactory factory) {
-                super(null, factory);
+    @Test
+    void testOnCompleteCancelsExecution() throws Exception {
+        var joiner = new Joiner<String, Void>() {
+            @Override
+            public boolean onComplete(Subtask<? extends String> subtask) {
+                return true;
             }
-            public void invokeEnsureOwnerAndJoined() {
-                super.ensureOwnerAndJoined();
+            @Override
+            public Void result() {
+                return null;
             }
-        }
-
-        // ensureOwnerAndJoined after close, join invoked
-        try (var scope = new MyScope<String>(factory)) {
+        };
+        try (var scope = StructuredTaskScope.open(joiner)) {
+            assertFalse(scope.isCancelled());
             scope.fork(() -> "foo");
+            while (!scope.isCancelled()) {
+                Thread.sleep(10);
+            }
             scope.join();
-            scope.close();
-            scope.invokeEnsureOwnerAndJoined();  // should not throw
-        }
-
-        // ensureOwnerAndJoined after close, join not invoked
-        try (var scope = new MyScope<String>(factory)) {
-            scope.fork(() -> "foo");
-            assertThrows(IllegalStateException.class, scope::close);
-            scope.invokeEnsureOwnerAndJoined();  // should not throw
         }
     }
 
-
     /**
      * Test toString.
      */
     @Test
     void testToString() throws Exception {
-        ThreadFactory factory = Thread.ofVirtual().factory();
-        try (var scope = new StructuredTaskScope<Object>("duke", factory)) {
-            // open
-            assertTrue(scope.toString().contains("duke"));
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withName("duke"))) {
 
-            // shutdown
-            scope.shutdown();
+            // open
             assertTrue(scope.toString().contains("duke"));
 
             // closed
-            scope.join();
             scope.close();
             assertTrue(scope.toString().contains("duke"));
         }
     }
 
@@ -1269,23 +973,22 @@
      * Test Subtask with task that completes successfully.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testSubtaskWhenSuccess(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
-            Callable<String> task = () -> "foo";
-            Subtask<String> subtask = scope.fork(task);
+        try (var scope = StructuredTaskScope.open(Joiner.<String>awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
+
+            Subtask<String> subtask = scope.fork(() -> "foo");
 
-            // before join, owner thread
-            assertEquals(task, subtask.task());
+            // before join
             assertThrows(IllegalStateException.class, subtask::get);
             assertThrows(IllegalStateException.class, subtask::exception);
 
             scope.join();
 
             // after join
-            assertEquals(task, subtask.task());
             assertEquals(Subtask.State.SUCCESS, subtask.state());
             assertEquals("foo", subtask.get());
             assertThrows(IllegalStateException.class, subtask::exception);
         }
     }
@@ -1294,23 +997,22 @@
      * Test Subtask with task that fails.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testSubtaskWhenFailed(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
-            Callable<String> task = () -> { throw new FooException(); };
-            Subtask<String> subtask = scope.fork(task);
+        try (var scope = StructuredTaskScope.open(Joiner.<String>awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
+
+            Subtask<String> subtask = scope.fork(() -> { throw new FooException(); });
 
-            // before join, owner thread
-            assertEquals(task, subtask.task());
+            // before join
             assertThrows(IllegalStateException.class, subtask::get);
             assertThrows(IllegalStateException.class, subtask::exception);
 
             scope.join();
 
             // after join
-            assertEquals(task, subtask.task());
             assertEquals(Subtask.State.FAILED, subtask.state());
             assertThrows(IllegalStateException.class, subtask::get);
             assertTrue(subtask.exception() instanceof FooException);
         }
     }
@@ -1319,53 +1021,55 @@
      * Test Subtask with a task that has not completed.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testSubtaskWhenNotCompleted(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            Callable<Void> task = () -> {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
+            Subtask<Void> subtask = scope.fork(() -> {
                 Thread.sleep(Duration.ofDays(1));
                 return null;
-            };
-            Subtask<Void> subtask = scope.fork(task);
+            });
 
             // before join
-            assertEquals(task, subtask.task());
             assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
             assertThrows(IllegalStateException.class, subtask::get);
             assertThrows(IllegalStateException.class, subtask::exception);
 
             // attempt join, join throws
             Thread.currentThread().interrupt();
             assertThrows(InterruptedException.class, scope::join);
 
             // after join
-            assertEquals(task, subtask.task());
             assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
             assertThrows(IllegalStateException.class, subtask::get);
             assertThrows(IllegalStateException.class, subtask::exception);
         }
     }
 
     /**
-     * Test Subtask when forked after shutdown.
+     * Test Subtask forked after execution cancelled.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testSubtaskWhenShutdown(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
-            Callable<Void> task = () -> {
-                Thread.sleep(Duration.ofDays(1));
-                return null;
-            };
+    void testSubtaskWhenCancelled(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(new CancelAfterOneJoiner<String>())) {
+            scope.fork(() -> "foo");
+            while (!scope.isCancelled()) {
+                Thread.sleep(20);
+            }
 
-            scope.shutdown();
+            var subtask = scope.fork(() -> "foo");
+
+            // before join
+            assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
+            assertThrows(IllegalStateException.class, subtask::get);
+            assertThrows(IllegalStateException.class, subtask::exception);
 
-            // fork after shutdown
-            Subtask<Void> subtask = scope.fork(task);
             scope.join();
-            assertEquals(task, subtask.task());
+
+            // after join
             assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
             assertThrows(IllegalStateException.class, subtask::get);
             assertThrows(IllegalStateException.class, subtask::exception);
         }
     }
@@ -1373,251 +1077,526 @@
     /**
      * Test Subtask::toString.
      */
     @Test
     void testSubtaskToString() throws Exception {
-        try (var scope = new StructuredTaskScope<Object>()) {
-            // success
-            var subtask1 = scope.fork(() -> "foo");
-            scope.join();
-            assertTrue(subtask1.toString().contains("Completed successfully"));
-
-            // failed
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            var latch = new CountDownLatch(1);
+            var subtask1 = scope.fork(() -> {
+                latch.await();
+                return "foo";
+            });
             var subtask2 = scope.fork(() -> { throw new FooException(); });
+
+            // subtask1 result is unavailable
+            assertTrue(subtask1.toString().contains("Unavailable"));
+            latch.countDown();
+
             scope.join();
-            assertTrue(subtask2.toString().contains("Failed"));
 
-            // not completed
-            Callable<Void> sleepForDay = () -> {
-                Thread.sleep(Duration.ofDays(1));
-                return null;
-            };
-            var subtask3 = scope.fork(sleepForDay);
-            assertTrue(subtask3.toString().contains("Unavailable"));
+            assertTrue(subtask1.toString().contains("Completed successfully"));
+            assertTrue(subtask2.toString().contains("Failed"));
+        }
+    }
 
-            scope.shutdown();
+    /**
+     * Test Joiner.allSuccessfulOrThrow() with no subtasks.
+     */
+    @Test
+    void testAllSuccessfulOrThrow1() throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow())) {
+            var subtasks = scope.join().toList();
+            assertTrue(subtasks.isEmpty());
+        }
+    }
 
-            // forked after shutdown
-            var subtask4 = scope.fork(sleepForDay);
-            assertTrue(subtask4.toString().contains("Unavailable"));
+    /**
+     * Test Joiner.allSuccessfulOrThrow() with subtasks that complete successfully.
+     */
+    @ParameterizedTest
+    @MethodSource("factories")
+    void testAllSuccessfulOrThrow2(ThreadFactory factory) throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow(),
+                cf -> cf.withThreadFactory(factory))) {
+            var subtask1 = scope.fork(() -> "foo");
+            var subtask2 = scope.fork(() -> "bar");
+            var subtasks = scope.join().toList();
+            assertEquals(List.of(subtask1, subtask2), subtasks);
+            assertEquals("foo", subtask1.get());
+            assertEquals("bar", subtask2.get());
+        }
+    }
 
-            scope.join();
+    /**
+     * Test Joiner.allSuccessfulOrThrow() with a subtask that complete successfully and
+     * a subtask that fails.
+     */
+    @ParameterizedTest
+    @MethodSource("factories")
+    void testAllSuccessfulOrThrow3(ThreadFactory factory) throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow(),
+                cf -> cf.withThreadFactory(factory))) {
+            scope.fork(() -> "foo");
+            scope.fork(() -> { throw new FooException(); });
+            try {
+                scope.join();
+            } catch (FailedException e) {
+                assertTrue(e.getCause() instanceof FooException);
+            }
         }
     }
 
     /**
-     * Test ShutdownOnSuccess with no completed tasks.
+     * Test Joiner.anySuccessfulResultOrThrow() with no subtasks.
      */
     @Test
-    void testShutdownOnSuccess1() throws Exception {
-        try (var scope = new ShutdownOnSuccess<Object>()) {
-            assertThrows(IllegalStateException.class, () -> scope.result());
-            assertThrows(IllegalStateException.class, () -> scope.result(e -> null));
+    void testAnySuccessfulResultOrThrow1() throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulResultOrThrow())) {
+            try {
+                scope.join();
+            } catch (FailedException e) {
+                assertTrue(e.getCause() instanceof NoSuchElementException);
+            }
         }
     }
 
     /**
-     * Test ShutdownOnSuccess with tasks that complete successfully.
+     * Test Joiner.anySuccessfulResultOrThrow() with a subtask that completes successfully.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testShutdownOnSuccess2(ThreadFactory factory) throws Exception {
-        try (var scope = new ShutdownOnSuccess<String>(null, factory)) {
+    void testAnySuccessfulResultOrThrow2(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>anySuccessfulResultOrThrow(),
+                cf -> cf.withThreadFactory(factory))) {
             scope.fork(() -> "foo");
-            scope.join();  // ensures foo completes first
-            scope.fork(() -> "bar");
-            scope.join();
-            assertEquals("foo", scope.result());
-            assertEquals("foo", scope.result(e -> null));
+            String result = scope.join();
+            assertEquals("foo", result);
         }
     }
 
     /**
-     * Test ShutdownOnSuccess with a task that completes successfully with a null result.
+     * Test Joiner.anySuccessfulResultOrThrow() with a subtask that completes successfully
+     * with a null result.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testShutdownOnSuccess3(ThreadFactory factory) throws Exception {
-        try (var scope = new ShutdownOnSuccess<Object>(null, factory)) {
+    void testAnySuccessfulResultOrThrow3(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>anySuccessfulResultOrThrow(),
+                cf -> cf.withThreadFactory(factory))) {
             scope.fork(() -> null);
-            scope.join();
-            assertNull(scope.result());
-            assertNull(scope.result(e -> null));
+            String result = scope.join();
+            assertNull(result);
         }
     }
 
     /**
-     * Test ShutdownOnSuccess with tasks that complete succcessfully and tasks that fail.
+     * Test Joiner.anySuccessfulResultOrThrow() with a subtask that complete succcessfully
+     * and a subtask that fails.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testShutdownOnSuccess4(ThreadFactory factory) throws Exception {
-        try (var scope = new ShutdownOnSuccess<String>(null, factory)) {
+    void testAnySuccessfulResultOrThrow4(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>anySuccessfulResultOrThrow(),
+                cf -> cf.withThreadFactory(factory))) {
             scope.fork(() -> "foo");
-            scope.fork(() -> { throw new ArithmeticException(); });
-            scope.join();
-            assertEquals("foo", scope.result());
-            assertEquals("foo", scope.result(e -> null));
+            scope.fork(() -> { throw new FooException(); });
+            String first = scope.join();
+            assertEquals("foo", first);
         }
     }
 
     /**
-     * Test ShutdownOnSuccess with a task that fails.
+     * Test Joiner.anySuccessfulResultOrThrow() with a subtask that fails.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testShutdownOnSuccess5(ThreadFactory factory) throws Exception {
-        try (var scope = new ShutdownOnSuccess<Object>(null, factory)) {
-            scope.fork(() -> { throw new ArithmeticException(); });
-            scope.join();
-            Throwable ex = assertThrows(ExecutionException.class, () -> scope.result());
-            assertTrue(ex.getCause() instanceof ArithmeticException);
-            ex = assertThrows(FooException.class, () -> scope.result(e -> new FooException(e)));
-            assertTrue(ex.getCause() instanceof ArithmeticException);
+    void testAnySuccessfulResultOrThrow5(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.anySuccessfulResultOrThrow(),
+                cf -> cf.withThreadFactory(factory))) {
+            scope.fork(() -> { throw new FooException(); });
+            Throwable ex = assertThrows(FailedException.class, scope::join);
+            assertTrue(ex.getCause() instanceof FooException);
         }
     }
 
     /**
-     * Test ShutdownOnSuccess methods are confined to the owner.
+     * Test Joiner.awaitAllSuccessfulOrThrow() with no subtasks.
+     */
+    @Test
+    void testAwaitSuccessfulOrThrow1() throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAllSuccessfulOrThrow())) {
+            var result = scope.join();
+            assertNull(result);
+        }
+    }
+
+    /**
+     * Test Joiner.awaitAllSuccessfulOrThrow() with subtasks that complete successfully.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testShutdownOnSuccessConfined(ThreadFactory factory) throws Exception {
-        // owner before join
-        try (var scope = new ShutdownOnSuccess<Boolean>(null, factory)) {
-            scope.fork(() -> { throw new FooException(); });
-            assertThrows(IllegalStateException.class, scope::result);
-            assertThrows(IllegalStateException.class, () -> {
-                scope.result(e -> new RuntimeException(e));
-            });
-            scope.join();
+    void testAwaitSuccessfulOrThrow2(ThreadFactory factory) throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>awaitAllSuccessfulOrThrow(),
+                cf -> cf.withThreadFactory(factory))) {
+            var subtask1 = scope.fork(() -> "foo");
+            var subtask2 = scope.fork(() -> "bar");
+            var result = scope.join();
+            assertNull(result);
+            assertEquals("foo", subtask1.get());
+            assertEquals("bar", subtask2.get());
         }
+    }
 
-        // non-owner
-        try (var scope = new ShutdownOnSuccess<Boolean>(null, factory)) {
-            Subtask<Boolean> subtask = scope.fork(() -> {
-                assertThrows(WrongThreadException.class, scope::result);
-                assertThrows(WrongThreadException.class, () -> {
-                    scope.result(e -> new RuntimeException(e));
-                });
-                return true;
-            });
-            scope.join();
-            assertTrue(subtask.get());
+    /**
+     * Test Joiner.awaitAllSuccessfulOrThrow() with a subtask that complete successfully and
+     * a subtask that fails.
+     */
+    @ParameterizedTest
+    @MethodSource("factories")
+    void testAwaitSuccessfulOrThrow3(ThreadFactory factory) throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>awaitAllSuccessfulOrThrow(),
+                cf -> cf.withThreadFactory(factory))) {
+            scope.fork(() -> "foo");
+            scope.fork(() -> { throw new FooException(); });
+            try {
+                scope.join();
+            } catch (FailedException e) {
+                assertTrue(e.getCause() instanceof FooException);
+            }
         }
     }
 
     /**
-     * Test ShutdownOnFailure with no completed tasks.
+     * Test Joiner.awaitAll() with no subtasks.
      */
     @Test
-    void testShutdownOnFailure1() throws Throwable {
-        try (var scope = new ShutdownOnFailure()) {
-            assertTrue(scope.exception().isEmpty());
-            scope.throwIfFailed();
-            scope.throwIfFailed(e -> new FooException(e));
+    void testAwaitAll1() throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            var result = scope.join();
+            assertNull(result);
         }
     }
 
     /**
-     * Test ShutdownOnFailure with tasks that complete successfully.
+     * Test Joiner.awaitAll() with subtasks that complete successfully.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testShutdownOnFailure2(ThreadFactory factory) throws Throwable {
-        try (var scope = new ShutdownOnFailure(null, factory)) {
-            scope.fork(() -> "foo");
-            scope.fork(() -> "bar");
-            scope.join();
-
-            // no exception
-            assertTrue(scope.exception().isEmpty());
-            scope.throwIfFailed();
-            scope.throwIfFailed(e -> new FooException(e));
+    void testAwaitAll2(ThreadFactory factory) throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
+            var subtask1 = scope.fork(() -> "foo");
+            var subtask2 = scope.fork(() -> "bar");
+            var result = scope.join();
+            assertNull(result);
+            assertEquals("foo", subtask1.get());
+            assertEquals("bar", subtask2.get());
         }
     }
 
     /**
-     * Test ShutdownOnFailure with tasks that complete succcessfully and tasks that fail.
+     * Test Joiner.awaitAll() with a subtask that complete successfully and a subtask
+     * that fails.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testShutdownOnFailure3(ThreadFactory factory) throws Throwable {
-        try (var scope = new ShutdownOnFailure(null, factory)) {
+    void testAwaitAll3(ThreadFactory factory) throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
+            var subtask1 = scope.fork(() -> "foo");
+            var subtask2 = scope.fork(() -> { throw new FooException(); });
+            var result = scope.join();
+            assertNull(result);
+            assertEquals("foo", subtask1.get());
+            assertTrue(subtask2.exception() instanceof FooException);
+        }
+    }
 
-            // one task completes successfully, the other fails
-            scope.fork(() -> "foo");
-            scope.fork(() -> { throw new ArithmeticException(); });
-            scope.join();
+    /**
+     * Test Joiner.allUntil(Predicate) with no subtasks.
+     */
+    @Test
+    void testAllUntil1() throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.allUntil(s -> false))) {
+            var subtasks = scope.join();
+            assertEquals(0, subtasks.count());
+        }
+    }
+
+    /**
+     * Test Joiner.allUntil(Predicate) with no cancellation.
+     */
+    @ParameterizedTest
+    @MethodSource("factories")
+    void testAllUntil2(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>allUntil(s -> false),
+                cf -> cf.withThreadFactory(factory))) {
 
-            Throwable ex = scope.exception().orElse(null);
-            assertTrue(ex instanceof ArithmeticException);
+            var subtask1 = scope.fork(() -> "foo");
+            var subtask2 = scope.fork(() -> { throw new FooException(); });
 
-            ex = assertThrows(ExecutionException.class, () -> scope.throwIfFailed());
-            assertTrue(ex.getCause() instanceof ArithmeticException);
+            var subtasks = scope.join().toList();
+            assertEquals(2, subtasks.size());
 
-            ex = assertThrows(FooException.class,
-                              () -> scope.throwIfFailed(e -> new FooException(e)));
-            assertTrue(ex.getCause() instanceof ArithmeticException);
+            assertTrue(subtasks.get(0) == subtask1);
+            assertTrue(subtasks.get(1) == subtask2);
+            assertEquals("foo", subtask1.get());
+            assertTrue(subtask2.exception() instanceof FooException);
         }
     }
 
     /**
-     * Test ShutdownOnFailure methods are confined to the owner.
+     * Test Joiner.allUntil(Predicate) with cancellation after one subtask completes.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testShutdownOnFailureConfined(ThreadFactory factory) throws Exception {
-        // owner before join
-        try (var scope = new ShutdownOnFailure(null, factory)) {
-            scope.fork(() -> "foo");
-            assertThrows(IllegalStateException.class, scope::exception);
-            assertThrows(IllegalStateException.class, scope::throwIfFailed);
-            assertThrows(IllegalStateException.class, () -> {
-                scope.throwIfFailed(e -> new RuntimeException(e));
+    void testAllUntil3(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>allUntil(s -> true),
+                cf -> cf.withThreadFactory(factory))) {
+
+            var subtask1 = scope.fork(() -> "foo");
+            var subtask2 = scope.fork(() -> {
+                Thread.sleep(Duration.ofDays(1));
+                return "bar";
             });
-            scope.join();
+
+            var subtasks = scope.join().toList();
+
+            assertEquals(2, subtasks.size());
+            assertTrue(subtasks.get(0) == subtask1);
+            assertTrue(subtasks.get(1) == subtask2);
+            assertEquals("foo", subtask1.get());
+            assertEquals(Subtask.State.UNAVAILABLE, subtask2.state());
         }
+    }
 
-        // non-owner
-        try (var scope = new ShutdownOnFailure(null, factory)) {
-            Subtask<Boolean> subtask = scope.fork(() -> {
-                assertThrows(WrongThreadException.class, scope::exception);
-                assertThrows(WrongThreadException.class, scope::throwIfFailed);
-                assertThrows(WrongThreadException.class, () -> {
-                    scope.throwIfFailed(e -> new RuntimeException(e));
-                });
-                return true;
-            });
+    /**
+     * Test Joiner.allUntil(Predicate) with cancellation after serveral subtasks complete.
+     */
+    @ParameterizedTest
+    @MethodSource("factories")
+    void testAllUntil4(ThreadFactory factory) throws Exception {
+
+        // cancel execution after two or more failures
+        class CancelAfterTwoFailures<T> implements Predicate<Subtask<? extends T>> {
+            final AtomicInteger failedCount = new AtomicInteger();
+            @Override
+            public boolean test(Subtask<? extends T> subtask) {
+                return subtask.state() == Subtask.State.FAILED
+                        && failedCount.incrementAndGet() >= 2;
+            }
+        }
+        var joiner = Joiner.allUntil(new CancelAfterTwoFailures<String>());
+
+        try (var scope = StructuredTaskScope.open(joiner)) {
+            int forkCount = 0;
+
+            // fork subtasks until execution cancelled
+            while (!scope.isCancelled()) {
+                scope.fork(() -> "foo");
+                scope.fork(() -> { throw new FooException(); });
+                forkCount += 2;
+                Thread.sleep(Duration.ofMillis(10));
+            }
+
+            var subtasks = scope.join().toList();
+            assertEquals(forkCount, subtasks.size());
+
+            long failedCount = subtasks.stream()
+                    .filter(s -> s.state() == Subtask.State.FAILED)
+                    .count();
+            assertTrue(failedCount >= 2);
+        }
+    }
+
+    /**
+     * Test Config equals/hashCode/toString
+     */
+    @Test
+    void testConfigMethods() throws Exception {
+        Function<Config, Config> testConfig = cf -> {
+            var name = "duke";
+            var threadFactory = Thread.ofPlatform().factory();
+            var timeout = Duration.ofSeconds(10);
+
+            assertEquals(cf, cf);
+            assertEquals(cf.withName(name), cf.withName(name));
+            assertEquals(cf.withThreadFactory(threadFactory), cf.withThreadFactory(threadFactory));
+            assertEquals(cf.withTimeout(timeout), cf.withTimeout(timeout));
+
+            assertNotEquals(cf, cf.withName(name));
+            assertNotEquals(cf, cf.withThreadFactory(threadFactory));
+            assertNotEquals(cf, cf.withTimeout(timeout));
+
+            assertEquals(cf.withName(name).hashCode(), cf.withName(name).hashCode());
+            assertEquals(cf.withThreadFactory(threadFactory).hashCode(),
+                    cf.withThreadFactory(threadFactory).hashCode());
+            assertEquals(cf.withTimeout(timeout).hashCode(), cf.withTimeout(timeout).hashCode());
+
+            assertTrue(cf.withName(name).toString().contains(name));
+            assertTrue(cf.withThreadFactory(threadFactory).toString().contains(threadFactory.toString()));
+            assertTrue(cf.withTimeout(timeout).toString().contains(timeout.toString()));
+
+            return cf;
+        };
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), testConfig)) {
+            // do nothing
+        }
+    }
+
+    /**
+     * Test Joiner default methods.
+     */
+    @Test
+    void testJoinerDefaultMethods() throws Exception {
+        try (var scope = StructuredTaskScope.open(new CancelAfterOneJoiner<String>())) {
+
+            // need subtasks to test default methods
+            var subtask1 = scope.fork(() -> "foo");
+            while (!scope.isCancelled()) {
+                Thread.sleep(20);
+            }
+            var subtask2 = scope.fork(() -> "bar");
             scope.join();
-            assertTrue(subtask.get());
+
+            assertEquals(Subtask.State.SUCCESS, subtask1.state());
+            assertEquals(Subtask.State.UNAVAILABLE, subtask2.state());
+
+            // Joiner that does not override default methods
+            Joiner<Object, Void> joiner = () -> null;
+            assertThrows(NullPointerException.class, () -> joiner.onFork(null));
+            assertThrows(NullPointerException.class, () -> joiner.onComplete(null));
+            assertThrows(IllegalArgumentException.class, () -> joiner.onFork(subtask1));
+            assertFalse(joiner.onFork(subtask2));
+            assertFalse(joiner.onComplete(subtask1));
+            assertThrows(IllegalArgumentException.class, () -> joiner.onComplete(subtask2));
         }
     }
 
     /**
      * Test for NullPointerException.
      */
     @Test
     void testNulls() throws Exception {
-        assertThrows(NullPointerException.class, () -> new StructuredTaskScope("", null));
-        try (var scope = new StructuredTaskScope<Object>()) {
-            assertThrows(NullPointerException.class, () -> scope.fork(null));
-            assertThrows(NullPointerException.class, () -> scope.joinUntil(null));
+        assertThrows(NullPointerException.class,
+                () -> StructuredTaskScope.open(null));
+        assertThrows(NullPointerException.class,
+                () -> StructuredTaskScope.open(null, cf -> cf));
+        assertThrows(NullPointerException.class,
+                () -> StructuredTaskScope.open(Joiner.awaitAll(), null));
+        assertThrows(NullPointerException.class,
+                () -> StructuredTaskScope.open(Joiner.awaitAll(), cf -> null));
+
+        assertThrows(NullPointerException.class, () -> Joiner.allUntil(null));
+
+        // fork
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            assertThrows(NullPointerException.class, () -> scope.fork((Callable<Object>) null));
+            assertThrows(NullPointerException.class, () -> scope.fork((Runnable) null));
+        }
+
+        // withXXX
+        assertThrows(NullPointerException.class,
+                () -> StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withName(null)));
+        assertThrows(NullPointerException.class,
+                () -> StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withThreadFactory(null)));
+        assertThrows(NullPointerException.class,
+                () -> StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withTimeout(null)));
+
+        // Joiner.onFork/onComplete
+        assertThrows(NullPointerException.class,
+                () -> Joiner.awaitAllSuccessfulOrThrow().onFork(null));
+        assertThrows(NullPointerException.class,
+                () -> Joiner.awaitAllSuccessfulOrThrow().onComplete(null));
+        assertThrows(NullPointerException.class,
+                () -> Joiner.awaitAll().onFork(null));
+        assertThrows(NullPointerException.class,
+                () -> Joiner.awaitAll().onComplete(null));
+        assertThrows(NullPointerException.class,
+                () -> Joiner.allSuccessfulOrThrow().onFork(null));
+        assertThrows(NullPointerException.class,
+                () -> Joiner.allSuccessfulOrThrow().onComplete(null));
+        assertThrows(NullPointerException.class,
+                () -> Joiner.anySuccessfulResultOrThrow().onFork(null));
+        assertThrows(NullPointerException.class,
+                () -> Joiner.anySuccessfulResultOrThrow().onComplete(null));
+    }
+
+    /**
+     * ThreadFactory that counts usage.
+     */
+    private static class CountingThreadFactory implements ThreadFactory {
+        final ThreadFactory delegate;
+        final AtomicInteger threadCount = new AtomicInteger();
+        CountingThreadFactory(ThreadFactory delegate) {
+            this.delegate = delegate;
         }
+        @Override
+        public Thread newThread(Runnable task) {
+            threadCount.incrementAndGet();
+            return delegate.newThread(task);
+        }
+        int threadCount() {
+            return threadCount.get();
+        }
+    }
 
-        assertThrows(NullPointerException.class, () -> new ShutdownOnSuccess<Object>("", null));
-        try (var scope = new ShutdownOnSuccess<Object>()) {
-            assertThrows(NullPointerException.class, () -> scope.fork(null));
-            assertThrows(NullPointerException.class, () -> scope.joinUntil(null));
-            assertThrows(NullPointerException.class, () -> scope.result(null));
+    /**
+     * A joiner that counts that counts the number of subtasks that are forked and the
+     * number of subtasks that complete.
+     */
+    private static class CountingJoiner<T> implements Joiner<T, Void> {
+        final AtomicInteger onForkCount = new AtomicInteger();
+        final AtomicInteger onCompleteCount = new AtomicInteger();
+        @Override
+        public boolean onFork(Subtask<? extends T> subtask) {
+            onForkCount.incrementAndGet();
+            return false;
         }
+        @Override
+        public boolean onComplete(Subtask<? extends T> subtask) {
+            onCompleteCount.incrementAndGet();
+            return false;
+        }
+        @Override
+        public Void result() {
+            return null;
+        }
+        int onForkCount() {
+            return onForkCount.get();
+        }
+        int onCompleteCount() {
+            return onCompleteCount.get();
+        }
+    }
 
-        assertThrows(NullPointerException.class, () -> new ShutdownOnFailure("", null));
-        try (var scope = new ShutdownOnFailure()) {
-            assertThrows(NullPointerException.class, () -> scope.fork(null));
-            assertThrows(NullPointerException.class, () -> scope.joinUntil(null));
-            assertThrows(NullPointerException.class, () -> scope.throwIfFailed(null));
+    /**
+     * A joiner that cancels execution when a subtask completes. It also keeps a count
+     * of the number of subtasks that are forked and the number of subtasks that complete.
+     */
+    private static class CancelAfterOneJoiner<T> implements Joiner<T, Void> {
+        final AtomicInteger onForkCount = new AtomicInteger();
+        final AtomicInteger onCompleteCount = new AtomicInteger();
+        @Override
+        public boolean onFork(Subtask<? extends T> subtask) {
+            onForkCount.incrementAndGet();
+            return false;
+        }
+        @Override
+        public boolean onComplete(Subtask<? extends T> subtask) {
+            onCompleteCount.incrementAndGet();
+            return true;
+        }
+        @Override
+        public Void result() {
+            return null;
+        }
+        int onForkCount() {
+            return onForkCount.get();
+        }
+        int onCompleteCount() {
+            return onCompleteCount.get();
         }
     }
 
     /**
      * A runtime exception for tests.
diff a/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredThreadDumpTest.java b/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredThreadDumpTest.java
--- a/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredThreadDumpTest.java
+++ b/test/jdk/java/util/concurrent/StructuredTaskScope/StructuredThreadDumpTest.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -29,10 +29,11 @@
  * @library /test/lib
  * @run junit/othervm StructuredThreadDumpTest
  */
 
 import java.util.concurrent.StructuredTaskScope;
+import java.util.concurrent.StructuredTaskScope.Joiner;
 import java.io.IOException;
 import java.lang.management.ManagementFactory;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.concurrent.ThreadFactory;
@@ -51,12 +52,11 @@
      * Test that a thread dump with a tree of task scopes contains a thread grouping for
      * each task scope.
      */
     @Test
     void testTree() throws Exception {
-        ThreadFactory factory = Thread.ofVirtual().factory();
-        try (var scope = new StructuredTaskScope<>("scope", factory)) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withName("scope"))) {
             Thread thread1 = fork(scope, "child-scope-A");
             Thread thread2 = fork(scope, "child-scope-B");
             try {
                 ThreadDump threadDump = threadDump();
 
@@ -81,11 +81,12 @@
                 // thread1 and threads2 should be in threads array of "scope"
                 container1.findThread(thread1.threadId()).orElseThrow();
                 container1.findThread(thread2.threadId()).orElseThrow();
 
             } finally {
-                scope.shutdown();
+                LockSupport.unpark(thread1);
+                LockSupport.unpark(thread2);
                 scope.join();
             }
         }
     }
 
@@ -93,15 +94,14 @@
      * Test that a thread dump with nested tasks scopes contains a thread grouping for
      * each task scope.
      */
     @Test
     void testNested() throws Exception {
-        ThreadFactory factory = Thread.ofVirtual().factory();
-        try (var scope1 = new StructuredTaskScope<>("scope-A", factory)) {
+        try (var scope1 = StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withName("scope-A"))) {
             Thread thread1 = fork(scope1);
 
-            try (var scope2 = new StructuredTaskScope<>("scope-B", factory)) {
+            try (var scope2 = StructuredTaskScope.open(Joiner.awaitAll(), cf -> cf.withName("scope-B"))) {
                 Thread thread2 = fork(scope2);
                 try {
                     ThreadDump threadDump = threadDump();
 
                     // thread dump should have a thread container for both scopes
@@ -125,15 +125,15 @@
 
                     // thread2 should be in threads array of "scope-B"
                     container2.findThread(thread2.threadId()).orElseThrow();
 
                 } finally {
-                    scope2.shutdown();
+                    LockSupport.unpark(thread2);
                     scope2.join();
                 }
             } finally {
-                scope1.shutdown();
+                LockSupport.unpark(thread1);
                 scope1.join();
             }
         }
     }
 
@@ -158,11 +158,11 @@
 
     /**
      * Forks a subtask in the given scope that parks, returning the Thread that executes
      * the subtask.
      */
-    private static Thread fork(StructuredTaskScope<Object> scope) throws Exception {
+    private static Thread fork(StructuredTaskScope<Object, Void> scope) throws Exception {
         var ref = new AtomicReference<Thread>();
         scope.fork(() -> {
             ref.set(Thread.currentThread());
             LockSupport.park();
             return null;
@@ -176,20 +176,19 @@
 
     /**
      * Forks a subtask in the given scope. The subtask creates a new child scope with
      * the given name, then parks. This method returns Thread that executes the subtask.
      */
-    private static Thread fork(StructuredTaskScope<Object> scope,
+    private static Thread fork(StructuredTaskScope<Object, Void> scope,
                                String childScopeName) throws Exception {
         var ref = new AtomicReference<Thread>();
         scope.fork(() -> {
-            ThreadFactory factory = Thread.ofVirtual().factory();
-            try (var childScope = new StructuredTaskScope<Object>(childScopeName, factory)) {
+            try (var childScope = StructuredTaskScope.open(Joiner.awaitAll(),
+                    cf -> cf.withName(childScopeName))) {
                 ref.set(Thread.currentThread());
                 LockSupport.park();
             }
-            return null;
         });
         Thread thread;
         while ((thread = ref.get()) == null) {
             Thread.sleep(10);
         }
diff a/test/jdk/java/util/concurrent/StructuredTaskScope/WithScopedValue.java b/test/jdk/java/util/concurrent/StructuredTaskScope/WithScopedValue.java
--- a/test/jdk/java/util/concurrent/StructuredTaskScope/WithScopedValue.java
+++ b/test/jdk/java/util/concurrent/StructuredTaskScope/WithScopedValue.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -29,10 +29,11 @@
  * @run junit WithScopedValue
  */
 
 import java.util.concurrent.StructuredTaskScope;
 import java.util.concurrent.StructuredTaskScope.Subtask;
+import java.util.concurrent.StructuredTaskScope.Joiner;
 import java.util.concurrent.StructureViolationException;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Stream;
 
@@ -52,12 +53,13 @@
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testForkInheritsScopedValue1(ThreadFactory factory) throws Exception {
         ScopedValue<String> name = ScopedValue.newInstance();
-        String value = ScopedValue.callWhere(name, "x", () -> {
-            try (var scope = new StructuredTaskScope<String>(null, factory)) {
+        String value = ScopedValue.where(name, "x").call(() -> {
+            try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                                                      cf -> cf.withThreadFactory(factory))) {
                 Subtask<String> subtask = scope.fork(() -> {
                     return name.get(); // child should read "x"
                 });
                 scope.join();
                 return subtask.get();
@@ -71,14 +73,16 @@
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testForkInheritsScopedValue2(ThreadFactory factory) throws Exception {
         ScopedValue<String> name = ScopedValue.newInstance();
-        String value = ScopedValue.callWhere(name, "x", () -> {
-            try (var scope1 = new StructuredTaskScope<String>(null, factory)) {
+        String value = ScopedValue.where(name, "x").call(() -> {
+            try (var scope1 = StructuredTaskScope.open(Joiner.awaitAll(),
+                                                       cf -> cf.withThreadFactory(factory))) {
                 Subtask<String> subtask1 = scope1.fork(() -> {
-                    try (var scope2 = new StructuredTaskScope<String>(null, factory)) {
+                    try (var scope2 = StructuredTaskScope.open(Joiner.awaitAll(),
+                                                               cf -> cf.withThreadFactory(factory))) {
                         Subtask<String> subtask2 = scope2.fork(() -> {
                             return name.get(); // grandchild should read "x"
                         });
                         scope2.join();
                         return subtask2.get();
@@ -96,18 +100,20 @@
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testForkInheritsScopedValue3(ThreadFactory factory) throws Exception {
         ScopedValue<String> name = ScopedValue.newInstance();
-        String value = ScopedValue.callWhere(name, "x", () -> {
-            try (var scope1 = new StructuredTaskScope<String>(null, factory)) {
+        String value = ScopedValue.where(name, "x").call(() -> {
+            try (var scope1 = StructuredTaskScope.open(Joiner.awaitAll(),
+                                                       cf -> cf.withThreadFactory(factory))) {
                 Subtask<String> subtask1 = scope1.fork(() -> {
                     assertEquals(name.get(), "x");  // child should read "x"
 
                     // rebind name to "y"
-                    String grandchildValue = ScopedValue.callWhere(name, "y", () -> {
-                        try (var scope2 = new StructuredTaskScope<String>(null, factory)) {
+                    String grandchildValue = ScopedValue.where(name, "y").call(() -> {
+                        try (var scope2 = StructuredTaskScope.open(Joiner.awaitAll(),
+                                                                   cf -> cf.withThreadFactory(factory))) {
                             Subtask<String> subtask2 = scope2.fork(() -> {
                                 return name.get(); // grandchild should read "y"
                             });
                             scope2.join();
                             return subtask2.get();
@@ -129,33 +135,33 @@
      */
     @Test
     void testStructureViolation1() throws Exception {
         ScopedValue<String> name = ScopedValue.newInstance();
         class Box {
-            StructuredTaskScope<Object> scope;
+            StructuredTaskScope<Object, Void> scope;
         }
         var box = new Box();
         try {
             try {
-                ScopedValue.runWhere(name, "x", () -> {
-                    box.scope = new StructuredTaskScope<Object>();
+                ScopedValue.where(name, "x").run(() -> {
+                    box.scope = StructuredTaskScope.open(Joiner.awaitAll());
                 });
                 fail();
             } catch (StructureViolationException expected) { }
 
             // underlying flock should be closed and fork should fail to start a thread
-            StructuredTaskScope<Object> scope = box.scope;
+            StructuredTaskScope<Object, Void> scope = box.scope;
             AtomicBoolean ran = new AtomicBoolean();
             Subtask<Object> subtask = scope.fork(() -> {
                 ran.set(true);
                 return null;
             });
             scope.join();
             assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
             assertFalse(ran.get());
         } finally {
-            StructuredTaskScope<Object> scope = box.scope;
+            StructuredTaskScope<Object, Void> scope = box.scope;
             if (scope != null) {
                 scope.close();
             }
         }
     }
@@ -164,12 +170,12 @@
      * Test closing a StructuredTaskScope while executing in a dynamic scope.
      */
     @Test
     void testStructureViolation2() throws Exception {
         ScopedValue<String> name = ScopedValue.newInstance();
-        try (var scope = new StructuredTaskScope<String>()) {
-            ScopedValue.runWhere(name, "x", () -> {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            ScopedValue.where(name, "x").run(() -> {
                 assertThrows(StructureViolationException.class, scope::close);
             });
         }
     }
 
@@ -177,12 +183,12 @@
      * Test fork when a scoped value is bound after a StructuredTaskScope is created.
      */
     @Test
     void testStructureViolation3() throws Exception {
         ScopedValue<String> name = ScopedValue.newInstance();
-        try (var scope = new StructuredTaskScope<String>()) {
-            ScopedValue.runWhere(name, "x", () -> {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            ScopedValue.where(name, "x").run(() -> {
                 assertThrows(StructureViolationException.class,
                         () -> scope.fork(() -> "foo"));
             });
         }
     }
@@ -194,23 +200,23 @@
     void testStructureViolation4() throws Exception {
         ScopedValue<String> name1 = ScopedValue.newInstance();
         ScopedValue<String> name2 = ScopedValue.newInstance();
 
         // rebind
-        ScopedValue.runWhere(name1, "x", () -> {
-            try (var scope = new StructuredTaskScope<String>()) {
-                ScopedValue.runWhere(name1, "y", () -> {
+        ScopedValue.where(name1, "x").run(() -> {
+            try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+                ScopedValue.where(name1, "y").run(() -> {
                     assertThrows(StructureViolationException.class,
                             () -> scope.fork(() -> "foo"));
                 });
             }
         });
 
         // new binding
-        ScopedValue.runWhere(name1, "x", () -> {
-            try (var scope = new StructuredTaskScope<String>()) {
-                ScopedValue.runWhere(name2, "y", () -> {
+        ScopedValue.where(name1, "x").run(() -> {
+            try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+                ScopedValue.where(name2, "y").run(() -> {
                     assertThrows(StructureViolationException.class,
                             () -> scope.fork(() -> "foo"));
                 });
             }
         });
diff a/test/jdk/javax/security/auth/Subject/CallAsWithScopedValue.java b/test/jdk/javax/security/auth/Subject/CallAsWithScopedValue.java
--- a/test/jdk/javax/security/auth/Subject/CallAsWithScopedValue.java
+++ b/test/jdk/javax/security/auth/Subject/CallAsWithScopedValue.java
@@ -64,11 +64,12 @@
             return null;
         });
 
         // Observable in structured concurrency in SV mode, but not in ACC mode
         Subject.callAs(subject, () -> {
-            try (var scope = new StructuredTaskScope<>()) {
+            var joiner = StructuredTaskScope.Joiner.awaitAll();
+            try (var scope = StructuredTaskScope.open(joiner)) {
                 scope.fork(() -> check(3, Subject.current(), usv ? "Duke" : null));
                 scope.join();
             }
             return null;
         });
diff a/test/jdk/jdk/internal/misc/ThreadFlock/ThreadFlockTest.java b/test/jdk/jdk/internal/misc/ThreadFlock/ThreadFlockTest.java
--- a/test/jdk/jdk/internal/misc/ThreadFlock/ThreadFlockTest.java
+++ b/test/jdk/jdk/internal/misc/ThreadFlock/ThreadFlockTest.java
@@ -748,55 +748,10 @@
                 latch.countDown();
             }
         }
     }
 
-    /**
-     * Test wakeup is flock confined.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testWakeupConfined(ThreadFactory factory) throws Exception {
-        try (var flock = ThreadFlock.open(null)) {
-            // thread in flock
-            testWakeupConfined(flock, task -> {
-                Thread thread = factory.newThread(task);
-                return flock.start(thread);
-            });
-
-            // thread not in flock
-            testWakeupConfined(flock, task -> {
-                Thread thread = factory.newThread(task);
-                thread.start();
-                return thread;
-            });
-        }
-    }
-
-    /**
-     * Test that a thread created with the given factory cannot wakeup the
-     * given flock.
-     */
-    private void testWakeupConfined(ThreadFlock flock,
-                                    Function<Runnable, Thread> factory) throws Exception {
-        var exception = new AtomicReference<Exception>();
-        Thread thread = factory.apply(() -> {
-            try {
-                flock.wakeup();
-            } catch (Exception e) {
-                exception.set(e);
-            }
-        });
-        thread.join();
-        Throwable cause = exception.get();
-        if (flock.containsThread(thread)) {
-            assertNull(cause);
-        } else {
-            assertTrue(cause instanceof WrongThreadException);
-        }
-    }
-
     /**
      * Test close with no threads running.
      */
     @Test
     void testCloseWithNoThreads() {
@@ -931,63 +886,10 @@
             assertTrue(Thread.interrupted());  // clear interrupt
         }
         assertNull(exception.get());
     }
 
-    /**
-     * Test shutdown is confined to threads in the flock.
-     */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testShutdownConfined(ThreadFactory factory) throws Exception {
-        try (var flock = ThreadFlock.open(null)) {
-            // thread in flock
-            testShutdownConfined(flock, task -> {
-                Thread thread = factory.newThread(task);
-                return flock.start(thread);
-            });
-
-            // thread in flock
-            try (var flock2 = ThreadFlock.open(null)) {
-                testShutdownConfined(flock, task -> {
-                    Thread thread = factory.newThread(task);
-                    return flock2.start(thread);
-                });
-            }
-
-            // thread not contained in flock
-            testShutdownConfined(flock, task -> {
-                Thread thread = factory.newThread(task);
-                thread.start();
-                return thread;
-            });
-        }
-    }
-
-    /**
-     * Test that a thread created with the given factory cannot shut down the
-     * given flock.
-     */
-    private void testShutdownConfined(ThreadFlock flock,
-                                      Function<Runnable, Thread> factory) throws Exception {
-        var exception = new AtomicReference<Exception>();
-        Thread thread = factory.apply(() -> {
-            try {
-                flock.shutdown();
-            } catch (Exception e) {
-                exception.set(e);
-            }
-        });
-        thread.join();
-        Throwable cause = exception.get();
-        if (flock.containsThread(thread)) {
-            assertNull(cause);
-        } else {
-            assertTrue(cause instanceof WrongThreadException);
-        }
-    }
-
     /**
      * Test that closing an enclosing thread flock closes a nested thread flocks.
      */
     @Test
     void testStructureViolation() {
diff a/test/jdk/jdk/internal/misc/ThreadFlock/WithScopedValue.java b/test/jdk/jdk/internal/misc/ThreadFlock/WithScopedValue.java
--- a/test/jdk/jdk/internal/misc/ThreadFlock/WithScopedValue.java
+++ b/test/jdk/jdk/internal/misc/ThreadFlock/WithScopedValue.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.
@@ -51,11 +51,11 @@
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testInheritsScopedValue(ThreadFactory factory) throws Exception {
         ScopedValue<String> name = ScopedValue.newInstance();
-        String value = ScopedValue.callWhere(name, "duke", () -> {
+        String value = ScopedValue.where(name, "duke").call(() -> {
             var result = new AtomicReference<String>();
             try (var flock = ThreadFlock.open(null)) {
                 Thread thread = factory.newThread(() -> {
                     // child
                     result.set(name.get());
@@ -77,11 +77,11 @@
             ThreadFlock flock1;
             ThreadFlock flock2;
         }
         var box = new Box();
         try {
-            ScopedValue.runWhere(name, "x1", () -> {
+            ScopedValue.where(name, "x1").run(() -> {
                 box.flock1 = ThreadFlock.open(null);
                 box.flock2 = ThreadFlock.open(null);
             });
             fail();
         } catch (StructureViolationException expected) { }
@@ -95,15 +95,15 @@
      */
     @Test
     void testStructureViolation2() {
         ScopedValue<String> name = ScopedValue.newInstance();
         try (var flock1 = ThreadFlock.open("flock1")) {
-            ScopedValue.runWhere(name, "x1", () -> {
+            ScopedValue.where(name, "x1").run(() -> {
                 try (var flock2 = ThreadFlock.open("flock2")) {
-                    ScopedValue.runWhere(name, "x2", () -> {
+                    ScopedValue.where(name, "x2").run(() -> {
                         try (var flock3 = ThreadFlock.open("flock3")) {
-                            ScopedValue.runWhere(name, "x3", () -> {
+                            ScopedValue.where(name, "x3").run(() -> {
                                 var flock4 = ThreadFlock.open("flock4");
 
                                 try {
                                     flock1.close();
                                     fail();
@@ -127,15 +127,15 @@
      */
     @Test
     void testStructureViolation3() {
         ScopedValue<String> name = ScopedValue.newInstance();
         try (var flock1 = ThreadFlock.open("flock1")) {
-            ScopedValue.runWhere(name, "x1", () -> {
+            ScopedValue.where(name, "x1").run(() -> {
                 try (var flock2 = ThreadFlock.open("flock2")) {
-                    ScopedValue.runWhere(name, "x2", () -> {
+                    ScopedValue.where(name, "x2").run(() -> {
                         try (var flock3 = ThreadFlock.open("flock3")) {
-                            ScopedValue.runWhere(name, "x3", () -> {
+                            ScopedValue.where(name, "x3").run(() -> {
                                 var flock4 = ThreadFlock.open("flock4");
 
                                 try {
                                     flock2.close();
                                     fail();
@@ -159,15 +159,15 @@
      */
     @Test
     void testStructureViolation4() {
         ScopedValue<String> name = ScopedValue.newInstance();
         try (var flock1 = ThreadFlock.open("flock1")) {
-            ScopedValue.runWhere(name, "x1", () -> {
+            ScopedValue.where(name, "x1").run(() -> {
                 try (var flock2 = ThreadFlock.open("flock2")) {
-                    ScopedValue.runWhere(name, "x2", () -> {
+                    ScopedValue.where(name, "x2").run(() -> {
                         try (var flock3 = ThreadFlock.open("flock3")) {
-                            ScopedValue.runWhere(name, "x3", () -> {
+                            ScopedValue.where(name, "x3").run(() -> {
                                 var flock4 = ThreadFlock.open("flock4");
 
                                 try {
                                     flock3.close();
                                     fail();
@@ -191,11 +191,11 @@
     @ParameterizedTest
     @MethodSource("factories")
     void testStructureViolation5(ThreadFactory factory) throws Exception {
         ScopedValue<String> name = ScopedValue.newInstance();
         try (var flock = ThreadFlock.open(null)) {
-            ScopedValue.runWhere(name, "duke", () -> {
+            ScopedValue.where(name, "duke").run(() -> {
                 Thread thread = factory.newThread(() -> { });
                 assertThrows(StructureViolationException.class, () -> flock.start(thread));
             });
         }
     }
@@ -205,13 +205,13 @@
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testStructureViolation6(ThreadFactory factory) throws Exception {
         ScopedValue<String> name = ScopedValue.newInstance();
-        ScopedValue.runWhere(name, "duke", () -> {
+        ScopedValue.where(name, "duke").run(() -> {
             try (var flock = ThreadFlock.open(null)) {
-                ScopedValue.runWhere(name, "duchess", () -> {
+                ScopedValue.where(name, "duchess").run(() -> {
                     Thread thread = factory.newThread(() -> { });
                     assertThrows(StructureViolationException.class, () -> flock.start(thread));
                 });
             }
         });
diff a/test/jdk/jdk/internal/vm/Continuation/Basic.java b/test/jdk/jdk/internal/vm/Continuation/Basic.java
--- a/test/jdk/jdk/internal/vm/Continuation/Basic.java
+++ b/test/jdk/jdk/internal/vm/Continuation/Basic.java
@@ -1,7 +1,7 @@
 /*
-* Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved.
+* Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
@@ -26,10 +26,11 @@
 /**
 * @test
 * @summary Basic tests for jdk.internal.vm.Continuation
 * @requires vm.continuations
 * @modules java.base/jdk.internal.vm
+* @library /test/lib
 * @build java.base/java.lang.StackWalkerHelper
 *
 * @run testng/othervm -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames -Xint Basic
 *
 * @run testng/othervm -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames -Xcomp -XX:TieredStopAtLevel=3 -XX:CompileOnly=jdk.internal.vm.Continuation::*,Basic::* Basic
@@ -42,26 +43,32 @@
 /**
 * @test
 * @requires vm.continuations
 * @requires vm.debug
 * @modules java.base/jdk.internal.vm
+* @library /test/lib
 * @build java.base/java.lang.StackWalkerHelper
 *
 * @run testng/othervm -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames -XX:+VerifyStack -Xint Basic
 * @run testng/othervm -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames -XX:+VerifyStack -Xcomp -XX:TieredStopAtLevel=3 -XX:CompileOnly=jdk.internal.vm.Continuation::*,Basic::* Basic
 * @run testng/othervm -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames -XX:+VerifyStack -Xcomp -XX:-TieredCompilation -XX:CompileOnly=jdk.internal.vm.Continuation::*,Basic::* Basic
 */
 
 import jdk.internal.vm.Continuation;
 import jdk.internal.vm.ContinuationScope;
 
+import jdk.test.lib.Platform;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 
+import com.sun.management.HotSpotDiagnosticMXBean;
+import java.lang.management.ManagementFactory;
+
 import org.testng.annotations.Test;
 import org.testng.annotations.DataProvider;
 import static org.testng.Assert.*;
 
 import java.util.concurrent.atomic.AtomicInteger;
@@ -274,10 +281,12 @@
         return "" + r;
     }
 
     @Test
     public void testPinnedMonitor() {
+        if (!legacyLockingMode()) return;
+
         // Test pinning due to held monitor
         final AtomicReference<Continuation.Pinned> res = new AtomicReference<>();
 
         Continuation cont = new Continuation(FOO, ()-> {
             syncFoo(1);
@@ -408,6 +417,11 @@
     private static native int nativeBar(int x);
 
     static {
         System.loadLibrary("BasicJNI");
     }
+
+    static boolean legacyLockingMode() {
+        return ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class)
+                    .getVMOption("LockingMode").getValue().equals("1");
+    }
 }
diff a/test/jdk/jdk/internal/vm/Continuation/Fuzz.java b/test/jdk/jdk/internal/vm/Continuation/Fuzz.java
--- a/test/jdk/jdk/internal/vm/Continuation/Fuzz.java
+++ b/test/jdk/jdk/internal/vm/Continuation/Fuzz.java
@@ -1,7 +1,7 @@
 /*
-* Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved.
+* Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
@@ -73,10 +73,13 @@
 import jdk.test.whitebox.WhiteBox;
 
 import jdk.test.lib.Platform;
 import jtreg.SkippedException;
 
+import com.sun.management.HotSpotDiagnosticMXBean;
+import java.lang.management.ManagementFactory;
+
 public class Fuzz implements Runnable {
     static final boolean VERIFY_STACK = true; // could add significant time
     static final boolean FILE    = true;
     static final boolean RANDOM  = true;
     static final boolean VERBOSE = false;
@@ -468,11 +471,11 @@
         int computed = computeResult();
         assert result == computed : "result: " + result + " expected: " + computed;
     }
 
     boolean shouldPin() {
-        return traceHas(Op.PIN::contains);
+        return traceHas(Op.PIN::contains) && legacyLockingMode();
     }
 
     void verifyPin(boolean yieldResult) {
         if (yieldResult) yields++;
         if (!yieldResult && traceHas(op -> Op.INTERPRETED.contains(op) && Op.REFLECTED.contains(op))) return;
@@ -1027,6 +1030,11 @@
             }
         }
 
         return log((int)res);
     }
+
+    static boolean legacyLockingMode() {
+        return ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class)
+                    .getVMOption("LockingMode").getValue().equals("1");
+    }
 }
diff a/test/langtools/ProblemList-Virtual.txt b/test/langtools/ProblemList-Virtual.txt
--- /dev/null
+++ b/test/langtools/ProblemList-Virtual.txt
@@ -0,0 +1,31 @@
+####
+## Filed bugs
+
+####
+# Incompatible tests.
+
+####
+# Failed because jtreg wrapper can't catch exceptions in thread groups
+# or incorrently process negative tests
+tools/javac/NullQualifiedNew.java 0000000 generic-all
+tools/javac/NullQualifiedSuper2.java 0000000 generic-all
+tools/javac/generics/T5094318.java 0000000 generic-all
+tools/javac/api/T6395981.java 0000000 generic-all
+jdk/javadoc/tool/sourceOption/SourceOption.java 0000000 generic-all
+
+####
+# Tests failed because of unexpected --enable-preview
+tools/javac/launcher/GetResourceTest.java 0000000 generic-all
+tools/javac/launcher/SourceLauncherTest.java 0000000 generic-all
+
+# unstable when main thread is a virtual thread
+jdk/jshell/ToolMultilineSnippetHistoryTest.java 0000000 generic-all
+jdk/jshell/ToolShiftTabTest.java 0000000 generic-all
+
+# more unstable when main thread is a virtual thread
+jdk/jshell/ToolTabCommandTest.java 8287078 generic-all
+jdk/jshell/ToolTabSnippetTest.java 8287078 generic-all
+jdk/jshell/UndefinedClassTest.java 8287078 generic-all
+
+# output not as expected
+jdk/javadoc/doclet/testLinkOption/TestRedirectLinks.java 8338439 generic-all
diff a/test/langtools/ProblemList-vthread-Xcomp.txt b/test/langtools/ProblemList-vthread-Xcomp.txt
--- /dev/null
+++ b/test/langtools/ProblemList-vthread-Xcomp.txt
@@ -0,0 +1,34 @@
+#
+# Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+#
+# This code is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 2 only, as
+# published by the Free Software Foundation.
+#
+# This code is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# version 2 for more details (a copy is included in the LICENSE file that
+# accompanied this code).
+#
+# You should have received a copy of the GNU General Public License version
+# 2 along with this work; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+# or visit www.oracle.com if you need additional information or have any
+# questions.
+#
+
+#############################################################################
+#
+# List of quarantined tests for testing in Xcomp mode with wrapper.
+#
+#############################################################################
+
+####
+## Timeouts
+
+tools/javac/failover/CheckAttributedTree.java
+tools/sjavac/IdleShutdown.java
diff a/test/langtools/ProblemList.txt b/test/langtools/ProblemList.txt
--- a/test/langtools/ProblemList.txt
+++ b/test/langtools/ProblemList.txt
@@ -60,10 +60,12 @@
 tools/javac/annotations/typeAnnotations/referenceinfos/Lambda.java              8057687    generic-all    emit correct byte code an attributes for type annotations
 tools/javac/annotations/typeAnnotations/referenceinfos/NestedTypes.java         8057687    generic-all    emit correct byte code an attributes for type annotations
 tools/javac/warnings/suppress/TypeAnnotations.java                              8057683    generic-all    improve ordering of errors with type annotations
 tools/javac/modules/SourceInSymlinkTest.java                                    8180263    windows-all    fails when run on a subst drive
 
+tools/javac/lambda/T8031967.java                                                8323041    linux-aarch64  fails with -XX:TieredStopAtLevel=n (n=1,2,3)
+
 ###########################################################################
 #
 # javap
 
 tools/javap/output/RepeatingTypeAnnotations.java                                8057687    generic-all    emit correct byte code an attributes for type annotations
diff a/make/langtools/test/TEST.ROOT b/test/lib-test/ProblemList-Virtual.txt
--- a/make/langtools/test/TEST.ROOT
+++ b/test/lib-test/ProblemList-Virtual.txt
diff a/test/micro/org/openjdk/bench/loom/Monitors.java b/test/micro/org/openjdk/bench/loom/Monitors.java
--- /dev/null
+++ b/test/micro/org/openjdk/bench/loom/Monitors.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package org.openjdk.bench.loom;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import jdk.internal.vm.Continuation;
+import jdk.internal.vm.ContinuationScope;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.concurrent.TimeUnit;
+
+//@Fork(2)
+@Fork(1)
+@Warmup(iterations = 3, time = 5)
+@Measurement(iterations = 3, time = 5)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@State(Scope.Thread)
+@SuppressWarnings("preview")
+public class Monitors {
+    static final ContinuationScope SCOPE = new ContinuationScope() { };
+
+    static class RunSynchronized implements Runnable {
+        private final int stackDepth;
+        private final Object[] syncLockArray;
+
+        private RunSynchronized(int stackDepth, ReentrantLock[] lockArray) {
+            this.stackDepth = stackDepth;
+            this.syncLockArray = lockArray;
+        }
+
+        public void recursive(int depth) {
+            if (syncLockArray[depth] != null) {
+                synchronized (syncLockArray[depth]) {
+                    if (depth > 0) {
+                        recursive(depth - 1);
+                    } else {
+                        Continuation.yield(SCOPE);
+                    }
+                }
+            } else {
+                if (depth > 0) {
+                    recursive(depth - 1);
+                } else {
+                    Continuation.yield(SCOPE);
+                }
+            }
+        }
+
+        @Override
+        public void run() {
+            while (true) {
+                recursive(stackDepth);
+            }
+        }
+    }
+
+    static class RunReentrantLock implements Runnable {
+        private final int stackDepth;
+        private final ReentrantLock[] lockArray;
+
+        private RunReentrantLock(int stackDepth, ReentrantLock[] lockArray) {
+            this.stackDepth = stackDepth;
+            this.lockArray = lockArray;
+        }
+
+        public void recursive(int depth) {
+            if (lockArray[depth] != null) {
+                lockArray[depth].lock();
+                if (depth > 0) {
+                    recursive(depth - 1);
+                } else {
+                    Continuation.yield(SCOPE);
+                }
+                lockArray[depth].unlock();
+            } else {
+                if (depth > 0) {
+                    recursive(depth - 1);
+                } else {
+                    Continuation.yield(SCOPE);
+                }
+            }
+        }
+
+        @Override
+        public void run() {
+            while (true) {
+                recursive(stackDepth);
+            }
+        }
+    }
+
+    static void inflateAllLocks(ReentrantLock[] lockArray) {
+        for (ReentrantLock lock : lockArray) {
+            if (lock != null) {
+                synchronized (lock) {
+                    try {
+                        lock.wait(1);
+                    } catch (Exception e) {}
+                }
+            }
+        }
+    }
+
+    static ReentrantLock[] createLockArray(int stackDepth, int monitorCount) {
+        ReentrantLock[] lockArray = new ReentrantLock[stackDepth + 1];
+
+        // Always a lock on the bottom most frame
+        lockArray[stackDepth] = new ReentrantLock();
+        // Limit extra monitors to no more than one per recursive frame.
+        int remainingMonitors = Math.min(Math.max(0, monitorCount - 1), stackDepth);
+
+        if (remainingMonitors > 0) {
+            // Calculate which other frames will use a lock.
+            int stride = Math.max(2, (stackDepth / monitorCount));
+            int frameIndex = (stackDepth - stride) % (stackDepth + 1);
+            while (remainingMonitors > 0) {
+                if (lockArray[frameIndex] == null) {
+                    lockArray[frameIndex] = new ReentrantLock();
+                    frameIndex = (frameIndex - stride) % (stackDepth + 1);
+                    remainingMonitors--;
+                } else {
+                    frameIndex = (frameIndex - 1) % (stackDepth + 1);
+                }
+            }
+        }
+        System.out.println("Created lock array. synchronized/ReentrantLock will be used at following indexes:");
+        for (int i = stackDepth; i >= 0; i--) {
+            System.out.println("[" + i + "] : " + (lockArray[i] != null ? "synchronized/ReentrantLock" : "-"));
+        }
+        return lockArray;
+    }
+
+    static Continuation createContinuation(int stackDepth, ReentrantLock[] lockArray, boolean useSyncronized) {
+        Runnable task = useSyncronized ? new RunSynchronized(stackDepth, lockArray)
+                                       : new RunReentrantLock(stackDepth, lockArray);
+        //inflateAllLocks(lockArray);
+        return new Continuation(SCOPE, task);
+    }
+
+    @State(Scope.Thread)
+    public static class MyState {
+        @Param({"0", "5", "10", "20"})
+        public int stackDepth;
+
+        @Param({"1", "2", "3"})
+        public int monitorCount;
+
+        public Continuation syncCont;
+        public Continuation reentCont;
+
+        @Setup(Level.Trial)
+        public void doSetup() {
+            ReentrantLock[] lockArray = createLockArray(stackDepth, monitorCount);
+            // We don't care which object we synchronize on so just pass
+            // the same ReentrantLock array to the synchronized task.
+            syncCont = createContinuation(stackDepth, lockArray, true /* useSyncronized */);
+            reentCont = createContinuation(stackDepth, lockArray, false /* useSyncronized */);
+            System.out.println("Created continuation used for testing with stackDepth: " + stackDepth + " and monitorCount: " + monitorCount);
+        }
+    }
+
+    @Benchmark
+    public void syncLock(MyState state) {
+        state.syncCont.run();
+    }
+
+    @Benchmark
+    public void reentrantLock(MyState state) {
+        state.reentCont.run();
+    }
+}
diff a/test/micro/org/openjdk/bench/loom/Monitors2.java b/test/micro/org/openjdk/bench/loom/Monitors2.java
--- /dev/null
+++ b/test/micro/org/openjdk/bench/loom/Monitors2.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package org.openjdk.bench.loom;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.concurrent.*;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+//@Fork(2)
+@Fork(1)
+@Warmup(iterations = 3, time = 5)
+@Measurement(iterations = 3, time = 5)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@State(Scope.Thread)
+@SuppressWarnings("preview")
+public class Monitors2 {
+    static Object[] globalLockArray;
+    static ReentrantLock[] globalReentrantLockArray;
+    static AtomicInteger workerCount = new AtomicInteger(0);
+    static AtomicInteger dummyCounter = new AtomicInteger(0);
+    static int workIterations;
+
+    static int VT_COUNT;
+    static int CARRIER_COUNT = 8;
+    static ExecutorService scheduler = Executors.newFixedThreadPool(CARRIER_COUNT);
+
+    //@Param({"8", "32", "256", "4096", "131072"})
+    @Param({"8", "32", "256", "4096"})
+    static int VT_MULTIPLIER;
+
+    @Param({"1", "5", "12", "32"})
+    static int MONITORS_CNT;
+
+    @Param({"0", "50000", "100000", "200000"})
+    static int WORKLOAD;
+
+    @Param({"10"})
+    static int STACK_DEPTH;
+
+    void recursive4_1(int depth, int lockNumber) {
+        if (depth > 0) {
+            recursive4_1(depth - 1, lockNumber);
+        } else {
+            if (Math.random() < 0.5) {
+                Thread.yield();
+            }
+            recursive4_2(lockNumber);
+        }
+    }
+
+    void recursive4_2(int lockNumber) {
+        if (lockNumber + 2 <= MONITORS_CNT - 1) {
+            lockNumber += 2;
+            synchronized(globalLockArray[lockNumber]) {
+                Thread.yield();
+                recursive4_2(lockNumber);
+            }
+        }
+    }
+
+    final Runnable FOO4 = () -> {
+        int iterations = 10;
+        while (iterations-- > 0) {
+            int lockNumber = ThreadLocalRandom.current().nextInt(0, MONITORS_CNT - 1);
+            synchronized(globalLockArray[lockNumber]) {
+                recursive4_1(lockNumber, lockNumber);
+            }
+        }
+        workerCount.getAndIncrement();
+    };
+
+    /**
+     *  Test contention on monitorenter with extra monitors on stack shared by all threads.
+     */
+    //@Benchmark
+    public void testContentionMultipleMonitors(MyState state) throws Exception {
+        workerCount.getAndSet(0);
+
+        Thread batch[] = new Thread[VT_COUNT];
+        for (int i = 0; i < VT_COUNT; i++) {
+            batch[i] = virtualThreadBuilder(scheduler).name("BatchVT-" + i).start(FOO4);
+        }
+
+        for (int i = 0; i < VT_COUNT; i++) {
+            batch[i].join();
+        }
+
+        if (workerCount.get() != VT_COUNT) {
+            throw new RuntimeException("testContentionMultipleMonitors failed. Expected " + VT_COUNT + "but found " + workerCount.get());
+        }
+    }
+
+    static void recursive5_1(int depth, int lockNumber, Object[] myLockArray) {
+        if (depth > 0) {
+            recursive5_1(depth - 1, lockNumber, myLockArray);
+        } else {
+            if (Math.random() < 0.5) {
+                Thread.yield();
+            }
+            recursive5_2(lockNumber, myLockArray);
+        }
+    }
+
+    static void recursive5_2(int lockNumber, Object[] myLockArray) {
+        if (lockNumber + 2 <= MONITORS_CNT - 1) {
+            lockNumber += 2;
+            synchronized (myLockArray[lockNumber]) {
+                if (Math.random() < 0.5) {
+                    Thread.yield();
+                }
+                synchronized (globalLockArray[lockNumber]) {
+                    Thread.yield();
+                    recursive5_2(lockNumber, myLockArray);
+                }
+            }
+        }
+    }
+
+    static final Runnable FOO5 = () -> {
+        Object[] myLockArray = new Object[MONITORS_CNT];
+        for (int i = 0; i < MONITORS_CNT; i++) {
+            myLockArray[i] = new Object();
+        }
+
+        int iterations = 10;
+        while (iterations-- > 0) {
+            int lockNumber = ThreadLocalRandom.current().nextInt(0, MONITORS_CNT - 1);
+            synchronized (myLockArray[lockNumber]) {
+                synchronized (globalLockArray[lockNumber]) {
+                    recursive5_1(lockNumber, lockNumber, myLockArray);
+                }
+            }
+        }
+        workerCount.getAndIncrement();
+    };
+
+    /**
+     *  Test contention on monitorenter with extra monitors on stack both local only and shared by all threads.
+     */
+    //@Benchmark
+    public void testContentionMultipleMonitors2(MyState state) throws Exception {
+        workerCount.getAndSet(0);
+
+        // Create batch of VT threads.
+        Thread batch[] = new Thread[VT_COUNT];
+        for (int i = 0; i < VT_COUNT; i++) {
+            //Thread.ofVirtual().name("FirstBatchVT-" + i).start(FOO);
+            batch[i] = virtualThreadBuilder(scheduler).name("BatchVT-" + i).start(FOO5);
+        }
+
+        for (int i = 0; i < VT_COUNT; i++) {
+            batch[i].join();
+        }
+
+        if (workerCount.get() != VT_COUNT) {
+            throw new RuntimeException("testContentionMultipleMonitors2 failed. Expected " + VT_COUNT + "but found " + workerCount.get());
+        }
+    }
+
+    static void recursiveSync(int depth) {
+        if (depth > 0) {
+            recursiveSync(depth - 1);
+        } else {
+            int lockNumber = ThreadLocalRandom.current().nextInt(0, MONITORS_CNT);
+            synchronized(globalLockArray[lockNumber]) {
+                //Thread.yield();
+                for (int i = 0; i < WORKLOAD; i++) {
+                    dummyCounter.getAndIncrement();
+                }
+                workerCount.getAndIncrement();
+            }
+        }
+    }
+
+    static final Runnable SYNC = () -> {
+        recursiveSync(STACK_DEPTH);
+    };
+
+    static void recursiveReentrant(int depth) {
+        if (depth > 0) {
+            recursiveReentrant(depth - 1);
+        } else {
+            int lockNumber = ThreadLocalRandom.current().nextInt(0, MONITORS_CNT);
+            globalReentrantLockArray[lockNumber].lock();
+            //Thread.yield();
+            for (int i = 0; i < WORKLOAD; i++) {
+                dummyCounter.getAndIncrement();
+            }
+            workerCount.getAndIncrement();
+            globalReentrantLockArray[lockNumber].unlock();
+        }
+    }
+
+    static final Runnable REENTRANTLOCK = () -> {
+        recursiveReentrant(STACK_DEPTH);
+    };
+
+    public void runBenchmark(Runnable r) throws Exception {
+        workerCount.getAndSet(0);
+
+        // Create batch of VT threads.
+        Thread batch[] = new Thread[VT_COUNT];
+        for (int i = 0; i < VT_COUNT; i++) {
+            batch[i] = virtualThreadBuilder(scheduler).name("BatchVT-" + i).start(r);
+        }
+
+        for (int i = 0; i < VT_COUNT; i++) {
+            batch[i].join();
+        }
+
+        if (workerCount.get() != VT_COUNT) {
+            throw new RuntimeException("testContentionMultipleMonitors2 failed. Expected " + VT_COUNT + "but found " + workerCount.get());
+        }
+    }
+
+    @Benchmark
+    public void testContentionReentrantLock(MyState state) throws Exception {
+        runBenchmark(REENTRANTLOCK);
+    }
+
+    @Benchmark
+    public void testContentionASync(MyState state) throws Exception {
+        runBenchmark(SYNC);
+    }
+
+    //@Benchmark
+    public void testExtraTime(MyState state) throws Exception {
+        dummyCounter.getAndSet(0);
+        // Takes ~120us
+        for (int i = 0; i < 50000; i++) {
+            dummyCounter.getAndIncrement();
+        }
+        if (dummyCounter.get() != 50000) {
+            throw new RuntimeException("testContentionMultipleMonitors2 failed. Expected " + 50000 + "but found " + dummyCounter.get());
+        }
+    }
+
+    @State(Scope.Thread)
+    public static class MyState {
+
+        @Setup(Level.Trial)
+        public void doSetup() {
+            // Setup up monitors/locks
+            globalLockArray = new Object[MONITORS_CNT];
+            globalReentrantLockArray = new ReentrantLock[MONITORS_CNT];
+            for (int i = 0; i < MONITORS_CNT; i++) {
+                globalLockArray[i] = new Object();
+                globalReentrantLockArray[i] = new ReentrantLock();
+            }
+            // Setup VirtualThread count
+            VT_COUNT = CARRIER_COUNT * VT_MULTIPLIER;
+
+            System.out.println("Running test with MONITORS_CNT = " + MONITORS_CNT + " VT_COUNT = " + VT_COUNT + " and WORKLOAD = " + WORKLOAD);
+            //System.out.println("Running test with VT_COUNT = " + VT_COUNT);
+        }
+
+        @TearDown(Level.Trial)
+        public void doTearDown() {
+            scheduler.shutdown();
+            System.out.println("Do TearDown");
+        }
+    }
+
+    private static Thread.Builder.OfVirtual virtualThreadBuilder(Executor scheduler) {
+        Thread.Builder.OfVirtual builder = Thread.ofVirtual();
+        try {
+            Class<?> clazz = Class.forName("java.lang.ThreadBuilders$VirtualThreadBuilder");
+            Constructor<?> ctor = clazz.getDeclaredConstructor(Executor.class);
+            ctor.setAccessible(true);
+            return (Thread.Builder.OfVirtual) ctor.newInstance(scheduler);
+        } catch (InvocationTargetException e) {
+            Throwable cause = e.getCause();
+            if (cause instanceof RuntimeException re) {
+                throw re;
+            }
+            throw new RuntimeException(e);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
