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
@@ -63,10 +63,11 @@
 int C1_MacroAssembler::lock_object(Register hdr, Register obj, Register disp_hdr, Register temp, Label& slow_case) {
   const int aligned_mask = BytesPerWord -1;
   const int hdr_offset = oopDesc::mark_offset_in_bytes();
   assert_different_registers(hdr, obj, disp_hdr, temp, rscratch2);
   int null_check_offset = -1;
+  Label count_locking, done;
 
   verify_oop(obj);
 
   // save object being locked into the BasicObjectLock
   str(obj, Address(disp_hdr, BasicObjectLock::obj_offset()));
@@ -80,23 +81,24 @@
     br(Assembler::NE, slow_case);
   }
 
   if (LockingMode == LM_LIGHTWEIGHT) {
     lightweight_lock(obj, hdr, temp, rscratch2, slow_case);
+    b(done);
   } else if (LockingMode == LM_LEGACY) {
-    Label done;
     // Load object header
     ldr(hdr, Address(obj, hdr_offset));
     // and mark it as unlocked
     orr(hdr, hdr, markWord::unlocked_value);
     // save unlocked object header into the displaced header location on the stack
     str(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
     lea(rscratch2, Address(obj, hdr_offset));
-    cmpxchgptr(hdr, disp_hdr, rscratch2, rscratch1, done, /*fallthough*/nullptr);
+    // if the object header was the same, we're done
+    cmpxchgptr(hdr, disp_hdr, rscratch2, rscratch1, count_locking, /*fallthough*/nullptr);
     // if the object header was the same, we're done
     // 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
@@ -116,22 +118,24 @@
     // location (null in the displaced hdr location indicates recursive locking)
     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);
+    b(done);
   }
-  increment(Address(rthread, JavaThread::held_monitor_count_offset()));
+  bind(count_locking);
+  inc_held_monitor_count();
+  bind(done);
   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;
   const int hdr_offset = oopDesc::mark_offset_in_bytes();
   assert_different_registers(hdr, obj, disp_hdr, temp, rscratch2);
-  Label done;
+  Label count_locking, done;
 
   if (LockingMode != LM_LIGHTWEIGHT) {
     // load displaced header
     ldr(hdr, Address(disp_hdr, 0));
     // if the loaded hdr is null we had recursive locking
@@ -143,26 +147,28 @@
   ldr(obj, Address(disp_hdr, BasicObjectLock::obj_offset()));
   verify_oop(obj);
 
   if (LockingMode == LM_LIGHTWEIGHT) {
     lightweight_unlock(obj, hdr, temp, rscratch2, slow_case);
+    b(done);
   } else if (LockingMode == LM_LEGACY) {
     // test if object header is pointing to the displaced header, and if so, restore
     // the displaced header in the object - if the object header is not pointing to
     // the displaced header, get the object header instead
     // if the object header was not pointing to the displaced header,
     // we do unlocking via runtime call
     if (hdr_offset) {
       lea(rscratch1, Address(obj, hdr_offset));
-      cmpxchgptr(disp_hdr, hdr, rscratch1, rscratch2, done, &slow_case);
+      cmpxchgptr(disp_hdr, hdr, rscratch1, rscratch2, count_locking, &slow_case);
     } else {
-      cmpxchgptr(disp_hdr, hdr, obj, rscratch2, done, &slow_case);
+      cmpxchgptr(disp_hdr, hdr, obj, rscratch2, count_locking, &slow_case);
     }
     // done
-    bind(done);
+    bind(count_locking);
+    dec_held_monitor_count();
   }
-  decrement(Address(rthread, JavaThread::held_monitor_count_offset()));
+  bind(done);
 }
 
 
 // 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) {
+  // use_pop when this frame may have been frozen on one carrier
+  // thread then thawed on another carrier thread, rendering the fp
+  // register invalid. We must restore the previous FP because it is
+  // used as a call-saved scratch register by compiled code.
+  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(Runtime1::monitorenter_id) ||
+         cb == Runtime1::blob_for(Runtime1::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 monitorenter_nofpu_id:
       save_fpu_registers = false;
       // fall through
     case 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_CodeStubs_aarch64.cpp b/src/hotspot/cpu/aarch64/c2_CodeStubs_aarch64.cpp
--- a/src/hotspot/cpu/aarch64/c2_CodeStubs_aarch64.cpp
+++ b/src/hotspot/cpu/aarch64/c2_CodeStubs_aarch64.cpp
@@ -66,29 +66,34 @@
 
 int C2HandleAnonOMOwnerStub::max_size() const {
   // Max size of stub has been determined by testing with 0, in which case
   // C2CodeStubList::emit() will throw an assertion and report the actual size that
   // is needed.
-  return 24;
+  return 52;
 }
 
 void C2HandleAnonOMOwnerStub::emit(C2_MacroAssembler& masm) {
   __ bind(entry());
   Register mon = monitor();
   Register t = tmp();
   assert(t != noreg, "need tmp register");
 
   // Fix owner to be the current thread.
-  __ str(rthread, Address(mon, ObjectMonitor::owner_offset()));
-
-  // Pop owner object from lock-stack.
-  __ ldrw(t, Address(rthread, JavaThread::lock_stack_top_offset()));
-  __ subw(t, t, oopSize);
-#ifdef ASSERT
-  __ str(zr, Address(rthread, t));
-#endif
-  __ strw(t, Address(rthread, JavaThread::lock_stack_top_offset()));
+  __ ldr(t, Address(rthread, JavaThread::lock_id_offset()));
+  __ str(t, Address(mon, ObjectMonitor::owner_offset()));
 
+  if (LockingMode == LM_LIGHTWEIGHT) {
+    // Pop owner object from lock-stack.
+    __ ldrw(t, Address(rthread, JavaThread::lock_stack_top_offset()));
+    __ subw(t, t, oopSize);
+  #ifdef ASSERT
+    __ str(zr, Address(rthread, t));
+  #endif
+    __ strw(t, Address(rthread, JavaThread::lock_stack_top_offset()));
+  } else {
+    __ str(zr, Address(mon, ObjectMonitor::stack_locker_offset()));
+    __ dec_held_monitor_count();
+  }
   __ b(continuation());
 }
 
 #undef __
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
@@ -50,33 +50,32 @@
                                   Register tmp2Reg, Register tmp3Reg) {
   Register oop = objectReg;
   Register box = boxReg;
   Register disp_hdr = tmpReg;
   Register tmp = tmp2Reg;
-  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, rscratch1);
 
   // Load markWord from object into displaced_header.
   ldr(disp_hdr, Address(oop, oopDesc::mark_offset_in_bytes()));
 
   if (DiagnoseSyncOnValueBasedClasses != 0) {
     load_klass(tmp, oop);
     ldrw(tmp, Address(tmp, Klass::access_flags_offset()));
     tstw(tmp, JVM_ACC_IS_VALUE_BASED_CLASS);
-    br(Assembler::NE, cont);
+    br(Assembler::NE, no_count);
   }
 
   // Check for existing monitor
   tbnz(disp_hdr, exact_log2(markWord::monitor_value), object_has_monitor);
 
   if (LockingMode == LM_MONITOR) {
     tst(oop, oop); // Set NE to indicate 'failure' -> take slow-path. We know that oop != 0.
-    b(cont);
+    b(no_count);
   } else {
     assert(LockingMode == LM_LEGACY, "must be");
     // Set tmp to be (markWord of object | UNLOCK_VALUE).
     orr(tmp, disp_hdr, markWord::unlocked_value);
 
@@ -86,11 +85,11 @@
     // Compare object markWord with an unlocked value (tmp) and if
     // equal exchange the stack address of our box with object markWord.
     // On failure disp_hdr contains the possibly locked markWord.
     cmpxchg(oop, tmp, box, Assembler::xword, /*acquire*/ true,
             /*release*/ true, /*weak*/ false, disp_hdr);
-    br(Assembler::EQ, cont);
+    br(Assembler::EQ, count);
 
     assert(oopDesc::mark_offset_in_bytes() == 0, "offset of _mark is not 0");
 
     // If the compare-and-exchange succeeded, then we found an unlocked
     // object, will have now locked it will continue at label cont
@@ -102,80 +101,80 @@
     mov(tmp, (address) (~(os::vm_page_size()-1) | markWord::lock_mask_in_place));
     // If condition is true we are cont and hence we can store 0 as the
     // displaced header in the box, which indicates that it is a recursive lock.
     ands(tmp/*==0?*/, disp_hdr, tmp);   // Sets flags for result
     str(tmp/*==0, perhaps*/, Address(box, BasicLock::displaced_header_offset_in_bytes()));
-    b(cont);
+    b(no_count);
   }
 
   // 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
   // relevant bit set, and also matches ObjectSynchronizer::enter.
   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
+  br(Assembler::EQ, no_count); // CAS success means locking succeeded
 
-  cmp(tmp3Reg, rthread);
-  br(Assembler::NE, cont); // Check for recursive locking
+  cmp(tmp3Reg, rscratch2);
+  br(Assembler::NE, no_count); // 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
-
-  bind(cont);
-  // flag == EQ indicates success
-  // flag == NE indicates failure
-  br(Assembler::NE, no_count);
+  b(no_count);
 
   bind(count);
-  increment(Address(rthread, JavaThread::held_monitor_count_offset()));
+  inc_held_monitor_count();
 
   bind(no_count);
+  // flag == EQ indicates success
+  // flag == NE indicates failure
 }
 
 void C2_MacroAssembler::fast_unlock(Register objectReg, Register boxReg, Register tmpReg,
                                     Register tmp2Reg) {
   Register oop = objectReg;
   Register box = boxReg;
   Register disp_hdr = tmpReg;
   Register tmp = tmp2Reg;
   Label cont;
   Label object_has_monitor;
-  Label count, no_count;
+  Label no_count;
 
   assert(LockingMode != LM_LIGHTWEIGHT, "lightweight locking should use fast_unlock_lightweight");
   assert_different_registers(oop, box, tmp, disp_hdr);
 
   if (LockingMode == LM_LEGACY) {
     // Find the lock address and load the displaced header from the stack.
     ldr(disp_hdr, Address(box, BasicLock::displaced_header_offset_in_bytes()));
 
     // If the displaced header is 0, we have a recursive unlock.
     cmp(disp_hdr, zr);
-    br(Assembler::EQ, cont);
+    br(Assembler::EQ, no_count);
   }
 
   // Handle existing monitor.
   ldr(tmp, Address(oop, oopDesc::mark_offset_in_bytes()));
   tbnz(tmp, exact_log2(markWord::monitor_value), object_has_monitor);
 
   if (LockingMode == LM_MONITOR) {
     tst(oop, oop); // Set NE to indicate 'failure' -> take slow-path. We know that oop != 0.
-    b(cont);
+    b(no_count);
   } else {
     assert(LockingMode == LM_LEGACY, "must be");
     // Check if it is still a light weight lock, this is is true if we
     // see the stack address of the basicLock in the markWord of the
     // object.
@@ -190,38 +189,50 @@
   // Handle existing monitor.
   bind(object_has_monitor);
   STATIC_ASSERT(markWord::monitor_value <= INT_MAX);
   add(tmp, tmp, -(int)markWord::monitor_value); // monitor
 
+  // If the owner is anonymous, we need to fix it -- in an outline stub.
+  Register tmp2 = disp_hdr;
+  ldr(tmp2, Address(tmp, ObjectMonitor::owner_offset()));
+  // We cannot use tbnz here, the target might be too far away and cannot
+  // be encoded.
+  mov(rscratch1, (uint64_t)ObjectMonitor::ANONYMOUS_OWNER);
+  cmp(tmp2, rscratch1);
+  C2HandleAnonOMOwnerStub* stub = new (Compile::current()->comp_arena()) C2HandleAnonOMOwnerStub(tmp, tmp2);
+  Compile::current()->output()->add_stub(stub);
+  br(Assembler::EQ, stub->entry());
+  bind(stub->continuation());
+
   ldr(disp_hdr, Address(tmp, ObjectMonitor::recursions_offset()));
 
   Label notRecursive;
   cbz(disp_hdr, notRecursive);
 
   // Recursive lock
   sub(disp_hdr, disp_hdr, 1u);
   str(disp_hdr, Address(tmp, ObjectMonitor::recursions_offset()));
   cmp(disp_hdr, disp_hdr); // Sets flags for result
-  b(cont);
+  b(no_count);
 
   bind(notRecursive);
   ldr(rscratch1, Address(tmp, ObjectMonitor::EntryList_offset()));
   ldr(disp_hdr, Address(tmp, ObjectMonitor::cxq_offset()));
   orr(rscratch1, rscratch1, disp_hdr); // Will be 0 if both are 0.
   cmp(rscratch1, zr); // Sets flags for result
-  cbnz(rscratch1, cont);
+  cbnz(rscratch1, no_count);
   // need a release store here
   lea(tmp, Address(tmp, ObjectMonitor::owner_offset()));
   stlr(zr, tmp); // set unowned
+  b(no_count);
 
   bind(cont);
   // flag == EQ indicates success
   // flag == NE indicates failure
   br(Assembler::NE, no_count);
 
-  bind(count);
-  decrement(Address(rthread, JavaThread::held_monitor_count_offset()));
+  dec_held_monitor_count();
 
   bind(no_count);
 }
 
 void C2_MacroAssembler::fast_lock_lightweight(Register obj, Register t1,
@@ -296,25 +307,25 @@
     const Register t3_owner = t3;
 
     // Compute owner address.
     lea(t2_owner_addr, Address(t1_tagged_monitor, (in_bytes(ObjectMonitor::owner_offset()) - monitor_tag)));
 
-    // 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, locked);
 
     // Check if recursive.
-    cmp(t3_owner, rthread);
+    cmp(t3_owner, rscratch2);
     br(Assembler::NE, slow_path);
 
     // Recursive.
     increment(Address(t1_tagged_monitor, in_bytes(ObjectMonitor::recursions_offset()) - monitor_tag), 1);
   }
 
   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);
@@ -448,22 +459,22 @@
     cmp(rscratch1, zr);
     br(Assembler::EQ, release);
 
     // The owner may be anonymous and we removed the last obj entry in
     // the lock-stack. This loses the information about the owner.
-    // Write the thread to the owner field so the runtime knows the owner.
-    str(rthread, Address(t2_owner_addr));
+    // Write the thread id to the owner field so the runtime knows the owner.
+    ldr(t3_t, Address(rthread, JavaThread::lock_id_offset()));
+    str(t3_t, Address(t2_owner_addr));
     b(slow_path);
 
     bind(release);
     // Set owner to null.
     // Release to satisfy the JMM
     stlr(zr, t2_owner_addr);
   }
 
   bind(unlocked);
-  decrement(Address(rthread, JavaThread::held_monitor_count_offset()));
 
 #ifdef ASSERT
   // Check that unlocked label is reached with Flags == EQ.
   Label flag_correct;
   br(Assembler::EQ, 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
@@ -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,98 @@
 
 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 void ThawBase::fix_native_wrapper_return_pc_pd(frame& top) {
+  // Nothing to do since the last pc saved before making the call to
+  // JVM_MonitorWait() was already set to the correct resume pc. Just
+  // do some sanity check.
+#ifdef ASSERT
+  Method* method = top.is_interpreted_frame() ? top.interpreter_frame_method() : CodeCache::find_blob(top.pc())->as_nmethod()->method();
+  assert(method->is_object_wait0(), "");
+#endif
+}
+
+inline intptr_t* ThawBase::push_resume_adapter(frame& top) {
+  intptr_t* sp = top.sp();
+  CodeBlob* cb = top.cb();
+
+#ifdef ASSERT
+  RegisterMap map(JavaThread::current(),
+                  RegisterMap::UpdateMap::skip,
+                  RegisterMap::ProcessFrames::skip,
+                  RegisterMap::WalkContinuation::skip);
+  frame caller = top.sender(&map);
+  intptr_t link_addr = (intptr_t)ContinuationHelper::Frame::callee_link_address(caller);
+  assert(sp[-2] == link_addr, "wrong link address: " INTPTR_FORMAT " != " INTPTR_FORMAT, sp[-2], link_addr);
+#endif
+
+  bool interpreted = top.is_interpreted_frame();
+  if (!interpreted && 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. We just copy lr/rfp again. These 2 top words
+    // will be the ones popped in generate_cont_preempt_rerun_compiler_adapter(). The other 2 words
+    // will just be discarded once back in the runtime stub (add sp, sp, #0x10).
+    sp -= 2;
+    sp[-2] = sp[0];
+    sp[-1] = sp[1];
+  }
+
+  intptr_t* fp = sp - frame::sender_sp_offset;
+  address pc = interpreted ? Interpreter::cont_resume_interpreter_adapter()
+                           : StubRoutines::cont_resume_compiler_adapter();
+
+  sp -= frame::metadata_words;
+  *(address*)(sp - frame::sender_sp_ret_address_offset()) = pc;
+  *(intptr_t**)(sp - frame::sender_sp_offset) = fp;
+
+  log_develop_trace(continuations, preempt)("push_resume_%s_adapter() initial sp: " INTPTR_FORMAT " final sp: " INTPTR_FORMAT " fp: " INTPTR_FORMAT,
+    interpreted ? "interpreter" : "compiler", p2i(sp + frame::metadata_words), p2i(sp), p2i(fp));
+  return sp;
+}
+
+inline intptr_t* ThawBase::push_resume_monitor_operation(stackChunkOop chunk) {
+  frame enterSpecial = new_entry_frame();
+  intptr_t* sp = enterSpecial.sp();
+
+  // First push the return barrier frame
+  sp -= frame::metadata_words;
+  sp[1] = (intptr_t)StubRoutines::cont_returnBarrier();
+  sp[0] = (intptr_t)enterSpecial.fp();
+
+  // Now push the ObjectWaiter*
+  sp -= frame::metadata_words;
+  sp[1] = (intptr_t)chunk->object_waiter(); // alignment
+  sp[0] = (intptr_t)chunk->object_waiter();
+
+  // Finally arrange to return to the resume_monitor_operation stub
+  sp[-1] = (intptr_t)StubRoutines::cont_resume_monitor_operation();
+  sp[-2] = (intptr_t)enterSpecial.fp();
+  log_develop_trace(continuations, preempt)("push_resume_monitor_operation 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
@@ -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.cpp b/src/hotspot/cpu/aarch64/frame_aarch64.cpp
--- a/src/hotspot/cpu/aarch64/frame_aarch64.cpp
+++ b/src/hotspot/cpu/aarch64/frame_aarch64.cpp
@@ -701,11 +701,11 @@
   // unwind them; for everything else we assume that the native frame
   // pointer chain is intact.
   frame this_frame((intptr_t*)sp, (intptr_t*)fp, (address)pc);
   if (this_frame.is_compiled_frame() ||
       this_frame.is_interpreted_frame()) {
-    frame sender = this_frame.sender(reg_map);
+    frame sender = this_frame.sender_raw(reg_map);
     nextfp = (uintptr_t)sender.fp();
     nextpc = (uintptr_t)sender.pc();
     nextsp = (uintptr_t)sender.unextended_sp();
   } else {
     nextfp = p[frame::link_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,13 +664,15 @@
 //      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) {
+    push_cont_fastpath();
     call_VM(noreg,
             CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
             lock_reg);
+    pop_cont_fastpath();
   } else {
     Label count, done;
 
     const Register swap_reg = r0;
     const Register tmp = c_rarg2;
@@ -695,11 +697,11 @@
       br(Assembler::NE, slow_case);
     }
 
     if (LockingMode == LM_LIGHTWEIGHT) {
       lightweight_lock(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,29 +747,32 @@
       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);
+      b(done);
     }
+
+    bind(count);
+    inc_held_monitor_count();
+    b(done);
+
     bind(slow_case);
 
     // Call the runtime routine for slow case
+    push_cont_fastpath();
     if (LockingMode == LM_LIGHTWEIGHT) {
       call_VM(noreg,
               CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter_obj),
               obj_reg);
     } else {
       call_VM(noreg,
               CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
               lock_reg);
     }
-    b(done);
-
-    bind(count);
-    increment(Address(rthread, JavaThread::held_monitor_count_offset()));
-
+    pop_cont_fastpath();
     bind(done);
   }
 }
 
 
@@ -808,34 +813,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);
+      cbz(header_reg, done);
 
       // 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);
     }
-    // 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()));
+    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);
     bind(done);
     restore_bcp();
   }
 }
 
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"
@@ -1949,19 +1950,29 @@
 }
 
 void MacroAssembler::call_VM_leaf_base(address entry_point,
                                        int number_of_arguments,
                                        Label *retaddr) {
-  Label E, L;
+  Label not_preempted;
 
   stp(rscratch1, rmethod, Address(pre(sp, -2 * wordSize)));
+  str(zr, Address(rthread, JavaThread::preempt_alternate_return_offset()));
 
   mov(rscratch1, entry_point);
   blr(rscratch1);
   if (retaddr)
     bind(*retaddr);
 
+  if (entry_point == CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter) ||
+      entry_point == CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter_obj)) {
+    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(not_preempted);
   ldp(rscratch1, rmethod, Address(post(sp, 2 * wordSize)));
 }
 
 void MacroAssembler::call_VM_leaf(address entry_point, int number_of_arguments) {
   call_VM_leaf_base(entry_point, number_of_arguments);
@@ -5322,10 +5333,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
@@ -1087,10 +1090,11 @@
   // Restore cpu control state after JNI call
   void restore_cpu_control_state_after_jni(Register tmp1, Register tmp2);
 
   // prints msg, dumps registers and stops execution
   void stop(const char* msg);
+  void trace(const char* msg);
 
   static void debug64(char* msg, int64_t pc, int64_t regs[]);
 
   void untested()                                { stop("untested"); }
 
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
@@ -1177,10 +1177,11 @@
 
   __ 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();
 
@@ -1279,10 +1280,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);
@@ -1747,13 +1752,12 @@
   }
 
   // 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
-
-  Label native_return;
-  __ set_last_Java_frame(sp, noreg, native_return, rscratch1);
+  Label resume_pc;
+  __ set_last_Java_frame(sp, noreg, resume_pc, rscratch1);
 
   Label dtrace_method_entry, dtrace_method_entry_done;
   if (DTraceMethodProbes) {
     __ b(dtrace_method_entry);
     __ bind(dtrace_method_entry_done);
@@ -1827,16 +1831,18 @@
       __ 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);
+      __ b(lock_done);
     } else {
       assert(LockingMode == LM_LIGHTWEIGHT, "must be");
       __ lightweight_lock(obj_reg, swap_reg, tmp, lock_tmp, slow_path_lock);
+      __ b(lock_done);
     }
     __ bind(count);
-    __ increment(Address(rthread, JavaThread::held_monitor_count_offset()));
+    __ inc_held_monitor_count();
 
     // Slow path will re-enter here
     __ bind(lock_done);
   }
 
@@ -1851,15 +1857,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) {
@@ -1922,10 +1923,24 @@
   __ mov(rscratch1, _thread_in_Java);
   __ lea(rscratch2, Address(rthread, JavaThread::thread_state_offset()));
   __ stlrw(rscratch1, rscratch2);
   __ bind(after_transition);
 
+  // Check preemption for Object.wait()
+  if (method->is_object_wait0()) {
+    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(not_preempted);
+  }
+  __ bind(resume_pc);
+
+  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);
@@ -1945,11 +1960,10 @@
 
     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()));
       __ b(done);
     }
 
     __ bind(not_recursive);
 
@@ -1968,15 +1982,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) {
@@ -2040,11 +2053,13 @@
     __ 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
+    __ 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())));
@@ -2579,10 +2594,14 @@
 
 uint SharedRuntime::out_preserve_stack_slots() {
   return 0;
 }
 
+VMReg SharedRuntime::thread_register() {
+  return rthread->as_VMReg();
+}
+
 #ifdef COMPILER2
 //------------------------------generate_uncommon_trap_blob--------------------
 void SharedRuntime::generate_uncommon_trap_blob() {
   // Allocate space for the code
   ResourceMark rm;
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
@@ -7061,11 +7061,13 @@
     }
 
     // If we want, we can templatize thaw by kind, and have three different entries
     __ movw(c_rarg1, (uint32_t)kind);
 
+    __ set_last_Java_frame(sp, rfp, rscratch1, rscratch2);
     __ call_VM_leaf(Continuation::thaw_entry(), rthread, c_rarg1);
+    __ reset_last_Java_frame(true);
     __ mov(rscratch2, r0); // r0 is the sp of the yielding frame
 
     if (return_barrier) {
       // restore return value (no safepoint in the call to thaw, so even an oop return value should be OK)
       __ ldp(rscratch1, r0, Address(__ post(sp, 2 * wordSize)));
@@ -7138,10 +7140,106 @@
     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);
+
+    // reset the flag
+    __ strb(zr, Address(rthread, JavaThread::preempting_offset()));
+
+    // Set sp to enterSpecial frame and then remove it from the stack
+    __ 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()
+    SharedRuntime::continuation_enter_cleanup(_masm);
+    __ leave();
+    __ ret(lr);
+
+    __ bind(preemption_cancelled);
+    __ strb(zr, Address(rthread, JavaThread::preemption_cancelled_offset()));
+    __ lea(rfp, Address(sp, checked_cast<int32_t>(ContinuationEntry::size())));
+    __ lea(rscratch1, ExternalAddress((address)&ContinuationEntry::_thaw_call_pc));
+    __ ldr(rscratch1, Address(rscratch1));
+    __ br(rscratch1);
+
+    return start;
+  }
+
+  address generate_cont_resume_compiler_adapter() {
+    if (!Continuations::enabled()) return nullptr;
+    StubCodeMark mark(this, "StubRoutines", "Continuation resume compiler adapter");
+    address start = __ pc();
+
+    // The safepoint blob handler expects that r20, being a callee saved register, will be preserved
+    // during the VM call. It is used to check if the return pc back to Java was modified in the runtime.
+    // If it wasn't, the return pc is modified so on return the poll instruction is skipped. Saving this
+    // additional value of r20 during freeze will complicate too much the code, so we just zero it here
+    // so that the comparison fails and the skip is not attempted in case the pc was indeed changed.
+    __ movptr(r20, NULL_WORD);
+
+    __ leave();
+    __ ret(lr);
+
+    return start;
+  }
+
+  address generate_cont_resume_monitor_operation() {
+    if (!Continuations::enabled()) return nullptr;
+    StubCodeMark mark(this, "StubRoutines","Continuation resume monitor operation");
+    address start = __ pc();
+
+    const Register waiter_reg = c_rarg1;
+    __ ldr(waiter_reg, __ post(sp, 2 * wordSize));
+
+#ifdef ASSERT
+    { Label L;
+      __ cbnz(waiter_reg, L);
+      __ stop("ObjectMonitor to use is null");
+      __ bind(L);
+    }
+#endif // ASSERT
+
+    __ set_last_Java_frame(sp, rfp, lr, rscratch1);
+    __ mov(c_rarg0, rthread);
+    __ rt_call(CAST_FROM_FN_PTR(address, SharedRuntime::resume_monitor_operation));
+    __ reset_last_Java_frame(true);
+
+    Label failAcquire;
+    __ ldrb(rscratch1, Address(rthread, JavaThread::preempting_offset()));
+    __ cbnz(rscratch1, failAcquire);
+    // We have the lock now, just return to caller (we will actually hit the
+    // return barrier to thaw more frames)
+
+    // ThawBase::push_resume_monitor_operation set things up so that
+    // SP now points to {fp, lr}.
+    __ ldp(rfp, lr, Address(__ post(sp, 2 * wordSize)));
+    __ ret(lr);
+
+    __ bind(failAcquire);
+    __ strb(/*false*/zr, Address(rthread, JavaThread::preempting_offset()));
+    // Set sp to enterSpecial frame
+    __ ldr(rscratch1, Address(rthread, JavaThread::cont_entry_offset()));
+    __ mov(sp, rscratch1);
+    // Remove enterSpecial frame from the stack and return to Continuation.run()
+    SharedRuntime::continuation_enter_cleanup(_masm);
+    __ leave();
+    __ ret(lr);
+
+    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
@@ -8406,10 +8504,13 @@
   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();
+    StubRoutines::_cont_resume_monitor_operation = generate_cont_resume_monitor_operation();
+    StubRoutines::_cont_resume_compiler_adapter = generate_cont_resume_compiler_adapter();
 
     JFR_ONLY(generate_jfr_stubs();)
   }
 
 #if INCLUDE_JFR
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
@@ -610,10 +610,45 @@
   __ 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 rfp first since we need it to restore rest of registers
+  __ leave();
+
+  // 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)));
+
+  // Prepare for adjustment on return to call_VM_leaf_base()
+  __ ldr(rmethod, Address(rfp, frame::interpreter_frame_method_offset * wordSize));
+  __ stp(rscratch1, rmethod, Address(__ pre(sp, -2 * 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
@@ -1317,10 +1352,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);
@@ -1355,12 +1392,12 @@
   __ 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.
-  Label native_return;
-  __ set_last_Java_frame(esp, rfp, native_return, rscratch1);
+  Label resume_pc;
+  __ set_last_Java_frame(esp, rfp, resume_pc, rscratch1);
 
   // change thread state
 #ifdef ASSERT
   {
     Label L;
@@ -1375,13 +1412,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);
@@ -1442,10 +1483,22 @@
   // change thread state
   __ mov(rscratch1, _thread_in_Java);
   __ lea(rscratch2, Address(rthread, JavaThread::thread_state_offset()));
   __ stlrw(rscratch1, rscratch2);
 
+  // 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(resume_pc);
+  // On resume we need to set up stack as expected
+  __ push(dtos);
+  __ push(ltos);
+  __ bind(not_preempted);
+
   // reset_last_Java_frame
   __ reset_last_Java_frame(true);
 
   if (CheckJNICalls) {
     // clear_pending_jni_exception_check
@@ -1460,10 +1513,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/aarch64/templateTable_aarch64.cpp b/src/hotspot/cpu/aarch64/templateTable_aarch64.cpp
--- a/src/hotspot/cpu/aarch64/templateTable_aarch64.cpp
+++ b/src/hotspot/cpu/aarch64/templateTable_aarch64.cpp
@@ -3958,16 +3958,26 @@
   // expression stack looks correct.
   __ increment(rbcp);
 
   // store object
   __ str(r0, Address(c_rarg1, BasicObjectLock::obj_offset()));
+  __ set_last_Java_frame(sp, rfp, rscratch1, rscratch2);
   __ lock_object(c_rarg1);
+  __ reset_last_Java_frame(true);
 
   // check to make sure this monitor doesn't cause stack overflow after locking
   __ save_bcp();  // in case of exception
   __ generate_stack_overflow_check(0);
 
+  // Check preemption
+  Label ok;
+  __ ldrb(rscratch1, Address(rthread, in_bytes(JavaThread::preempting_offset())));
+  __ cbz(rscratch1, ok);
+  __ lea(rscratch1, RuntimeAddress(StubRoutines::cont_preempt_stub()));
+  __ br(rscratch1);
+  __ bind(ok);
+
   // The bcp has already been incremented. Just need to dispatch to
   // next instruction.
   __ dispatch_next(vtos);
 }
 
@@ -4012,10 +4022,12 @@
     // if not at bottom then check this entry
     __ br(Assembler::NE, loop);
   }
 
   // error handling. Unlocking was not block-structured
+  __ adr(rscratch1, __ pc());
+  __ set_last_Java_frame(sp, rfp, rscratch1, rscratch2);
   __ call_VM(noreg, CAST_FROM_FN_PTR(address,
                    InterpreterRuntime::throw_illegal_monitor_state_exception));
   __ should_not_reach_here();
 
   // call run-time routine
diff a/src/hotspot/cpu/aarch64/vm_version_aarch64.hpp b/src/hotspot/cpu/aarch64/vm_version_aarch64.hpp
--- a/src/hotspot/cpu/aarch64/vm_version_aarch64.hpp
+++ b/src/hotspot/cpu/aarch64/vm_version_aarch64.hpp
@@ -166,10 +166,11 @@
   static int dcache_line_size() { return _dcache_line_size; }
   static int get_initial_sve_vector_length()  { return _initial_sve_vector_length; };
 
   // Aarch64 supports fast class initialization checks
   static bool supports_fast_class_init_checks() { return true; }
+  static bool supports_cont_preemption() { return true; }
   constexpr static bool supports_stack_watermark_barrier() { return true; }
   constexpr static bool supports_recursive_lightweight_locking() { return true; }
 
   constexpr static bool supports_secondary_supers_table() { return true; }
 
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::push_resume_adapter(frame& top, bool is_interpreted_frame) {
+  Unimplemented();
+  return nullptr;
+}
+
+inline intptr_t* ThawBase::push_resume_monitor_operation(stackChunkOop chunk) {
+  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
@@ -1355,10 +1355,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;
   CodeBuffer buffer("deopt_blob", 1024, 1024);
   int frame_size_in_words;
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
@@ -456,10 +456,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/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
@@ -255,10 +255,14 @@
     sp_offset += BytesPerWord;
   }
   frame_size_in_bytes = align_up(sp_offset, frame::alignment_in_bytes);
 }
 
+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);
 
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,14 @@
 
 void FreezeBase::adjust_interpreted_frame_unextended_sp(frame& f) {
   // nothing to do
 }
 
+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) {
   intptr_t* vfp = f.fp();
   intptr_t* hfp = hf.fp();
   assert(f.fp() > (intptr_t*)f.interpreter_frame_esp(), "");
 
@@ -541,16 +545,30 @@
   assert(f.at_absolute(ijava_idx(esp)) <= f.at_absolute(ijava_idx(monitors)), "");
 
   // Keep top_frame_sp relativized.
 }
 
+inline intptr_t* ThawBase::push_resume_adapter(frame& top, bool is_interpreted_frame) {
+  Unimplemented();
+  return nullptr;
+}
+
+inline intptr_t* ThawBase::push_resume_monitor_operation(stackChunkOop chunk) {
+  Unimplemented();
+  return nullptr;
+}
+
 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) {
+  Unimplemented();
+}
+
 //
 // 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
@@ -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) {
   return size & 1;
 }
 
 inline intptr_t* ContinuationHelper::frame_align_pointer(intptr_t* p) {
@@ -57,15 +61,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/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
@@ -2626,24 +2626,26 @@
   // 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);
@@ -2706,11 +2708,13 @@
   addi(current_header, current_header, -(int)markWord::monitor_value); // monitor
   ld(temp,             in_bytes(ObjectMonitor::owner_offset()), current_header);
 
   // In case of LM_LIGHTWEIGHT, we may reach here with (temp & ObjectMonitor::ANONYMOUS_OWNER) != 0.
   // This is handled like owner thread mismatches: We take the slow path.
-  cmpd(flag, temp, R16_thread);
+  Register thread_id = displaced_header;
+  ld(thread_id, in_bytes(JavaThread::lock_id_offset()), R16_thread);
+  cmpd(flag, temp, thread_id);
   bne(flag, failure);
 
   ld(displaced_header, in_bytes(ObjectMonitor::recursions_offset()), current_header);
 
   addic_(displaced_header, displaced_header, -1);
@@ -2810,22 +2814,24 @@
     const Register owner_addr = tmp2;
 
     // Compute owner address.
     addi(owner_addr, tagged_monitor, in_bytes(ObjectMonitor::owner_offset()) - monitor_tag);
 
-    // CAS owner (null => current thread).
+    // CAS owner (null => current thread id).
+    Register thread_id = tmp1;
+    ld(thread_id, in_bytes(JavaThread::lock_id_offset()), R16_thread);
     cmpxchgd(/*flag=*/flag,
             /*current_value=*/t,
             /*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(flag, locked);
 
     // Check if recursive.
-    cmpd(flag, t, R16_thread);
+    cmpd(flag, t, thread_id);
     bne(flag, slow_path);
 
     // Recursive.
     ld(tmp1, in_bytes(ObjectMonitor::recursions_offset() - ObjectMonitor::owner_offset()), owner_addr);
     addi(tmp1, tmp1, 1);
@@ -2976,11 +2982,13 @@
     beq(flag, release_);
 
     // The owner may be anonymous and we removed the last obj entry in
     // the lock-stack. This loses the information about the owner.
     // Write the thread to the owner field so the runtime knows the owner.
-    std(R16_thread, in_bytes(ObjectMonitor::owner_offset()), monitor);
+    Register thread_id = tmp2;
+    ld(thread_id, in_bytes(JavaThread::lock_id_offset()), R16_thread);
+    std(thread_id, in_bytes(ObjectMonitor::owner_offset()), monitor);
     b(slow_path);
 
     bind(release_);
     // Set owner to null.
     release();
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
@@ -2735,10 +2735,15 @@
 #else
   return 0;
 #endif
 }
 
+VMReg SharedRuntime::thread_register() {
+  Unimplemented();
+  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/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,15 @@
   __ 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.
 //
 // Note: checking for negative value instead of overflow
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
@@ -185,11 +185,11 @@
 void StubAssembler::prologue(const char* name, bool must_gc_arguments) {
   set_info(name, must_gc_arguments);
   enter();
 }
 
-void StubAssembler::epilogue() {
+void StubAssembler::epilogue(bool use_pop) {
   leave();
   ret();
 }
 
 #define __ _sasm->
@@ -354,10 +354,15 @@
     cpu_reg_save_offsets[i] = sp_offset;
     sp_offset += step;
   }
 }
 
+uint Runtime1::runtime_blob_current_thread_offset(frame f) {
+  Unimplemented();
+  return 0;
+}
+
 // 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
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
@@ -73,21 +73,22 @@
 
 int C2HandleAnonOMOwnerStub::max_size() const {
   // Max size of stub has been determined by testing with 0 without using RISC-V compressed
   // instruction-set extension, in which case C2CodeStubList::emit() will throw an assertion
   // and report the actual size that is needed.
-  return 20 DEBUG_ONLY(+8);
+  return 24 DEBUG_ONLY(+8);
 }
 
 void C2HandleAnonOMOwnerStub::emit(C2_MacroAssembler& masm) {
   __ bind(entry());
   Register mon = monitor();
   Register t = tmp();
   assert(t != noreg, "need tmp register");
 
   // Fix owner to be the current thread.
-  __ sd(xthread, Address(mon, ObjectMonitor::owner_offset()));
+  __ mv(t, Address(xthread, JavaThread::lock_id_offset()));
+  __ sd(t, Address(mon, ObjectMonitor::owner_offset()));
 
   // Pop owner object from lock-stack.
   __ lwu(t, Address(xthread, JavaThread::lock_stack_top_offset()));
   __ subw(t, t, oopSize);
 #ifdef ASSERT
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
@@ -116,13 +116,15 @@
   // 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.
   //
-  // Try to CAS m->owner from null to current thread.
+  // Try to CAS m->owner from null to current thread id.
+  Register tid = flag;
+  mv(tid, Address(xthread, JavaThread::lock_id_offset()));
   add(tmp, disp_hdr, (in_bytes(ObjectMonitor::owner_offset()) - markWord::monitor_value));
-  cmpxchg(/*memory address*/tmp, /*expected value*/zr, /*new value*/xthread, Assembler::int64,
+  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,11 +132,11 @@
   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);
@@ -165,11 +167,11 @@
   Register oop = objectReg;
   Register box = boxReg;
   Register disp_hdr = tmp1Reg;
   Register tmp = tmp2Reg;
   Label object_has_monitor;
-  // Finish fast lock successfully. MUST branch to with flag == 0
+  // Finish fast lock successfully. MUST branch to vwith flag == 0
   Label unlocked;
   // Finish fast lock unsuccessfully. slow_path MUST branch to with flag != 0
   Label slow_path;
 
   assert(LockingMode != LM_LIGHTWEIGHT, "lightweight locking should use fast_unlock_lightweight");
@@ -330,17 +332,19 @@
     const Register tmp3_owner = tmp3;
 
     // Compute owner address.
     la(tmp2_owner_addr, Address(tmp1_tagged_monitor, (in_bytes(ObjectMonitor::owner_offset()) - monitor_tag)));
 
-    // CAS owner (null => current thread).
-    cmpxchg(/*addr*/ tmp2_owner_addr, /*expected*/ zr, /*new*/ xthread, Assembler::int64,
+    // CAS owner (null => current thread id).
+    Register tid = flag;
+    mv(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, locked);
 
     // Check if recursive.
-    bne(tmp3_owner, xthread, slow_path);
+    bne(tmp3_owner, tid, slow_path);
 
     // Recursive.
     increment(Address(tmp1_tagged_monitor, in_bytes(ObjectMonitor::recursions_offset()) - monitor_tag), 1, tmp2, tmp3);
   }
 
@@ -487,11 +491,13 @@
     beqz(t0, release);
 
     // The owner may be anonymous and we removed the last obj entry in
     // the lock-stack. This loses the information about the owner.
     // Write the thread to the owner field so the runtime knows the owner.
-    sd(xthread, Address(tmp2_owner_addr));
+    Register tid = flag;
+    mv(tid, Address(xthread, JavaThread::lock_id_offset()));
+    sd(tid, Address(tmp2_owner_addr));
     j(slow_path);
 
     bind(release);
     // Set owner to null.
     membar(MacroAssembler::LoadStore | MacroAssembler::StoreStore);
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,14 @@
   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) {
+  Unimplemented();
+}
+
 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), "");
@@ -277,10 +281,24 @@
 
 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) {
+  Unimplemented();
+}
+
+inline intptr_t* ThawBase::push_resume_adapter(frame& top, bool is_interpreted_frame) {
+  Unimplemented();
+  return nullptr;
+}
+
+inline intptr_t* ThawBase::push_resume_monitor_operation(stackChunkOop chunk) {
+  Unimplemented();
+  return nullptr;
+}
+
 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/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,14 @@
   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) {
+  Unimplemented();
+}
+
 inline int ContinuationHelper::frame_align_words(int size) {
 #ifdef _LP64
   return size & 1;
 #else
   return 0;
@@ -70,16 +74,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/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
@@ -2439,10 +2439,15 @@
 
 uint SharedRuntime::out_preserve_stack_slots() {
   return 0;
 }
 
+VMReg SharedRuntime::thread_register() {
+  Unimplemented();
+  return nullptr;
+}
+
 #ifdef COMPILER2
 //------------------------------generate_uncommon_trap_blob--------------------
 void SharedRuntime::generate_uncommon_trap_blob() {
   // Allocate space for the code
   ResourceMark rm;
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
@@ -537,10 +537,15 @@
   __ membar(MacroAssembler::AnyAny);
   __ 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/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::push_resume_adapter(frame& top, bool is_interpreted_frame) {
+  Unimplemented();
+  return nullptr;
+}
+
+inline intptr_t* ThawBase::push_resume_monitor_operation(stackChunkOop chunk) {
+  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
@@ -3267,13 +3267,14 @@
   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.
+  // 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_R1_scratch, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner), monitor_tagged);
   z_csg(zero, Z_thread, 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);
 
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
@@ -2384,10 +2384,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/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
@@ -46,10 +46,11 @@
   const int aligned_mask = BytesPerWord -1;
   const int hdr_offset = oopDesc::mark_offset_in_bytes();
   assert(hdr == rax, "hdr must be rax, for the cmpxchg instruction");
   assert_different_registers(hdr, obj, disp_hdr, tmp);
   int null_check_offset = -1;
+  Label count_locking, done;
 
   verify_oop(obj);
 
   // save object being locked into the BasicObjectLock
   movptr(Address(disp_hdr, BasicObjectLock::obj_offset()), obj);
@@ -70,11 +71,10 @@
     const Register thread = disp_hdr;
     get_thread(thread);
 #endif
     lightweight_lock(obj, hdr, thread, tmp, slow_case);
   } else  if (LockingMode == LM_LEGACY) {
-    Label done;
     // Load object header
     movptr(hdr, Address(obj, hdr_offset));
     // and mark it as unlocked
     orptr(hdr, markWord::unlocked_value);
     // save unlocked object header into the displaced header location on the stack
@@ -83,11 +83,11 @@
     // displaced header address in the object header - if it is not the same, get the
     // object header instead
     MacroAssembler::lock(); // must be immediately before cmpxchg!
     cmpxchgptr(disp_hdr, Address(obj, hdr_offset));
     // if the object header was the same, we're done
-    jcc(Assembler::equal, done);
+    jcc(Assembler::equal, count_locking);
     // 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) rsp <= hdr
@@ -105,14 +105,16 @@
     // location (null in the displaced hdr location indicates recursive locking)
     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);
   }
+  jmp(done);
 
+  bind(count_locking);
   inc_held_monitor_count();
+  bind(done);
 
   return null_check_offset;
 }
 
 void C1_MacroAssembler::unlock_object(Register hdr, Register obj, Register disp_hdr, Label& slow_case) {
@@ -151,14 +153,14 @@
     MacroAssembler::lock(); // must be immediately before cmpxchg!
     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);
+    dec_held_monitor_count();
     // done
   }
   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,34 @@
 // 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) {
+  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 +239,11 @@
   __ load_parameter(offset_in_words, reg);
 }
 
 
 StubFrame::~StubFrame() {
-  __ epilogue();
+  __ epilogue(_use_pop_on_epilog);
 }
 
 #undef __
 
 
@@ -630,10 +632,13 @@
 
 void Runtime1::initialize_pd() {
   // nothing to do
 }
 
+uint Runtime1::runtime_blob_current_thread_offset(frame f) {
+  return r15_off / 2;
+}
 
 // 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) {
@@ -1307,11 +1312,11 @@
     case monitorenter_nofpu_id:
       save_fpu_registers = false;
       // fall through
     case 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_CodeStubs_x86.cpp b/src/hotspot/cpu/x86/c2_CodeStubs_x86.cpp
--- a/src/hotspot/cpu/x86/c2_CodeStubs_x86.cpp
+++ b/src/hotspot/cpu/x86/c2_CodeStubs_x86.cpp
@@ -71,36 +71,63 @@
   __ bind(entry());
   __ call(RuntimeAddress(StubRoutines::method_entry_barrier()));
   __ jmp(continuation(), false /* maybe_short */);
 }
 
+#ifdef _LP64
+int C2HandleAnonOMOwnerStub::max_size() const {
+  // Max size of stub has been determined by testing with 0, in which case
+  // C2CodeStubList::emit() will throw an assertion and report the actual size that
+  // is needed.
+  return DEBUG_ONLY(40) NOT_DEBUG(25);
+}
+
+void C2HandleAnonOMOwnerStub::emit(C2_MacroAssembler& masm) {
+  __ bind(entry());
+  Register mon = monitor();
+  Register t = tmp();
+  __ movptr(t, Address(r15_thread, JavaThread::lock_id_offset()));
+  __ movptr(Address(mon, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner)), t);
+  if (LockingMode == LM_LIGHTWEIGHT) {
+    __ subl(Address(r15_thread, JavaThread::lock_stack_top_offset()), oopSize);
+#ifdef ASSERT
+    __ movl(t, Address(r15_thread, JavaThread::lock_stack_top_offset()));
+    __ movptr(Address(r15_thread, t), 0);
+#endif
+  } else {
+    __ movptr(Address(mon, OM_OFFSET_NO_MONITOR_VALUE_TAG(stack_locker)), NULL_WORD);
+    __ decrementq(Address(r15_thread, JavaThread::held_monitor_count_offset()));
+  }
+  __ jmp(continuation());
+}
+#endif
+
 int C2FastUnlockLightweightStub::max_size() const {
   return 128;
 }
 
 void C2FastUnlockLightweightStub::emit(C2_MacroAssembler& masm) {
-  assert(_t == rax, "must be");
+  assert(_t1 == rax, "must be");
 
-  Label restore_held_monitor_count_and_slow_path;
+  Label slow_path;
 
   { // Restore lock-stack and handle the unlock in runtime.
 
     __ bind(_push_and_slow_path);
 #ifdef ASSERT
     // The obj was only cleared in debug.
-    __ movl(_t, Address(_thread, JavaThread::lock_stack_top_offset()));
-    __ movptr(Address(_thread, _t), _obj);
+    __ movl(_t1, Address(_thread, JavaThread::lock_stack_top_offset()));
+    __ movptr(Address(_thread, _t1), _obj);
 #endif
     __ addl(Address(_thread, JavaThread::lock_stack_top_offset()), oopSize);
   }
 
-  { // Restore held monitor count and slow path.
+  { // Handle the unlock in runtime
 
-    __ bind(restore_held_monitor_count_and_slow_path);
-    // Restore held monitor count.
-    __ increment(Address(_thread, JavaThread::held_monitor_count_offset()));
-    // increment will always result in ZF = 0 (no overflows).
+    __ bind(slow_path);
+    // set ZF=0 to indicate failure
+    __ orl(_t1, 1);
     __ jmp(slow_path_continuation());
   }
 
   { // Handle monitor medium path.
 
@@ -108,15 +135,15 @@
 
     Label fix_zf_and_unlocked;
     const Register monitor = _mark;
 
 #ifndef _LP64
-    __ jmpb(restore_held_monitor_count_and_slow_path);
+    __ jmpb(slow_path);
 #else // _LP64
     // successor null check.
     __ cmpptr(Address(monitor, OM_OFFSET_NO_MONITOR_VALUE_TAG(succ)), NULL_WORD);
-    __ jccb(Assembler::equal, restore_held_monitor_count_and_slow_path);
+    __ jccb(Assembler::equal, slow_path);
 
     // Release lock.
     __ movptr(Address(monitor, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner)), NULL_WORD);
 
     // Fence.
@@ -131,12 +158,13 @@
     // Try to relock, if it fails the monitor has been handed over
     // TODO: Caveat, this may fail due to deflation, which does
     //       not handle the monitor handoff. Currently only works
     //       due to the responsible thread.
     __ xorptr(rax, rax);
-    __ lock(); __ cmpxchgptr(_thread, Address(monitor, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner)));
-    __ jccb  (Assembler::equal, restore_held_monitor_count_and_slow_path);
+    __ movptr(_t2, Address(_thread, JavaThread::lock_id_offset()));
+    __ lock(); __ cmpxchgptr(_t2, Address(monitor, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner)));
+    __ jccb  (Assembler::equal, slow_path);
 #endif
 
     __ bind(fix_zf_and_unlocked);
     __ xorl(rax, rax);
     __ jmp(unlocked_continuation());
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
@@ -271,11 +271,11 @@
   //       = sp-proximity test hits
   //       = sp-proximity test generates false-negative
   //    -- by other
   //
 
-  Label IsInflated, DONE_LABEL, NO_COUNT, COUNT;
+  Label IsInflated, DONE_LABEL, COUNT;
 
   if (DiagnoseSyncOnValueBasedClasses != 0) {
     load_klass(tmpReg, objReg, scrReg);
     movl(tmpReg, Address(tmpReg, Klass::access_flags_offset()));
     testl(tmpReg, JVM_ACC_IS_VALUE_BASED_CLASS);
@@ -304,10 +304,11 @@
     subptr(tmpReg, rsp);
     // Next instruction set ZFlag == 1 (Success) if difference is less then one page.
     andptr(tmpReg, (int32_t) (NOT_LP64(0xFFFFF003) LP64_ONLY(7 - (int)os::vm_page_size())) );
     movptr(Address(boxReg, 0), tmpReg);
   }
+  // After recursive stack locking attempt case
   jmp(DONE_LABEL);
 
   bind(IsInflated);
   // The object is inflated. tmpReg contains pointer to ObjectMonitor* + markWord::monitor_value
 
@@ -328,66 +329,66 @@
   // 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.
+  // to CAS the register containing thread id 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
+  // we later store thread id 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);
+  jccb  (Assembler::notZero, DONE_LABEL);
   // update _owner from BasicLock to thread
   get_thread (scrReg);                    // beware: clobbers ICCs
+  movptr(scrReg, Address(scrReg, JavaThread::lock_id_offset()));
   movptr(Address(boxReg, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner)), scrReg);
   xorptr(boxReg, boxReg);                 // set icc.ZFlag = 1 to indicate success
+  jmp(DONE_LABEL);
 
   // 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
+  // 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, DONE_LABEL);    // CAS above succeeded; propagate ZF = 1 (success)
 
-  cmpptr(thread, rax);                // Check if we are already the owner (recursive lock)
-  jccb(Assembler::notEqual, NO_COUNT);    // If not recursive, ZF = 0 at this point (fail)
+  cmpptr(boxReg, rax);                // Check if we are already the owner (recursive lock)
+  jccb(Assembler::notEqual, DONE_LABEL);    // 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
+  jmp(DONE_LABEL);
 #endif // _LP64
-  bind(DONE_LABEL);
-
-  // 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()));
-
   xorl(tmpReg, tmpReg); // Set ZF == 1
 
-  bind(NO_COUNT);
+  bind(DONE_LABEL);
 
-  // At NO_COUNT the icc ZFlag is set as follows ...
+  // At DONE_LABEL the icc ZFlag is set as follows ...
   // fast_unlock uses the same protocol.
   // ZFlag == 1 -> Success
   // ZFlag == 0 -> Failure - force control through the slow path
 }
 
@@ -421,28 +422,43 @@
 // could reasonably *avoid* checking owner in fast_unlock().
 // In the interest of performance we elide m->Owner==Self check in unlock.
 // A perfectly viable alternative is to elide the owner check except when
 // Xcheck:jni is enabled.
 
-void C2_MacroAssembler::fast_unlock(Register objReg, Register boxReg, Register tmpReg) {
+void C2_MacroAssembler::fast_unlock(Register objReg, Register boxReg, Register tmpReg, Register scrReg) {
   assert(LockingMode != LM_LIGHTWEIGHT, "lightweight locking should use fast_unlock_lightweight");
   assert(boxReg == rax, "");
   assert_different_registers(objReg, boxReg, tmpReg);
 
-  Label DONE_LABEL, Stacked, COUNT, NO_COUNT;
+  Label DONE_LABEL, Stacked, COUNT;
 
   if (LockingMode == LM_LEGACY) {
     cmpptr(Address(boxReg, 0), NULL_WORD);                            // Examine the displaced header
-    jcc   (Assembler::zero, COUNT);                                   // 0 indicates recursive stack-lock
+    jcc   (Assembler::zero, DONE_LABEL);                              // 0 indicates recursive stack-lock
   }
   movptr(tmpReg, Address(objReg, oopDesc::mark_offset_in_bytes()));   // Examine the object's markword
   if (LockingMode != LM_MONITOR) {
     testptr(tmpReg, markWord::monitor_value);                         // Inflated?
     jcc(Assembler::zero, Stacked);
   }
 
   // It's inflated.
+  // If the owner is ANONYMOUS, we need to fix it -  in an outline stub.
+  cmpptr(Address(tmpReg, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner)), (int32_t) ObjectMonitor::ANONYMOUS_OWNER);
+#ifdef _LP64
+  if (!Compile::current()->output()->in_scratch_emit_size()) {
+    C2HandleAnonOMOwnerStub* stub = new (Compile::current()->comp_arena()) C2HandleAnonOMOwnerStub(tmpReg, boxReg);
+    Compile::current()->output()->add_stub(stub);
+    jcc(Assembler::equal, stub->entry());
+    bind(stub->continuation());
+  } else
+#endif
+  {
+    // We can't easily implement this optimization on 32 bit because we don't have a thread register.
+    // Call the slow-path instead.
+    jcc(Assembler::notEqual, DONE_LABEL);
+  }
 
   // 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
@@ -480,13 +496,15 @@
   cmpptr(Address(tmpReg, OM_OFFSET_NO_MONITOR_VALUE_TAG(recursions)), 0);
   jccb(Assembler::equal, LNotRecursive);
 
   // Recursive inflated unlock
   decq(Address(tmpReg, OM_OFFSET_NO_MONITOR_VALUE_TAG(recursions)));
-  jmpb(LSuccess);
+  xorl(tmpReg, tmpReg); // Set ZF == 1
+  jmp(DONE_LABEL);
 
   bind(LNotRecursive);
+
   movptr(boxReg, Address(tmpReg, OM_OFFSET_NO_MONITOR_VALUE_TAG(cxq)));
   orptr(boxReg, Address(tmpReg, OM_OFFSET_NO_MONITOR_VALUE_TAG(EntryList)));
   jccb  (Assembler::notZero, CheckSucc);
   // Without cast to int32_t this style of movptr will destroy r10 which is typically obj.
   movptr(Address(tmpReg, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner)), NULL_WORD);
@@ -530,12 +548,13 @@
   // coherence traffic on the lock *and* artificially extended the critical section
   // length while by virtue of passing control into the slow path.
 
   // box is really RAX -- the following CMPXCHG depends on that binding
   // cmpxchg R,[M] is equivalent to rax = CAS(M,rax,R)
+  movptr(scrReg, Address(r15_thread, JavaThread::lock_id_offset()));
   lock();
-  cmpxchgptr(r15_thread, Address(tmpReg, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner)));
+  cmpxchgptr(scrReg, Address(tmpReg, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner)));
   // There's no successor so we tried to regrab the lock.
   // If that didn't work, then another thread grabbed the
   // lock so we're done (and exit was a success).
   jccb  (Assembler::notEqual, LSuccess);
   // Intentional fall-through into slow path
@@ -552,42 +571,35 @@
   if (LockingMode == LM_LEGACY) {
     bind  (Stacked);
     movptr(tmpReg, Address (boxReg, 0));      // re-fetch
     lock();
     cmpxchgptr(tmpReg, Address(objReg, oopDesc::mark_offset_in_bytes())); // Uses RAX which is box
-    // Intentional fall-thru into DONE_LABEL
-  }
-
-  bind(DONE_LABEL);
-
-  // ZFlag == 1 count in fast path
-  // ZFlag == 0 count in slow path
-  jccb(Assembler::notZero, NO_COUNT);
-
-  bind(COUNT);
-  // Count monitors in fast path
+    jccb(Assembler::notZero, DONE_LABEL);
+    // Count monitors in fast path
 #ifndef _LP64
-  get_thread(tmpReg);
-  decrementl(Address(tmpReg, JavaThread::held_monitor_count_offset()));
+    get_thread(tmpReg);
+    decrementl(Address(tmpReg, JavaThread::held_monitor_count_offset()));
 #else // _LP64
-  decrementq(Address(r15_thread, JavaThread::held_monitor_count_offset()));
+    decrementq(Address(r15_thread, JavaThread::held_monitor_count_offset()));
 #endif
+    xorl(tmpReg, tmpReg); // Set ZF == 1
+  }
 
-  xorl(tmpReg, tmpReg); // Set ZF == 1
-
-  bind(NO_COUNT);
+  // ZFlag == 1 -> Success
+  // ZFlag == 0 -> Failure - force control through the slow path
+  bind(DONE_LABEL);
 }
 
 void C2_MacroAssembler::fast_lock_lightweight(Register obj, Register box, Register rax_reg,
                                               Register t, Register thread) {
   assert(LockingMode == LM_LIGHTWEIGHT, "must be");
   assert(rax_reg == rax, "Used for CAS");
   assert_different_registers(obj, box, rax_reg, t, thread);
 
   // Handle inflated monitor.
   Label inflated;
-  // Finish fast lock successfully. ZF value is irrelevant.
+  // Finish fast lock successfully.
   Label locked;
   // Finish fast lock unsuccessfully. MUST jump with ZF == 0
   Label slow_path;
 
   if (DiagnoseSyncOnValueBasedClasses != 0) {
@@ -632,36 +644,35 @@
 
     bind(push);
     // After successful lock, push object on lock-stack.
     movptr(Address(thread, top), obj);
     addl(Address(thread, JavaThread::lock_stack_top_offset()), oopSize);
+    xorl(rax_reg, rax_reg);
     jmpb(locked);
   }
 
   { // Handle inflated monitor.
     bind(inflated);
 
     const Register tagged_monitor = mark;
 
     // CAS owner (null => current thread).
     xorptr(rax_reg, rax_reg);
-    lock(); cmpxchgptr(thread, Address(tagged_monitor, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner)));
+    movptr(box, Address(thread, JavaThread::lock_id_offset()));
+    lock(); cmpxchgptr(box, Address(tagged_monitor, OM_OFFSET_NO_MONITOR_VALUE_TAG(owner)));
     jccb(Assembler::equal, locked);
 
     // Check if recursive.
-    cmpptr(thread, rax_reg);
+    cmpptr(box, rax_reg);
     jccb(Assembler::notEqual, slow_path);
 
     // Recursive.
     increment(Address(tagged_monitor, OM_OFFSET_NO_MONITOR_VALUE_TAG(recursions)));
+    xorl(rax_reg, rax_reg);
   }
 
   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.
   Label zf_correct;
   Label zf_bad_zero;
   jcc(Assembler::zero, zf_correct);
@@ -678,31 +689,28 @@
   bind(zf_correct);
 #endif
   // C2 uses the value of ZF to determine the continuation.
 }
 
-void C2_MacroAssembler::fast_unlock_lightweight(Register obj, Register reg_rax, Register t, Register thread) {
+void C2_MacroAssembler::fast_unlock_lightweight(Register obj, Register reg_rax, Register t1, Register t2, Register thread) {
   assert(LockingMode == LM_LIGHTWEIGHT, "must be");
   assert(reg_rax == rax, "Used for CAS");
-  assert_different_registers(obj, reg_rax, t);
+  assert_different_registers(obj, reg_rax, t1, t2);
 
   // Handle inflated monitor.
   Label inflated, inflated_check_lock_stack;
   // Finish fast unlock successfully.  MUST jump with ZF == 1
   Label unlocked;
 
-  // Assume success.
-  decrement(Address(thread, JavaThread::held_monitor_count_offset()));
-
-  const Register mark = t;
+  const Register mark = t1;
   const Register top = reg_rax;
 
   Label dummy;
   C2FastUnlockLightweightStub* stub = nullptr;
 
   if (!Compile::current()->output()->in_scratch_emit_size()) {
-    stub = new (Compile::current()->comp_arena()) C2FastUnlockLightweightStub(obj, mark, reg_rax, thread);
+    stub = new (Compile::current()->comp_arena()) C2FastUnlockLightweightStub(obj, mark, reg_rax, t2, thread);
     Compile::current()->output()->add_stub(stub);
   }
 
   Label& push_and_slow_path = stub == nullptr ? dummy : stub->push_and_slow_path();
   Label& check_successor = stub == nullptr ? dummy : stub->check_successor();
@@ -791,11 +799,11 @@
     jmpb(unlocked);
 
     // Recursive unlock.
     bind(recursive);
     decrement(Address(monitor, OM_OFFSET_NO_MONITOR_VALUE_TAG(recursions)));
-    xorl(t, t);
+    xorl(t1, t1);
 #endif
   }
 
   bind(unlocked);
   if (stub != nullptr) {
diff a/src/hotspot/cpu/x86/c2_MacroAssembler_x86.hpp b/src/hotspot/cpu/x86/c2_MacroAssembler_x86.hpp
--- a/src/hotspot/cpu/x86/c2_MacroAssembler_x86.hpp
+++ b/src/hotspot/cpu/x86/c2_MacroAssembler_x86.hpp
@@ -36,15 +36,15 @@
   // Code used by cmpFastLock and cmpFastUnlock mach instructions in .ad file.
   // See full description in macroAssembler_x86.cpp.
   void fast_lock(Register obj, Register box, Register tmp,
                  Register scr, Register cx1, Register cx2, Register thread,
                  Metadata* method_data);
-  void fast_unlock(Register obj, Register box, Register tmp);
+  void fast_unlock(Register obj, Register box, Register tmp, Register scr);
 
   void fast_lock_lightweight(Register obj, Register box, Register rax_reg,
                              Register t, Register thread);
-  void fast_unlock_lightweight(Register obj, Register reg_rax, Register t, Register thread);
+  void fast_unlock_lightweight(Register obj, Register reg_rax, Register t1, Register t2, Register thread);
 
   // Generic instructions support for use in .ad files C2 code generation
   void vabsnegd(int opcode, XMMRegister dst, XMMRegister src);
   void vabsnegd(int opcode, XMMRegister dst, XMMRegister src, int vector_len);
   void vabsnegf(int opcode, XMMRegister dst, XMMRegister src);
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
@@ -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,71 @@
 
 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 void ThawBase::fix_native_wrapper_return_pc_pd(frame& top) {
+  bool from_interpreted = top.is_interpreted_frame();
+  address resume_address = from_interpreted ? Interpreter::native_frame_resume_entry() : SharedRuntime::native_frame_resume_entry();
+  DEBUG_ONLY(Method* method = from_interpreted ? top.interpreter_frame_method() : CodeCache::find_blob(resume_address)->as_nmethod()->method();)
+  assert(method->is_object_wait0(), "");
+  ContinuationHelper::Frame::patch_pc(top, resume_address);
+}
+
+inline intptr_t* ThawBase::push_resume_adapter(frame& top) {
+  intptr_t* sp = top.sp();
+
+#ifdef ASSERT
+  RegisterMap map(JavaThread::current(),
+                  RegisterMap::UpdateMap::skip,
+                  RegisterMap::ProcessFrames::skip,
+                  RegisterMap::WalkContinuation::skip);
+  frame caller = top.sender(&map);
+  intptr_t link_addr = (intptr_t)ContinuationHelper::Frame::callee_link_address(caller);
+  assert(sp[-2] == link_addr, "wrong link address: " INTPTR_FORMAT " != " INTPTR_FORMAT, sp[-2], link_addr);
+#endif
+
+  intptr_t* fp = sp - frame::sender_sp_offset;
+  address pc = top.is_interpreted_frame() ? Interpreter::cont_resume_interpreter_adapter()
+                                          : StubRoutines::cont_resume_compiler_adapter();
+
+  sp -= frame::metadata_words;
+  *(address*)(sp - frame::sender_sp_ret_address_offset()) = pc;
+  *(intptr_t**)(sp - frame::sender_sp_offset) = fp;
+
+  log_develop_trace(continuations, preempt)("push_resume_%s_adapter() initial sp: " INTPTR_FORMAT " final sp: " INTPTR_FORMAT " fp: " INTPTR_FORMAT,
+    top.is_interpreted_frame() ? "interpreter" : "compiler", p2i(sp + frame::metadata_words), p2i(sp), p2i(fp));
+  return sp;
+}
+
+inline intptr_t* ThawBase::push_resume_monitor_operation(stackChunkOop chunk) {
+  frame enterSpecial = new_entry_frame();
+  intptr_t* sp = enterSpecial.sp();
+
+  // First push the return barrier frame
+  sp -= frame::metadata_words;
+  sp[1] = (intptr_t)StubRoutines::cont_returnBarrier();
+  sp[0] = (intptr_t)enterSpecial.fp();
+
+  // Now push the ObjectWaiter*
+  sp -= frame::metadata_words;
+  sp[1] = (intptr_t)chunk->object_waiter(); // alignment
+  sp[0] = (intptr_t)chunk->object_waiter();
+
+  // Finally arrange to return to the resume_monitor_operation stub
+  sp[-1] = (intptr_t)StubRoutines::cont_resume_monitor_operation();
+  sp[-2] = (intptr_t)enterSpecial.fp();
+
+  log_develop_trace(continuations, preempt)("push_resume_monitor_operation 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
@@ -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/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
@@ -1152,13 +1152,15 @@
 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) {
+    push_cont_fastpath();
     call_VM(noreg,
             CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
             lock_reg);
+    pop_cont_fastpath();
   } else {
     Label count_locking, done, slow_case;
 
     const Register swap_reg = rax; // Must use rax for cmpxchg instruction
     const Register tmp_reg = rbx;
@@ -1238,28 +1240,31 @@
       andptr(swap_reg, zero_bits - (int)os::vm_page_size());
 
       // 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);
     }
+    jmp(done);
+
+    bind(count_locking);
     inc_held_monitor_count();
     jmp(done);
 
     bind(slow_case);
 
+    push_cont_fastpath();
     // Call the runtime routine for slow case
     if (LockingMode == LM_LIGHTWEIGHT) {
       call_VM(noreg,
               CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter_obj),
               obj_reg);
     } else {
       call_VM(noreg,
               CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter),
               lock_reg);
     }
+    pop_cont_fastpath();
     bind(done);
   }
 }
 
 
@@ -1280,11 +1285,11 @@
          "The argument is only for looks. It must be c_rarg1");
 
   if (LockingMode == LM_MONITOR) {
     call_VM_leaf(CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorexit), lock_reg);
   } else {
-    Label count_locking, done, slow_case;
+    Label done, slow_case;
 
     const Register swap_reg   = rax;  // Must use rax for cmpxchg instruction
     const Register header_reg = LP64_ONLY(c_rarg2) NOT_LP64(rbx);  // Will contain the old oopMark
     const Register obj_reg    = LP64_ONLY(c_rarg3) NOT_LP64(rcx);  // Will contain the oop
 
@@ -1318,22 +1323,20 @@
 
       // Test for recursion
       testptr(header_reg, header_reg);
 
       // zero for recursive case
-      jcc(Assembler::zero, count_locking);
+      jcc(Assembler::zero, done);
 
       // Atomic swap back the old header
       lock();
       cmpxchgptr(header_reg, Address(obj_reg, oopDesc::mark_offset_in_bytes()));
 
       // 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/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"
@@ -527,10 +528,20 @@
 #ifdef _WIN64
   // restore stack pointer
   addq(rsp, frame::arg_reg_save_area_bytes);
 #endif
 
+  if (entry_point == CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter) ||
+      entry_point == CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter_obj)) {
+    Label not_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);
+    bind(not_preempted);
+  }
 }
 
 void MacroAssembler::cmp64(Register src1, AddressLiteral src2, Register rscratch) {
   assert(!src2.is_lval(), "should use cmpptr");
   assert(rscratch != noreg || always_reachable(src2), "missing");
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
@@ -2046,10 +2046,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
@@ -180,10 +180,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.
@@ -1429,11 +1430,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");
@@ -1619,10 +1620,11 @@
 
   // --- 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();
@@ -1721,10 +1723,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);
@@ -2284,10 +2290,12 @@
       __ jcc(Assembler::notEqual, slow_path_lock);
     } else {
       assert(LockingMode == LM_LIGHTWEIGHT, "must be");
       __ lightweight_lock(obj_reg, swap_reg, r15_thread, rscratch1, slow_path_lock);
     }
+    __ jmp (lock_done);
+
     __ bind(count_mon);
     __ inc_held_monitor_count();
 
     // Slow path will re-enter here
     __ bind(lock_done);
@@ -2376,10 +2384,22 @@
 
   // change thread state
   __ movl(Address(r15_thread, JavaThread::thread_state_offset()), _thread_in_Java);
   __ bind(after_transition);
 
+  // Check preemption for Object.wait()
+  if (method->is_object_wait0()) {
+    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(not_preempted);
+  }
+  int resume_wait_offset = ((intptr_t)__ pc()) - start;
+
   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);
@@ -2399,11 +2419,10 @@
     if (LockingMode == LM_LEGACY) {
       Label not_recur;
       // Simple recursive lock?
       __ cmpptr(Address(rsp, lock_slot_offset * VMRegImpl::stack_slot_size), NULL_WORD);
       __ jcc(Assembler::notEqual, not_recur);
-      __ dec_held_monitor_count();
       __ jmpb(fast_done);
       __ bind(not_recur);
     }
 
     // Must save rax if it is live now because cmpxchg must use it
@@ -2425,11 +2444,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) {
@@ -2500,11 +2518,14 @@
     __ 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 on ObjectMonitor::enter() for now which will fail with freeze_pinned_native.
+    __ 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);
@@ -2592,10 +2613,14 @@
                                             stack_slots / VMRegImpl::slots_per_word,
                                             (is_static ? in_ByteSize(klass_offset) : in_ByteSize(receiver_offset)),
                                             in_ByteSize(lock_slot_offset*VMRegImpl::stack_slot_size),
                                             oop_maps);
 
+  if (nm != nullptr && method->is_object_wait0()) {
+    SharedRuntime::set_native_frame_resume_entry(nm->code_begin() + resume_wait_offset);
+  }
+
   return nm;
 }
 
 // this function returns the adjust size (in number of words) to a c2i adapter
 // activation for use during deoptimization
@@ -2615,10 +2640,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
@@ -32,10 +32,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
@@ -3776,10 +3777,137 @@
 
 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();
+
+#ifdef ASSERT
+  __ push(rax);
+  { Label L;
+    __ get_thread(rax);
+    __ cmpptr(r15_thread, rax);
+    __ jcc(Assembler::equal, L);
+    __ stop("r15 should have been preserved across VM call");
+    __ bind(L);
+  }
+  __ pop(rax);
+#endif
+
+  __ reset_last_Java_frame(true);
+
+  // reset _preempting flag
+#ifdef ASSERT
+  { Label L;
+    __ movbool(rscratch1, Address(r15_thread, JavaThread::preempting_offset()));
+    __ testbool(rscratch1);
+    __ jcc(Assembler::notZero, L);
+    __ stop("preempting flag should be set");
+    __ bind(L);
+  }
+#endif
+  __ movbool(Address(r15_thread, JavaThread::preempting_offset()), false);
+
+  // Set rsp to enterSpecial frame
+  __ 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()
+  SharedRuntime::continuation_enter_cleanup(_masm);
+  __ pop(rbp);
+  __ ret(0);
+
+  __ 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((address)&ContinuationEntry::_thaw_call_pc));
+  __ jmp(rscratch1);
+
+  return start;
+}
+
+address StubGenerator::generate_cont_resume_monitor_operation() {
+  if (!Continuations::enabled()) return nullptr;
+  StubCodeMark mark(this, "StubRoutines","Continuation resume monitor operation");
+  address start = __ pc();
+
+#ifdef ASSERT
+  __ push(rax);
+  { Label L;
+    __ get_thread(rax);
+    __ cmpptr(r15_thread, rax);
+    __ jcc(Assembler::equal, L);
+    __ stop("r15 should have been preserved across VM call");
+    __ bind(L);
+  }
+  __ pop(rax);
+#endif
+
+  const Register waiter_reg = c_rarg1;
+  __ pop(waiter_reg);
+  __ pop(waiter_reg);
+
+#ifdef ASSERT
+  { Label L;
+    __ testptr(waiter_reg, waiter_reg);
+    __ jcc(Assembler::notEqual, L);
+    __ stop("ObjectMonitor to use is null");
+    __ bind(L);
+  }
+#endif // ASSERT
+
+  __ mov(c_rarg0, r15_thread);
+  __ subptr(rsp, frame::arg_reg_save_area_bytes);
+  __ call(RuntimeAddress(CAST_FROM_FN_PTR(address, SharedRuntime::resume_monitor_operation)));
+  __ addptr(rsp, frame::arg_reg_save_area_bytes);
+
+  Label failAcquire;
+  __ movbool(rscratch1, Address(r15_thread, JavaThread::preempting_offset()));
+  __ testbool(rscratch1);
+  __ jcc(Assembler::notEqual, failAcquire);
+  // We have the lock now, just return to caller (we will actually hit the
+  // return barrier to thaw more frames)
+  __ pop(rbp);
+  __ ret(0);
+
+  __ bind(failAcquire);
+  __ movbool(Address(r15_thread, JavaThread::preempting_offset()), false);
+  // Set rsp to enterSpecial frame
+  __ movptr(rsp, Address(r15_thread, JavaThread::cont_entry_offset()));
+  // Remove enterSpecial frame from the stack and return to Continuation.run()
+  SharedRuntime::continuation_enter_cleanup(_masm);
+  __ pop(rbp);
+  __ ret(0);
+
+  return start;
+}
+
+address StubGenerator::generate_cont_resume_compiler_adapter() {
+  if (!Continuations::enabled()) return nullptr;
+  StubCodeMark mark(this, "StubRoutines", "Continuation resume compiler adapter");
+  address start = __ pc();
+
+  // The safepoint blob handler expects that rbx, being a callee saved register, will be preserved
+  // during the VM call. It is used to check if the return pc back to Java was modified in the runtime.
+  // If it wasn't, the return pc is modified so on return the poll instruction is skipped. Saving this
+  // additional value of rbx during freeze will complicate too much the code, so we just zero it here
+  // so that the comparison fails and the skip is not attempted in case the pc was indeed changed.
+  __ movptr(rbx, NULL_WORD);
+
+  __ pop(rbp);
+  __ ret(0);
+
+  return start;
+}
+
 #if INCLUDE_JFR
 
 // For c2: c_rarg0 is junk, call to runtime to write a checkpoint.
 // It returns a jobject handle to the event writer.
 // The handle is dereferenced and the return value is the event writer oop.
@@ -4129,10 +4257,13 @@
 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();
+  StubRoutines::_cont_resume_monitor_operation = generate_cont_resume_monitor_operation();
+  StubRoutines::_cont_resume_compiler_adapter = generate_cont_resume_compiler_adapter();
 
   JFR_ONLY(generate_jfr_stubs();)
 }
 
 #if INCLUDE_JFR
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
@@ -584,10 +584,14 @@
 
   // 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();
+  address generate_cont_resume_compiler_adapter();
+
 #if INCLUDE_JFR
   void generate_jfr_stubs();
   // For c2: c_rarg0 is junk, call to runtime to write a checkpoint.
   // It returns a jobject handle to the event writer.
   // The handle is dereferenced and the return value is the event writer 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,34 @@
 
   __ 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();
+
+  __ pop(rbp);
+
+  // We will return to the intermediate call made in call_VM skipping the restoration
+  // of bcp and locals done in InterpreterMacroAssembler::call_VM_base, so fix them here.
+  __ 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.
 //
 
@@ -1047,15 +1071,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 +1103,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 +1176,23 @@
   }
 
   // change thread state
   __ movl(Address(thread, JavaThread::thread_state_offset()), _thread_in_Java);
 
+  // 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);
+  Interpreter::_native_frame_resume_entry = __ pc();
+  // On resume we need to set up stack as expected
+  __ push(dtos);
+  __ push(ltos);
+  __ bind(not_preempted);
+
   // reset_last_Java_frame
   __ reset_last_Java_frame(thread, true);
 
   if (CheckJNICalls) {
     // clear_pending_jni_exception_check
diff a/src/hotspot/cpu/x86/vm_version_x86.hpp b/src/hotspot/cpu/x86/vm_version_x86.hpp
--- a/src/hotspot/cpu/x86/vm_version_x86.hpp
+++ b/src/hotspot/cpu/x86/vm_version_x86.hpp
@@ -819,10 +819,12 @@
   // x86_64 supports fast class initialization checks
   static bool supports_fast_class_init_checks() {
     return LP64_ONLY(true) NOT_LP64(false); // not implemented on x86_32
   }
 
+  static bool supports_cont_preemption() { return true; }
+
   // x86_64 supports secondary supers table
   constexpr static bool supports_secondary_supers_table() {
     return LP64_ONLY(true) NOT_LP64(false); // not implemented on x86_32
   }
 
diff a/src/hotspot/cpu/x86/x86_32.ad b/src/hotspot/cpu/x86/x86_32.ad
--- a/src/hotspot/cpu/x86/x86_32.ad
+++ b/src/hotspot/cpu/x86/x86_32.ad
@@ -13625,18 +13625,18 @@
                  $scr$$Register, noreg, noreg, $thread$$Register, nullptr);
   %}
   ins_pipe(pipe_slow);
 %}
 
-instruct cmpFastUnlock(eFlagsReg cr, eRegP object, eAXRegP box, eRegP tmp ) %{
+instruct cmpFastUnlock(eFlagsReg cr, eRegP object, eAXRegP box, eRegP tmp, eRegP scr) %{
   predicate(LockingMode != LM_LIGHTWEIGHT);
   match(Set cr (FastUnlock object box));
-  effect(TEMP tmp, USE_KILL box);
+  effect(TEMP tmp, TEMP scr, USE_KILL box);
   ins_cost(300);
   format %{ "FASTUNLOCK $object,$box\t! kills $box,$tmp" %}
   ins_encode %{
-    __ fast_unlock($object$$Register, $box$$Register, $tmp$$Register);
+    __ fast_unlock($object$$Register, $box$$Register, $tmp$$Register, $scr$$Register);
   %}
   ins_pipe(pipe_slow);
 %}
 
 instruct cmpFastLockLightweight(eFlagsReg cr, eRegP object, eBXRegP box, eAXRegI eax_reg, eRegP tmp, eRegP thread) %{
@@ -13650,19 +13650,19 @@
     __ fast_lock_lightweight($object$$Register, $box$$Register, $eax_reg$$Register, $tmp$$Register, $thread$$Register);
   %}
   ins_pipe(pipe_slow);
 %}
 
-instruct cmpFastUnlockLightweight(eFlagsReg cr, eRegP object, eAXRegP eax_reg, eRegP tmp, eRegP thread) %{
+instruct cmpFastUnlockLightweight(eFlagsReg cr, eRegP object, eAXRegP eax_reg, eRegP tmp, eRegP scr, eRegP thread) %{
   predicate(LockingMode == LM_LIGHTWEIGHT);
   match(Set cr (FastUnlock object eax_reg));
-  effect(TEMP tmp, USE_KILL eax_reg, TEMP thread);
+  effect(TEMP tmp, TEMP scr, USE_KILL eax_reg, TEMP thread);
   ins_cost(300);
   format %{ "FASTUNLOCK $object,$eax_reg\t! kills $eax_reg,$tmp" %}
   ins_encode %{
     __ get_thread($thread$$Register);
-    __ fast_unlock_lightweight($object$$Register, $eax_reg$$Register, $tmp$$Register, $thread$$Register);
+    __ fast_unlock_lightweight($object$$Register, $eax_reg$$Register, $tmp$$Register, $scr$$Register, $thread$$Register);
   %}
   ins_pipe(pipe_slow);
 %}
 
 instruct mask_all_evexL_LT32(kReg dst, eRegL src) %{
diff a/src/hotspot/cpu/x86/x86_64.ad b/src/hotspot/cpu/x86/x86_64.ad
--- a/src/hotspot/cpu/x86/x86_64.ad
+++ b/src/hotspot/cpu/x86/x86_64.ad
@@ -12381,18 +12381,18 @@
                  $scr$$Register, noreg, noreg, r15_thread, nullptr);
   %}
   ins_pipe(pipe_slow);
 %}
 
-instruct cmpFastUnlock(rFlagsReg cr, rRegP object, rax_RegP box, rRegP tmp) %{
+instruct cmpFastUnlock(rFlagsReg cr, rRegP object, rax_RegP box, rRegP tmp, rRegP scr) %{
   predicate(LockingMode != LM_LIGHTWEIGHT);
   match(Set cr (FastUnlock object box));
-  effect(TEMP tmp, USE_KILL box);
+  effect(TEMP tmp, TEMP scr, USE_KILL box);
   ins_cost(300);
   format %{ "fastunlock $object,$box\t! kills $box,$tmp" %}
   ins_encode %{
-    __ fast_unlock($object$$Register, $box$$Register, $tmp$$Register);
+    __ fast_unlock($object$$Register, $box$$Register, $tmp$$Register, $scr$$Register);
   %}
   ins_pipe(pipe_slow);
 %}
 
 instruct cmpFastLockLightweight(rFlagsReg cr, rRegP object, rbx_RegP box, rax_RegI rax_reg, rRegP tmp) %{
@@ -12405,18 +12405,18 @@
     __ fast_lock_lightweight($object$$Register, $box$$Register, $rax_reg$$Register, $tmp$$Register, r15_thread);
   %}
   ins_pipe(pipe_slow);
 %}
 
-instruct cmpFastUnlockLightweight(rFlagsReg cr, rRegP object, rax_RegP rax_reg, rRegP tmp) %{
+instruct cmpFastUnlockLightweight(rFlagsReg cr, rRegP object, rax_RegP rax_reg, rRegP tmp, rRegP scr) %{
   predicate(LockingMode == LM_LIGHTWEIGHT);
   match(Set cr (FastUnlock object rax_reg));
-  effect(TEMP tmp, USE_KILL rax_reg);
+  effect(TEMP tmp, TEMP scr, USE_KILL rax_reg);
   ins_cost(300);
   format %{ "fastunlock $object,$rax_reg\t! kills $rax_reg,$tmp" %}
   ins_encode %{
-    __ fast_unlock_lightweight($object$$Register, $rax_reg$$Register, $tmp$$Register, r15_thread);
+    __ fast_unlock_lightweight($object$$Register, $rax_reg$$Register, $tmp$$Register, $scr$$Register, r15_thread);
   %}
   ins_pipe(pipe_slow);
 %}
 
 
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::push_resume_adapter(frame top, bool is_interpreted_frame) {
+  Unimplemented();
+  return nullptr;
+}
+
+inline intptr_t* ThawBase::push_resume_monitor_operation(stackChunkOop chunk) {
+  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/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
@@ -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
@@ -167,10 +167,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 (StubID id);
   static address   entry_for(StubID id)          { return blob_for(id)->code_begin(); }
   static const char* name_for (StubID 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
@@ -1664,10 +1664,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;
@@ -1999,16 +2000,26 @@
 
 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::_recheckInterval_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",      byte_signature,              false); \
+  macro(_notified_offset,                  k, "notified",           bool_signature,              false); \
+  macro(_recheckInterval_offset,           k, "recheckInterval",    byte_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);
@@ -2030,10 +2041,65 @@
 
 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) {
+  uint8_t* addr = vthread->field_addr<uint8_t>(_onWaitingList_offset);
+  uint8_t value = Atomic::load(addr);
+  assert(value == 0x00 || value == 0x01, "invariant");
+  if (value == 0x00) {
+    value = Atomic::cmpxchg(addr, (uint8_t)0x00, (uint8_t)0x01);
+    if (value == 0x00) {
+      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);
+}
+
+jbyte java_lang_VirtualThread::recheckInterval(oop vthread) {
+  return vthread->byte_field(_recheckInterval_offset);
+}
+
+void java_lang_VirtualThread::set_recheckInterval(oop vthread, jbyte value) {
+  vthread->release_byte_field_put(_recheckInterval_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;
@@ -2043,29 +2109,47 @@
     case PARKING:
     case TIMED_PARKING:
     case UNPARKED:
     case YIELDING:
     case YIELDED:
+    case BLOCKING:
+    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 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);
+  return jdk_internal_vm_Continuation::is_preempted(continuation);
+}
+
 #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
@@ -523,10 +523,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,
@@ -538,10 +543,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
   };
@@ -557,10 +569,21 @@
 
   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 jbyte recheckInterval(oop vthread);
+  static void set_recheckInterval(oop vthread, jbyte value);
+  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
@@ -128,10 +128,11 @@
   case vmIntrinsics::_doubleToRawLongBits:
   case vmIntrinsics::_longBitsToDouble:
   case vmIntrinsics::_currentCarrierThread:
   case vmIntrinsics::_currentThread:
   case vmIntrinsics::_setCurrentThread:
+  case vmIntrinsics::_setLockId:
   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
@@ -606,10 +606,12 @@
   do_intrinsic(_notifyJvmtiVThreadEnd, java_lang_VirtualThread, notifyJvmtiEnd_name, void_method_signature, F_RN)       \
   do_intrinsic(_notifyJvmtiVThreadMount, java_lang_VirtualThread, notifyJvmtiMount_name, bool_void_signature, F_RN)     \
   do_intrinsic(_notifyJvmtiVThreadUnmount, java_lang_VirtualThread, notifyJvmtiUnmount_name, bool_void_signature, F_RN) \
   do_intrinsic(_notifyJvmtiVThreadHideFrames, java_lang_VirtualThread, notifyJvmtiHideFrames_name, bool_void_signature, F_SN) \
   do_intrinsic(_notifyJvmtiVThreadDisableSuspend, java_lang_VirtualThread, notifyJvmtiDisableSuspend_name, bool_void_signature, F_SN) \
+  do_intrinsic(_setLockId, java_lang_VirtualThread, setLockId_name, long_void_signature, F_SN)                          \
+  do_name(     setLockId_name,                          "setLockId")                                                    \
                                                                                                                         \
   /* support for UnsafeConstants */                                                                                     \
   do_class(jdk_internal_misc_UnsafeConstants,      "jdk/internal/misc/UnsafeConstants")                                 \
                                                                                                                         \
   /* support for Unsafe */                                                                                              \
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
@@ -413,10 +413,11 @@
   template(numFrames_name,                            "numFrames")                                \
   template(stack_name,                                "stack")                                    \
   template(maxSize_name,                              "maxSize")                                  \
   template(reset_name,                                "reset")                                    \
   template(done_name,                                 "done")                                     \
+  template(preempted_name,                            "preempted")                                \
   template(mounted_name,                              "mounted")                                  \
   template(jfrTraceId_name,                           "jfrTraceId")                               \
   template(fp_name,                                   "fp")                                       \
   template(sp_name,                                   "sp")                                       \
   template(pc_name,                                   "pc")                                       \
@@ -512,10 +513,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 */                                                                   \
@@ -560,10 +563,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;")   \
@@ -738,14 +742,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
@@ -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/compiler/compilerEvent.cpp b/src/hotspot/share/compiler/compilerEvent.cpp
--- a/src/hotspot/share/compiler/compilerEvent.cpp
+++ b/src/hotspot/share/compiler/compilerEvent.cpp
@@ -55,66 +55,66 @@
 };
 
 Semaphore PhaseTypeGuard::_mutex_semaphore(1);
 
 // Table for mapping compiler phases names to int identifiers.
-static GrowableArray<const char*>* phase_names = nullptr;
+static GrowableArray<const char*>* cphase_names = nullptr;
 
 class CompilerPhaseTypeConstant : public JfrSerializer {
  public:
   void serialize(JfrCheckpointWriter& writer) {
     PhaseTypeGuard guard;
-    assert(phase_names != nullptr, "invariant");
-    assert(phase_names->is_nonempty(), "invariant");
-    const u4 nof_entries = phase_names->length();
+    assert(cphase_names != nullptr, "invariant");
+    assert(cphase_names->is_nonempty(), "invariant");
+    const u4 nof_entries = cphase_names->length();
     writer.write_count(nof_entries);
     for (u4 i = 0; i < nof_entries; i++) {
       writer.write_key(i);
-      writer.write(phase_names->at(i));
+      writer.write(cphase_names->at(i));
     }
   }
 };
 
-static int lookup_phase(const char* phase_name) {
-  for (int i = 0; i < phase_names->length(); i++) {
-    const char* name = phase_names->at(i);
-    if (strcmp(name, phase_name) == 0) {
+static int lookup_phase(const char* cphase_name) {
+  for (int i = 0; i < cphase_names->length(); i++) {
+    const char* name = cphase_names->at(i);
+    if (strcmp(name, cphase_name) == 0) {
       return i;
     }
   }
   return -1;
 }
 
-int CompilerEvent::PhaseEvent::get_phase_id(const char* phase_name, bool may_exist, bool use_strdup, bool sync) {
+int CompilerEvent::PhaseEvent::get_phase_id(const char* cphase_name, bool may_exist, bool use_strdup, bool sync) {
   int index;
   bool register_jfr_serializer = false;
   {
     PhaseTypeGuard guard(sync);
-    if (phase_names == nullptr) {
-      phase_names = new (mtInternal) GrowableArray<const char*>(100, mtCompiler);
+    if (cphase_names == nullptr) {
+      cphase_names = new (mtInternal) GrowableArray<const char*>(100, mtCompiler);
       register_jfr_serializer = true;
     } else if (may_exist) {
-      index = lookup_phase(phase_name);
+      index = lookup_phase(cphase_name);
       if (index != -1) {
         return index;
       }
     } else {
-      assert((index = lookup_phase(phase_name)) == -1, "phase name \"%s\" already registered: %d", phase_name, index);
+      assert((index = lookup_phase(cphase_name)) == -1, "phase name \"%s\" already registered: %d", cphase_name, index);
     }
 
-    index = phase_names->length();
-    phase_names->append(use_strdup ? os::strdup(phase_name) : phase_name);
+    index = cphase_names->length();
+    cphase_names->append(use_strdup ? os::strdup(cphase_name) : cphase_name);
   }
   if (register_jfr_serializer) {
     JfrSerializer::register_serializer(TYPE_COMPILERPHASETYPE, false, new CompilerPhaseTypeConstant());
   } else if (Jfr::is_recording()) {
     // serialize new phase.
     JfrCheckpointWriter writer;
     writer.write_type(TYPE_COMPILERPHASETYPE);
     writer.write_count(1);
     writer.write_key(index);
-    writer.write(phase_name);
+    writer.write(cphase_name);
   }
   return index;
 }
 
 // As part of event commit, a Method* is tagged as a function of an epoch.
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
@@ -1146,10 +1146,19 @@
 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 void JNICALL
+JVM_SetLockId(JNIEnv* env, jclass clazz, jlong tid);
+
+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
@@ -730,15 +730,16 @@
   current->last_frame().interpreter_frame_verify_monitor(elem);
 #endif
   Handle h_obj(current, elem->obj());
   assert(Universe::heap()->is_in_or_null(h_obj()),
          "must be null or an object");
+  ThreadOnMonitorEnter tme(current);
   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
 
 // NOTE: We provide a separate implementation for the new lightweight locking to workaround a limitation
 // of registers in x86_32. This entry point accepts an oop instead of a BasicObjectLock*.
@@ -751,10 +752,11 @@
 JRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter_obj(JavaThread* current, oopDesc* obj))
   assert(LockingMode == LM_LIGHTWEIGHT, "Should call monitorenter() when not using the new lightweight locking");
   Handle h_obj(current, cast_to_oop(obj));
   assert(Universe::heap()->is_in_or_null(h_obj()),
          "must be null or an object");
+  ThreadOnMonitorEnter tme(current);
   ObjectSynchronizer::enter(h_obj, nullptr, current);
   return;
 JRT_END
 
 JRT_LEAF(void, InterpreterRuntime::monitorexit(BasicObjectLock* elem))
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
@@ -210,10 +210,12 @@
 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;
+address    TemplateInterpreter::_native_frame_resume_entry                  = 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
@@ -109,10 +109,13 @@
   static address    _throw_NullPointerException_entry;
   static address    _throw_exception_entry;
 
   static address    _throw_StackOverflowError_entry;
 
+  static address    _cont_resume_interpreter_adapter;
+  static address    _native_frame_resume_entry;
+
   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 +155,13 @@
   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; }
+  static address    native_frame_resume_entry()                 { return _native_frame_resume_entry; }
+
   // 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 rerun 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/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
@@ -35,10 +35,11 @@
 #include "oops/methodCounters.hpp"
 #include "oops/objArrayKlass.hpp"
 #include "prims/jvmtiThreadState.hpp"
 #include "runtime/deoptimization.hpp"
 #include "runtime/flags/jvmFlag.hpp"
+#include "runtime/objectMonitor.hpp"
 #include "runtime/osThread.hpp"
 #include "runtime/sharedRuntime.hpp"
 #include "runtime/stubRoutines.hpp"
 #include "runtime/vm_version.hpp"
 #if INCLUDE_G1GC
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
@@ -1097,10 +1097,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,23 @@
   }
   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) {
+  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 +235,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, nullptr);
   #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
@@ -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
@@ -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
@@ -869,10 +869,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
@@ -587,10 +587,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
@@ -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,16 @@
   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_MONITORENTER = 1 << 6; // Continuation was preempted on monitorenter
+  static const uint8_t FLAG_PREEMPTED_WAIT = 1 << 7; // Continuation was preempted on Object.wait()
+
+  static const uint8_t FLAGS_PREEMPTED = FLAG_PREEMPTED_MONITORENTER | FLAG_PREEMPTED_WAIT;
+  static const int preempt_shift = 6;
 
   bool try_acquire_relativization();
   void release_relativization();
 
 public:
@@ -89,10 +97,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>
@@ -120,15 +137,22 @@
 
   inline bool is_flag(uint8_t flag) const;
   inline bool is_flag_acquire(uint8_t flag) const;
   inline void set_flag(uint8_t flag, bool value);
   inline bool try_set_flags(uint8_t prev_flags, uint8_t new_flags);
+  inline void clear_flags(uint8_t flag);
   inline void clear_flags();
 
   inline bool has_mixed_frames() const;
   inline void set_has_mixed_frames(bool value);
 
+  inline void set_preempt_kind(int freeze_kind);
+  inline int get_and_clear_preempt_kind();
+
+  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 +169,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
@@ -37,10 +37,11 @@
 #include "oops/instanceStackChunkKlass.inline.hpp"
 #include "runtime/continuationJavaClasses.inline.hpp"
 #include "runtime/frame.inline.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
@@ -159,20 +166,55 @@
 }
 inline void stackChunkOopDesc::set_flag(uint8_t flag, bool value) {
   uint32_t flags = this->flags();
   set_flags((uint8_t)(value ? flags |= flag : flags &= ~flag));
 }
+inline void stackChunkOopDesc::clear_flags(uint8_t flag) {
+  uint32_t flags = this->flags();
+  set_flags((uint8_t)(flags &= ~flag));
+}
 inline void stackChunkOopDesc::clear_flags() {
   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 | FLAGS_PREEMPTED)) == 0, "other flags should not be set");
   set_flag(FLAG_HAS_INTERPRETED_FRAMES, value);
 }
 
+inline void stackChunkOopDesc::set_preempt_kind(int freeze_kind) {
+  assert((flags() & FLAGS_PREEMPTED) == 0, "");
+  assert(freeze_kind == freeze_on_monitorenter || freeze_kind == freeze_on_wait, "");
+  uint8_t flag = freeze_kind == freeze_on_monitorenter ? FLAG_PREEMPTED_MONITORENTER : FLAG_PREEMPTED_WAIT;
+  set_flag(flag, true);
+}
+
+inline int stackChunkOopDesc::get_and_clear_preempt_kind() {
+  assert((is_flag(FLAG_PREEMPTED_MONITORENTER) && !is_flag(FLAG_PREEMPTED_WAIT))
+         || (is_flag(FLAG_PREEMPTED_WAIT) && !is_flag(FLAG_PREEMPTED_MONITORENTER)), "");
+  int kind = is_flag(FLAG_PREEMPTED_MONITORENTER) ? freeze_on_monitorenter : freeze_on_wait;
+  clear_flags(FLAGS_PREEMPTED);
+  return kind;
+}
+
+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 +233,22 @@
     f.handle_deopted();
   }
   do_barriers0<barrier>(f, map);
 }
 
+template <typename OopT, class StackChunkLockStackClosureType>
+inline void stackChunkOopDesc::iterate_lockstack(StackChunkLockStackClosureType* closure) {
+  if (LockingMode != LM_LIGHTWEIGHT) {
+    return;
+  }
+  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 +265,17 @@
     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(), "");
+    if (f.is_done()) return;
 
     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)
+    f.next(&map);
   }
   assert(!f.is_stub(), "");
 
   for(; should_continue && !f.is_done(); f.next(map)) {
     if (frame_kind == ChunkFrames::Mixed) {
@@ -280,11 +332,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/c2_CodeStubs.hpp b/src/hotspot/share/opto/c2_CodeStubs.hpp
--- a/src/hotspot/share/opto/c2_CodeStubs.hpp
+++ b/src/hotspot/share/opto/c2_CodeStubs.hpp
@@ -99,18 +99,19 @@
 
 class C2FastUnlockLightweightStub : public C2CodeStub {
 private:
   Register _obj;
   Register _mark;
-  Register _t;
+  Register _t1;
+  Register _t2;
   Register _thread;
   Label _push_and_slow_path;
   Label _check_successor;
   Label _unlocked_continuation;
 public:
-  C2FastUnlockLightweightStub(Register obj, Register mark, Register t, Register thread) : C2CodeStub(),
-    _obj(obj), _mark(mark), _t(t), _thread(thread) {}
+  C2FastUnlockLightweightStub(Register obj, Register mark, Register t1, Register t2, Register thread) : C2CodeStub(),
+    _obj(obj), _mark(mark), _t1(t1), _t2(t2), _thread(thread) {}
   int max_size() const;
   void emit(C2_MacroAssembler& masm);
   Label& push_and_slow_path() { return _push_and_slow_path; }
   Label& check_successor() { return _check_successor; }
   Label& unlocked_continuation() { return _unlocked_continuation; }
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
@@ -727,10 +727,11 @@
   case vmIntrinsics::_storeStoreFence:
   case vmIntrinsics::_fullFence:
   case vmIntrinsics::_currentCarrierThread:
   case vmIntrinsics::_currentThread:
   case vmIntrinsics::_setCurrentThread:
+  case vmIntrinsics::_setLockId:
   case vmIntrinsics::_scopedValueCache:
   case vmIntrinsics::_setScopedValueCache:
 #ifdef JFR_HAVE_INTRINSICS
   case vmIntrinsics::_counterTime:
   case vmIntrinsics::_getEventWriter:
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
@@ -476,10 +476,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::_setLockId:                return inline_native_setLockId();
 
   case vmIntrinsics::_scopedValueCache:          return inline_native_scopedValueCache();
   case vmIntrinsics::_setScopedValueCache:       return inline_native_setScopedValueCache();
 
 #if INCLUDE_JVMTI
@@ -3665,14 +3666,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_setLockId() {
+  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
@@ -234,10 +234,11 @@
   bool inline_unsafe_setMemory();
 
   bool inline_native_currentCarrierThread();
   bool inline_native_currentThread();
   bool inline_native_setCurrentThread();
+  bool inline_native_setLockId();
 
   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
@@ -3085,10 +3085,14 @@
 
 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(jlong, JVM_GetNextThreadIdOffset(JNIEnv* env, jclass threadClass))
   return ThreadIdentifier::unsafe_offset();
@@ -3971,10 +3975,47 @@
          "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_NO_ENV(void, JVM_SetLockId(JNIEnv* env, jclass clazz, jlong tid))
+  thread->set_lock_id(tid);
+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
@@ -1363,22 +1363,20 @@
   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();
+  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) {
@@ -1421,22 +1419,20 @@
   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();
+  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
@@ -783,10 +783,11 @@
 }
 
 jint
 JvmtiEnvBase::get_vthread_state(oop thread_oop, JavaThread* java_thread) {
   jint state = 0;
+  bool is_mon_responsible = java_lang_VirtualThread::recheckInterval(thread_oop) > 0;
   bool ext_suspended = JvmtiVTSuspender::is_vthread_suspended(thread_oop);
   jint interrupted = java_lang_Thread::interrupted(thread_oop);
 
   if (java_thread != nullptr) {
     // If virtual thread is blocked on a monitor enter the BLOCKED_ON_MONITOR_ENTER bit
@@ -795,10 +796,12 @@
     oop ct_oop = java_lang_VirtualThread::carrier_thread(thread_oop);
     jint filtered_bits = JVMTI_THREAD_STATE_SUSPENDED | JVMTI_THREAD_STATE_INTERRUPTED;
 
     // This call can trigger a safepoint, so thread_oop must not be used after it.
     state = get_thread_state_base(ct_oop, java_thread) & ~filtered_bits;
+  } else if (is_mon_responsible) {
+    state = (jint) JavaThreadStatus::BLOCKED_ON_MONITOR_ENTER;
   } else {
     int vt_state = java_lang_VirtualThread::state(thread_oop);
     state = (jint)java_lang_VirtualThread::map_state_to_thread_status(vt_state);
   }
   // Ensure the thread has not exited after retrieving suspended/interrupted values.
@@ -1012,40 +1015,47 @@
 
   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;
       }
     }
   }
 
+  if (carrier == nullptr) {
+    // vthread gets pinned if monitors are acquired via jni MonitorEnter
+    // so nothing else to do for unmounted case.
+    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);
   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);
 
@@ -1058,24 +1068,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);
@@ -1519,13 +1537,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;
@@ -1569,19 +1591,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);
       }
     }
@@ -2496,22 +2522,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) {
@@ -2525,10 +2551,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,34 @@
       }
     }
   }
 }
 
+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());
+  {
+    MutexLocker mu(JvmtiThreadState_lock);
+    JvmtiThreadState* state = current->jvmti_thread_state();
+    if (state != NULL && state->is_pending_interp_only_mode()) {
+      JvmtiEventController::enter_interp_only_mode(state);
+    }
+  }
+  assert(current->is_in_VTMS_transition(), "sanity check");
+  assert(!current->is_in_tmp_VTMS_transition(), "sanity check");
+  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/jvmtiRedefineClasses.cpp b/src/hotspot/share/prims/jvmtiRedefineClasses.cpp
--- a/src/hotspot/share/prims/jvmtiRedefineClasses.cpp
+++ b/src/hotspot/share/prims/jvmtiRedefineClasses.cpp
@@ -345,10 +345,13 @@
 
   // Cannot redefine or retransform a hidden class.
   if (InstanceKlass::cast(k)->is_hidden()) {
     return false;
   }
+  if (InstanceKlass::cast(k) == vmClasses::Object_klass()) {
+    return false;
+  }
   if (InstanceKlass::cast(k) == vmClasses::Continuation_klass()) {
     // Don't redefine Continuation class. See 8302779.
     return false;
   }
   return true;
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
@@ -661,10 +661,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/abstract_vm_version.hpp b/src/hotspot/share/runtime/abstract_vm_version.hpp
--- a/src/hotspot/share/runtime/abstract_vm_version.hpp
+++ b/src/hotspot/share/runtime/abstract_vm_version.hpp
@@ -182,10 +182,13 @@
   static bool supports_on_spin_wait() { return false; }
 
   // Does platform support fast class initialization checks for static methods?
   static bool supports_fast_class_init_checks() { return false; }
 
+  // Does platform support continuation preemption?
+  static bool supports_cont_preemption() { return false; }
+
   // Does platform support stack watermark barriers for concurrent stack processing?
   constexpr static bool supports_stack_watermark_barrier() { return false; }
 
   // Is recursive lightweight locking implemented for this platform?
   constexpr static bool supports_recursive_lightweight_locking() { return false; }
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
@@ -70,10 +70,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,127 @@
      THROW_MSG(vmSymbols::java_lang_IllegalStateException(), "pin underflow");
   }
 }
 JVM_END
 
+#if INCLUDE_JVMTI
+class JvmtiUnmountBeginMark : public StackObj {
+  Handle _vthread;
+  JavaThread* _target;
+  int _preempt_result;
+
+ public:
+  JvmtiUnmountBeginMark(JavaThread* t) :
+    _vthread(t, t->vthread()), _target(t), _preempt_result(freeze_pinned_native) {
+    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);
+    } 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; }
+};
+
+static bool is_safe_vthread_to_preempt_for_jvmti(JavaThread* target, oop vthread) {
+  assert(!target->has_pending_popframe(), "should be true; no support for vthreads yet");
+  JvmtiThreadState* state = target->jvmti_thread_state();
+  assert(state == nullptr || !state->is_earlyret_pending(), "should be true; no support for vthreads yet");
+
+  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
+
+static bool is_safe_vthread_to_preempt(JavaThread* target, oop vthread) {
+  if (!java_lang_VirtualThread::is_instance(vthread) ||                               // inside 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*, int);
+
+int Continuation::try_preempt(JavaThread* target, oop continuation, int preempt_kind) {
+  assert(target == JavaThread::current(), "no support for external preemption");
+  assert((preempt_kind == freeze_on_monitorenter && target->is_on_monitorenter()) ||
+         (preempt_kind == freeze_on_wait && target->current_waiting_monitor() != nullptr), "");
+  assert(target->has_last_Java_frame(), "");
+  assert(!target->preempting(), "");
+  assert(target->last_continuation() != nullptr, "");
+  assert(target->last_continuation()->cont_oop(target) == continuation, "");
+  assert(!is_continuation_preempted(continuation), "");
+  assert(Continuation::continuation_scope(continuation) == java_lang_VirtualThread::vthread_scope(), "");
+  assert(!target->has_pending_exception(), "");
+  assert(!target->is_suspended() || target->is_disable_suspend(), "");
+
+  if (!VM_Version::supports_cont_preemption()) {
+    return unsupported;
+  }
+
+  if (is_continuation_done(continuation)) {
+    return freeze_not_mounted;
+  }
+
+  // Continuation is mounted and it's not done so check if it's safe to preempt.
+  if (!is_safe_vthread_to_preempt(target, target->vthread())) {
+    return freeze_pinned_native;
+  }
+
+  JVMTI_ONLY(JvmtiUnmountBeginMark jubm(target);)
+  target->set_preempting(true);
+  int res = CAST_TO_FN_PTR(FreezeContFnT, freeze_preempt_entry())(target, target->last_Java_sp(), preempt_kind);
+  log_trace(continuations, preempt)("try_preempt: %d", res);
+  JVMTI_ONLY(jubm.set_preempt_result(res);)
+  if (res != freeze_ok) {
+    target->set_preempting(false);
+  }
+  return res;
+}
+
+bool Continuation::is_continuation_preempted(oop cont) {
+  return jdk_internal_vm_Continuation::is_preempted(cont);
+}
+
+bool Continuation::is_continuation_done(oop cont) {
+  return jdk_internal_vm_Continuation::done(cont);
+}
+
 #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
@@ -24,10 +24,11 @@
 
 #ifndef SHARE_VM_RUNTIME_CONTINUATION_HPP
 #define SHARE_VM_RUNTIME_CONTINUATION_HPP
 
 #include "jni.h"
+#include "classfile/javaClasses.hpp"
 #include "memory/allStatic.hpp"
 #include "oops/oopsHierarchy.hpp"
 
 class ContinuationEntry;
 class frame;
@@ -45,10 +46,28 @@
 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,
+  unsupported = 7
+};
+
+enum freeze_kind {
+  freeze_self_from_java  = 0,
+  freeze_on_monitorenter = 1,
+  freeze_on_wait         = 2
+};
+
 class Continuation : AllStatic {
 public:
 
   enum thaw_kind {
     thaw_top = 0,
@@ -67,18 +86,24 @@
   }
 
   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, int preempt_kind);
+
   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);
+  static bool is_continuation_preempted(oop cont);
+  static bool is_continuation_done(oop cont);
+
 
   static bool is_cont_barrier_frame(const frame& f);
   static bool is_return_barrier_entry(const address pc);
   static bool is_continuation_enterSpecial(const frame& f);
   static bool is_continuation_entry_frame(const frame& f, const RegisterMap *map);
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,24 @@
 #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;
 address ContinuationEntry::_return_pc = nullptr;
+address ContinuationEntry::_thaw_call_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;
 
   _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
@@ -54,10 +54,12 @@
   }
 #endif
 
 public:
   static int _return_pc_offset; // friend gen_continuation_enter
+  static int _thaw_call_pc_offset;
+  static address _thaw_call_pc;
   static void set_enter_code(nmethod* nm, int interpreted_entry_offset);
   static bool is_interpreted_call(address call_address);
 
 private:
   static address _return_pc;
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);
 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* thread, intptr_t* const sp, int freeze_kind = freeze_self_from_java);
 
 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, int preempt_kind) {
+    return freeze_internal<SelfT, true>(thread, sp, preempt_kind);
   }
 
   static intptr_t* thaw(JavaThread* thread, Continuation::thaw_kind kind) {
     return thaw_internal<SelfT>(thread, kind);
   }
@@ -291,29 +282,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());
@@ -321,10 +314,35 @@
 
   assert(thread->has_last_Java_frame(), "");
   assert(thread->last_frame().cb() != nullptr, "");
 }
 
+#ifdef ASSERT
+static int monitors_to_fix_on_stack(JavaThread* thread) {
+  ResourceMark rm(JavaThread::current());
+  ContinuationEntry* ce = thread->last_continuation();
+  RegisterMap map(thread,
+                  RegisterMap::UpdateMap::include,
+                  RegisterMap::ProcessFrames::include,
+                  RegisterMap::WalkContinuation::include);
+  map.set_include_argument_oops(false);
+  ResourceHashtable<oopDesc*, bool> rhtable;
+  int monitor_count = 0;
+  for (frame f = thread->last_frame(); Continuation::is_frame_in_continuation(thread, f); f = f.sender(&map)) {
+    if (f.is_interpreted_frame()) {
+      frame abs = !f.is_heap_frame() ? f : map.stack_chunk()->derelativize(f);
+      monitor_count += ContinuationHelper::InterpretedFrame::monitors_to_fix(thread, abs, rhtable, map.stack_chunk()());
+    } else if (f.is_compiled_frame()) {
+      monitor_count += ContinuationHelper::CompiledFrame::monitors_to_fix(thread, &map, f, rhtable);
+    } else if (f.is_native_frame()) {
+      monitor_count += ContinuationHelper::NativeFrame::monitors_to_fix(thread, f, rhtable);
+    }
+  }
+  return monitor_count;
+}
+#endif
+
 #if CONT_JFR
 class FreezeThawJfrInfo : public StackObj {
   short _e_size;
   short _e_num_interpreted_frames;
  public:
@@ -352,15 +370,22 @@
 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;
 
+  const bool _preempt;
+  // Used on preemption only
+  frame _last_frame;
+  oop _monitorenter_obj;
+
+  // Used to support freezing with held monitors
+  int _monitors_to_fix;
+  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;
@@ -376,21 +401,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:
@@ -408,33 +435,40 @@
   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 fix_monitors_in_interpreted_frame(frame& f);
+  template <typename RegisterMapT>
+  void fix_monitors_in_compiled_frame(frame& f, RegisterMapT* map);
+  void fix_monitors_in_fast_path();
+
   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);
@@ -444,21 +478,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(), "");
 
@@ -485,28 +519,167 @@
 #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_LEGACY) {
+    _monitors_to_fix = thread->held_monitor_count();
+    assert(_monitors_to_fix >= 0, "invariant");
+    _monitors_in_lockstack = 0;
+    assert(_thread->lock_stack().monitor_count() == 0, "should be set only for LM_LIGHTWEIGHT");
+  } else {
+    _monitors_in_lockstack = _thread->lock_stack().monitor_count();
+    assert(_monitors_in_lockstack >= 0 && _monitors_in_lockstack <= 8, "_monitors_in_lockstack=%d", _monitors_in_lockstack);
+    _monitors_to_fix = _monitors_in_lockstack;
+    assert(thread->held_monitor_count() == 0, "should be set only for LM_LEGACY");
+  }
+  DEBUG_ONLY(int verified_cnt = monitors_to_fix_on_stack(_thread);)
+  assert(verified_cnt == _monitors_to_fix || thread->obj_locker_count() > 0 ||
+         (LockingMode == LM_LIGHTWEIGHT && verified_cnt == _thread->lock_stack().unique_count()),
+         "wrong monitor count. Found %d in the stack but counter is %d", verified_cnt, _monitors_to_fix);
 }
 
 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::fix_monitors_in_interpreted_frame(frame& f) {
+  assert(LockingMode == LM_LEGACY, "invariant");
+  BasicObjectLock* first_mon = f.interpreter_frame_monitor_begin();
+  BasicObjectLock* last_mon = f.interpreter_frame_monitor_end();
+  assert(last_mon <= first_mon, "must be");
+
+  if (first_mon == last_mon) {
+    // No monitors in this frame
+    return;
+  }
+
+  for (BasicObjectLock* current = f.previous_monitor_in_interpreter_frame(first_mon);
+       current >= last_mon; current = f.previous_monitor_in_interpreter_frame(current)) {
+    oop obj = current->obj();
+    if (obj == nullptr) {
+      continue;
+    }
+    if (obj == _monitorenter_obj) {
+      assert(_preempt, "should be preemption on monitorenter case");
+      assert(obj->mark().monitor() != nullptr, "failed to acquire the lock but its not inflated");
+      continue;
+    }
+    markWord mark = obj->mark();
+    if (mark.has_monitor() && !mark.monitor()->is_owner_anonymous()) {
+      // Good for freezing, nothing to do.
+      assert(mark.monitor()->is_owner(_thread), "invariant");
+      continue;
+    }
+    // Found locked monitor that needs fixing. Inflate will fix the owner for stack-locked case or inflated but anonymously owned.
+    ObjectMonitor* om = ObjectSynchronizer::inflate(_thread, obj, ObjectSynchronizer::InflateCause::inflate_cause_cont_freeze);
+    assert(om != nullptr, "invariant");
+    assert(om->is_owner(_thread), "invariant");
+    if (--_monitors_to_fix == 0) break;
+  }
+  assert(_monitors_to_fix >= 0, "invariant");
+}
+
+template <typename RegisterMapT>
+void FreezeBase::fix_monitors_in_compiled_frame(frame& f, RegisterMapT* map) {
+  assert(LockingMode == LM_LEGACY, "invariant");
+  assert(!f.is_interpreted_frame(), "");
+  assert(ContinuationHelper::CompiledFrame::is_instance(f), "");
+
+  nmethod* nm = f.cb()->as_nmethod();
+
+  if (!nm->has_monitors()) {
+    // No monitors in this frame
+    return;
+  }
+
+  for (ScopeDesc* scope = nm->scope_desc_at(f.pc()); scope != nullptr; scope = scope->sender()) {
+    GrowableArray<MonitorValue*>* mons = scope->monitors();
+    if (mons == nullptr || mons->is_empty()) {
+      continue;
+    }
+
+    for (int index = (mons->length()-1); index >= 0; index--) { // see compiledVFrame::monitors()
+      MonitorValue* mon = mons->at(index);
+      if (mon->eliminated()) {
+        continue; // we ignore eliminated monitors
+      }
+      ScopeValue* ov = mon->owner();
+      StackValue* owner_sv = StackValue::create_stack_value(&f, map, ov); // it is an oop
+      oop obj = owner_sv->get_obj()();
+      if (obj == nullptr) {
+        continue;
+      }
+      if (obj == _monitorenter_obj) {
+        assert(_preempt, "should be preemption on monitorenter case");
+        assert(obj->mark().monitor() != nullptr, "failed to acquire the lock but its not inflated");
+        continue;
+      }
+      markWord mark = obj->mark();
+      if (mark.has_monitor() && !mark.monitor()->is_owner_anonymous()) {
+        // Good for freezing, nothing to do.
+        assert(mark.monitor()->is_owner(_thread), "invariant");
+        continue;
+      }
+      // Found locked monitor that needs fixing. Inflate will fix the owner for stack-locked case or inflated but anonymously owned.
+      ObjectMonitor* om = ObjectSynchronizer::inflate(_thread, obj, ObjectSynchronizer::InflateCause::inflate_cause_cont_freeze);
+      assert(om != nullptr, "invariant");
+      assert(om->is_owner(_thread), "invariant");
+      if (--_monitors_to_fix == 0) break;
+    }
+  }
+  assert(_monitors_to_fix >= 0, "invariant");
+}
+
+void FreezeBase::fix_monitors_in_fast_path() {
+  if (LockingMode == LM_LIGHTWEIGHT) {
+    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);
+
+    DEBUG_ONLY(_monitors_to_fix -= _monitors_in_lockstack;)
+  } else {
+    assert(LockingMode == LM_LEGACY, "invariant");
+    _monitorenter_obj = _thread->is_on_monitorenter() ? _thread->current_pending_monitor()->object() : nullptr;
+
+    ResourceMark rm(_thread);
+    RegisterMap map(JavaThread::current(),
+                  RegisterMap::UpdateMap::include,
+                  RegisterMap::ProcessFrames::skip, // already processed in unwind_frames()
+                  RegisterMap::WalkContinuation::skip);
+    map.set_include_argument_oops(false);
+    frame freezed_top(_cont_stack_top);
+    frame first = !_preempt ? freezed_top : freezed_top.sender(&map);
+    for (frame f = first; _monitors_to_fix > 0 && Continuation::is_frame_in_continuation(_cont.entry(), f); f = f.sender(&map)) {
+      fix_monitors_in_compiled_frame(f, &map);
+    }
+  }
+  assert(_monitors_to_fix == 0, "missing monitors in stack");
+  assert(_thread->held_monitor_count() == 0, "should be 0 now");
+}
+
 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);)
 
@@ -533,11 +706,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;
@@ -566,10 +739,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;
@@ -647,11 +822,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));
@@ -676,11 +851,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
@@ -713,13 +888,29 @@
   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, 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()));
+  }
+
+  // Fix monitors after unwinding frames to make sure oops are valid.
+  if (_monitors_to_fix > 0) {
+    fix_monitors_in_fast_path();
+  }
 
   _cont.write();
 
   log_develop_trace(continuations)("FREEZE CHUNK #" INTPTR_FORMAT " (young)", _cont.hash());
   LogTarget(Trace, continuations) lt;
@@ -775,53 +966,44 @@
   freeze_result res = recurse_freeze(f, caller, 0, false, true);
 
   if (res == freeze_ok) {
     finish_freeze(f, caller);
     _cont.write();
+    assert(_monitors_to_fix == 0, "missing monitors in stack");
+    assert(_thread->held_monitor_count() == 0, "should be 0 now");
   }
 
   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;
   }
 
@@ -830,19 +1012,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;
   }
 }
 
@@ -899,10 +1077,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);
 
@@ -941,10 +1120,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());
@@ -995,23 +1176,47 @@
   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);
   }
 
+  // Fix monitors after unwinding frames to make sure oops are valid. Also do it now to avoid unnecessary
+  // calls to fix_monitors_in_interpreted_frame/fix_monitors_in_compiled_frame() later.
+  if (_monitors_to_fix > 0) {
+    if (_monitors_in_lockstack > 0) {
+      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);
+
+      _monitors_to_fix -= _monitors_in_lockstack;
+      assert(_monitors_to_fix == 0, "invariant");
+    }
+    _monitorenter_obj = _thread->is_on_monitorenter() ? _thread->current_pending_monitor()->object() : nullptr;
+  }
+
   // 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);)
 
@@ -1027,11 +1232,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;
 }
@@ -1094,12 +1299,13 @@
   // 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);
-  // we'd rather not yield inside methods annotated with @JvmtiMountTransition
-  assert(!ContinuationHelper::Frame::frame_method(f)->jvmti_mount_transition(), "");
+  // we'd rather not yield inside methods annotated with @JvmtiMountTransition. In the preempt case
+  // we already checked it is safe to do so in Continuation::is_safe_vthread_to_preempt().
+  assert(!ContinuationHelper::Frame::frame_method(f)->jvmti_mount_transition() || _preempt, "");
 
   freeze_result result = recurse_freeze_java_frame<ContinuationHelper::InterpretedFrame>(f, caller, fsize, argsize);
   if (UNLIKELY(result > freeze_ok_bottom)) {
     return result;
   }
@@ -1130,10 +1336,15 @@
   caller = hf;
 
   // Mark frame_method's GC epoch for class redefinition on_stack calculation.
   frame_method->record_gc_epoch();
 
+  if (_monitors_to_fix > 0) {
+    // Check if we have monitors in this frame
+    fix_monitors_in_interpreted_frame(f);
+  }
+
   return freeze_ok;
 }
 
 // The parameter callee_argsize includes metadata that has to be part of caller/callee overlap.
 // See also StackChunkFrameStream<frame_kind>::frame_size()
@@ -1149,12 +1360,13 @@
 
   log_develop_trace(continuations)("recurse_freeze_compiled_frame %s _size: %d fsize: %d argsize: %d",
                              ContinuationHelper::Frame::frame_method(f) != nullptr ?
                              ContinuationHelper::Frame::frame_method(f)->name_and_sig_as_C_string() : "",
                              _freeze_size, fsize, argsize);
-  // we'd rather not yield inside methods annotated with @JvmtiMountTransition
-  assert(!ContinuationHelper::Frame::frame_method(f)->jvmti_mount_transition(), "");
+  // we'd rather not yield inside methods annotated with @JvmtiMountTransition. In the preempt case
+  // we already checked it is safe to do so in Continuation::is_safe_vthread_to_preempt().
+  assert(!ContinuationHelper::Frame::frame_method(f)->jvmti_mount_transition() || _preempt, "");
 
   freeze_result result = recurse_freeze_java_frame<ContinuationHelper::CompiledFrame>(f, caller, fsize, argsize);
   if (UNLIKELY(result > freeze_ok_bottom)) {
     return result;
   }
@@ -1179,52 +1391,95 @@
 
   assert(is_bottom_frame || Interpreter::contains(ContinuationHelper::CompiledFrame::real_pc(caller)) == caller.is_interpreted_frame(), "");
 
   DEBUG_ONLY(after_freeze_java_frame(hf, is_bottom_frame);)
   caller = hf;
+
+  if (_monitors_to_fix > 0) {
+    // Check if we have monitors in this frame
+    fix_monitors_in_compiled_frame(f, SmallRegisterMap::instance);
+  }
+
   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");
+  assert(_thread->is_on_monitorenter(), "");
+
+  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
+  bool is_bottom_frame = result == freeze_ok_bottom;
+  assert(!caller.is_empty() || (is_bottom_frame && !_cont.is_empty()), "");
+
+  DEBUG_ONLY(before_freeze_java_frame(f, caller, fsize, 0, 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, is_bottom_frame);
+
+  DEBUG_ONLY(after_freeze_java_frame(hf, 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(_thread->is_on_monitorenter(), "");
+    // 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);
+  const int fsize = f.cb()->frame_size();
+
+  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, 0);
   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, 0 /* 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;
 }
 
@@ -1243,10 +1498,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.
@@ -1272,10 +1529,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(), "");
 }
 
@@ -1295,10 +1553,11 @@
 class StackChunkAllocator : public MemAllocator {
   const size_t                                 _stack_size;
   int                                          _argsize_md;
   ContinuationWrapper&                         _continuation_wrapper;
   JvmtiSampledObjectAllocEventCollector* const _jvmti_event_collector;
+  JavaThread* const                            _target;
   mutable bool                                 _took_slow_path;
 
   // Does the minimal amount of initialization needed for a TLAB allocation.
   // We don't need to do a full initialization, as such an allocation need not be immediately walkable.
   virtual oop initialize(HeapWord* mem) const override {
@@ -1306,10 +1565,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);
@@ -1331,23 +1591,27 @@
 
     oop obj = initialize(mem);
     return stackChunkOopDesc::cast(obj);
   }
 
+  bool is_preempt() const { return _thread != _target; }
+
 public:
   StackChunkAllocator(Klass* klass,
                       size_t word_size,
                       Thread* thread,
                       size_t stack_size,
                       int argsize_md,
                       ContinuationWrapper& continuation_wrapper,
-                      JvmtiSampledObjectAllocEventCollector* jvmti_event_collector)
+                      JvmtiSampledObjectAllocEventCollector* jvmti_event_collector,
+                      JavaThread* target)
     : MemAllocator(klass, word_size, thread),
       _stack_size(stack_size),
       _argsize_md(argsize_md),
       _continuation_wrapper(continuation_wrapper),
       _jvmti_event_collector(jvmti_event_collector),
+      _target(target),
       _took_slow_path(false) {}
 
   // Provides it's own, specialized allocation which skips instrumentation
   // if the memory can be allocated without going to a slow-path.
   stackChunkOop allocate() const {
@@ -1397,11 +1661,11 @@
   //
   // This might safepoint while allocating, but all safepointing due to
   // instrumentation have been deferred. This property is important for
   // some GCs, as this ensures that the allocated object is in the young
   // generation / newly allocated memory.
-  StackChunkAllocator allocator(klass, size_in_words, current, stack_size, argsize_md, _cont, _jvmti_event_collector);
+  StackChunkAllocator allocator(klass, size_in_words, current, stack_size, argsize_md, _cont, _jvmti_event_collector, _thread);
   stackChunkOop chunk = allocator.allocate();
 
   if (chunk == nullptr) {
     return nullptr; // OOME
   }
@@ -1414,10 +1678,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());
 
@@ -1486,28 +1752,55 @@
     ContinuationWrapper::SafepointOp so(Thread::current(), cont);
     JvmtiExport::continuation_yield_cleanup(JavaThread::current(), num_frames);
   }
   invalidate_jvmti_stack(thread);
 }
-#endif // INCLUDE_JVMTI
 
-#ifdef ASSERT
-static bool monitors_on_stack(JavaThread* thread) {
-  ContinuationEntry* ce = thread->last_continuation();
-  RegisterMap map(thread,
-                  RegisterMap::UpdateMap::include,
-                  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))) {
-      return true;
+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());
+    {
+      MutexLocker mu(JvmtiThreadState_lock);
+      JvmtiThreadState* state = current->jvmti_thread_state();
+      if (state != NULL && state->is_pending_interp_only_mode()) {
+        JvmtiEventController::enter_interp_only_mode(state);
+      }
     }
-  }
-  return false;
+    assert(current->is_in_VTMS_transition(), "sanity check");
+    assert(!current->is_in_tmp_VTMS_transition(), "sanity check");
+    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
 
 // There are no interpreted frames if we're not called from the interpreter and we haven't ancountered an i2c
 // adapter or called Deoptimization::unpack_frames. As for native frames, upcalls from JNI also go through the
 // interpreter (see JavaCalls::call_helper), while the UpcallLinker explicitly sets cont_fastpath.
 bool FreezeBase::check_valid_fast_path() {
@@ -1515,27 +1808,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)) {
@@ -1543,19 +1834,33 @@
     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 inline int freeze_internal(JavaThread* current, intptr_t* const sp) {
+static int preempt_epilog(ContinuationWrapper& cont, freeze_result res, frame& old_last_frame, int freeze_kind) {
+  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.set_preempted(true);
+  cont.tail()->set_preempt_kind(freeze_kind);
+
+  return freeze_epilog(cont);
+}
+
+template<typename ConfigT, bool preempt>
+static inline int freeze_internal(JavaThread* current, intptr_t* const sp, int freeze_kind) {
   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;)
 
@@ -1569,39 +1874,47 @@
   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),
-         "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());
-
-  if (entry->is_pinned() || current->held_monitor_count() > 0) {
+  const bool pinned_monitor = NOT_LOOM_MONITOR_SUPPORT(current->held_monitor_count() > 0) LOOM_MONITOR_SUPPORT_ONLY(current->jni_monitor_count() > 0);
+  if (entry->is_pinned() || pinned_monitor) {
     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(), freeze_kind);
+  }
+
+  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(), freeze_kind);
+    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);
 
@@ -1677,10 +1990,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 push_preempt_rerun_adapter)
   return size;
 }
 
 // make room on the stack for thaw
 // returns the size in bytes, or 0 on failure
@@ -1757,15 +2071,20 @@
 
   // 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, int preempt_kind, bool fast_case);
+  inline intptr_t* push_resume_adapter(frame& top);
+  inline intptr_t* push_resume_monitor_operation(stackChunkOop chunk);
+  inline void fix_native_wrapper_return_pc_pd(frame& top);
+  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();
 
@@ -1775,16 +2094,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);
@@ -1805,10 +2125,11 @@
            && !PreserveFramePointer;
   }
 
   inline intptr_t* thaw(Continuation::thaw_kind kind);
   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) {
@@ -1820,11 +2141,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;
@@ -1860,17 +2181,31 @@
   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();
+  bool is_stub = f.is_stub();
 
   f.next(SmallRegisterMap::instance, true /* stop */);
   empty = f.is_done();
   assert(!empty || argsize == chunk->argsize(), "");
 
+  assert(!is_stub || !empty, "runtime stub should have caller frame");
+  if (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.get_cb();
+    frame_size += f.cb()->frame_size();
+    argsize = f.stack_argsize();
+    f.next(SmallRegisterMap::instance, true /* stop */);
+    empty = f.is_done();
+    assert(!empty || argsize == chunk->argsize(), "");
+  }
+
   if (empty) {
     clear_chunk(chunk);
   } else {
     chunk->set_sp(chunk->sp() + frame_size);
     chunk->set_max_thawing_size(chunk->max_thawing_size() - frame_size);
@@ -1997,15 +2332,68 @@
 
 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) {
+  int preempt_kind;
+  bool retry_fast_path = false;
+
+  bool preempted_case = _cont.is_preempted();
+  if (preempted_case) {
+    if (chunk->object_waiter() != nullptr) {
+      assert(chunk->current_pending_monitor() != nullptr || chunk->current_waiting_monitor() != nullptr, "");
+      return push_resume_monitor_operation(chunk);
+    }
+    retry_fast_path = true;
+    relativize_chunk_concurrently(chunk);
+    // Clear FLAGS_PREEMPTED after relativize_derived_pointers_concurrently()
+    // is called to avoid racing with GC threads while modifying the flags.
+    preempt_kind = chunk->get_and_clear_preempt_kind();
+  } else {
+    relativize_chunk_concurrently(chunk);
+  }
+
+  // First thaw after freeze. If there were oops in the stacklock
+  // during freeze, restore them now.
+  if (chunk->lockstack_size() > 0) {
+    assert(kind == Continuation::thaw_top || preempted_case, "");
+    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 FLAGS_PREEMPTED flags from the stackChunk.
+  if (retry_fast_path && can_thaw_fast(chunk)) {
+    intptr_t* sp = thaw_fast(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;
@@ -2015,11 +2403,12 @@
   }
 #endif
 
   DEBUG_ONLY(_frames = 0;)
   _align_size = 0;
-  int num_frames = (return_barrier ? 1 : 2);
+  bool is_return_barrier = kind != Continuation::thaw_top;
+  int num_frames = (is_return_barrier ? 1 : 2);
 
   _stream = StackChunkFrameStream<ChunkFrames::Mixed>(chunk);
   _top_unextended_sp_before_thaw = _stream.unextended_sp();
 
   frame heap_frame = _stream.to_frame();
@@ -2028,40 +2417,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, nullptr);
   }
 
-#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(if (!return_barrier) invalidate_jvmti_stack(_thread));
+  JVMTI_ONLY(if (!is_return_barrier) 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);
   }
@@ -2087,11 +2473,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) {
@@ -2177,10 +2563,67 @@
   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, int preempt_kind, bool fast_case) {
+  assert(preempt_kind == freeze_on_wait || preempt_kind == freeze_on_monitorenter, "");
+  frame top(sp);
+  assert(top.pc() == *(address*)(sp - frame::sender_sp_ret_address_offset()), "");
+
+  assert(_cont.is_preempted(), "must be");
+  _cont.set_preempted(false);
+
+#if INCLUDE_JVMTI
+  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 == freeze_on_wait) {
+    if (_thread->pending_interrupted_exception()) {
+      throw_interrupted_exception(_thread, top);
+      _thread->set_pending_interrupted_exception(false);
+    }
+    // Possibly update return pc on the top native wrapper frame so
+    // that we resume execution at the right instruction.
+    fix_native_wrapper_return_pc_pd(top);
+  } 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;
+  }
+  sp = push_resume_adapter(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);
@@ -2220,11 +2663,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) {
@@ -2237,11 +2682,11 @@
   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(hf.is_compiled_frame(), "");
   assert(_cont.is_preempted() || !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);
   }
@@ -2251,11 +2696,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.
@@ -2287,11 +2732,10 @@
     maybe_set_fastpath(f.sp());
   } else if (_thread->is_interp_only_mode()
               || (_cont.is_preempted() && 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
@@ -2314,54 +2758,122 @@
   caller = f;
 }
 
 void ThawBase::recurse_thaw_stub_frame(const frame& hf, frame& caller, int num_frames) {
   DEBUG_ONLY(_frames++;)
+  bool is_bottom_frame = false;
 
-  {
+  if (UNLIKELY(seen_by_gc())) {
+    // Process the stub's caller here since we might need the full map (if the stub was
+    // generated on a poll on return we shouldn't need a 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
+    if (!_stream.is_done()) {
       _cont.tail()->do_barriers<stackChunkOopDesc::BarrierType::Store>(_stream, &map);
     }
-    assert(!_stream.is_done(), "");
+  } else {
+    _stream.next(SmallRegisterMap::instance);
   }
 
-  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);)
+  if (_stream.is_done()) {
+    finalize_thaw(caller, 0);
+    is_bottom_frame = true;
+  } else {
+    frame f = _stream.to_frame();
+    if (f.is_interpreted_frame()) {
+      recurse_thaw_interpreted_frame(f, caller, num_frames);
+    } else {
+      recurse_thaw_compiled_frame(f, caller, num_frames, true);
+    }
+  }
 
-  assert(ContinuationHelper::Frame::is_stub(hf.cb()), "");
+  assert(!is_bottom_frame || !_cont.is_empty(), "");
   assert(caller.sp() == caller.unextended_sp(), "");
-  assert(!caller.is_interpreted_frame(), "");
+  assert(!caller.is_native_frame() || (is_bottom_frame && Continuation::is_continuation_enterSpecial(caller)), "caller should't be native except for enterSpecial case");
 
-  int fsize = ContinuationHelper::StubFrame::size(hf);
+  DEBUG_ONLY(before_thaw_java_frame(hf, caller, is_bottom_frame, num_frames);)
+
+  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 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)
+  patch(f, caller, is_bottom_frame);
+
+  if (!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.clear();
+                    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);)
+  DEBUG_ONLY(after_thaw_java_frame(f, 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(_cont.is_preempted() && 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);
+  }
+
+  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_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.
+  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();
@@ -2448,30 +2960,11 @@
 
   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, nullptr);
-  }
-#endif
+  DEBUG_ONLY(log_frames_after_thaw(thread, cont, sp);)
 
   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());
@@ -2565,18 +3058,18 @@
   }
   return true;
 }
 
 static void log_frames(JavaThread* thread) {
-  const static int show_entry_callers = 3;
+  const static int show_entry_callers = 100;
   LogTarget(Trace, continuations) lt;
   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,
@@ -2596,22 +3089,87 @@
     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) {
+  intptr_t* sp0 = sp;
+  address pc0 = *(address*)(sp - frame::sender_sp_ret_address_offset());
+  bool use_cont_entry = false;
+
+  // Preemption cases need to use and adjusted version of sp.
+  if (pc0 == Interpreter::cont_resume_interpreter_adapter() ||
+      pc0 == StubRoutines::cont_resume_compiler_adapter()) {
+    // Resuming after being preempted case. We skip the adapter pushed
+    // into the stack (see push_preempt_rerun_adapter()).
+    sp0 += frame::metadata_words;
+
+    if (pc0 == StubRoutines::cont_resume_compiler_adapter()) {
+      address pc1 = *(address*)(sp0 - frame::sender_sp_ret_address_offset());
+      if (pc1 == SharedRuntime::native_frame_resume_entry()) {
+        // When top is the compiled native wrapper (Object.wait()) the pc
+        // would have been modified from its original value to return to
+        // the correct place. But that means we won't find the oopMap for
+        // that fixed pc when getting the sender which will trigger asserts.
+        // So just start walking the frames from the sender instead.
+        CodeBlob* cb = CodeCache::find_blob(pc1);
+        assert(cb->as_nmethod()->method()->is_object_wait0(), "");
+        sp0 += cb->frame_size();
+        if (sp0 == cont.entrySP()) {
+          // sp0[-1] will be the return barrier pc. This is a stub, i.e. associated
+          // codeblob has _frame_size = 0. Force using cont.entryPC() to avoid
+          // asserts while trying to get the sender frame.
+          use_cont_entry = true;
+        }
+      }
+#ifdef AARCH64
+      else {
+        // Monitorenter case returning to c2 runtime stub requires extra
+        // adjustment on aarch64 (see push_preempt_rerun_adapter()).
+        address pc1 = *(address*)(sp0 - frame::sender_sp_ret_address_offset());
+        CodeBlob* cb = CodeCache::find_blob(pc1);
+        assert(cb != nullptr, "should be either c1 or c2 runtime stub");
+        if (cb->frame_size() == 2) {
+          sp0 += frame::metadata_words;
+        }
+      }
+#endif
+    }
+  } else if (pc0 == StubRoutines::cont_resume_monitor_operation()) {
+    // Redoing monitor operation after being preempted case. We skip
+    // the adapter + data pushed into the stack (see push_resume_monitor_operation()).
+    use_cont_entry = true;
+  }
+
+  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, nullptr);
+  }
+}
 #endif // ASSERT
 
 #include CPU_HEADER_INLINE(continuationFreezeThaw)
 
 #ifdef ASSERT
@@ -2628,26 +3186,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() {
@@ -2677,10 +3240,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
@@ -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);
@@ -97,13 +99,11 @@
   static void patch_sender_sp(frame& f, const frame& caller);
 
   static int size(const frame& f);
   static inline int expression_stack_size(const frame &f, InterpreterOopMap* mask);
 
-#ifdef ASSERT
-  static bool is_owning_locks(const frame& f);
-#endif
+  static int monitors_to_fix(JavaThread* thread, const frame& f, ResourceHashtable<oopDesc*, bool> &table, stackChunkOop chunk) NOT_DEBUG_RETURN0;
 
   static bool is_instance(const frame& f);
 
   typedef InterpreterOopMap* ExtraT;
 };
@@ -125,19 +125,28 @@
 
 class ContinuationHelper::CompiledFrame : public ContinuationHelper::NonInterpretedFrame {
 public:
   static bool is_instance(const frame& f);
 
-#ifdef ASSERT
   template <typename RegisterMapT>
-  static bool is_owning_locks(JavaThread* thread, RegisterMapT* map, const frame& f);
-#endif
+  static int monitors_to_fix(JavaThread* thread, RegisterMapT* map, const frame& f, ResourceHashtable<oopDesc*, bool> &table) NOT_DEBUG_RETURN0;
+};
+
+class ContinuationHelper::NativeFrame : public ContinuationHelper::NonInterpretedFrame {
+public:
+  static const bool native = true;
+
+  static bool is_instance(const frame& f);
+
+  static int monitors_to_fix(JavaThread* thread, const frame& f, ResourceHashtable<oopDesc*, bool> &table) NOT_DEBUG_RETURN0;
+  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,10 +30,11 @@
 #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 "utilities/macros.hpp"
 
 #include CPU_HEADER_INLINE(continuationHelper)
 
@@ -51,11 +52,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();
 }
@@ -106,26 +107,54 @@
   assert(size <= f.interpreter_frame_expression_stack_size(), "size1: %d size2: %d", size, f.interpreter_frame_expression_stack_size());
   return size;
 }
 
 #ifdef ASSERT
-inline bool ContinuationHelper::InterpretedFrame::is_owning_locks(const frame& f) {
-  assert(f.interpreter_frame_monitor_end() <= f.interpreter_frame_monitor_begin(), "must be");
-  if (f.interpreter_frame_monitor_end() == f.interpreter_frame_monitor_begin()) {
-    return false;
+inline int ContinuationHelper::InterpretedFrame::monitors_to_fix(JavaThread* thread, const frame& f, ResourceHashtable<oopDesc*, bool> &table, stackChunkOop chunk) {
+  BasicObjectLock* first_mon = f.interpreter_frame_monitor_begin();
+  BasicObjectLock* last_mon = f.interpreter_frame_monitor_end();
+  assert(last_mon <= first_mon, "must be");
+
+  if (first_mon == last_mon) {
+    // No monitors in this frame
+    return 0;
   }
 
-  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)) {
+  int monitor_count = 0;
+  oop monitorenter_oop = thread->is_on_monitorenter() ? thread->current_pending_monitor()->object() : nullptr;
 
-      oop obj = current->obj();
-      if (obj != nullptr) {
-        return true;
+  for (BasicObjectLock* current = f.previous_monitor_in_interpreter_frame(first_mon);
+       current >= last_mon; current = f.previous_monitor_in_interpreter_frame(current)) {
+    oop* obj_adr = current->obj_adr();
+
+    oop obj;
+    if (f.is_heap_frame()) {
+      assert(chunk != nullptr, "null stackChunk");
+      obj = chunk->has_bitmap() && UseCompressedOops ? chunk->load_oop((narrowOop*)obj_adr) : chunk->load_oop(obj_adr);
+    } else {
+      // We have already processed oops when getting this frame.
+      obj = *obj_adr;
+    }
+    assert(obj == nullptr || dbg_is_good_oop(obj), "obj_adr: " PTR_FORMAT " obj: " PTR_FORMAT, p2i(obj_adr), p2i(obj));
+
+    if (obj != nullptr && obj != monitorenter_oop) {
+      markWord mark = obj->mark();
+      if (mark.has_monitor() && !mark.monitor()->is_owner_anonymous()) {
+        // Nothing to do
+        assert(mark.monitor()->is_owner(thread), "invariant");
+        continue;
+      }
+      assert(!f.is_heap_frame() || LockingMode == LM_LIGHTWEIGHT,
+             "monitors found on heap frame that need to be fixed should only be those saved in the LockStack");
+      bool created;
+      table.put_if_absent(obj, true, &created);
+      if (created) {
+        monitor_count++;
       }
+    }
   }
-  return false;
+  return monitor_count;
 }
 #endif
 
 inline intptr_t* ContinuationHelper::InterpretedFrame::frame_top(const frame& f) { // inclusive; this will be copied with the frame
   return f.unextended_sp();
@@ -154,46 +183,100 @@
 
 inline bool ContinuationHelper::CompiledFrame::is_instance(const frame& f) {
   return f.is_compiled_frame();
 }
 
+inline bool ContinuationHelper::NativeFrame::is_instance(const frame& f) {
+  return f.is_native_frame();
+}
+
 #ifdef ASSERT
 template<typename RegisterMapT>
-bool ContinuationHelper::CompiledFrame::is_owning_locks(JavaThread* thread, RegisterMapT* map, const frame& f) {
+int ContinuationHelper::CompiledFrame::monitors_to_fix(JavaThread* thread, RegisterMapT* map, const frame& f, ResourceHashtable<oopDesc*, bool> &table) {
   assert(!f.is_interpreted_frame(), "");
   assert(CompiledFrame::is_instance(f), "");
 
   nmethod* nm = f.cb()->as_nmethod();
-  assert(!nm->is_native_method(), ""); // See compiledVFrame::compiledVFrame(...) in vframe_hp.cpp
+  assert(!nm->is_nmethod() || !nm->as_nmethod()->is_native_method(), ""); // See compiledVFrame::compiledVFrame(...) in vframe_hp.cpp
 
   if (!nm->has_monitors()) {
-    return false;
+    // No monitors in this frame
+    return 0;
   }
 
-  frame::update_map_with_saved_link(map, Frame::callee_link_address(f)); // the monitor object could be stored in the link register
-  ResourceMark rm;
+  int monitor_count = 0;
+  oop monitorenter_oop = thread->is_on_monitorenter() ? thread->current_pending_monitor()->object() : nullptr;
+
   for (ScopeDesc* scope = nm->scope_desc_at(f.pc()); scope != nullptr; scope = scope->sender()) {
     GrowableArray<MonitorValue*>* mons = scope->monitors();
     if (mons == nullptr || mons->is_empty()) {
       continue;
     }
 
     for (int index = (mons->length()-1); index >= 0; index--) { // see compiledVFrame::monitors()
       MonitorValue* mon = mons->at(index);
       if (mon->eliminated()) {
-        continue; // we ignore scalar-replaced monitors
+        continue; // we ignore eliminated monitors
       }
+
       ScopeValue* ov = mon->owner();
       StackValue* owner_sv = StackValue::create_stack_value(&f, map, ov); // it is an oop
       oop owner = owner_sv->get_obj()();
-      if (owner != nullptr) {
-        //assert(nm->has_monitors(), "");
-        return true;
+      if (owner != nullptr && owner != monitorenter_oop) {
+        markWord mark = owner->mark();
+        if (mark.has_monitor() && !mark.monitor()->is_owner_anonymous()) {
+          // Nothing to do
+          assert(mark.monitor()->is_owner(thread), "invariant");
+          continue;
+        }
+        assert(!f.is_heap_frame() || LockingMode == LM_LIGHTWEIGHT,
+             "monitors found on heap frame that need to be fixed should only be those saved in the LockStack");
+        bool created;
+        table.put_if_absent(owner, true, &created);
+        if (created) {
+          monitor_count++;
+        }
       }
     }
   }
-  return false;
+  return monitor_count;
+}
+
+inline int ContinuationHelper::NativeFrame::monitors_to_fix(JavaThread* thread, const frame& f, ResourceHashtable<oopDesc*, bool> &table) {
+  assert(NativeFrame::is_instance(f), "");
+
+  Method* method = f.cb()->as_nmethod()->method();
+  if (!method->is_synchronized()) {
+    return 0;
+  }
+
+  oop synced_obj = f.get_native_receiver();
+  oop monitorenter_oop = thread->is_on_monitorenter() ? thread->current_pending_monitor()->object() : nullptr;
+
+  bool is_first_frame = f.sp() == thread->last_Java_sp();
+  if (!is_first_frame) {
+    assert(ObjectSynchronizer::current_thread_holds_lock(thread, Handle(thread, synced_obj)), "must be owner");
+    assert(monitorenter_oop == nullptr || monitorenter_oop != synced_obj, "owner already, should not be contended");
+
+    markWord mark = synced_obj->mark();
+    if (mark.has_monitor() && !mark.monitor()->is_owner_anonymous()) {
+      // Nothing to do
+      assert(mark.monitor()->is_owner(thread), "invariant");
+      return 0;
+    }
+    assert(!f.is_heap_frame(), "native frame on the heap???");
+    bool created;
+    table.put_if_absent(synced_obj, true, &created);
+    if (created) {
+      return 1;
+    }
+  } else {
+    assert(thread->is_on_monitorenter() && monitorenter_oop != nullptr && monitorenter_oop == synced_obj,
+           "should be freeze case due to preempt on monitorenter contention");
+    assert(!ObjectSynchronizer::current_thread_holds_lock(thread, Handle(thread, synced_obj)), "should not be owner");
+  }
+  return 0;
 }
 #endif
 
 inline bool ContinuationHelper::StubFrame::is_instance(const frame& f) {
   return !f.is_interpreted_frame() && is_stub(f.cb());
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
@@ -63,11 +63,11 @@
   macro(_parent_offset,    k, vmSymbols::parent_name(),    continuation_signature,      false); \
   macro(_yieldInfo_offset, k, vmSymbols::yieldInfo_name(), object_signature,            false); \
   macro(_tail_offset,      k, vmSymbols::tail_name(),      stackchunk_signature,        false); \
   macro(_mounted_offset,   k, vmSymbols::mounted_name(),   bool_signature,              false); \
   macro(_done_offset,      k, vmSymbols::done_name(),      bool_signature,              false); \
-  macro(_preempted_offset, k, "preempted",                 bool_signature,              false);
+  macro(_preempted_offset, k, vmSymbols::preempted_name(), bool_signature,              false);
 
 void jdk_internal_vm_Continuation::compute_offsets() {
   InstanceKlass* k = vmClasses::Continuation_klass();
   CONTINUATION_FIELDS_DO(FIELD_COMPUTE_OFFSET);
 }
@@ -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
@@ -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
@@ -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/continuationWrapper.inline.hpp b/src/hotspot/share/runtime/continuationWrapper.inline.hpp
--- a/src/hotspot/share/runtime/continuationWrapper.inline.hpp
+++ b/src/hotspot/share/runtime/continuationWrapper.inline.hpp
@@ -113,10 +113,11 @@
   oop continuation()                 { return _continuation; }
   stackChunkOop tail() const         { return _tail; }
   void set_tail(stackChunkOop chunk) { _tail = chunk; }
 
   inline bool is_preempted();
+  inline void set_preempted(bool value);
   inline void read();
   inline void write();
 
   NOT_PRODUCT(intptr_t hash();)
 
@@ -161,10 +162,14 @@
 
 inline bool ContinuationWrapper::is_preempted() {
   return jdk_internal_vm_Continuation::is_preempted(_continuation);
 }
 
+inline void ContinuationWrapper::set_preempted(bool value) {
+  jdk_internal_vm_Continuation::set_preempted(_continuation, value);
+}
+
 inline void ContinuationWrapper::read() {
   _tail  = jdk_internal_vm_Continuation::tail(_continuation);
 }
 
 inline void ContinuationWrapper::write() {
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
@@ -117,10 +117,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);
@@ -1611,10 +1615,11 @@
 
 // relock objects for which synchronization was eliminated
 bool Deoptimization::relock_objects(JavaThread* thread, GrowableArray<MonitorInfo*>* monitors,
                                     JavaThread* deoptee_thread, frame& fr, int exec_mode, bool realloc_failures) {
   bool relocked_objects = false;
+  LOOM_MONITOR_SUPPORT_ONLY(int compensate_extra_increment = 0;)
   for (int i = 0; i < monitors->length(); i++) {
     MonitorInfo* mon_info = monitors->at(i);
     if (mon_info->eliminated()) {
       assert(!mon_info->owner_is_scalar_replaced() || realloc_failures, "reallocation was missed");
       relocked_objects = true;
@@ -1626,10 +1631,11 @@
             // With exec_mode == Unpack_none obj may be thread local and locked in
             // a callee frame. Make the lock in the callee a recursive lock and restore the displaced header.
             markWord dmw = mark.displaced_mark_helper();
             mark.locker()->set_displaced_header(markWord::encode((BasicLock*) nullptr));
             obj->set_mark(dmw);
+            LOOM_MONITOR_SUPPORT_ONLY(compensate_extra_increment++;)
           }
           if (mark.has_monitor()) {
             // defer relocking if the deoptee thread is currently waiting for obj
             ObjectMonitor* waiting_monitor = deoptee_thread->current_waiting_monitor();
             if (waiting_monitor != nullptr && waiting_monitor->object() == obj()) {
@@ -1645,19 +1651,20 @@
           // Inflate the locks instead. Enter then inflate to avoid races with
           // deflation.
           ObjectSynchronizer::enter_for(obj, nullptr, deoptee_thread);
           assert(mon_info->owner()->is_locked(), "object must be locked now");
           ObjectMonitor* mon = ObjectSynchronizer::inflate_for(deoptee_thread, obj(), ObjectSynchronizer::inflate_cause_vm_internal);
-          assert(mon->owner() == deoptee_thread, "must be");
+          assert(mon->is_owner(deoptee_thread), "must be");
         } else {
           BasicLock* lock = mon_info->lock();
           ObjectSynchronizer::enter_for(obj, lock, deoptee_thread);
           assert(mon_info->owner()->is_locked(), "object must be locked now");
         }
       }
     }
   }
+  LOOM_MONITOR_SUPPORT_ONLY(deoptee_thread->dec_held_monitor_count(compensate_extra_increment);)
   return relocked_objects;
 }
 #endif // COMPILER2_OR_JVMCI
 
 vframeArray* Deoptimization::create_vframeArray(JavaThread* thread, frame fr, RegisterMap *reg_map, GrowableArray<compiledVFrame*>* chunk, bool realloc_failures) {
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
@@ -575,14 +575,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
@@ -1081,20 +1092,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");
@@ -1270,11 +1281,11 @@
     _base->push(base_loc);
     _derived->push(derived_loc);
   }
 
   bool is_good(oop* p) {
-    return *p == nullptr || (dbg_is_safe(*p, -1) && dbg_is_safe((*p)->klass(), -1) && oopDesc::is_oop_or_null(*p));
+    return *p == nullptr || (dbg_is_safe(*p, -1) && oopDesc::is_oop_or_null(*p));
   }
   void describe(FrameValues& values, int frame_no) {
     for (int i = 0; i < _oops->length(); i++) {
       oop* p = _oops->at(i);
       values.describe(frame_no, (intptr_t*)p, err_msg("oop%s for #%d", is_good(p) ? "" : " (BAD)", frame_no));
@@ -1323,13 +1334,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);
 
@@ -1397,11 +1413,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),
@@ -1500,21 +1516,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);
 
@@ -422,10 +422,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);
@@ -437,11 +439,11 @@
   void interpreter_frame_print_on(outputStream* st) const;
   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);
 
   // 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);
 
   // Conversion from a VMReg to physical stack location
   template <typename RegisterMapT>
   address oopmapreg_to_location(VMReg reg, const RegisterMapT* reg_map) const;
   template <typename RegisterMapT>
diff a/src/hotspot/share/runtime/frame.inline.hpp b/src/hotspot/share/runtime/frame.inline.hpp
--- a/src/hotspot/share/runtime/frame.inline.hpp
+++ b/src/hotspot/share/runtime/frame.inline.hpp
@@ -25,10 +25,11 @@
 #ifndef SHARE_RUNTIME_FRAME_INLINE_HPP
 #define SHARE_RUNTIME_FRAME_INLINE_HPP
 
 #include "runtime/frame.hpp"
 
+#include "c1/c1_Runtime1.hpp"
 #include "code/codeBlob.inline.hpp"
 #include "code/nmethod.inline.hpp"
 #include "interpreter/interpreter.hpp"
 #include "oops/stackChunkOop.inline.hpp"
 #include "oops/method.hpp"
@@ -79,10 +80,37 @@
     return nm->get_original_pc(this);
   }
   return nullptr;
 }
 
+#ifdef ASSERT
+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
+
+inline JavaThread** frame::saved_thread_address(const frame& f) {
+  CodeBlob* cb = f.cb();
+  assert(cb != nullptr && cb->is_runtime_stub(), "invalid frame");
+
+  JavaThread** thread_addr;
+  if (cb == Runtime1::blob_for(Runtime1::monitorenter_id) ||
+      cb == Runtime1::blob_for(Runtime1::monitorenter_nofpu_id)) {
+    thread_addr = (JavaThread**)(f.sp() + Runtime1::runtime_blob_current_thread_offset(f));
+  } else {
+    // 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;
+}
+
 template <typename RegisterMapT>
 inline address frame::oopmapreg_to_location(VMReg reg, const RegisterMapT* reg_map) const {
   if (reg->is_reg()) {
     // If it is passed in a register, it got spilled in the stub frame.
     return reg_map->location(reg, sp());
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
@@ -1915,11 +1915,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/handshake.cpp b/src/hotspot/share/runtime/handshake.cpp
--- a/src/hotspot/share/runtime/handshake.cpp
+++ b/src/hotspot/share/runtime/handshake.cpp
@@ -435,11 +435,11 @@
 
   target->handshake_state()->add_operation(op);
 }
 
 // Filters
-static bool non_self_executable_filter(HandshakeOperation* op) {
+static bool handshaker_filter(HandshakeOperation* op) {
   return !op->is_async();
 }
 static bool no_async_exception_filter(HandshakeOperation* op) {
   return !op->is_async_exception();
 }
@@ -476,11 +476,11 @@
   _queue.push(op);
   SafepointMechanism::arm_local_poll_release(_handshakee);
 }
 
 bool HandshakeState::operation_pending(HandshakeOperation* op) {
-  MutexLocker ml(&_lock, Mutex::_no_safepoint_check_flag);
+  ConditionalMutexLocker ml(&_lock, !_lock.owned_by_self(), Mutex::_no_safepoint_check_flag);
   MatchOp mo(op);
   return _queue.contains(mo);
 }
 
 HandshakeOperation* HandshakeState::get_op_for_self(bool allow_suspend, bool check_async_exception) {
@@ -530,28 +530,28 @@
     remove_op(op);
     delete op;
   }
 }
 
-bool HandshakeState::have_non_self_executable_operation() {
+bool HandshakeState::has_handshaker_operation() {
   assert(_handshakee != Thread::current(), "Must not be called by self");
   assert(_lock.owned_by_self(), "Lock must be held");
-  return _queue.contains(non_self_executable_filter);
+  return _queue.contains(handshaker_filter);
 }
 
-HandshakeOperation* HandshakeState::get_op() {
+HandshakeOperation* HandshakeState::get_op_for_handshaker() {
   assert(_handshakee != Thread::current(), "Must not be called by self");
   assert(_lock.owned_by_self(), "Lock must be held");
-  return _queue.peek(non_self_executable_filter);
-};
+  return _queue.peek(handshaker_filter);
+}
 
 void HandshakeState::remove_op(HandshakeOperation* op) {
   assert(_lock.owned_by_self(), "Lock must be held");
   MatchOp mo(op);
   HandshakeOperation* ret = _queue.pop(mo);
   assert(ret == op, "Popped op must match requested op");
-};
+}
 
 bool HandshakeState::process_by_self(bool allow_suspend, bool check_async_exception) {
   assert(Thread::current() == _handshakee, "should call from _handshakee");
   assert(!_handshakee->is_terminated(), "should not be a terminated thread");
 
@@ -623,11 +623,11 @@
   }
   // Operations are added lock free and then the poll is armed.
   // If all handshake operations for the handshakee are finished and someone
   // just adds an operation we may see it here. But if the handshakee is not
   // armed yet it is not safe to proceed.
-  if (have_non_self_executable_operation()) {
+  if (has_handshaker_operation()) {
     OrderAccess::loadload(); // Matches the implicit storestore in add_operation()
     if (SafepointMechanism::local_poll_armed(_handshakee)) {
       return true;
     }
   }
@@ -659,11 +659,11 @@
     return HandshakeState::_not_safe;
   }
 
   Thread* current_thread = Thread::current();
 
-  HandshakeOperation* op = get_op();
+  HandshakeOperation* op = get_op_for_handshaker();
 
   assert(op != nullptr, "Must have an op");
   assert(SafepointMechanism::local_poll_armed(_handshakee), "Must be");
   assert(op->_target == nullptr || _handshakee == op->_target, "Wrong thread");
 
diff a/src/hotspot/share/runtime/handshake.hpp b/src/hotspot/share/runtime/handshake.hpp
--- a/src/hotspot/share/runtime/handshake.hpp
+++ b/src/hotspot/share/runtime/handshake.hpp
@@ -105,13 +105,13 @@
 
   bool claim_handshake();
   bool possibly_can_process_handshake();
   bool can_process_handshake();
 
-  bool have_non_self_executable_operation();
+  bool has_handshaker_operation();
   HandshakeOperation* get_op_for_self(bool allow_suspend, bool check_async_exception);
-  HandshakeOperation* get_op();
+  HandshakeOperation* get_op_for_handshaker();
   void remove_op(HandshakeOperation* op);
 
   void set_active_handshaker(Thread* thread) { Atomic::store(&_active_handshaker, thread); }
 
   class MatchOp {
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,12 @@
   _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),
+  _on_monitorenter(false),
 
   _suspend_flags(0),
 
   _thread_state(_thread_new),
   _saved_exception_pc(nullptr),
@@ -447,10 +455,12 @@
   _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),
+  _contended_entered_monitor(nullptr),
 #ifdef ASSERT
   _is_VTMS_transition_disabler(false),
 #endif
 #endif
   _jni_attach_state(_not_attaching_via_jni),
@@ -485,10 +495,15 @@
   _cont_entry(nullptr),
   _cont_fastpath(0),
   _cont_fastpath_thread_state(1),
   _held_monitor_count(0),
   _jni_monitor_count(0),
+  _preempting(false),
+  _preemption_cancelled(false),
+  _pending_interrupted_exception(false),
+  _preempt_alternate_return(nullptr),
+  DEBUG_ONLY(_obj_locker_count(0) COMMA)
 
   _handshake(this),
 
   _popframe_preserved_args(nullptr),
   _popframe_preserved_args_size(0),
@@ -928,10 +943,11 @@
     // We would like a fatal here, but due to we never checked this before there
     // is a lot of tests which breaks, even with an error log.
     log_debug(jni)("JavaThread %s (tid: " UINTX_FORMAT ") with Objects still locked by JNI MonitorEnter.",
                    exit_type == JavaThread::normal_exit ? "exiting" : "detaching", os::current_thread_id());
   }
+  assert(obj_locker_count() == 0, "expected 0 but found: " INTX_FORMAT, obj_locker_count());
 
   // These things needs to be done while we are still a Java Thread. Make sure that thread
   // is in a consistent state, in case GC happens
   JFR_ONLY(Jfr::on_thread_exit(this);)
 
@@ -1157,10 +1173,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) {
@@ -1520,13 +1537,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
@@ -1706,10 +1722,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");
   }
@@ -2001,14 +2018,14 @@
 // 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
   _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.
@@ -2194,10 +2211,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
@@ -39,10 +39,11 @@
 #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
@@ -156,11 +157,27 @@
   JNIHandleBlock* _active_handles;
 
   // One-element thread local free list
   JNIHandleBlock* _free_handle_block;
 
+  // ID used as owner for inflated monitors. Same as the tid field 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:
+  bool _on_monitorenter;
+
+  bool is_on_monitorenter() { return _on_monitorenter; }
+  void set_on_monitorenter(bool val) { _on_monitorenter = val; }
+
+  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.
@@ -313,10 +330,12 @@
   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
+  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
 
@@ -461,10 +480,29 @@
   int _cont_fastpath_thread_state; // whether global thread state allows continuation fastpath (JVMTI)
 
   // It's signed for error detection.
   intx _held_monitor_count;  // used by continuations for fast lock detection
   intx _jni_monitor_count;
+  bool _preempting;
+  bool _preemption_cancelled;
+  bool _pending_interrupted_exception;
+  address _preempt_alternate_return; // used when preempting a thread
+
+#ifdef ASSERT
+  intx _obj_locker_count;
+
+ public:
+  intx obj_locker_count() { return _obj_locker_count; }
+  void inc_obj_locker_count() {
+    assert(_obj_locker_count >= 0, "Must always be greater than 0: " INTX_FORMAT, _obj_locker_count);
+    _obj_locker_count++;
+  }
+  void dec_obj_locker_count() {
+    _obj_locker_count--;
+    assert(_obj_locker_count >= 0, "Must always be greater than 0: " INTX_FORMAT, _obj_locker_count);
+  }
+#endif // ASSERT
 
 private:
 
   friend class VMThread;
   friend class ThreadWaitTransition;
@@ -607,18 +645,29 @@
   bool cont_fastpath() const                   { return _cont_fastpath == nullptr && _cont_fastpath_thread_state != 0; }
   bool cont_fastpath_thread_state() const      { return _cont_fastpath_thread_state != 0; }
 
   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; }
 
   inline bool is_vthread_mounted() const;
   inline const ContinuationEntry* vthread_continuation() const;
 
+  bool preempting()           { return _preempting; }
+  void set_preempting(bool b) { _preempting = b; }
+
+  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; }
+
+  void set_preempt_alternate_return(address val) { _preempt_alternate_return = val; }
+
  private:
   DEBUG_ONLY(void verify_frame_info();)
 
   // Support for thread handshake operations
   HandshakeState _handshake;
@@ -663,16 +712,23 @@
   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 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;
   }
@@ -821,14 +877,19 @@
     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 preempting_offset()         { return byte_offset_of(JavaThread, _preempting); }
+  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); }
 
 #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); }
   static ByteSize is_disable_suspend_offset()        { return byte_offset_of(JavaThread, _is_disable_suspend); }
@@ -1236,6 +1297,15 @@
     thread->push_jni_handle_block();
   }
   ~JNIHandleMark() { _thread->pop_jni_handle_block(); }
 };
 
+class ThreadOnMonitorEnter {
+  JavaThread* _thread;
+ public:
+  ThreadOnMonitorEnter(JavaThread* thread) : _thread(thread) {
+    _thread->set_on_monitorenter(true);
+  }
+  ~ThreadOnMonitorEnter() { _thread->set_on_monitorenter(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
@@ -31,10 +31,11 @@
 #include "classfile/javaClasses.hpp"
 #include "gc/shared/tlab_globals.hpp"
 #include "memory/universe.hpp"
 #include "oops/instanceKlass.hpp"
 #include "oops/oopHandle.inline.hpp"
+#include "prims/jvmtiThreadState.hpp"
 #include "runtime/atomic.hpp"
 #include "runtime/continuation.hpp"
 #include "runtime/continuationEntry.inline.hpp"
 #include "runtime/nonJavaThread.hpp"
 #include "runtime/orderAccess.hpp"
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
@@ -114,10 +114,15 @@
   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);
+  inline int unique_count() const NOT_DEBUG_RETURN0;
+
   // 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
@@ -211,10 +211,49 @@
   }
   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;
+  }
+}
+
+#ifdef ASSERT
+inline int LockStack::unique_count() const {
+  int end = to_index(_top);
+  oop previous = nullptr;
+  int count = 0;
+  for (int i = 0; i < end; i++) {
+    assert(_base[i] != nullptr, "invariant");
+    if (_base[i] != previous) {
+      previous = _base[i];
+      count++;
+    }
+  }
+  return count;
+}
+#endif
+
 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
@@ -50,10 +50,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/dtrace.hpp"
 #include "utilities/globalDefinitions.hpp"
 #include "utilities/macros.hpp"
 #include "utilities/preserveException.hpp"
@@ -110,10 +111,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.
@@ -247,10 +260,11 @@
 
 ObjectMonitor::ObjectMonitor(oop object) :
   _header(markWord::zero()),
   _object(_oop_storage, object),
   _owner(nullptr),
+  _stack_locker(nullptr),
   _previous_owner_tid(0),
   _next_om(nullptr),
   _recursions(0),
   _EntryList(nullptr),
   _cxq(nullptr),
@@ -314,11 +328,11 @@
     void* prev_owner = try_set_owner_from(nullptr, locking_thread);
 
     if (prev_owner == nullptr) {
       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,15 +346,10 @@
         // 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;
       }
-    } 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, "Failed to enter_for: locking_thread=" INTPTR_FORMAT
            ", this=" INTPTR_FORMAT "{owner=" INTPTR_FORMAT "}, observed owner: " INTPTR_FORMAT,
            p2i(locking_thread), p2i(this), p2i(owner_raw()), p2i(prev_owner));
   } else {
@@ -355,11 +364,11 @@
     }
   }
 
   add_to_contentions(-1);
 
-  assert(!success || owner_raw() == locking_thread, "must be");
+  assert(!success || is_owner(locking_thread), "must be");
 
   return success;
 }
 
 bool ObjectMonitor::enter(JavaThread* current) {
@@ -371,41 +380,34 @@
   if (cur == nullptr) {
     assert(_recursions == 0, "invariant");
     return true;
   }
 
-  if (cur == current) {
+  if (cur == owner_for(current)) {
     // TODO-FIXME: check for integer overflow!  BUGID 6557169.
     _recursions++;
     return true;
   }
 
-  if (LockingMode != LM_LIGHTWEIGHT && 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;
-  }
-
   // We've encountered genuine contention.
 
   // Try one round of spinning *before* enqueueing current
   // and before going through the awkward and expensive state
   // transitions.  The following spin is strictly optional ...
   // 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(owner_raw() == owner_for(current), "must be current: owner=" INTPTR_FORMAT, p2i(owner_raw()));
     assert(_recursions == 0, "must be 0: recursions=" INTX_FORMAT, _recursions);
     assert(object()->mark() == markWord::encode(this),
            "object mark must match encoded this: mark=" INTPTR_FORMAT
            ", encoded this=" INTPTR_FORMAT, object()->mark().value(),
            markWord::encode(this).value());
     return true;
   }
 
-  assert(owner_raw() != current, "invariant");
+  assert(owner_raw() != owner_for(current), "invariant");
   assert(_succ != current, "invariant");
   assert(!SafepointSynchronize::is_at_safepoint(), "invariant");
   assert(current->thread_state() != _thread_blocked, "invariant");
 
   // Keep track of contention for JVM/TI and M&M queries.
@@ -449,10 +451,36 @@
       // This means that the JVMTI_EVENT_MONITOR_CONTENDED_ENTER event
       // handler cannot accidentally consume an unpark() meant for the
       // ParkEvent associated with this ObjectMonitor.
     }
 
+#ifdef LOOM_MONITOR_SUPPORT
+    ContinuationEntry* ce = current->last_continuation();
+    if (ce != nullptr && ce->is_virtual_thread() && current->is_on_monitorenter()) {
+      int result = Continuation::try_preempt(current, ce->cont_oop(current), freeze_on_monitorenter);
+      if (result == freeze_ok) {
+        bool acquired = VThreadMonitorEnter(current);
+        if (acquired) {
+          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 true;
+      }
+      if (result == freeze_pinned_native) {
+        post_virtual_thread_pinned_event(current, "Native frame or <clinit> on stack");
+      }
+    }
+#endif
+
     OSThreadContendState osts(current->osthread());
 
     assert(current->thread_state() == _thread_in_vm, "invariant");
 
     for (;;) {
@@ -469,11 +497,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(owner_raw() == owner_for(current), "invariant");
         break;
       }
     }
 
     // We've just gotten past the enter-check-for-suspend dance and we now own
@@ -483,11 +511,11 @@
   add_to_contentions(-1);
   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(owner_raw() == owner_for(current), "invariant");
   assert(_succ != current, "invariant");
   assert(object()->mark() == markWord::encode(this), "invariant");
 
   // The thread -- now the owner -- is back in vm mode.
   // Report the glorious news via TI,DTrace and jvmstat.
@@ -558,31 +586,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(nullptr, 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(nullptr, DEFLATER_MARKER) != nullptr) {
       // 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, nullptr) != DEFLATER_MARKER) {
         // Deferred decrement for the JT EnterI() that cancelled the async deflation.
         add_to_contentions(-1);
       }
       return false;
     }
@@ -591,11 +619,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, nullptr) != DEFLATER_MARKER) {
         // Deferred decrement for the JT EnterI() that cancelled the async deflation.
         add_to_contentions(-1);
       }
       return false;
     }
@@ -687,11 +715,11 @@
 // 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=" INTPTR_FORMAT
             ", cxq=" PTR_FORMAT
             ", EntryList=" PTR_FORMAT,
             _waiters,
             (contentions() > 0 ? contentions() : 0),
             owner_is_DEFLATER_MARKER()
@@ -710,11 +738,11 @@
   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(owner_raw() == owner_for(current), "invariant");
     assert(_Responsible != current, "invariant");
     return;
   }
 
   if (try_set_owner_from(DEFLATER_MARKER, current) == DEFLATER_MARKER) {
@@ -743,19 +771,19 @@
   // 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(owner_raw() == owner_for(current), "invariant");
     assert(_succ != current, "invariant");
     assert(_Responsible != current, "invariant");
     return;
   }
 
   // The Spin failed -- Enqueue and park the thread ...
   assert(_succ != current, "invariant");
-  assert(owner_raw() != current, "invariant");
+  assert(owner_raw() != owner_for(current), "invariant");
   assert(_Responsible != current, "invariant");
 
   // Enqueue "current" on ObjectMonitor's _cxq.
   //
   // Node acts as a proxy for current.
@@ -781,11 +809,11 @@
 
     // 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(owner_raw() == owner_for(current), "invariant");
       assert(_Responsible != current, "invariant");
       return;
     }
   }
 
@@ -828,20 +856,26 @@
   // 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 ...
 
   int recheckInterval = 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(owner_raw() != owner_for(current), "invariant");
 
     // park self
-    if (_Responsible == current) {
+    if (_Responsible == current || do_timed_parked) {
       current->_ParkEvent->park((jlong) recheckInterval);
       // Increase the recheckInterval, but clamp the value.
       recheckInterval *= 8;
       if (recheckInterval > MAX_RECHECK_INTERVAL) {
         recheckInterval = MAX_RECHECK_INTERVAL;
@@ -906,11 +940,11 @@
   // 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(owner_raw() == owner_for(current), "invariant");
 
   UnlinkAfterAcquire(current, &node);
   if (_succ == current) _succ = nullptr;
 
   assert(_succ != current, "invariant");
@@ -978,13 +1012,13 @@
   assert(object()->mark() == markWord::encode(this), "invariant");
 
   assert(current->thread_state() != _thread_blocked, "invariant");
 
   for (;;) {
-    ObjectWaiter::TStates v = currentNode->TState;
+    uint8_t v = currentNode->TState;
     guarantee(v == ObjectWaiter::TS_ENTER || v == ObjectWaiter::TS_CXQ, "invariant");
-    assert(owner_raw() != current, "invariant");
+    assert(owner_raw() != owner_for(current), "invariant");
 
     // This thread has been notified so try to reacquire the lock.
     if (TryLock(current) == TryLockResult::Success) {
       break;
     }
@@ -1039,26 +1073,206 @@
   // 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(owner_raw() == owner_for(current), "invariant");
   assert(object()->mark() == markWord::encode(this), "invariant");
   UnlinkAfterAcquire(current, currentNode);
   if (_succ == current) _succ = nullptr;
   assert(_succ != 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(owner_raw() == owner_for(current), "invariant");
+    assert(_succ != current, "invariant");
+    assert(_Responsible != current, "invariant");
+    add_to_contentions(-1);
+    return true;
+  }
+
+  if (try_set_owner_from(DEFLATER_MARKER, current) == DEFLATER_MARKER) {
+    // Cancelled the in-progress async deflation by changing owner from
+    // DEFLATER_MARKER to current. As part of the contended enter protocol,
+    // contentions was incremented to a positive value before this call to
+    // VThreadMonitorEnter(). We avoid decrementing contentions to prevent
+    // the deflater thread from winning the last part of the 2-part async
+    // deflation protocol. The deflater thread will decrement contentions
+    // after it recognizes that the async deflation was cancelled.
+    assert(_succ != current, "invariant");
+    assert(_Responsible != current, "invariant");
+    assert(waiter != nullptr, "monitor currently in used marked for deflation??");
+    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(owner_raw() == owner_for(current), "invariant");
+      assert(_succ != current, "invariant");
+      assert(_Responsible != current, "invariant");
+      add_to_contentions(-1);
+      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(owner_raw() == owner_for(current), "invariant");
+    assert(_Responsible != current, "invariant");
+    UnlinkAfterAcquire(current, node);
+    if (_succ == (JavaThread*)java_lang_Thread::thread_id(vthread)) _succ = nullptr;
+    add_to_contentions(-1);
+    if (waiter == nullptr) delete node;  // for Object.wait() don't delete yet
+    return true;
+  }
+
+  if (nxt == nullptr && _EntryList == nullptr) {
+    // The C2 unlock() fast path first checks if _cxq and _EntryList are empty and
+    // if they are it just clears the _owner field. Since we always run the risk of
+    // having that check happening before we added the node to _cxq and the release
+    // of the monitor happening after the last TryLock attempt we need to do something
+    // to avoid stranding. We set the _Responsible field which results in a timed-wait.
+    if (Atomic::replace_if_null(&_Responsible, (JavaThread*)java_lang_Thread::thread_id(vthread))) {
+      java_lang_VirtualThread::set_recheckInterval(vthread, 1);
+    }
+  }
+
+  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 save 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);
+  return false;
+}
+
+void ObjectMonitor::resume_operation(JavaThread* current, ObjectWaiter* node) {
+  assert(java_lang_VirtualThread::state(current->vthread()) == java_lang_VirtualThread::RUNNING, "wrong state for vthread");
+  assert(current->is_in_VTMS_transition(), "must be");
+
+  if (node->is_wait() && !node->at_reenter()) {
+    bool notified = VThreadWaitReenter(current, node);
+    if (!notified) return;
+    // Notified case. We were already added to CXQ or TS_ENTER
+    // by the notifier so just try to reenter the monitor.
+  }
+
+  // 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;
+  }
+
+  oop vthread = current->vthread();
+  if (_succ == (JavaThread*)java_lang_Thread::thread_id(vthread)) _succ = nullptr;
+
+  // Invariant: after clearing _succ a thread *must* retry _owner before parking.
+  OrderAccess::fence();
+
+  if (TryLock(current) == TryLockResult::Success) {
+    VThreadEpilog(current, node);
+    return;
+  }
+
+  // Update recheck interval in case we are the _Responsible.
+  if (_Responsible == (JavaThread*)java_lang_Thread::thread_id(vthread)) {
+    int recheckInterval = java_lang_VirtualThread::recheckInterval(vthread);
+    assert(recheckInterval >= 1 && recheckInterval <= 6, "invariant");
+    if (recheckInterval < 6) {
+      recheckInterval++;
+      java_lang_VirtualThread::set_recheckInterval(vthread, recheckInterval);
+    }
+  } else if (java_lang_VirtualThread::recheckInterval(vthread) > 0) {
+    // No need to do timed park anymore
+    java_lang_VirtualThread::set_recheckInterval(vthread, 0);
+  }
+
+  // The JT will read this variable on return to the resume_monitor_operation stub
+  // and will unmount (enterSpecial frame removed and return to Continuation.run()).
+  current->set_preempting(true);
+  java_lang_VirtualThread::set_state(vthread, java_lang_VirtualThread::BLOCKING);
+}
+
+void ObjectMonitor::VThreadEpilog(JavaThread* current, ObjectWaiter* node) {
+  assert(owner_raw() == owner_for(current), "invariant");
+  add_to_contentions(-1);
+
+  oop vthread = current->vthread();
+  if (java_lang_VirtualThread::recheckInterval(vthread) > 0) {
+    java_lang_VirtualThread::set_recheckInterval(vthread, 0);
+  }
+  int64_t threadid = java_lang_Thread::thread_id(vthread);
+  if (_succ == (JavaThread*)threadid) _succ = nullptr;
+  if (_Responsible == (JavaThread*)threadid) {
+    _Responsible = nullptr;
+    OrderAccess::fence(); // Dekker pivot-point
+  }
+
+  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;
+
+  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(owner_raw() == owner_for(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;
@@ -1173,35 +1387,29 @@
 // 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
+  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;
@@ -1218,17 +1426,17 @@
     _previous_owner_tid = JFR_THREAD_ID(current);
   }
 #endif
 
   for (;;) {
-    assert(current == owner_raw(), "invariant");
+    assert(owner_for(current) == owner_raw(), "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) {
@@ -1274,11 +1482,11 @@
     //
     if (try_set_owner_from(nullptr, current) != nullptr) {
       return;
     }
 
-    guarantee(owner_raw() == current, "invariant");
+    guarantee(owner_raw() == owner_for(current), "invariant");
 
     ObjectWaiter* w = nullptr;
 
     w = _EntryList;
     if (w != nullptr) {
@@ -1351,20 +1559,31 @@
     }
   }
 }
 
 void ObjectMonitor::ExitEpilog(JavaThread* current, ObjectWaiter* Wakee) {
-  assert(owner_raw() == current, "invariant");
+  assert(owner_raw() == owner_for(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;
+    _succ = t;
+  } else {
+    vthread = Wakee->vthread();
+    assert(vthread != nullptr, "");
+    Trigger = ObjectMonitor::vthread_unparker_ParkEvent();
+    _succ = (JavaThread*)java_lang_Thread::thread_id(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;
@@ -1373,11 +1592,17 @@
   // 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)) {
+    Trigger->unpark();
+  }
 
   // Maintain stats and report events to JVMTI
   OM_PERFDATA_OP(Parks, inc());
 }
 
@@ -1388,23 +1613,23 @@
 // 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)) {
+  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(owner_for(current) == owner_raw(), "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(owner_raw() != owner_for(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
@@ -1424,17 +1649,11 @@
 // (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;
+  if (cur == owner_for(current)) {
     return true;
   }
   THROW_MSG_(vmSymbols::java_lang_IllegalMonitorStateException(),
              "current thread is not owner", false);
 }
@@ -1465,10 +1684,29 @@
   event->set_notifier(notifier_tid);
   event->set_timedOut(timedout);
   event->commit();
 }
 
+static void vthread_monitor_waited_event(JavaThread *current, ObjectWaiter* node, 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());
+
+  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()) {
+      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
@@ -1504,10 +1742,28 @@
     return;
   }
 
   current->set_current_waiting_monitor(this);
 
+#ifdef LOOM_MONITOR_SUPPORT
+  ContinuationEntry* ce = current->last_continuation();
+  if (interruptible && ce != nullptr && ce->is_virtual_thread()) {
+    int result = Continuation::try_preempt(current, ce->cont_oop(current), freeze_on_wait);
+    if (result == freeze_ok) {
+      VThreadWait(current, millis);
+      current->set_current_waiting_monitor(nullptr);
+      return;
+    }
+    if (result == freeze_pinned_native) {
+      const Klass* monitor_klass = object()->klass();
+      if (!is_excluded(monitor_klass)) {
+        post_virtual_thread_pinned_event(current, "Native frame or <clinit> on stack");
+      }
+    }
+  }
+#endif
+
   // 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;
@@ -1529,11 +1785,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(owner_raw() != owner_for(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.
   //
@@ -1555,11 +1811,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);
         }
@@ -1583,11 +1839,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);
     }
 
@@ -1609,11 +1865,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 && _succ == 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.
         //
@@ -1625,21 +1881,21 @@
         // 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(owner_raw() != owner_for(current), "invariant");
     ObjectWaiter::TStates v = node.TState;
     if (v == ObjectWaiter::TS_RUN) {
       enter(current);
     } else {
       guarantee(v == ObjectWaiter::TS_ENTER || v == ObjectWaiter::TS_CXQ, "invariant");
@@ -1650,25 +1906,25 @@
     // 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(owner_raw() == owner_for(current), "invariant");
     assert(_succ != current, "invariant");
   } // OSThreadWaitState()
 
   current->set_current_waiting_monitor(nullptr);
 
   guarantee(_recursions == 0, "invariant");
   int relock_count = JvmtiDeferredUpdates::get_and_reset_relock_count_after_wait(current);
   _recursions =   save          // restore the old recursion count
                 + relock_count; //  increased by the deferred relock count
-  current->inc_held_monitor_count(relock_count); // Deopt never entered these counts.
+  NOT_LOOM_MONITOR_SUPPORT(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(owner_raw() == owner_for(current), "invariant");
   assert(_succ != current, "invariant");
   assert(object()->mark() == markWord::encode(this), "invariant");
 
   // check if the notification happened
   if (!WasNotified) {
@@ -1681,31 +1937,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");
@@ -1733,12 +2004,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
@@ -1785,10 +2057,102 @@
   }
 
   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);
+
+  _Responsible = nullptr;
+
+  node->_recursions = _recursions;   // record the old recursion count
+  _recursions = 0;                   // set the recursion level to be 1
+  _waiters++;                        // increment the number of waiters
+  exit(current);                     // exit the monitor
+  guarantee(owner_raw() != owner_for(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) {
+  // First time we run after being preempted on Object.wait().
+  // We need to check if we were interrupted or 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);
+  }
+
+  ObjectWaiter::TStates state = node->TState;
+  bool was_notified = state == ObjectWaiter::TS_ENTER || state == ObjectWaiter::TS_CXQ;
+  assert(was_notified || state == ObjectWaiter::TS_RUN, "");
+
+  // save it so that once we re-acquire the monitor we know if we need to throw IE.
+  node->_interrupted = !was_notified && current->is_interrupted(false);
+
+  EventJavaMonitorWait event;
+  if (event.should_commit() || JvmtiExport::should_post_monitor_waited()) {
+    vthread_monitor_waited_event(current, node, &event, !was_notified && !node->_interrupted);
+  }
+
+  node->_at_reenter = true;
+  add_to_contentions(1);
+  assert(owner_raw() != owner_for(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;
+      oop cont = java_lang_VirtualThread::continuation(current->vthread());
+      stackChunkOop chunk  = jdk_internal_vm_Continuation::tail(cont);
+      chunk->set_object_waiter(nullptr);
+    } else {
+      // The JT will read this variable on return to the resume_monitor_operation stub
+      // and will unmount (enterSpecial frame removed and return to Continuation.run()).
+      current->set_preempting(true);
+    }
+  }
+  return was_notified;
+}
+
 // -----------------------------------------------------------------------------
 // Adaptive Spinning Support
 //
 // Adaptive spin-then-block - rational spinning
 //
@@ -1941,11 +2305,11 @@
   // when preparing to LD...CAS _owner, etc and the CAS is likely
   // to succeed.
   if (_succ == nullptr) {
     _succ = current;
   }
-  Thread* prv = nullptr;
+  void* prv = nullptr;
 
   // 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
@@ -1979,13 +2343,13 @@
     // 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());
+    void* ox = owner_raw();
     if (ox == nullptr) {
-      ox = static_cast<JavaThread*>(try_set_owner_from(nullptr, current));
+      ox = try_set_owner_from(nullptr, current);
       if (ox == nullptr) {
         // The CAS succeeded -- this thread acquired ownership
         // Take care of some bookkeeping to exit spin state.
         if (_succ == current) {
           _succ = nullptr;
@@ -2046,17 +2410,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);
 }
@@ -2170,10 +2554,15 @@
   _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 "}",
             contentions(), waiters(), recursions(),
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,37 +26,55 @@
 #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 ParkEvent;
+class BasicLock;
 
 // 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
@@ -125,14 +143,18 @@
 
 class ObjectMonitor : public CHeapObj<mtObjectMonitor> {
   friend class ObjectSynchronizer;
   friend class ObjectWaiter;
   friend class VMStructs;
+  friend class MonitorList;
   JVMCI_ONLY(friend class JVMCIVMStructs;)
 
   static OopStorage* _oop_storage;
 
+  static OopHandle _vthread_cxq_head;
+  static ParkEvent* _vthread_unparker_ParkEvent;
+
   // The sync code expects the header field to be at offset zero (0).
   // Enforced by the assert() in header_addr().
   volatile markWord _header;        // displaced object header word - mark
   WeakHandle _object;               // backward object pointer
   // Separate _header and _owner on different cache lines since both can
@@ -156,16 +178,17 @@
 
 private:
   static void* anon_owner_ptr() { return reinterpret_cast<void*>(ANONYMOUS_OWNER); }
 
   void* volatile _owner;            // pointer to owning thread OR BasicLock
+  BasicLock* volatile _stack_locker;      // can this share a cache line with owner? they're used together
   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.
-  DEFINE_PAD_MINUS_SIZE(1, OM_CACHE_LINE_SIZE, sizeof(void* volatile) +
+  DEFINE_PAD_MINUS_SIZE(1, OM_CACHE_LINE_SIZE, 2 * sizeof(void* volatile) +
                         sizeof(volatile uint64_t));
   ObjectMonitor* _next_om;          // Next ObjectMonitor* linkage
   volatile intx _recursions;        // recursion count, 0 for first entry
   ObjectWaiter* volatile _EntryList;  // Threads blocked on entry or reentry.
                                       // The list is actually composed of WaitNodes,
@@ -188,10 +211,14 @@
   volatile int _WaitSetLock;        // protects Wait Queue - simple spinlock
 
  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.
   //
@@ -216,10 +243,11 @@
   static ByteSize owner_offset()       { return byte_offset_of(ObjectMonitor, _owner); }
   static ByteSize recursions_offset()  { return byte_offset_of(ObjectMonitor, _recursions); }
   static ByteSize cxq_offset()         { return byte_offset_of(ObjectMonitor, _cxq); }
   static ByteSize succ_offset()        { return byte_offset_of(ObjectMonitor, _succ); }
   static ByteSize EntryList_offset()   { return byte_offset_of(ObjectMonitor, _EntryList); }
+  static ByteSize stack_locker_offset(){ return byte_offset_of(ObjectMonitor, _stack_locker); }
 
   // ObjectMonitor references can be ORed with markWord::monitor_value
   // as part of the ObjectMonitor tagging mechanism. When we combine an
   // ObjectMonitor reference with an offset, we need to remove the tag
   // value in order to generate the proper address.
@@ -250,55 +278,62 @@
     return ret_code != 0;
   }
   const char* is_busy_to_string(stringStream* ss);
 
   bool is_entered(JavaThread* current) const;
+  int contentions() 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.
+  bool   has_owner() const;
+  void*  owner() const;  // Returns null if DEFLATER_MARKER is observed.
+  bool   is_owner(JavaThread* thread) const { return owner() == owner_for(thread); }
+  bool   is_owner_anonymous() const { return owner_raw() == anon_owner_ptr(); }
+  bool   is_stack_locker(JavaThread* current);
+  BasicLock* stack_locker() const;
+
+ private:
   void*     owner_raw() const;
+  void*     owner_for(JavaThread* thread) const;
   // 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(void* old_value, void* new_value);
+  void      set_owner_from(void* 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);
+  void*     try_set_owner_from_raw(void* old_value, void* new_value);
+  void*     try_set_owner_from(void* old_value, JavaThread* current);
 
-  void set_owner_anonymous() {
-    set_owner_from(nullptr, anon_owner_ptr());
-  }
+  void set_stack_locker(BasicLock* locker);
 
-  bool is_owner_anonymous() const {
-    return owner_raw() == anon_owner_ptr();
+  void set_owner_anonymous() {
+    set_owner_from_raw(nullptr, anon_owner_ptr());
   }
 
-  void set_owner_from_anonymous(Thread* owner) {
+  void set_owner_from_anonymous(JavaThread* owner) {
     set_owner_from(anon_owner_ptr(), owner);
   }
 
   // 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);
 
+ public:
   // 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);
@@ -329,10 +364,11 @@
     void operator()(JavaThread* current);
   };
  public:
   bool      enter_for(JavaThread* locking_thread);
   bool      enter(JavaThread* current);
+  void      resume_operation(JavaThread* current, ObjectWaiter* node);
   void      exit(JavaThread* current, bool not_suspended = true);
   void      wait(jlong millis, bool interruptible, TRAPS);
   void      notify(TRAPS);
   void      notifyAll(TRAPS);
 
@@ -348,14 +384,18 @@
  private:
   void      AddWaiter(ObjectWaiter* waiter);
   void      INotify(JavaThread* current);
   ObjectWaiter* DequeueWaiter();
   void      DequeueSpecificWaiter(ObjectWaiter* waiter);
+  void      UnlinkAfterAcquire(JavaThread* current, ObjectWaiter* current_node);
   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);
+  void      VThreadEpilog(JavaThread* current, ObjectWaiter* node);
 
   enum class TryLockResult { Interference = -1, HasOwner = 0, Success = 1 };
 
   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
@@ -28,25 +28,30 @@
 #include "runtime/objectMonitor.hpp"
 
 #include "logging/log.hpp"
 #include "oops/access.inline.hpp"
 #include "runtime/atomic.hpp"
+#include "runtime/javaThread.inline.hpp"
 #include "runtime/lockStack.inline.hpp"
 #include "runtime/synchronizer.hpp"
+#include "runtime/threadIdentifier.hpp"
+
+inline void* ObjectMonitor::owner_for(JavaThread* thread) const {
+  int64_t tid = thread->lock_id();
+  assert(tid >= 3 && tid < ThreadIdentifier::current(), "must be reasonable");
+  return (void*)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 markWord ObjectMonitor::header() const {
@@ -78,10 +83,22 @@
 
 inline void* 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;
@@ -107,11 +124,12 @@
   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) {
+  void* 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));
 #endif
@@ -121,53 +139,63 @@
                                      p2i(this), p2i(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(void* old_value, void* new_value) {
 #ifdef ASSERT
   void* prev = Atomic::load(&_owner);
+  assert((int64_t)prev < ThreadIdentifier::current(), "must be reasonable");
   assert(prev == old_value, "unexpected prev owner=" INTPTR_FORMAT
          ", expected=" INTPTR_FORMAT, p2i(prev), p2i(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));
 }
 
+inline void ObjectMonitor::set_owner_from(void* 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(), "should be anon for now");
+
   // 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));
 }
 
 // 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) {
+inline void* ObjectMonitor::try_set_owner_from_raw(void* old_value, void* new_value) {
+  assert((int64_t)new_value < ThreadIdentifier::current(), "must be reasonable");
   void* 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));
   }
   return prev;
 }
 
+inline void* ObjectMonitor::try_set_owner_from(void* old_value, JavaThread* current) {
+  return try_set_owner_from_raw(old_value, owner_for(current));
+}
+
 // 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/osThread.hpp b/src/hotspot/share/runtime/osThread.hpp
--- a/src/hotspot/share/runtime/osThread.hpp
+++ b/src/hotspot/share/runtime/osThread.hpp
@@ -26,11 +26,10 @@
 #define SHARE_RUNTIME_OSTHREAD_HPP
 
 #include "runtime/frame.hpp"
 #include "runtime/handles.hpp"
 #include "runtime/javaFrameAnchor.hpp"
-#include "runtime/objectMonitor.hpp"
 #include "runtime/suspendedThreadTask.hpp"
 #include "utilities/macros.hpp"
 
 #if defined(LINUX) || defined(AIX) || defined(BSD)
 #include "suspendResume_posix.hpp"
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
@@ -63,10 +63,11 @@
 #include "runtime/init.hpp"
 #include "runtime/interfaceSupport.inline.hpp"
 #include "runtime/java.hpp"
 #include "runtime/javaCalls.hpp"
 #include "runtime/jniHandles.inline.hpp"
+#include "runtime/objectMonitor.inline.hpp"
 #include "runtime/perfData.hpp"
 #include "runtime/sharedRuntime.hpp"
 #include "runtime/stackWatermarkSet.hpp"
 #include "runtime/stubRoutines.hpp"
 #include "runtime/synchronizer.hpp"
@@ -92,10 +93,11 @@
 RuntimeStub*        SharedRuntime::_ic_miss_blob;
 RuntimeStub*        SharedRuntime::_resolve_opt_virtual_call_blob;
 RuntimeStub*        SharedRuntime::_resolve_virtual_call_blob;
 RuntimeStub*        SharedRuntime::_resolve_static_call_blob;
 address             SharedRuntime::_resolve_static_call_entry;
+address             SharedRuntime::_native_frame_resume_entry = nullptr;
 
 DeoptimizationBlob* SharedRuntime::_deopt_blob;
 SafepointBlob*      SharedRuntime::_polling_page_vectors_safepoint_handler_blob;
 SafepointBlob*      SharedRuntime::_polling_page_safepoint_handler_blob;
 SafepointBlob*      SharedRuntime::_polling_page_return_handler_blob;
@@ -1899,20 +1901,30 @@
   // would leave you with the lock held and it would never be released.
   // The normal monitorenter NullPointerException is thrown without acquiring a lock
   // and the model is that an exception implies the method failed.
   JRT_BLOCK_NO_ASYNC
   Handle h_obj(THREAD, obj);
+  ThreadOnMonitorEnter tme(current);
   ObjectSynchronizer::enter(h_obj, lock, current);
   assert(!HAS_PENDING_EXCEPTION, "Should have no exception here");
   JRT_BLOCK_END
 }
 
 // Handles the uncommon case in locking, i.e., contention or an inflated lock.
 JRT_BLOCK_ENTRY(void, SharedRuntime::complete_monitor_locking_C(oopDesc* obj, BasicLock* lock, JavaThread* current))
   SharedRuntime::monitor_enter_helper(obj, lock, current);
 JRT_END
 
+JRT_BLOCK_ENTRY(void, SharedRuntime::resume_monitor_operation(JavaThread* current, ObjectWaiter* node))
+  assert(current == JavaThread::current(), "invariant");
+  ObjectMonitor* monitor = node->monitor();
+  assert(!monitor->is_owner(current), "invariant");
+
+  monitor->resume_operation(current, node);
+  assert(monitor->is_owner(current) || current->preempting(), "invariant");
+JRT_END
+
 void SharedRuntime::monitor_exit_helper(oopDesc* obj, BasicLock* lock, JavaThread* current) {
   assert(JavaThread::current() == current, "invariant");
   // Exit must be non-blocking, and therefore no exceptions can be thrown.
   ExceptionMark em(current);
   // The object could become unlocked through a JNI call, which we have no other checks for.
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
@@ -33,10 +33,11 @@
 #include "utilities/macros.hpp"
 
 class AdapterHandlerEntry;
 class AdapterFingerPrint;
 class vframeStream;
+class ObjectWaiter;
 
 // Runtime is the base class for various runtime interfaces
 // (InterpreterRuntime, CompilerRuntime, etc.). It provides
 // shared functionality such as exception forwarding (C++ to
 // Java exceptions), locking/unlocking mechanisms, statistical
@@ -53,10 +54,11 @@
   static RuntimeStub*        _ic_miss_blob;
   static RuntimeStub*        _resolve_opt_virtual_call_blob;
   static RuntimeStub*        _resolve_virtual_call_blob;
   static RuntimeStub*        _resolve_static_call_blob;
   static address             _resolve_static_call_entry;
+  static address             _native_frame_resume_entry;
 
   static DeoptimizationBlob* _deopt_blob;
 
   static SafepointBlob*      _polling_page_vectors_safepoint_handler_blob;
   static SafepointBlob*      _polling_page_safepoint_handler_blob;
@@ -196,10 +198,16 @@
   static void    throw_StackOverflowError_common(JavaThread* current, bool delayed);
   static address continuation_for_implicit_exception(JavaThread* current,
                                                      address faulting_pc,
                                                      ImplicitExceptionKind exception_kind);
 
+  static address native_frame_resume_entry() { return _native_frame_resume_entry; }
+  static void set_native_frame_resume_entry(address val) {
+    assert(_native_frame_resume_entry == nullptr, "");
+    _native_frame_resume_entry = val;
+  }
+
   // Post-slow-path-allocation, pre-initializing-stores step for
   // implementing e.g. ReduceInitialCardMarks
   static void on_slowpath_allocation_exit(JavaThread* current);
 
   static void enable_stack_reserved_zone(JavaThread* current);
@@ -462,10 +470,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
@@ -493,10 +505,11 @@
   static bool should_fixup_call_destination(address destination, address entry_point, address caller_pc, Method* moop, CodeBlob* cb);
 
   // Slow-path Locking and Unlocking
   static void complete_monitor_locking_C(oopDesc* obj, BasicLock* lock, JavaThread* current);
   static void complete_monitor_unlocking_C(oopDesc* obj, BasicLock* lock, JavaThread* current);
+  static void resume_monitor_operation(JavaThread* current, ObjectWaiter* node);
 
   // Resolving of calls
   static address get_resolved_entry        (JavaThread* current, methodHandle callee_method);
   static address resolve_static_call_C     (JavaThread* current);
   static address resolve_virtual_call_C    (JavaThread* current);
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
@@ -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);
@@ -231,11 +238,11 @@
   }
 
   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());
+    _oopmap = cb()->oop_map_for_return_address(orig_pc());
   }
 }
 
 template <ChunkFrames frame_kind>
 inline void StackChunkFrameStream<frame_kind>::get_cb() {
@@ -298,13 +305,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
@@ -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
@@ -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
@@ -188,10 +188,13 @@
 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::_cont_resume_monitor_operation = nullptr;
+address StubRoutines::_cont_resume_compiler_adapter = nullptr;
 
 JFR_ONLY(RuntimeStub* StubRoutines::_jfr_write_checkpoint_stub = nullptr;)
 JFR_ONLY(address StubRoutines::_jfr_write_checkpoint = nullptr;)
 JFR_ONLY(RuntimeStub* StubRoutines::_jfr_return_lease_stub = nullptr;)
 JFR_ONLY(address StubRoutines::_jfr_return_lease = 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
@@ -266,10 +266,13 @@
   static address _method_entry_barrier;
 
   static address _cont_thaw;
   static address _cont_returnBarrier;
   static address _cont_returnBarrierExc;
+  static address _cont_preempt_stub;
+  static address _cont_resume_monitor_operation;
+  static address _cont_resume_compiler_adapter;
 
   JFR_ONLY(static RuntimeStub* _jfr_write_checkpoint_stub;)
   JFR_ONLY(static address _jfr_write_checkpoint;)
   JFR_ONLY(static RuntimeStub* _jfr_return_lease_stub;)
   JFR_ONLY(static address _jfr_return_lease;)
@@ -484,10 +487,13 @@
   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 cont_resume_monitor_operation() { return _cont_resume_monitor_operation; }
+  static address cont_resume_compiler_adapter()  { return _cont_resume_compiler_adapter; }
 
   JFR_ONLY(static address jfr_write_checkpoint() { return _jfr_write_checkpoint; })
   JFR_ONLY(static address jfr_return_lease() { return _jfr_return_lease; })
 
   static address 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
@@ -349,11 +349,11 @@
   }
 
   if (mark.has_monitor()) {
     ObjectMonitor* const mon = mark.monitor();
     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.
@@ -399,11 +399,11 @@
       // Always go into runtime if the lock stack is full.
       return false;
     }
     if (lock_stack.try_recursive_enter(obj)) {
       // Recursive lock successful.
-      current->inc_held_monitor_count();
+      NOT_LOOM_MONITOR_SUPPORT(current->inc_held_monitor_count();)
       return true;
     }
   }
 
   const markWord mark = obj->mark();
@@ -414,20 +414,19 @@
     // 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();
+      NOT_LOOM_MONITOR_SUPPORT(current->inc_held_monitor_count();)
       return true;
     }
 
     if (LockingMode != LM_LIGHTWEIGHT) {
       // This Java Monitor is inflated so obj's header will never be
@@ -440,13 +439,13 @@
       // 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_from(nullptr, current) == nullptr) {
       assert(m->_recursions == 0, "invariant");
-      current->inc_held_monitor_count();
+      NOT_LOOM_MONITOR_SUPPORT(current->inc_held_monitor_count();)
       return true;
     }
   }
 
   // Note that we could inflate in quick_enter.
@@ -522,10 +521,13 @@
 void ObjectSynchronizer::enter_for(Handle obj, BasicLock* lock, JavaThread* locking_thread) {
   // When called with locking_thread != Thread::current() some mechanism must synchronize
   // the locking_thread with respect to the current thread. Currently only used when
   // deoptimizing and re-locking locks. See Deoptimization::relock_objects
   assert(locking_thread == Thread::current() || locking_thread->is_obj_deopt_suspend(), "must be");
+
+  NOT_LOOM_MONITOR_SUPPORT(locking_thread->inc_held_monitor_count();)
+
   if (!enter_fast_impl(obj, lock, locking_thread)) {
     // Inflated ObjectMonitor::enter_for is required
 
     // An async deflation can race after the inflate_for() call and before
     // enter_for() can make the ObjectMonitor busy. enter_for() returns false
@@ -540,10 +542,13 @@
   }
 }
 
 void ObjectSynchronizer::enter(Handle obj, BasicLock* lock, JavaThread* current) {
   assert(current == Thread::current(), "must be");
+
+  NOT_LOOM_MONITOR_SUPPORT(current->inc_held_monitor_count();)
+
   if (!enter_fast_impl(obj, lock, current)) {
     // Inflated ObjectMonitor::enter is required
 
     // An async deflation can race after the inflate() call and before
     // enter() can make the ObjectMonitor busy. enter() returns false if
@@ -559,17 +564,14 @@
 
 // The interpreter and compiler assembly code tries to lock using the fast path
 // of this algorithm. Make sure to update that code if the following function is
 // changed. The implementation is extremely sensitive to race condition. Be careful.
 bool ObjectSynchronizer::enter_fast_impl(Handle obj, BasicLock* lock, JavaThread* locking_thread) {
-
   if (obj->klass()->is_value_based()) {
     handle_sync_on_value_based_class(obj, locking_thread);
   }
 
-  locking_thread->inc_held_monitor_count();
-
   if (!useHeavyMonitors()) {
     if (LockingMode == LM_LIGHTWEIGHT) {
       // Fast-locking does not use the 'lock' argument.
       LockStack& lock_stack = locking_thread->lock_stack();
       if (lock_stack.is_full()) {
@@ -594,11 +596,11 @@
         // The least recently locked lock is chosen as it is the lock
         // with the longest critical section.
 
         log_info(monitorinflation)("LockStack capacity exceeded, inflating.");
         ObjectMonitor* monitor = inflate_for(locking_thread, lock_stack.bottom(), inflate_cause_vm_internal);
-        assert(monitor->owner() == Thread::current(), "must be owner=" PTR_FORMAT " current=" PTR_FORMAT " mark=" PTR_FORMAT,
+        assert(monitor->is_owner(JavaThread::current()), "must be owner=" PTR_FORMAT " current=" PTR_FORMAT " mark=" PTR_FORMAT,
                p2i(monitor->owner()), p2i(Thread::current()), monitor->object()->mark_acquire().value());
         assert(!lock_stack.is_full(), "must have made room here");
       }
 
       markWord mark = obj()->mark_acquire();
@@ -628,10 +630,11 @@
       if (mark.is_unlocked()) {
         // Anticipate successful CAS -- the ST of the displaced mark must
         // be visible <= the ST performed by the CAS.
         lock->set_displaced_header(mark);
         if (mark == obj()->cas_set_mark(markWord::from_pointer(lock), mark)) {
+          LOOM_MONITOR_SUPPORT_ONLY(locking_thread->inc_held_monitor_count();)
           return true;
         }
       } else if (mark.has_locker() &&
                  locking_thread->is_lock_owned((address) mark.locker())) {
         assert(lock != mark.locker(), "must not re-lock the same lock");
@@ -655,11 +658,11 @@
 
   return false;
 }
 
 void ObjectSynchronizer::exit(oop object, BasicLock* lock, JavaThread* current) {
-  current->dec_held_monitor_count();
+  NOT_LOOM_MONITOR_SUPPORT(current->dec_held_monitor_count();)
 
   if (!useHeavyMonitors()) {
     markWord mark = object->mark();
     if (LockingMode == LM_LIGHTWEIGHT) {
       // Fast-locking does not use the 'lock' argument.
@@ -720,10 +723,11 @@
       if (mark == markWord::from_pointer(lock)) {
         // If the object is stack-locked by the current thread, try to
         // swing the displaced header from the BasicLock back to the mark.
         assert(dhw.is_neutral(), "invariant");
         if (object->cas_set_mark(dhw, mark) == mark) {
+          LOOM_MONITOR_SUPPORT_ONLY(current->dec_held_monitor_count();)
           return;
         }
       }
     }
   } else if (VerifyHeavyMonitors) {
@@ -781,18 +785,20 @@
 // Internal VM locks on java objects
 // standard constructor, allows locking failures
 ObjectLocker::ObjectLocker(Handle obj, JavaThread* thread) {
   _thread = thread;
   _thread->check_for_valid_safepoint_state();
+  DEBUG_ONLY(_thread->inc_obj_locker_count();)
   _obj = obj;
 
   if (_obj() != nullptr) {
     ObjectSynchronizer::enter(_obj, &_lock, _thread);
   }
 }
 
 ObjectLocker::~ObjectLocker() {
+  DEBUG_ONLY(_thread->dec_obj_locker_count();)
   if (_obj() != nullptr) {
     ObjectSynchronizer::exit(_obj(), &_lock, _thread);
   }
 }
 
@@ -1162,11 +1168,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:
@@ -1212,28 +1218,28 @@
     // 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; };
+  auto thread_filter = [&](ObjectMonitor* monitor) { return monitor->is_owner(thread); };
   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
@@ -1399,41 +1405,35 @@
   (void)inflate(Thread::current(), obj, inflate_cause_vm_internal);
 }
 
 ObjectMonitor* ObjectSynchronizer::inflate(Thread* current, oop obj, const InflateCause cause) {
   assert(current == Thread::current(), "must be");
-  if (LockingMode == LM_LIGHTWEIGHT && current->is_Java_thread()) {
-    return inflate_impl(JavaThread::cast(current), obj, cause);
-  }
-  return inflate_impl(nullptr, 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");
   return inflate_impl(thread, obj, cause);
 }
 
 ObjectMonitor* ObjectSynchronizer::inflate_impl(JavaThread* inflating_thread, oop object, const InflateCause cause) {
-  // The JavaThread* inflating_thread parameter is only used by LM_LIGHTWEIGHT and requires
-  // that the inflating_thread == Thread::current() or is suspended throughout the call by
-  // some other mechanism.
-  // Even with LM_LIGHTWEIGHT 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 the correctness of the LM_LIGHTWEIGHT algorithm that the thread
-  // is set when called from ObjectSynchronizer::enter from the owning thread,
+  // 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.
   EventJavaMonitorInflate event;
 
   for (;;) {
     const markWord mark = object->mark_acquire();
 
     // The mark can be in one of the following states:
-    // *  inflated     - Just return if using stack-locking.
-    //                   If using fast-locking and the ObjectMonitor owner
-    //                   is anonymous and the inflating_thread owns the
-    //                   object lock, then we make the inflating_thread
-    //                   the ObjectMonitor owner and remove the lock from
+    // *  inflated     - If the ObjectMonitor owner is anonymous and the
+    //                   inflating_thread owns the object lock, then we
+    //                   make the inflating_thread the ObjectMonitor owner.
+    //                   For LM_LIGHTWEIGHT we also remove the lock from
     //                   the inflating_thread's lock stack.
     // *  fast-locked  - Coerce it to inflated from fast-locked.
     // *  stack-locked - Coerce it to inflated from stack-locked.
     // *  INFLATING    - Busy wait for conversion from stack-locked to
     //                   inflated.
@@ -1442,15 +1442,25 @@
     // 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 (LockingMode == LM_LIGHTWEIGHT && inf->is_owner_anonymous() &&
-          inflating_thread != nullptr && inflating_thread->lock_stack().contains(object)) {
-        inf->set_owner_from_anonymous(inflating_thread);
-        size_t removed = inflating_thread->lock_stack().remove(object);
-        inf->set_recursions(removed - 1);
+      if (inf->is_owner_anonymous() && inflating_thread != nullptr) {
+        if (LockingMode == LM_LIGHTWEIGHT) {
+          if (inflating_thread->lock_stack().contains(object)) {
+            inf->set_owner_from_anonymous(inflating_thread);
+            size_t removed = inflating_thread->lock_stack().remove(object);
+            inf->set_recursions(removed - 1);
+          }
+        } else {
+          assert(LockingMode == LM_LEGACY, "invariant");
+          if (inflating_thread->is_lock_owned((address)inf->stack_locker())) {
+            inf->set_owner_from_BasicLock(inflating_thread);
+            // Decrement monitor count now since this monitor is okay for freezing
+            LOOM_MONITOR_SUPPORT_ONLY(inflating_thread->dec_held_monitor_count();)
+          }
+        }
       }
       return inf;
     }
 
     if (LockingMode != LM_LIGHTWEIGHT) {
@@ -1580,16 +1590,23 @@
       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_from(nullptr, inflating_thread);
+        // Decrement monitor count now since this monitor is okay for freezing
+        LOOM_MONITOR_SUPPORT_ONLY(inflating_thread->dec_held_monitor_count();)
+      } 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");
@@ -1883,11 +1900,11 @@
 
  public:
   ReleaseJavaMonitorsClosure(JavaThread* thread) : _thread(thread) {}
   void do_monitor(ObjectMonitor* mid) {
     intx rec = mid->complete_exit(_thread);
-    _thread->dec_held_monitor_count(rec + 1);
+    _thread->dec_held_monitor_count(NOT_LOOM_MONITOR_SUPPORT((rec + 1)));
   }
 };
 
 // Release all inflated monitors owned by current thread.  Lightweight monitors are
 // ignored.  This is meant to be called during JNI thread detach which assumes
@@ -1923,10 +1940,11 @@
     case inflate_cause_wait:           return "Monitor Wait";
     case inflate_cause_notify:         return "Monitor Notify";
     case inflate_cause_hash_code:      return "Monitor Hash Code";
     case inflate_cause_jni_enter:      return "JNI Monitor Enter";
     case inflate_cause_jni_exit:       return "JNI Monitor Exit";
+    case inflate_cause_cont_freeze:    return "Continuation Freeze";
     default:
       ShouldNotReachHere();
   }
   return "Unknown";
 }
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
@@ -78,11 +78,12 @@
     inflate_cause_wait = 2,
     inflate_cause_notify = 3,
     inflate_cause_hash_code = 4,
     inflate_cause_jni_enter = 5,
     inflate_cause_jni_exit = 6,
-    inflate_cause_nof = 7 // Number of causes
+    inflate_cause_cont_freeze = 7,
+    inflate_cause_nof = 8 // Number of causes
   } InflateCause;
 
   typedef enum {
     NOT_ENABLED    = 0,
     FATAL_EXIT     = 1,
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
@@ -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
@@ -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
@@ -163,10 +163,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);
 }
@@ -528,10 +530,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
@@ -577,10 +584,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();
 
@@ -1214,39 +1223,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");
@@ -1263,21 +1253,27 @@
   }
   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;
+    int64_t owner_tid = (int64_t)monitor->owner();
+    for (JavaThread* q : *t_list) {
+      if (monitor->is_owner(q)) {
+        the_owner = q;
+        break;
+      }
+    }
+    return the_owner;
   }
 }
 
 class PrintOnClosure : public ThreadClosure {
 private:
@@ -1325,21 +1321,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
@@ -275,17 +275,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
@@ -366,11 +366,12 @@
     add(monitor);
   }
 
   // Implements the ObjectMonitorsView interface
   void visit(MonitorClosure* closure, JavaThread* thread) override {
-    ObjectMonitorLinkedList* list = get_list(thread);
+    void* key = (void*)java_lang_Thread::thread_id(thread->threadObj());
+    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
@@ -98,10 +98,11 @@
 #include "runtime/javaCalls.hpp"
 #include "runtime/javaThread.hpp"
 #include "runtime/jniHandles.hpp"
 #include "runtime/monitorDeflationThread.hpp"
 #include "runtime/notificationThread.hpp"
+#include "runtime/objectMonitor.hpp"
 #include "runtime/os.hpp"
 #include "runtime/osThread.hpp"
 #include "runtime/perfMemory.hpp"
 #include "runtime/serviceThread.hpp"
 #include "runtime/sharedRuntime.hpp"
@@ -658,10 +659,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*)                                \
                                                                                                                                      \
   /************/                                                                                                                     \
@@ -779,10 +781,11 @@
   /************/                                                                                                                     \
                                                                                                                                      \
   volatile_nonstatic_field(ObjectMonitor,      _header,                                       markWord)                              \
   unchecked_nonstatic_field(ObjectMonitor,     _object,                                       sizeof(void *)) /* NOTE: no type */    \
   unchecked_nonstatic_field(ObjectMonitor,     _owner,                                        sizeof(void *)) /* NOTE: no type */    \
+  unchecked_nonstatic_field(ObjectMonitor,     _stack_locker,                                 BasicLock*)                            \
   volatile_nonstatic_field(ObjectMonitor,      _next_om,                                      ObjectMonitor*)                        \
   volatile_nonstatic_field(BasicLock,          _displaced_header,                             markWord)                              \
   nonstatic_field(ObjectMonitor,               _contentions,                                  int)                                   \
   volatile_nonstatic_field(ObjectMonitor,      _waiters,                                      int)                                   \
   volatile_nonstatic_field(ObjectMonitor,      _recursions,                                   intx)                                  \
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));
@@ -1125,11 +1126,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;
@@ -1162,10 +1162,50 @@
   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());
+}
+
 CompilationMemoryStatisticDCmd::CompilationMemoryStatisticDCmd(outputStream* output, bool heap) :
     DCmdWithParser(output, heap),
   _human_readable("-H", "Human readable format", "BOOLEAN", false, "false"),
   _minsize("-s", "Minimum memory size", "MEMORY SIZE", false, "0") {
   _dcmdparser.add_dcmd_option(&_human_readable);
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
@@ -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 virtual thread summary";
+  }
+  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);
diff a/src/hotspot/share/utilities/globalDefinitions.hpp b/src/hotspot/share/utilities/globalDefinitions.hpp
--- a/src/hotspot/share/utilities/globalDefinitions.hpp
+++ b/src/hotspot/share/utilities/globalDefinitions.hpp
@@ -1356,6 +1356,16 @@
 
 // Quickly test to make sure IEEE-754 subnormal numbers are correctly
 // handled.
 bool IEEE_subnormal_handling_OK();
 
+#if defined(AMD64) || defined (AARCH64)
+#define LOOM_MONITOR_SUPPORT true
+#define LOOM_MONITOR_SUPPORT_ONLY(code) code
+#define NOT_LOOM_MONITOR_SUPPORT(code)
+#else
+#undef LOOM_MONITOR_SUPPORT
+#define LOOM_MONITOR_SUPPORT_ONLY(code)
+#define NOT_LOOM_MONITOR_SUPPORT(code) code
+#endif
+
 #endif // SHARE_UTILITIES_GLOBALDEFINITIONS_HPP
diff a/src/hotspot/share/utilities/macros.hpp b/src/hotspot/share/utilities/macros.hpp
--- a/src/hotspot/share/utilities/macros.hpp
+++ b/src/hotspot/share/utilities/macros.hpp
@@ -358,16 +358,18 @@
 
 #ifdef ASSERT
 #define DEBUG_ONLY(code) code
 #define NOT_DEBUG(code)
 #define NOT_DEBUG_RETURN  /*next token must be ;*/
+#define NOT_DEBUG_RETURN0
 // Historical.
 #define debug_only(code) code
 #else // ASSERT
 #define DEBUG_ONLY(code)
 #define NOT_DEBUG(code) code
 #define NOT_DEBUG_RETURN {}
+#define NOT_DEBUG_RETURN0 { return 0; }
 #define debug_only(code)
 #endif // ASSERT
 
 #ifdef  _LP64
 #define LP64_ONLY(code) code
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
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2005, 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
@@ -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
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2011, 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
@@ -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
@@ -156,17 +156,22 @@
  *
  * {@snippet lang=java :
  *     private static final ScopedValue<String> NAME = ScopedValue.newInstance();
 
  *     ScopedValue.runWhere(NAME, "duke", () -> {
- *         try (var scope = new StructuredTaskScope<String>()) {
+ *         // @link substring="open" target="StructuredTaskScope#open(Policy)" :
+ *         try (var scope = StructuredTaskScope.open(policy)) {
  *
+ *             // @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()" :
+ *             var result = scope.join();
  *          }
  *     });
  * }
  *
  * <p> Unless otherwise specified, passing a {@code null} argument to a method in this
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
@@ -63,10 +63,12 @@
 import java.util.Properties;
 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.logger.LoggerFinderLoader.TemporaryLoggerFinder;
@@ -2741,10 +2743,18 @@
                 } else {
                     throw new WrongThreadException();
                 }
             }
 
+            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
@@ -160,22 +160,28 @@
  *   // A ThreadFactory that creates virtual threads
  *   ThreadFactory factory = Thread.ofVirtual().factory();
  * }
  *
  * <h2><a id="inheritance">Inheritance when creating threads</a></h2>
- * A {@code Thread} inherits its initial values of {@linkplain InheritableThreadLocal
+ * A {@code Thread} created with one of the public constructors inherits the daemon
+ * status and thread priority from the parent thread at the time that the child {@code
+ * Thread} is created. The {@linkplain ThreadGroup thread group} is also inherited when
+ * not provided to the constructor (and not selected by the security manager).
+ * When using a {@code Thread.Builder} to create a platform thread, the daemon status,
+ * thread priority, and thread group (when not selected by the security manager) are
+ * inherited when not set on the builder. As with the constructors, inheriting from the
+ * parent thread is done when the child {@code Thread} is created.
+ *
+ * <p> A {@code Thread} inherits its initial values of {@linkplain InheritableThreadLocal
  * inheritable-thread-local} variables (including the context class loader) from
  * the parent thread values at the time that the child {@code Thread} is created.
  * The 5-param {@linkplain Thread#Thread(ThreadGroup, Runnable, String, long, boolean)
  * constructor} can be used to create a thread that does not inherit its initial
  * values from the constructing thread. When using a {@code Thread.Builder}, the
  * {@link Builder#inheritInheritableThreadLocals(boolean) inheritInheritableThreadLocals}
  * method can be used to select if the initial values are inherited.
  *
- * <p> Platform threads inherit the daemon status, thread priority, and when not
- * provided (or not selected by a security manager), the thread group.
- *
  * <p> Creating a platform thread {@linkplain AccessController#getContext() captures} the
  * {@linkplain AccessControlContext caller context} to limit the {@linkplain Permission
  * permissions} of the new thread when it executes code that performs a {@linkplain
  * AccessController#doPrivileged(PrivilegedAction) privileged action}. The captured
  * caller context is the new thread's "Inherited {@link AccessControlContext}". Creating
@@ -719,14 +725,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,24 +143,69 @@
 
     // 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
     private volatile boolean parkPermit;
 
+    // used to mark thread as ready to be unblocked
+    private volatile boolean unblocked;
+
+    // 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;
+
+    // a positive value if "responsible thread" blocked on monitor enter, accessed by VM
+    private volatile byte recheckInterval;
+
     // carrier thread when mounted, accessed by VM
     private volatile Thread carrierThread;
 
     // termination object when joining, created lazily if needed
     private volatile CountDownLatch termination;
 
+    // has the value 1 when on the list of virtual threads waiting to be unblocked
+    private volatile byte onWaitingList;
+
+    // next virtual thread on the list of virtual threads waiting to be unblocked
+    private volatile VirtualThread next;
+
+    /**
+     * 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;
@@ -187,22 +249,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() {
@@ -210,10 +262,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.
      */
@@ -224,11 +283,12 @@
             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
@@ -477,10 +537,11 @@
         notifyJvmtiHideFrames(true);
         Thread carrier = this.carrierThread;
         assert Thread.currentThread() == this
                 && carrier == Thread.currentCarrierThread();
         carrier.setCurrentThread(carrier);
+        setLockId(this.threadId()); // keep lockid of vthread
     }
 
     /**
      * Sets the current thread to the given virtual thread.
      */
@@ -564,10 +625,72 @@
                 submitRunContinuation();
             }
             return;
         }
 
+        // blocking on monitorenter
+        if (s == BLOCKING) {
+            setState(BLOCKED);
+
+            // may have been unblocked while blocking
+            if (unblocked && compareAndSetState(BLOCKED, UNBLOCKED)) {
+                unblocked = false;
+                submitRunContinuation();
+                return;
+            }
+
+            // if thread is the designated responsible thread for a monitor then schedule
+            // it to wakeup so that it can check and recover. See objectMonitor.cpp.
+            int recheckInterval = this.recheckInterval;
+            if (recheckInterval > 0) {
+                assert recheckInterval >= 1 && recheckInterval <= 6;
+                // 4 ^ (recheckInterval - 1) = 1, 4, 16, ... 1024
+                long delay = 1 << (recheckInterval - 1) << (recheckInterval - 1);
+                schedule(this::unblock, delay, MILLISECONDS);
+            }
+            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 (unblocked && compareAndSetState(BLOCKED, UNBLOCKED)) {
+                    unblocked = false;
+                    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.
@@ -731,18 +854,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);
@@ -754,22 +869,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
@@ -780,11 +888,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()) {
@@ -832,10 +940,74 @@
                 return;
             }
         }
     }
 
+    /**
+     * Invoked by unblocker thread to unblock this virtual thread.
+     */
+    private void unblock() {
+        assert !Thread.currentThread().isVirtual();
+        unblocked = true;
+        if (state() == BLOCKED && compareAndSetState(BLOCKED, UNBLOCKED)) {
+            unblocked = false;
+            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;
@@ -962,10 +1134,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);
         }
@@ -1009,11 +1187,22 @@
                 }
             case UNPARKED:
             case YIELDED:
                 // runnable, not mounted
                 return Thread.State.RUNNABLE;
+            case UNBLOCKED:
+                // if designated responsible thread for monitor then thread is blocked
+                if (isResponsibleForMonitor()) {
+                    return Thread.State.BLOCKED;
+                } else {
+                    return Thread.State.RUNNABLE;
+                }
             case RUNNING:
+                // if designated responsible thread for monitor then thread is blocked
+                if (isResponsibleForMonitor()) {
+                    return Thread.State.BLOCKED;
+                }
                 // if mounted then return state of carrier thread
                 if (Thread.currentThread() != this) {
                     disableSuspendAndPreempt();
                     try {
                         synchronized (carrierThreadAccessLock()) {
@@ -1029,25 +1218,40 @@
                 // runnable, mounted
                 return Thread.State.RUNNABLE;
             case PARKING:
             case TIMED_PARKING:
             case YIELDING:
+            case WAITING:
+            case TIMED_WAITING:
                 // runnable, in transition
                 return Thread.State.RUNNABLE;
             case PARKED:
             case PINNED:
+            case WAIT:
                 return State.WAITING;
             case TIMED_PARKED:
             case TIMED_PINNED:
+            case TIMED_WAIT:
                 return State.TIMED_WAITING;
+            case BLOCKING:
+            case BLOCKED:
+                return State.BLOCKED;
             case TERMINATED:
                 return Thread.State.TERMINATED;
             default:
                 throw new InternalError();
         }
     }
 
+    /**
+     * Returns true if thread is the designated responsible thread for a monitor.
+     * See objectMonitor.cpp for details.
+     */
+    private boolean isResponsibleForMonitor() {
+        return (recheckInterval > 0);
+    }
+
     @Override
     boolean alive() {
         int s = state;
         return (s != NEW && s != TERMINATED);
     }
@@ -1082,17 +1286,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);
         }
 
@@ -1109,18 +1313,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 unblocked && 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();
         }
@@ -1211,10 +1424,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);
@@ -1241,10 +1462,14 @@
 
     private boolean compareAndSetState(int expectedValue, int newValue) {
         return U.compareAndSetInt(this, STATE, expectedValue, newValue);
     }
 
+    private boolean compareAndSetOnWaitingList(byte expectedValue, byte newValue) {
+        return U.compareAndSetByte(this, ON_WAITING_LIST, expectedValue, newValue);
+    }
+
     private void setParkPermit(boolean newValue) {
         if (parkPermit != newValue) {
             parkPermit = newValue;
         }
     }
@@ -1260,10 +1485,13 @@
     private void setCarrierThread(Thread carrier) {
         // U.putReferenceRelease(this, CARRIER_THREAD, carrier);
         this.carrierThread = carrier;
     }
 
+    @IntrinsicCandidate
+    private static native void setLockId(long tid);
+
     // -- JVM TI support --
 
     @IntrinsicCandidate
     @JvmtiMountTransition
     private native void notifyJvmtiStart();
@@ -1291,20 +1519,43 @@
     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();
+                return (Executor) ctor.newInstance();
+            } 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 = () -> {
@@ -1374,20 +1625,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 == 1;
+                VirtualThread nextThread = vthread.next;
+
+                // remove from list and unblock
+                vthread.next = null;
+                boolean changed = vthread.compareAndSetOnWaitingList((byte) 1, (byte) 0);
+                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
@@ -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/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
@@ -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/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
@@ -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.  Oracle designates this
@@ -27,561 +27,1006 @@
 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;
 
 /**
- * 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
+ * An API for <em>structured concurrency</em>. {@code StructuredTaskScope} supports cases
+ * where a main 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.
  *
- * <h2>Basic operation</h2>
+ * <p> {@code StructuredTaskScope} defines the static method {@link #open(Joiner) 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 in 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 main 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}), and the {@code close} method throws an exception after closing
+ * if the owner did not invoke the {@code join} method.
  *
- * 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.
- * {@snippet lang=java :
- *     Callable<String> task1 = ...
- *     Callable<Integer> task2 = ...
+ * <p> A {@code StructuredTaskScope} is opened with a {@link Joiner} that handles subtask
+ * completion and produces the outcome (the result or an exception) for the {@link #join()
+ * join} method. The {@code Joiner} interface defines static methods to create a
+ * {@code Joiner} for common cases.
+ *
+ * <p> A {@code Joiner} may <a id="CancelExecution"><em>cancel execution</em></a>
+ * (sometimes called "short-circuiting") when some condition is reached that does not
+ * require the result of subtasks that are still executing. Cancelling execution 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 a result (or exception). The {@link #close() close}
+ * method always waits for threads executing subtasks to finish, even if execution is
+ * cancelled, so it cannot continue beyond the {@code close} method until the interrupted
+ * threads finish. Subtasks should be coded so that they finish as soon as possible when
+ * interrupted. Subtasks that block on methods that are not interruptible may delay the
+ * closing of a task scope.
  *
- *     try (var scope = new StructuredTaskScope<Object>()) {
+ * <p> Consider the example of a main 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 main task in this example is interested in the successful result from both
+ * subtasks. It uses {@link Joiner#awaitAllSuccessfulOrThrow()
+ * Joiner.awaitAllSuccessfulOrThrow()} to create a {@code Joiner} that waits for both
+ * subtasks to complete successfully or for either subtask to fail.
+ * {@snippet lang=java :
+ *    try (var scope = StructuredTaskScope.open(Joiner.awaitAllSuccessfulOrThrow())) {
  *
- *         Subtask<String> subtask1 = scope.fork(task1);   // @highlight substring="fork"
- *         Subtask<Integer> subtask2 = scope.fork(task2);  // @highlight substring="fork"
+ *        // @link substring="fork" target="#fork(Callable)" :
+ *        Subtask<String> subtask1 = scope.fork(() -> query(left));
+ *        Subtask<Integer> subtask2 = scope.fork(() -> query(right));
  *
- *         scope.join();                                   // @highlight substring="join"
+ *        // throws if either subtask fails
+ *        scope.join();  // @link substring="join" target="#join()"
  *
- *         ... process results/exceptions ...
+ *        // both subtasks completed successfully
+ *        return new MyResult(subtask1.get(), subtask2.get()); // @link substring="get" target="Subtask#get()"
  *
- *     } // close                                          // @highlight substring="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> In this example, the main task forks the two subtasks. The {@code fork} method
+ * returns a {@link Subtask Subtask} that is a handle to the forked subtask. The main task
+ * waits in the {@code join} method for both subtasks to complete successfully or for either
+ * subtask to fail. If both subtasks complete successfully then the {@code join} method
+ * completes and the main task uses the {@link Subtask#get() Subtask.get()} method to get
+ * the result of each subtask. If either subtask fails then the {@code Joiner} causes the
+ * other subtask to be cancelled (this will interrupt the thread executing the subtask)
+ * and the {@code join} method throws {@link ExecutionException} with the exception from
+ * the failed subtask as the {@linkplain Throwable#getCause() cause}.
  *
- *         List<Subtask<String>> subtasks = callables.stream().map(scope::fork).toList();
+ * <p> Now consider another example that also splits into two subtasks to concurrently
+ * fetch resources. In this example, the code in the main 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.join();
+ *        scope.fork(() -> query(left));  // @link substring="fork" target="#fork(Callable)"
+ *        scope.fork(() -> query(right));
  *
- *         Map<Boolean, Set<Subtask<String>>> map = subtasks.stream()
- *                 .collect(Collectors.partitioningBy(h -> h.state() == Subtask.State.SUCCESS,
- *                                                    Collectors.toSet()));
+ *        // throws if both subtasks fail
+ *        String firstResult = scope.join();
  *
- *     } // close
+ *    // @link substring="close" target="#close()" :
+ *    } // close
  * }
  *
- * <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.
- *
- * <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 main 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 first subtask. Cancelling the other
+ * subtask avoids the main task waiting for a result that it doesn't care about. If both
+ * subtasks fail then the {@code join} method throws {@link ExecutionException} 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 main 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
+ * ExecutionException} 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.
- * {@snippet lang=java :
- *     try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
+ * <p> Many of the details for how exceptions are handled will depend on usage. In some
+ * cases, the {@code join} method will be called in a {@code try-catch} block to catch
+ * {@code ExecutionException} and handle the cause. The exception handling may use
+ * {@code instanceof} with pattern matching to handle specific causes. In some cases it
+ * may not be useful to catch {@code ExecutionException} but instead leave it to propagate
+ * to the configured {@linkplain Thread.UncaughtExceptionHandler uncaught exception handler}
+ * for logging purposes.
  *
- *         scope.fork(() -> fetch(left));
- *         scope.fork(() -> fetch(right));
+ * <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 code in the main task handling the exception.
  *
- *         scope.join();
+ * <h2>Configuration</h2>
  *
- *         // @link regex="result(?=\()" target="ShutdownOnSuccess#result" :
- *         String result = scope.result(e -> new WebApplicationException(e));
+ * 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.
  *
- *         ...
- *     }
- * }
- * 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 = ...
+ * <p> The 1-arg {@link #open(Joiner) open} method creates 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.
  *
- *    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
+ * <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 execution 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.
  *
- *         Supplier<String> supplier1 = scope.fork(() -> query(left));
- *         Supplier<String> supplier2 = scope.fork(() -> query(right));
+ * <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();
  *
- *         scope.joinUntil(deadline);
+ *    // @link substring="withThreadFactory" target="Config#withThreadFactory(ThreadFactory)" :
+ *    try (var scope = StructuredTaskScope.open(joiner, cf -> cf.withThreadFactory(factory))) {
  *
- *         // @link substring="throwIfFailed" target="ShutdownOnFailure#throwIfFailed" :
- *         scope.throwIfFailed(e -> new WebApplicationException(e));
+ *        scope.fork( .. );   // runs in a virtual thread with name "duke-0"
+ *        scope.fork( .. );   // runs in a virtual thread with name "duke-1"
  *
- *         // both subtasks completed successfully
- *         String result = Stream.of(supplier1, supplier2)
- *                 .map(Supplier::get)
- *                 .collect(Collectors.joining(", ", "{ ", " }"));
+ *        scope.join();
  *
- *         ...
  *     }
- * }
- *
- * <h2>Extending StructuredTaskScope</h2>
- *
- * {@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.
- *
- * <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 task scope is opened. If the timeout expires before the {@code join}
+ * method has completed then <a href="#CancelExecution">execution is cancelled</a>. This
+ * interrupts the threads executing the two subtasks and causes the {@link #join() join}
+ * method to throw {@link ExecutionException} with {@link TimeoutException} as the cause.
  * {@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(() -> query(left));
+ *        scope.fork(() -> query(right));
  *
- *         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 task scope. When a {@code ScopedValue} object is bound to a value in the thread
+ * executing the main task then that binding is inherited by the threads created to
+ * execute the subtasks. The thread executing the main 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 task 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 main 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#callWhere" :
+ *     Result result = ScopedValue.callWhere(USERNAME, "duke", () -> {
  *
- *             scope.fork(() -> childTask());           // @highlight substring="fork"
- *             ...
- *          }
- *     });
+ *         try (var scope = StructuredTaskScope.open(Joiner.awaitAllSuccessfulOrThrow())) {
  *
- *     ...
+ *             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 main 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 main 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
+ * task 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 task 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 <R> the type of the result returned by the join method
+ *
+ * @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 {
+        try {
+            MethodHandles.Lookup l = MethodHandles.lookup();
+            CANCELLED = l.findVarHandle(StructuredTaskScope.class,"cancelled", boolean.class);
+        } catch (Exception e) {
+            throw new ExceptionInInitializerError(e);
+        }
+    }
+
+    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;
+    // fields that are only accessed by owner thread
+    private boolean needToJoin;     // set by fork to indicate that owner must join
+    private boolean joined;         // set to true when owner joins
+    private boolean closed;
+    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 IllegalStateException if the task scope is closed.
+     */
+    private void ensureOpen() {
+        assert Thread.currentThread() == flock.owner();
+        if (closed) {
+            throw new IllegalStateException("Task scope is closed");
+        }
+    }
+
+    /**
+     * 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 invoked by the owner thread and the owner thread
+     * has not joined.
+     */
+    private void ensureJoinedIfOwner() {
+        if (Thread.currentThread() == flock.owner() && !joined) {
+            String msg = needToJoin ? "Owner did not join" : "join did not complete";
+            throw new IllegalStateException(msg);
+        }
+    }
+
+    /**
+     * Interrupts all threads in this task 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 exception if not already cancelled.
+     */
+    private void cancelExecution() {
+        if (!cancelled && CANCELLED.compareAndSet(this, false, true)) {
+            // prevent new threads from starting
+            flock.shutdown();
+
+            // interrupt all unfinished threads
+            interruptAll();
+
+            // wakeup join
+            flock.wakeup();
+        }
+    }
+
+    /**
+     * Schedules a task to cancel execution on timeout.
+     */
+    private void scheduleTimeout(Duration timeout) {
+        assert Thread.currentThread() == flock.owner() && timerTask == null;
+        timerTask = TimerSupport.schedule(timeout, () -> {
+            if (!cancelled) {
+                timeoutExpired = true;
+                cancelExecution();
+            }
+        });
+    }
+
+    /**
+     * 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 execution
+     * was cancelled.
+     */
+    private void onComplete(SubtaskImpl<? extends T> subtask) {
+        assert subtask.state() != Subtask.State.UNAVAILABLE;
+        if (joiner.onComplete(subtask)) {
+            cancelExecution();
+        }
+    }
 
-    // state: set to SHUTDOWN by any thread, set to CLOSED by owner, read by any thread
-    private volatile int state;
+    /**
+     * 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;
 
-    // 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
+        if (name == null)
+            name = Objects.toIdentityString(this);
+        this.flock = ThreadFlock.open(name);
+    }
 
     /**
-     * Represents a subtask forked with {@link #fork(Callable)}.
+     * 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 execution
+             * was cancelled, or it was forked after execution 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 task scope owner and it has not joined
          * @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 task scope owner and it has not joined
          * @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 a main task 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 execution 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. It cancels
+     *   execution and 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 execution and
+     *   causes {@code join} to throw if any subtask fails.
+     *   <li> {@link #awaitAll() awaitAll()} creates a {@code Joiner} that waits for all
+     *   subtasks. If does not cancel execution.
+     * </ul>
      *
-     * @param name the name of the task scope, can be null
-     * @param factory the thread factory
-     */
-    @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);
-    }
-
-    /**
-     * Creates an unnamed structured task scope that creates virtual threads. The task
-     * scope is owned by the current thread.
+     * <p> In addition to the methods to create {@code Joiner} objects for common cases,
+     * the {@link #all(Predicate) all(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 execution should continue or be cancelled.
+     * This {@code Joiner} can be built upon to create custom policies that cancel
+     * execution 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 execution
+     * should be cancelled. These methods can be used to collect subtasks, results, or
+     * exceptions, and control when to cancel execution. 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.
+     *
+     * @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 task 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.
      *
-     * @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.
+     * @param <T> the result type of tasks executed in the task scope
+     * @param <R> the type of results returned by the join method
+     * @since 24
+     * @see #open(Joiner)
      */
-    public StructuredTaskScope() {
-        this(null, Thread.ofVirtual().factory());
-    }
+    @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
+    @FunctionalInterface
+    public interface Joiner<T, R> {
 
-    private IllegalStateException newIllegalStateExceptionScopeClosed() {
-        return new IllegalStateException("Task scope is closed");
-    }
+        /**
+         * 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 execution
+         */
+        default boolean onFork(Subtask<? extends T> subtask) {
+            if (subtask.state() != Subtask.State.UNAVAILABLE) {
+                throw new IllegalArgumentException();
+            }
+            return false;
+        }
 
-    private IllegalStateException newIllegalStateExceptionNoJoin() {
-        return new IllegalStateException("Owner did not join after forking subtasks");
-    }
+        /**
+         * 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 execution has been 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 execution
+         */
+        default boolean onComplete(Subtask<? extends T> subtask) {
+            if (subtask.state() == Subtask.State.UNAVAILABLE) {
+                throw new IllegalArgumentException();
+            }
+            return false;
+        }
 
-    /**
-     * Throws IllegalStateException if the scope is closed, returning the state if not
-     * closed.
-     */
-    private int ensureOpen() {
-        int s = state;
-        if (s == CLOSED)
-            throw newIllegalStateExceptionScopeClosed();
-        return s;
-    }
+        /**
+         * Invoked by {@link #join()} to produce the result (or exception) after waiting
+         * for all subtasks to complete or execution to be cancelled. The result from this
+         * method is returned by the {@code join} method. If this method throws, then
+         * {@code join} throws {@link ExecutionException} with the exception thrown by
+         * this method as the cause.
+         *
+         * <p> In normal usage, this method will be called at most once to produce the
+         * result (or exception). If the {@code join} method is called more than once
+         * then this method may be called more than once to produce the result. 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;
 
-    /**
-     * 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");
-    }
+        /**
+         * {@return a new Joiner object that yields a stream of all subtasks when all
+         * subtasks complete successfully, or throws if any subtask fails}
+         * If any subtask fails then execution is cancelled.
+         *
+         * <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 This joiner 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. A typical usage will be when the
+         * subtasks return results of the same type, the returned stream of forked
+         * subtasks can be used to get the results.
+         *
+         * @param <T> the result type of subtasks
+         */
+        static <T> Joiner<T, Stream<Subtask<T>>> allSuccessfulOrThrow() {
+            return new AllSuccessful<>();
+        }
 
-    /**
-     * Throws WrongThreadException if the current thread is not the owner
-     * or a thread contained in the tree.
-     */
-    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");
-    }
+        /**
+         * {@return a new Joiner object that yields the result of a subtask that completed
+         * successfully, or throws if all subtasks fail} If any subtask completes
+         * successfully then execution is cancelled.
+         *
+         * <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.
+         *
+         * @apiNote This joiner 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.
+         *
+         * @param <T> the result type of subtasks
+         */
+        static <T> Joiner<T, T> anySuccessfulResultOrThrow() {
+            return new AnySuccessful<>();
+        }
 
-    /**
-     * Throws IllegalStateException if the current thread is the owner, and the owner did
-     * not join after forking a subtask in the given fork round.
-     */
-    private void ensureJoinedIfOwner(int round) {
-        if (Thread.currentThread() == flock.owner() && (round > lastJoinCompleted)) {
-            throw newIllegalStateExceptionNoJoin();
+        /**
+         * {@return a new Joiner object that waits for all successful subtasks. It
+         * <a href="StructuredTaskScope.html#CancelExecution">cancels execution</a> 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 This joiner is intended for cases where the results for all subtasks
+         * are required ("invoke all"), and where the code {@linkplain #fork(Callable)
+         * forking} subtasks keeps a reference to the {@linkplain Subtask Subtask} objects.
+         * A typical usage will be when subtasks return results of different types.
+         *
+         * @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}
+         *
+         * <p> The joiner's {@link Joiner#result() result} method returns {@code null}.
+         *
+         * @apiNote This joiner is intended 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, cancelling
+         * execution when evaluating a completed subtask with the given predicate returns
+         * {@code true}}
+         *
+         * <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#CancelExecution">
+         * execution 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 execution 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#CancelExecution">cancels execution</a> 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 joinPolicy = 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>>> all(Predicate<Subtask<? extends T>> isDone) {
+            return new AllSubtasks<>(isDone);
         }
     }
 
     /**
-     * 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.
+     * Represents the configuration for a {@code StructuredTaskScope}.
      *
-     * @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.
+     * <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.
      *
-     * @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
+     * <p> Creating a {@code StructuredTaskScope} with its 1-arg {@link #open(Joiner) open}
+     * method 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.
+     *
+     * <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
      */
-    protected final void ensureOwnerAndJoined() {
-        ensureOwner();
-        if (forkRound > lastJoinCompleted) {
-            throw newIllegalStateExceptionNoJoin();
-        }
+    @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 task 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);
+
+        /**
+         * {@return a new {@code Config} object with the given name}
+         * The other components are the same as this object. A task scope is optionally
+         * named for the purposes of monitoring and management.
+         * @param name the name
+         * @see StructuredTaskScope#toString()
+         */
+        Config withName(String name);
+
+        /**
+         * {@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);
     }
 
     /**
-     * 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 structured task scope 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 structured task scope. The function may, for example,
+     * set the {@linkplain Config#withThreadFactory(ThreadFactory) ThreadFactory} or set
+     * a {@linkplain Config#withTimeout(Duration) timeout}.
      *
-     * @implSpec The default implementation throws {@code NullPointerException} if the
-     * subtask is {@code null}. It throws {@link IllegalArgumentException} if the subtask
-     * has not completed.
+     * <p> If a {@linkplain Config#withThreadFactory(ThreadFactory) ThreadFactory} is set
+     * then the {@code ThreadFactory}'s {@link ThreadFactory#newThread(Runnable) newThread}
+     * method will be used to create threads when forking subtasks in this task scope.
      *
-     * @apiNote The {@code handleComplete} method should be thread safe. It may be
-     * invoked by several threads concurrently.
+     * <p> If a {@linkplain Config#withTimeout(Duration) timeout} is set then it starts
+     * when the task scope is opened. If the timeout expires before the task scope has
+     * {@linkplain #join() joined} then execution is cancelled and the {@code join} method
+     * throws {@link ExecutionException} with {@link TimeoutException} as the cause.
      *
-     * @param subtask the subtask
+     * <p> The new task 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 task scope.
      *
-     * @throws IllegalArgumentException if called with a subtask that has not completed
+     * <p> Construction captures the current thread's {@linkplain ScopedValue scoped
+     * value} bindings for inheritance by threads started in the task scope.
+     *
+     * @param joiner the joiner
+     * @param configFunction a function to produce the configuration
+     * @return a new task scope
+     * @param <T> the result type of tasks executed in the task scope
+     * @param <R> the type of the result returned by the join method
+     * @since 24
      */
-    protected void handleComplete(Subtask<? extends T> subtask) {
-        if (subtask.state() == Subtask.State.UNAVAILABLE)
-            throw new IllegalArgumentException();
+    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 done = false;
+            try {
+                scope.scheduleTimeout(timeout);
+                done = true;
+            } finally {
+                if (!done) {
+                    scope.close();  // pop if scheduling timeout failed
+                }
+            }
+        }
+
+        return scope;
     }
 
     /**
-     * Starts a new thread in this task scope to execute a value-returning task, thus
-     * creating a <em>subtask</em> of this task scope.
+     * Opens a new structured task scope to use the given {@code Joiner} object. The
+     * task 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.
      *
-     * <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
-     * {@linkplain ScopedValue scoped value} bindings. The bindings must match the bindings
-     * captured when the task scope was created.
+     * @implSpec
+     * This factory method is equivalent to invoking the 2-arg open method with the given
+     * joiner and the {@linkplain Function#identity() identity function}.
      *
-     * <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.
+     * @param joiner the joiner
+     * @return a new task scope
+     * @param <T> the result type of tasks executed in the task scope
+     * @param <R> the type of the result returned by the join method
+     * @since 24
+     */
+    public static <T, R> StructuredTaskScope<T, R> open(Joiner<? super T, ? extends R> joiner) {
+        return open(joiner, Function.identity());
+    }
+
+    /**
+     * Starts a new thread in this task 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> 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 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 execution is
+     * {@linkplain #isCancelled() cancelled}, or {@code onFork} returns {@code true} to
+     * cancel execution, then this method returns the {@code Subtask} (in the {@link
+     * Subtask.State#UNAVAILABLE UNAVAILABLE} state) without creating a thread to execute
+     * the subtask. If execution is not cancelled then a thread is created with the
+     * {@link ThreadFactory} configured when the task 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 opened. If the subtask completes (successfully or with an exception)
+     * before execution 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> This method may only be invoked by the task scope owner or threads contained
-     * in the task scope.
+     * <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 task 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 task scope owner after it has joined and the subtask
+     * failed. If execution was cancelled before the subtask was forked, or before it
+     * completes, then neither method can be used to obtain the outcome.
      *
-     * @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 task 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 IllegalStateException if this task scope is closed or the owner has already
+     * joined
+     * @throws WrongThreadException if the current thread is not the task scope owner
      * @throws StructureViolationException if the current scoped value bindings are not
      * the same as when the task 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();
+        ensureOpen();
+        if (joined) {
+            throw new IllegalStateException("Already joined");
         }
 
-        SubtaskImpl<U> subtask = new SubtaskImpl<>(this, task, round);
-        if (s < SHUTDOWN) {
+        var subtask = new SubtaskImpl<U>(this, task);
+
+        // notify joiner, even if cancelled
+        if (joiner.onFork(subtask)) {
+            cancelExecution();
+        }
+
+        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
@@ -591,217 +1036,165 @@
                 // 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
+        needToJoin = true;
         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.
-     *
-     * <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 may only be invoked by the task scope owner.
+     * Starts a new thread in this task scope to execute a task that does not return a
+     * result, creating a <em>subtask</em>.
      *
-     * @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.
+     * <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}.
      *
-     * @return this task scope
-     * @throws IllegalStateException if this task scope is closed
+     * @param task the task for the thread to execute
+     * @return the subtask
+     * @throws IllegalStateException if this task scope is closed or the owner has already
+     * joined
      * @throws WrongThreadException if the current thread is not the task scope owner
-     * @throws InterruptedException if interrupted while waiting
+     * @throws StructureViolationException if the current scoped value bindings are not
+     * the same as when the task 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 task scope to complete or execution to be
+     * cancelled. If a {@linkplain  Config#withTimeout(Duration) timeout} has been set
+     * then execution will be 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 ExecutionException} 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}.
+     * when all threads finish, the {@code Joiner}'s {@link Joiner#onFork(Subtask)
+     * onFork} or {@link Joiner#onComplete(Subtask) onComplete} returns {@code true}
+     * to cancel execution, the timeout (if set) expires, or the current thread is
+     * {@linkplain Thread#interrupt() interrupted}.
      *
      * <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.joinUntil} to ensure that the method waits for threads in this task scope to
-     * finish.
-     *
-     * @param deadline the deadline
-     * @return this task scope
+     * @return the {@link Joiner#result() result}
      * @throws IllegalStateException if this task scope is closed
      * @throws WrongThreadException if the current thread is not the task scope owner
+     * @throws ExecutionException if the joiner's {@code result} method throws, or with
+     * cause {@link TimeoutException} if a timeout is set and the timeout expires
      * @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 ExecutionException, InterruptedException {
+        ensureOwner();
+        ensureOpen();
 
-    @SuppressWarnings("removal")
-    private void interruptAll() {
-        if (System.getSecurityManager() == null) {
-            implInterruptAll();
-        } else {
-            PrivilegedAction<Void> pa = () -> {
-                implInterruptAll();
-                return null;
-            };
-            AccessController.doPrivileged(pa);
-        }
-    }
+        if (!joined) {
+            // owner has attempted to join
+            needToJoin = false;
 
-    /**
-     * 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 threads, execution to be cancelled, or interrupt
+            flock.awaitAll();
 
-                // set status before interrupting tasks
-                state = SHUTDOWN;
+            // throw if timeout expired while waiting
+            if (timeoutExpired) {
+                throw new ExecutionException(new TimeoutException());
+            }
 
-                // interrupt all unfinished threads
-                interruptAll();
+            // join completed successfully
+            cancelTimeout();
+            joined = true;
+        }
 
-                return true;
-            } else {
-                // already shutdown
-                return false;
-            }
-        } finally {
-            shutdownLock.unlock();
+        // invoke joiner to get result
+        try {
+            return joiner.result();
+        } catch (Throwable e) {
+            throw new ExecutionException(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>
+     * Cancels execution. This method allows the task scope owner to explicitly
+     * <a href="#CancelExecution">cancel execution</a>. If not already cancelled, this
+     * method {@linkplain Thread#interrupt() interrupts} the threads executing subtasks
+     * that have not completed, and prevents new threads from being started in the task
+     * scope.
      *
-     * <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.
+     * @apiNote This method is intended for cases where a task scope is created with a
+     * {@link Joiner Joiner} that doesn't cancel execution or where the code in the main
+     * task needs to cancel execution due to some exception or other condition in the main
+     * task. The following example accepts network connections indefinitely, forking a
+     * subtask to handle each connection. If the {@code accept()} method in the example
+     * throws then the main task cancels execution before joining and closing the scope.
+     * The {@link #close() close} method waits for the interrupted threads to finish.
      *
-     * <p> This method may only be invoked by the task scope owner or threads contained
-     * in the task scope.
+     * {@snippet lang=java :
+     *    // @link substring="awaitAll" target="Joiner#awaitAll()" :
+     *    try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
      *
-     * @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.
+     *        try {
+     *            while (true) {
+     *                Socket peer = listener.accept();
+     *                // @link substring="fork" target="#fork(Runnable)" :
+     *                scope.fork(() -> handle(peer));
+     *            }
+     *        } finally {
+     *            scope.cancel();
+     *            scope.join();    // completes immediately
+     *        }
      *
-     * @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()
+     * @throws WrongThreadException if the current thread is not the task scope owner
+     * @since 24
      */
-    public void shutdown() {
-        ensureOwnerOrContainsThread();
-        int s = ensureOpen();  // throws ISE if closed
-        if (s < SHUTDOWN && implShutdown())
-            flock.wakeup();
+    public void cancel() {
+        ensureOwner();
+        ensureOpen();
+        cancelExecution();
     }
 
     /**
-     * {@return true if this task scope is shutdown, otherwise false}
-     * @see #shutdown()
+     * {@return {@code true} if <a href="#CancelExecution">execution is cancelled</a>,
+     * or in the process of being cancelled, otherwise {@code false}}
+     *
+     * <p> Cancelling execution prevents new threads from starting in the task 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.
+     *
+     * @apiNote A main task with a lengthy "forking phase" (the code that executes before
+     * the main task invokes {@link #join() join}) may use this method to avoid doing work
+     * in cases where execution was cancelled by the completion of a previously forked
+     * subtask or timeout.
+     *
+     * @since 24
      */
-    public final boolean isShutdown() {
-        return state >= SHUTDOWN;
+    public boolean isCancelled() {
+        return cancelled;
     }
 
     /**
      * Closes this task 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="#CancelExecution">cancels execution</a>, 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> A {@code StructuredTaskScope} is intended to be used in a <em>structured
@@ -817,49 +1210,49 @@
      * 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.
      *
-     * @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
      * owner did not attempt to join after forking
      * @throws WrongThreadException if the current thread is not the task scope owner
      * @throws StructureViolationException if a structure violation was detected
      */
     @Override
     public void close() {
         ensureOwner();
-        int s = state;
-        if (s == CLOSED)
+        if (closed) {
             return;
+        }
+
+        // cancel execution if not already joined
+        if (!joined) {
+            cancelExecution();
+            cancelTimeout();
+        }
 
+        // wait for stragglers
         try {
-            if (s < SHUTDOWN)
-                implShutdown();
             flock.close();
         } finally {
-            state = CLOSED;
+            closed = true;
         }
 
-        // 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 (needToJoin) {
+            needToJoin = false;
+            throw new IllegalStateException("Owner did not join");
         }
     }
 
+    /**
+     * {@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.
      */
@@ -870,21 +1263,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;
@@ -893,26 +1282,21 @@
                 result = task.call();
             } catch (Throwable e) {
                 ex = e;
             }
 
-            // nothing to do if task scope is shutdown
-            if (scope.isShutdown())
+            // nothing to do if task 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;
@@ -924,13 +1308,14 @@
             } else {
                 return State.SUCCESS;
             }
         }
 
+
         @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")
@@ -941,11 +1326,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(
@@ -965,366 +1350,209 @@
             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. If any subtask fails then execution is cancelled.
      */
-    @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 {
             try {
                 MethodHandles.Lookup l = MethodHandles.lookup();
-                FIRST_RESULT = l.findVarHandle(ShutdownOnSuccess.class, "firstResult", Object.class);
-                FIRST_EXCEPTION = l.findVarHandle(ShutdownOnSuccess.class, "firstException", Throwable.class);
+                FIRST_EXCEPTION = l.findVarHandle(AllSuccessful.class, "firstException", Throwable.class);
             } catch (Exception e) {
                 throw new ExceptionInInitializerError(e);
             }
         }
-        private volatile Object firstResult;
+        // list of forked subtasks, only accessed by owner thread
+        private final List<Subtask<T>> subtasks = new ArrayList<>();
         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);
+        @Override
+        public boolean onFork(Subtask<? extends T> subtask) {
+            @SuppressWarnings("unchecked")
+            var tmp = (Subtask<T>) 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.
+     * If any subtask completes successfully then execution is cancelled.
+     */
+    private static final class AnySuccessful<T> implements Joiner<T, T> {
+        private static final VarHandle FIRST_SUCCESS;
+        private static final VarHandle FIRST_EXCEPTION;
+        static {
+            try {
+                MethodHandles.Lookup l = MethodHandles.lookup();
+                FIRST_SUCCESS = l.findVarHandle(AnySuccessful.class, "firstSuccess", Subtask.class);
+                FIRST_EXCEPTION = l.findVarHandle(AnySuccessful.class, "firstException", Throwable.class);
+            } catch (Exception e) {
+                throw new ExceptionInInitializerError(e);
             }
         }
+        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) {
+            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;
-        }
-
-        /**
-         * {@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);
-        }
-
-        /**
-         * 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;
+        public T result() throws Throwable {
+            Subtask<T> firstSuccess = this.firstSuccess;
+            if (firstSuccess != null) {
+                return firstSuccess.get();
             }
-
-            Throwable exception = firstException;
-            if (exception != null) {
-                X ex = esf.apply(exception);
-                Objects.requireNonNull(ex, "esf returned null");
-                throw ex;
+            Throwable firstException = this.firstException;
+            if (firstException != null) {
+                throw firstException;
+            } else {
+                throw new NoSuchElementException("No subtasks completed");
             }
-
-            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 that waits for all successful subtasks. If any subtask fails the
+     * execution is cancelled.
      */
-    @PreviewFeature(feature = PreviewFeature.Feature.STRUCTURED_CONCURRENCY)
-    public static final class ShutdownOnFailure extends StructuredTaskScope<Object> {
+    private static final class AwaitSuccessful<T> implements Joiner<T, Void> {
         private static final VarHandle FIRST_EXCEPTION;
         static {
             try {
                 MethodHandles.Lookup l = MethodHandles.lookup();
-                FIRST_EXCEPTION = l.findVarHandle(ShutdownOnFailure.class, "firstException", Throwable.class);
+                FIRST_EXCEPTION = l.findVarHandle(AwaitSuccessful.class, "firstException", Throwable.class);
             } catch (Exception e) {
                 throw new ExceptionInInitializerError(e);
             }
         }
         private volatile Throwable firstException;
 
-        /**
-         * 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);
+        @Override
+        public boolean onComplete(Subtask<? extends T> subtask) {
+            return (subtask.state() == Subtask.State.FAILED)
+                    && (firstException == null)
+                    && FIRST_EXCEPTION.compareAndSet(this, null, subtask.exception());
         }
 
-        /**
-         * 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 Void result() throws Throwable {
+            Throwable ex = firstException;
+            if (ex != null) {
+                throw ex;
+            } else {
+                return null;
+            }
+        }
+    }
+
+    /**
+     * A joiner that returns a stream of all subtasks.
+     */
+    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<>();
+
+        AllSubtasks(Predicate<Subtask<? extends T>> isDone) {
+            this.isDone = Objects.requireNonNull(isDone);
         }
 
         @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 onFork(Subtask<? extends T> subtask) {
+            @SuppressWarnings("unchecked")
+            var tmp = (Subtask<T>) subtask;
+            subtasks.add(tmp);
+            return false;
         }
 
-        /**
-         * 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 boolean onComplete(Subtask<? extends T> subtask) {
+            return isDone.test(subtask);
         }
 
-        /**
-         * 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 Stream<Subtask<T>> result() {
+            return subtasks.stream();
         }
+    }
 
-        /**
-         * 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);
+    /**
+     * 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);
         }
 
-        /**
-         * 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 withThreadFactory(ThreadFactory threadFactory) {
+            return new ConfigImpl(Objects.requireNonNull(threadFactory), name, 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;
-            }
+        @Override
+        public Config withName(String name) {
+            return new ConfigImpl(threadFactory, Objects.requireNonNull(name), timeout);
+        }
+
+        @Override
+        public Config withTimeout(Duration timeout) {
+            return new ConfigImpl(threadFactory, name, Objects.requireNonNull(timeout));
+        }
+    }
+
+    /**
+     * Used to schedule a task to cancel exception 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/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
@@ -41,11 +41,13 @@
 import java.security.ProtectionDomain;
 import java.util.List;
 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.misc.CarrierThreadLocal;
 import jdk.internal.module.ServicesCatalog;
 import jdk.internal.reflect.ConstantPool;
@@ -574,10 +576,20 @@
      * @throws IllegalArgumentException if the thread is not a virtual thread
      * @throws RejectedExecutionException if the scheduler cannot accept a task
      */
     void unparkVirtualThread(Thread thread);
 
+    /**
+     * 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
@@ -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/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
@@ -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
@@ -273,19 +273,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;
         }
     }
 
@@ -371,16 +364,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
@@ -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,208 @@
+/*
+ * 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 sun.nio.ch.Poller;
+
+/**
+ * The implementation for the jcmd Thread.vthread_summary diagnostic command.
+ */
+public class VThreadSummary {
+    private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess();
+
+    // maximum number of thread containers to print
+    private static final int MAX_THREAD_CONTAINERS = 256;
+
+    // set to true if I/O poller in use
+    private static volatile boolean pollerInitialized;
+
+    private VThreadSummary() { }
+
+    /**
+     * Invoked by the poller I/O mechanism when it initializes.
+     */
+    public static void pollerInitialized() {
+        pollerInitialized = true;
+    }
+
+    /**
+     * 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 thread containers (thread groupings)
+        new ThreadContainersPrinter(sb, MAX_THREAD_CONTAINERS).run();
+        sb.append(System.lineSeparator());
+
+        // print virtual thread scheduler
+        printSchedulerInfo(sb);
+        sb.append(System.lineSeparator());
+
+        // print I/O pollers if initialized
+        if (pollerInitialized) {
+            printPollerInfo(sb);
+        }
+
+        return sb.toString().getBytes(StandardCharsets.UTF_8);
+    }
+
+    /**
+     * Prints the tree of thread containers starting from the root container.
+     */
+    private static class ThreadContainersPrinter {
+        private final StringBuilder sb;
+        private final int max;
+        private int count;
+
+        ThreadContainersPrinter(StringBuilder sb, int max) {
+            this.sb = sb;
+            this.max = max;
+        }
+
+        void run() {
+            printThreadContainers(ThreadContainers.root(), 0);
+        }
+
+        /**
+         * Prints the given thread container and its children.
+         * @return true if the thread container and all its children were printed,
+         *   false if the output was truncated because the max was reached
+         */
+        private boolean printThreadContainers(ThreadContainer container, int depth) {
+            if (!printThreadContainer(container, depth)) {
+                return false;
+            }
+            boolean truncated = container.children()
+                    .map(c -> printThreadContainers(c, depth + 1))
+                    .anyMatch(b -> b == false);
+            return !truncated;
+        }
+
+        /**
+         * Prints the given thread container or a "truncated" message if the maximum
+         * number of thread containers has already been printed.
+         * @param container the thread container
+         * @param depth the depth in the tree, for indentation purposes
+         * @return true if the thread container was printed, false if beyond max
+         */
+        private boolean printThreadContainer(ThreadContainer container, int depth) {
+            count++;
+            if (count > max) {
+                sb.append("<truncated ...>")
+                        .append(System.lineSeparator());
+                return false;
+            }
+
+            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);
+            if (depth > 0) {
+                int indent = depth * 4;
+                sb.append(" ".repeat(indent)).append("+-- ");
+            }
+            sb.append(container)
+                    .append(" [platform threads = ")
+                    .append(platformThreadCount)
+                    .append(", virtual threads = ")
+                    .append(virtualThreadCount)
+                    .append("]")
+                    .append(System.lineSeparator());
+
+            return true;
+        }
+    }
+
+    /**
+     * Print information on the virtual thread schedulers to given string buffer.
+     */
+    static void printSchedulerInfo(StringBuilder sb) {
+        sb.append("Default 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 buffer.
+     */
+    private static void printPollerInfo(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()));
+    }
+}
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,34 +24,40 @@
  */
 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;
 import java.util.function.BooleanSupplier;
 import jdk.internal.misc.InnocuousThread;
+import jdk.internal.vm.VThreadSummary;
 import sun.security.action.GetPropertyAction;
 
 /**
  * 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();
             POLLERS = pollers;
         } catch (IOException ioe) {
             throw new ExceptionInInitializerError(ioe);
         }
+
+        // notify serviceability support that the poller I/O mechanism has started
+        VThreadSummary.pollerInitialized();
     }
 
     // maps file descriptors to parked Thread
     private final Map<Integer, Thread> map = new ConcurrentHashMap<>();
 
@@ -140,10 +146,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 +276,15 @@
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
 
+    @Override
+    public String toString() {
+        return Objects.toIdentityString(this) + " [registered = " + map.size() + "]";
+    }
+
     /**
      * The Pollers used for read and write events.
      */
     private static class Pollers {
         private final PollerProvider provider;
@@ -342,10 +367,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 +390,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 +438,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/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
@@ -36,11 +36,20 @@
     { "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 },
+    { "setLockId",                 "(J)V", (void *)&JVM_SetLockId},
+    { "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
@@ -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, 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.
@@ -49,10 +49,12 @@
     headerFieldOffset = 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          headerFieldOffset;
   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
@@ -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.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/doc-files/threadDump.schema.json b/src/jdk.management/share/classes/com/sun/management/doc-files/threadDump.schema.json
--- /dev/null
+++ b/src/jdk.management/share/classes/com/sun/management/doc-files/threadDump.schema.json
@@ -0,0 +1,78 @@
+{
+  "type": "object",
+  "properties": {
+    "threadDump": {
+      "type": "object",
+      "properties": {
+        "processId": {
+          "type": "string",
+          "description": "The native process id of the Java virtual machine."
+        },
+        "time": {
+          "type": "string",
+          "description": "The time in ISO 8601 format when the thread dump was generated."
+        },
+        "runtimeVersion": {
+          "type": "string",
+          "description": "The runtime version, see java.lang.Runtime.version()."
+        },
+        "threadContainers": {
+          "type": "array",
+          "description": "The array of thread containers.",
+          "items": [
+            {
+              "type": "object",
+              "properties": {
+                "container": {
+                  "type": "string",
+                  "description": "The container name. The container name is unique."
+                },
+                "parent": {
+                  "type": ["string", "null"],
+                  "description": "The parent container name or null for the root container."
+                },
+                "owner": {
+                  "type": ["string", "null"],
+                  "description": "The thread identifier of the owner thread or null if not owned."
+                },
+                "threads": {
+                  "type": "array",
+                  "description": "The array of threads in the thread container.",
+                  "items": [
+                    {
+                      "type": "object",
+                      "properties": {
+                        "tid": {
+                          "type": "string",
+                          "description": "The thread identifier, see java.lang.Thread.threadId()."
+                        },
+                        "name": {
+                          "type": "string",
+                          "description": "The thread name."
+                        },
+                        "stack": {
+                          "type": "array",
+                          "description": "The thread stack. The first element is the top of the stack.",
+                          "items": [
+                            {
+                              "type": "string",
+                              "description": "A stack trace element, see java.lang.StackTraceElement."
+                            }
+                          ]
+                        }
+                      }
+                    }
+                  ]
+                },
+                "threadCount": {
+                  "type": "string",
+                  "description": "The number of threads in the thread container."
+                }
+              }
+            }
+          ]
+        }
+      }
+    }
+  }
+}
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
@@ -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/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 0000000 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 0000000 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
 
 ##########
@@ -96,9 +112,27 @@
 ## There is no goal to run all test with virtual test thread factory.
 ## So any test migth be added as incompatible, the bug is not required.
 
 gc/arguments/TestNewSizeThreadIncrease.java 0000000 generic-all
 gc/g1/TestSkipRebuildRemsetPhase.java 0000000 generic-all
+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
@@ -54,5 +54,19 @@
 vmTestbase/nsk/jvmti/scenarios/capability/CM03/cm03t001/TestDescription.java 8299493 macosx-x64,windows-x64
 
 vmTestbase/nsk/stress/thread/thread006.java 8321476 linux-all
 
 gc/arguments/TestNewSizeFlags.java 8299116 macosx-aarch64
+
+############################################################################
+
+# 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
@@ -136,14 +136,26 @@
 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
 
+# redefine java.lang.Object returns JVMTI_ERROR_UNMODIFIABLE_CLASS
+serviceability/jvmti/RedefineClasses/RedefineObject.java 0000000 generic-all
+vmTestbase/nsk/jvmti/scenarios/bcinstr/BI04/bi04t002/TestDescription.java 0000000 generic-all
+
 #############################################################################
 
 # :hotspot_misc
 
 #############################################################################
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
@@ -770,5 +770,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/TEST.quick-groups b/test/hotspot/jtreg/TEST.quick-groups
--- a/test/hotspot/jtreg/TEST.quick-groups
+++ b/test/hotspot/jtreg/TEST.quick-groups
@@ -1183,11 +1183,10 @@
   vmTestbase/nsk/jvmti/scenarios/bcinstr/BI01/bi01t002/TestDescription.java \
   vmTestbase/nsk/jvmti/scenarios/bcinstr/BI02/bi02t001/TestDescription.java \
   vmTestbase/nsk/jvmti/scenarios/bcinstr/BI02/bi02t002/TestDescription.java \
   vmTestbase/nsk/jvmti/scenarios/bcinstr/BI03/bi03t001/TestDescription.java \
   vmTestbase/nsk/jvmti/scenarios/bcinstr/BI03/bi03t002/TestDescription.java \
-  vmTestbase/nsk/jvmti/scenarios/bcinstr/BI04/bi04t002/TestDescription.java \
   vmTestbase/nsk/jvmti/scenarios/capability/CM01/cm01t001/TestDescription.java \
   vmTestbase/nsk/jvmti/scenarios/capability/CM01/cm01t002/TestDescription.java \
   vmTestbase/nsk/jvmti/scenarios/capability/CM01/cm01t003/TestDescription.java \
   vmTestbase/nsk/jvmti/scenarios/capability/CM01/cm01t004/TestDescription.java \
   vmTestbase/nsk/jvmti/scenarios/capability/CM01/cm01t005/TestDescription.java \
diff a/test/hotspot/jtreg/runtime/locking/TestRecursiveMonitorChurn.java b/test/hotspot/jtreg/runtime/locking/TestRecursiveMonitorChurn.java
--- a/test/hotspot/jtreg/runtime/locking/TestRecursiveMonitorChurn.java
+++ b/test/hotspot/jtreg/runtime/locking/TestRecursiveMonitorChurn.java
@@ -55,10 +55,11 @@
     public static volatile Monitor monitor;
     public static void main(String[] args) {
         if (WB.getIntVMFlag("LockingMode") == LM_MONITOR) {
             throw new SkippedException("LM_MONITOR always inflates. Invalid test.");
         }
+        System.out.println("Starting test...");
         final long pre_monitor_count = WB.getInUseMonitorCount();
         System.out.println(" Precount = " + pre_monitor_count);
         for (int i = 0; i < COUNT; i++) {
             monitor = new Monitor();
             monitor.doSomething();
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
@@ -217,16 +217,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/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,86 @@
+/*
+ * 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 Basic test for jcmd Thread.vthread_summary
+ * @enablePreview
+ * @library /test/lib
+ * @run main/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.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
+import jdk.test.lib.dcmd.PidJcmdExecutor;
+import jdk.test.lib.process.OutputAnalyzer;
+
+public class VThreadSummaryTest {
+    public static void main(String[] args) throws Exception {
+
+        // ensure common pool is initialized
+        CompletableFuture.runAsync(() -> { });
+
+        // ensure at least one thread pool executor and one thread-per-task executor
+        try (var pool = Executors.newFixedThreadPool(1);
+             var executor = Executors.newVirtualThreadPerTaskExecutor()) {
+
+            // ensure poller mechanism is initialized by doing blocking I/O on a virtual thread
+            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();
+                        throw new RuntimeException("Connection from " + s + " ???");
+                    } catch (SocketTimeoutException e) {
+                        // expected
+                    }
+                }
+                return null;
+            }).get();
+
+            OutputAnalyzer output = new PidJcmdExecutor().execute("Thread.vthread_summary");
+
+            // thread groupings
+            output.shouldContain("<root>")
+                    .shouldContain("java.util.concurrent.ThreadPoolExecutor")
+                    .shouldContain("java.util.concurrent.ThreadPerTaskExecutor")
+                    .shouldContain("ForkJoinPool.commonPool");
+
+            // virtual thread schedulers
+            output.shouldContain("Default virtual thread scheduler:")
+                    .shouldContain("Running, parallelism")
+                    .shouldContain("Timeout schedulers:");
+
+            // I/O pollers
+            output.shouldContain("Read I/O pollers:")
+                    .shouldContain("Write I/O pollers:");
+        }
+    }
+}
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
@@ -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
@@ -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
@@ -23,25 +23,29 @@
 
 /*
  * @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;
 
 /*
  *     The test exercises the JVMTI function: StopThread(jthread).
  *     The test creates a new virtual or platform thread.
@@ -51,11 +55,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 +108,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 +123,17 @@
             }
 
             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 = (is_virtual && !isBoundVThread && (Platform.isX64() || Platform.isAArch64())) ? JVMTI_ERROR_OPAQUE_FRAME : JVMTI_ERROR_NONE;
+            String expectedRetCodeName = (is_virtual && !isBoundVThread && (Platform.isX64() || Platform.isAArch64())) ?
+                "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 +186,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 +211,14 @@
             } catch (AssertionError ex) {
                 log("TestTask.run: caught expected AssertionError from method A()");
                 seenExceptionFromA = true;
             }
             Thread.interrupted();
-            if (!seenExceptionFromA) {
+            if (!seenExceptionFromA
+                && !(is_virtual
+                     && !isBoundVThread
+                     && (Platform.isX64() || (Platform.isAArch64())) )) {
                 StopThreadTest.setFailed("TestTask.run: expected AssertionError from method A()");
             }
             sleep(1); // to cause yield
 
             boolean seenExceptionFromB = false;
@@ -239,11 +252,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");
         }
 
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
@@ -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,11 +21,10 @@
  * questions.
  */
 
 package java.lang;
 
-import jdk.internal.misc.Blocker;
 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,
@@ -366,25 +365,28 @@
      *             this exception is thrown.
      * @see        java.lang.Object#notify()
      * @see        java.lang.Object#notifyAll()
      */
     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);
         }
     }
 
     /**
      * Causes current thread to wait until another thread invokes the
@@ -459,15 +461,15 @@
         if (nanos < 0 || nanos > 999999) {
             throw new IllegalArgumentException(
                                 "nanosecond timeout value out of range");
         }
 
-            if (nanos >= 500000 || (nanos != 0 && timeout == 0)) {
-                timeout++;
-            }
+        if (nanos > 0 && timeout < Long.MAX_VALUE) {
+            timeout++;
+        }
 
-            wait(timeout);
+        wait(timeout);
     }
 
     // final modifier so method not in vtable
     private final native void wait0(long timeoutMillis) throws InterruptedException;
 
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,81 @@
 
 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.
 
+# test runs with jdk.virtualThreadScheduler.maxPoolSize=1
+java/lang/Thread/virtual/GetStackTraceWhenRunnable.java 0000000 generic-all
+
 # 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/foreign/TestConcurrentClose.java b/test/jdk/java/foreign/TestConcurrentClose.java
--- a/test/jdk/java/foreign/TestConcurrentClose.java
+++ b/test/jdk/java/foreign/TestConcurrentClose.java
@@ -69,11 +69,11 @@
         }
     }
 
     static final int ITERATIONS = 5;
     static final int SEGMENT_SIZE = 10_000;
-    static final int MAX_EXECUTOR_WAIT_SECONDS = 20;
+    static final int MAX_EXECUTOR_WAIT_SECONDS = 60;
     static final int NUM_ACCESSORS = 50;
 
     static final AtomicLong start = new AtomicLong();
     static final AtomicBoolean started = new AtomicBoolean();
 
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/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
@@ -138,10 +138,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/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
@@ -22,15 +22,95 @@
  */
 
 /*
  * @test id=default
  * @summary Test virtual thread with monitor enter/exit
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
  * @modules java.base/java.lang:+open
  * @library /test/lib
  * @run junit/othervm --enable-native-access=ALL-UNNAMED MonitorEnterExit
  */
 
+/*
+ * @test id=LM_LEGACY
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=LM_LIGHTWEIGHT
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xint-LM_LEGACY
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xint -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xint-LM_LIGHTWEIGHT
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xint -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xcomp-LM_LEGACY
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xcomp -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xcomp-LM_LIGHTWEIGHT
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xcomp -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xcomp-TieredStopAtLevel1-LM_LEGACY
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @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
+ * @library /test/lib
+ * @run junit/othervm -Xcomp -XX:TieredStopAtLevel=1 -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xcomp-noTieredCompilation-LM_LEGACY
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xcomp -XX:-TieredCompilation -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorEnterExit
+ */
+
+/*
+ * @test id=Xcomp-noTieredCompilation-LM_LIGHTWEIGHT
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @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;
@@ -55,10 +135,11 @@
 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
@@ -148,10 +229,56 @@
                 assertTrue(Thread.holdsLock(lock));
             }
         }
     }
 
+    /**
+     * Test monitor reenter when there are other threads blocked trying to enter.
+     */
+    @Test
+    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 {
@@ -211,11 +338,11 @@
      */
     private void testEnterWithContentionWhenPinned() throws Exception {
         var lock = new Object();
         var started = new CountDownLatch(1);
         var entered = new AtomicBoolean();
-        Thread vthread = Thread.ofVirtual().unstarted(() -> {
+        Thread vthread  = Thread.ofVirtual().unstarted(() -> {
             VThreadPinner.runPinned(() -> {
                 started.countDown();
                 synchronized (lock) {
                     entered.set(true);
                 }
@@ -232,10 +359,164 @@
 
         // check thread entered monitor
         assertTrue(entered.get());
     }
 
+    /**
+     * Test that parking while holding a monitor releases the carrier.
+     */
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    void testReleaseWhenParked(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 that blocking waiting to enter a monitor releases the carrier.
+     */
+    @Test
+    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 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
+    void testManyParkedThreads() 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();
+        }
+    }
+
+    /**
+     * 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
+    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
@@ -22,15 +22,95 @@
  */
 
 /*
  * @test id=default
  * @summary Test virtual threads using Object.wait/notifyAll
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
  * @modules java.base/java.lang:+open
  * @library /test/lib
  * @run junit/othervm --enable-native-access=ALL-UNNAMED MonitorWaitNotify
  */
 
+/*
+ * @test id=LM_LEGACY
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
+/*
+ * @test id=LM_LIGHTWEIGHT
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
+/*
+ * @test id=Xint-LM_LEGACY
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xint -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
+/*
+ * @test id=Xint-LM_LIGHTWEIGHT
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xint -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
+/*
+ * @test id=Xcomp-LM_LEGACY
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xcomp -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
+/*
+ * @test id=Xcomp-LM_LIGHTWEIGHT
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xcomp -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
+/*
+ * @test id=Xcomp-TieredStopAtLevel1-LM_LEGACY
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @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
+ * @library /test/lib
+ * @run junit/othervm -Xcomp -XX:TieredStopAtLevel=1 -XX:LockingMode=2 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
+/*
+ * @test id=Xcomp-noTieredCompilation-LM_LEGACY
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xcomp -XX:-TieredCompilation -XX:LockingMode=1 --enable-native-access=ALL-UNNAMED MonitorWaitNotify
+ */
+
+/*
+ * @test id=Xcomp-noTieredCompilation-LM_LIGHTWEIGHT
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @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;
@@ -49,11 +129,13 @@
 import jdk.test.lib.thread.VThreadRunner;
 import jdk.test.lib.thread.VThreadRunner;
 import jdk.test.lib.thread.VThreadPinner;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.BeforeAll;
+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 {
@@ -202,10 +284,131 @@
                 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")
+    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")
+    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 {
@@ -488,10 +691,99 @@
         // 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 })
+    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 })
+    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/MonitorsTest.java b/test/jdk/java/lang/Thread/virtual/MonitorsTest.java
--- /dev/null
+++ b/test/jdk/java/lang/Thread/virtual/MonitorsTest.java
@@ -0,0 +1,485 @@
+/*
+ * 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 id=default
+ * @summary Test virtual threads using synchronized
+ * @library /test/lib
+ * @requires vm.continuations
+ * @modules java.base/java.lang:+open
+ * @run junit/othervm MonitorsTest
+ */
+
+/*
+ * @test id=Xint
+ * @library /test/lib
+ * @requires vm.continuations
+ * @modules java.base/java.lang:+open
+ * @run junit/othervm -Xint MonitorsTest
+ */
+
+/*
+ * @test id=Xcomp
+ * @library /test/lib
+ * @requires vm.continuations
+ * @modules java.base/java.lang:+open
+ * @run junit/othervm -Xcomp MonitorsTest
+ */
+
+/*
+ * @test id=Xcomp-TieredStopAtLevel3
+ * @library /test/lib
+ * @requires vm.continuations
+ * @modules java.base/java.lang:+open
+ * @run junit/othervm -Xcomp -XX:TieredStopAtLevel=3 MonitorsTest
+ */
+
+/*
+ * @test id=Xcomp-noTieredCompilation
+ * @summary Test virtual threads using synchronized
+ * @library /test/lib
+ * @requires vm.continuations
+ * @modules java.base/java.lang:+open
+ * @run junit/othervm -Xcomp -XX:-TieredCompilation MonitorsTest
+ */
+
+/*
+ * @test id=gc
+ * @requires vm.debug == true & vm.continuations
+ * @library /test/lib
+ * @modules java.base/java.lang:+open
+ * @run junit/othervm -XX:+UnlockDiagnosticVMOptions -XX:+FullGCALot -XX:FullGCALotInterval=1000 MonitorsTest
+ */
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.*;
+
+import jdk.test.lib.thread.VThreadScheduler;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class MonitorsTest {
+    final int CARRIER_COUNT = 8;
+    ExecutorService scheduler = Executors.newFixedThreadPool(CARRIER_COUNT);
+
+    static final Object globalLock = new Object();
+    static volatile boolean finish = false;
+    static volatile int counter = 0;
+
+    /////////////////////////////////////////////////////////////////////
+    //////////////////////////// BASIC TESTS ////////////////////////////
+    /////////////////////////////////////////////////////////////////////
+
+    static final Runnable FOO = () -> {
+        Object lock = new Object();
+        synchronized(lock) {
+            while(!finish) {
+                Thread.yield();
+            }
+        }
+        System.err.println("Exiting FOO from thread " + Thread.currentThread().getName());
+    };
+
+    static final Runnable BAR = () -> {
+        synchronized(globalLock) {
+            counter++;
+        }
+        System.err.println("Exiting BAR from thread " + Thread.currentThread().getName());
+    };
+
+    /**
+     *  Test yield while holding monitor.
+     */
+    @Test
+    void testBasic() throws Exception {
+        final int VT_COUNT = CARRIER_COUNT;
+
+        // Create first batch of VT threads.
+        Thread firstBatch[] = new Thread[VT_COUNT];
+        for (int i = 0; i < VT_COUNT; i++) {
+            firstBatch[i] = VThreadScheduler.virtualThreadBuilder(scheduler).name("FirstBatchVT-" + i).start(FOO);
+        }
+
+        // Give time for all threads to reach Thread.yield
+        Thread.sleep(1000);
+
+        // Create second batch of VT threads.
+        Thread secondBatch[] = new Thread[VT_COUNT];
+        for (int i = 0; i < VT_COUNT; i++) {
+            secondBatch[i] = VThreadScheduler.virtualThreadBuilder(scheduler).name("SecondBatchVT-" + i).start(BAR);
+        }
+
+        while(counter != VT_COUNT) {}
+
+        finish = true;
+
+        for (int i = 0; i < VT_COUNT; i++) {
+            firstBatch[i].join();
+        }
+        for (int i = 0; i < VT_COUNT; i++) {
+            secondBatch[i].join();
+        }
+    }
+
+    static final Runnable BAR2 = () -> {
+        synchronized(globalLock) {
+            counter++;
+        }
+        recursive2(10);
+        System.err.println("Exiting BAR2 from thread " + Thread.currentThread().getName() + "with counter=" + counter);
+    };
+
+    static void recursive2(int count) {
+        synchronized(Thread.currentThread()) {
+            if (count > 0) {
+                recursive2(count - 1);
+            } else {
+                synchronized(globalLock) {
+                    counter++;
+                    Thread.yield();
+                }
+            }
+        }
+    }
+
+    /**
+     *  Test yield while holding monitor with recursive locking.
+     */
+    @Test
+    void testRecursive() throws Exception {
+        final int VT_COUNT = CARRIER_COUNT;
+        counter = 0;
+        finish = false;
+
+        // Create first batch of VT threads.
+        Thread firstBatch[] = new Thread[VT_COUNT];
+        for (int i = 0; i < VT_COUNT; i++) {
+            firstBatch[i] = VThreadScheduler.virtualThreadBuilder(scheduler).name("FirstBatchVT-" + i).start(FOO);
+        }
+
+        // Give time for all threads to reach Thread.yield
+        Thread.sleep(1000);
+
+        // Create second batch of VT threads.
+        Thread secondBatch[] = new Thread[VT_COUNT];
+        for (int i = 0; i < VT_COUNT; i++) {
+            secondBatch[i] = VThreadScheduler.virtualThreadBuilder(scheduler).name("SecondBatchVT-" + i).start(BAR2);
+        }
+
+        while(counter != 2*VT_COUNT) {}
+
+        finish = true;
+
+        for (int i = 0; i < VT_COUNT; i++) {
+            firstBatch[i].join();
+        }
+        for (int i = 0; i < VT_COUNT; i++) {
+            secondBatch[i].join();
+        }
+    }
+
+    static final Runnable FOO3 = () -> {
+        synchronized(globalLock) {
+            while(!finish) {
+                Thread.yield();
+            }
+        }
+        System.err.println("Exiting FOO3 from thread " + Thread.currentThread().getName());
+    };
+
+    /**
+     *  Test contention on monitorenter.
+     */
+    @Test
+    void testContention() throws Exception {
+        final int VT_COUNT = CARRIER_COUNT * 8;
+        counter = 0;
+        finish = false;
+
+        // Create batch of VT threads.
+        Thread batch[] = new Thread[VT_COUNT];
+        for (int i = 0; i < VT_COUNT; i++) {
+            batch[i] = VThreadScheduler.virtualThreadBuilder(scheduler).name("BatchVT-" + i).start(FOO3);
+        }
+
+        // Give time for all threads to reach synchronized(globalLock)
+        Thread.sleep(2000);
+
+        finish = true;
+
+        for (int i = 0; i < VT_COUNT; i++) {
+            batch[i].join();
+        }
+    }
+
+    /////////////////////////////////////////////////////////////////////
+    //////////////////////////// MAIN TESTS /////////////////////////////
+    /////////////////////////////////////////////////////////////////////
+
+    static final int MONITORS_CNT = 12;
+    static Object[] globalLockArray;
+    static AtomicInteger workerCount = new AtomicInteger(0);
+
+    static 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);
+        }
+    }
+
+    static void recursive4_2(int lockNumber) {
+        if (lockNumber + 2 <= MONITORS_CNT - 1) {
+            lockNumber += 2;
+            synchronized(globalLockArray[lockNumber]) {
+                Thread.yield();
+                recursive4_2(lockNumber);
+            }
+        }
+    }
+
+    static final Runnable FOO4 = () -> {
+        while (!finish) {
+            int lockNumber = ThreadLocalRandom.current().nextInt(0, MONITORS_CNT - 1);
+            synchronized(globalLockArray[lockNumber]) {
+                recursive4_1(lockNumber, lockNumber);
+            }
+        }
+        workerCount.getAndIncrement();
+        System.err.println("Exiting FOO4 from thread " + Thread.currentThread().getName());
+    };
+
+    /**
+     *  Test contention on monitorenter with extra monitors on stack shared by all threads.
+     */
+    @Test
+    void testContentionMultipleMonitors() throws Exception {
+        final int VT_COUNT = CARRIER_COUNT * 8;
+        workerCount.getAndSet(0);
+        finish = false;
+
+        globalLockArray = new Object[MONITORS_CNT];
+        for (int i = 0; i < MONITORS_CNT; i++) {
+            globalLockArray[i] = new Object();
+        }
+
+        Thread batch[] = new Thread[VT_COUNT];
+        for (int i = 0; i < VT_COUNT; i++) {
+            batch[i] = VThreadScheduler.virtualThreadBuilder(scheduler).name("BatchVT-" + i).start(FOO4);
+        }
+
+        Thread.sleep(5000);
+        finish = true;
+
+        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 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();
+        }
+
+        while (!finish) {
+            int lockNumber = ThreadLocalRandom.current().nextInt(0, MONITORS_CNT - 1);
+            synchronized (myLockArray[lockNumber]) {
+                synchronized (globalLockArray[lockNumber]) {
+                    recursive5_1(lockNumber, lockNumber, myLockArray);
+                }
+            }
+        }
+        workerCount.getAndIncrement();
+        System.err.println("Exiting FOO5 from thread " + Thread.currentThread().getName());
+    };
+
+    /**
+     *  Test contention on monitorenter with extra monitors on stack both local only and shared by all threads.
+     */
+    @Test
+    void testContentionMultipleMonitors2() throws Exception {
+        final int VT_COUNT = CARRIER_COUNT * 8;
+        workerCount.getAndSet(0);
+        finish = false;
+
+        globalLockArray = new Object[MONITORS_CNT];
+        for (int i = 0; i < MONITORS_CNT; i++) {
+            globalLockArray[i] = new Object();
+        }
+
+        // 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] = VThreadScheduler.virtualThreadBuilder(scheduler).name("BatchVT-" + i).start(FOO5);
+        }
+
+        Thread.sleep(5000);
+
+        finish = true;
+
+        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 synchronized void recursive6(int depth, Object myLock) {
+        if (depth > 0) {
+            recursive6(depth - 1, myLock);
+        } else {
+            if (Math.random() < 0.5) {
+                Thread.yield();
+            } else {
+                synchronized (myLock) {
+                    Thread.yield();
+                }
+            }
+        }
+    }
+
+    static final Runnable FOO6 = () -> {
+        Object myLock = new Object();
+
+        while (!finish) {
+            int lockNumber = ThreadLocalRandom.current().nextInt(0, MONITORS_CNT - 1);
+            synchronized (myLock) {
+                synchronized (globalLockArray[lockNumber]) {
+                    recursive6(lockNumber, myLock);
+                }
+            }
+        }
+        workerCount.getAndIncrement();
+        System.err.println("Exiting FOO5 from thread " + Thread.currentThread().getName());
+    };
+
+    /**
+     *  Test contention on monitorenter with synchronized methods.
+     */
+    @Test
+    void testContentionMultipleMonitors3() throws Exception {
+        final int VT_COUNT = CARRIER_COUNT * 8;
+        workerCount.getAndSet(0);
+        finish = false;
+
+
+        globalLockArray = new Object[MONITORS_CNT];
+        for (int i = 0; i < MONITORS_CNT; i++) {
+            globalLockArray[i] = new Object();
+        }
+
+        // Create batch of VT threads.
+        Thread batch[] = new Thread[VT_COUNT];
+        for (int i = 0; i < VT_COUNT; i++) {
+            batch[i] = VThreadScheduler.virtualThreadBuilder(scheduler).name("BatchVT-" + i).start(FOO6);
+        }
+
+        Thread.sleep(5000);
+
+        finish = true;
+
+        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());
+        }
+    }
+
+    @Test
+    void waitNotifyTest() throws Exception {
+        int threadCount = 1000;
+        int waitTime = 50;
+        long start = System.currentTimeMillis();
+        Thread[] vthread = new Thread[threadCount];
+        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;
+            }
+        }
+    }
+}
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,141 @@
+/*
+ * 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
+ * @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,523 @@
+/*
+ * 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
+ * @library /test/lib
+ * @run junit/othervm SynchronizedNative
+ */
+
+/*
+ * @test id=Xint
+ * @requires vm.continuations
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xint SynchronizedNative
+ */
+
+/*
+ * @test id=Xcomp-TieredStopAtLevel1
+ * @requires vm.continuations
+ * @modules java.base/java.lang:+open
+ * @library /test/lib
+ * @run junit/othervm -Xcomp -XX:TieredStopAtLevel=1 SynchronizedNative
+ */
+
+/*
+ * @test id=Xcomp-noTieredCompilation
+ * @requires vm.continuations
+ * @modules java.base/java.lang:+open
+ * @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;
+import jdk.test.lib.thread.VThreadScheduler;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.AfterAll;
+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 {
+    private static int initialParallelism;
+
+    @BeforeAll
+    static void setup() throws Exception {
+        // need at least two carriers when main thread is a virtual thread
+        if (Thread.currentThread().isVirtual()) {
+            initialParallelism = VThreadRunner.ensureParallelism(2);
+        }
+        System.loadLibrary("SynchronizedNative");
+    }
+
+    @AfterAll
+    static void finish() {
+        // restore parallelism if needed
+        if (initialParallelism > 0) {
+            VThreadRunner.setParallelism(initialParallelism);
+        }
+    }
+
+    /**
+     * 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
@@ -1104,14 +1104,44 @@
         }
         assertEquals(List.of("A", "B", "A", "B"), list);
     }
 
     /**
-     * Test Thread.yield when thread is pinned by native frame.
+     * Test Thread.yield releases carrier when virtual thread holds a monitor.
      */
     @Test
     void testYield2() 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 by native frame.
+     */
+    @Test
+    void testYield3() 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 +1164,11 @@
 
     /**
      * Test Thread.yield does not consume the thread's parking permit.
      */
     @Test
-    void testYield3() throws Exception {
+    void testYield4() throws Exception {
         var thread = Thread.ofVirtual().start(() -> {
             LockSupport.unpark(Thread.currentThread());
             Thread.yield();
             LockSupport.park();  // should not park
         });
@@ -1147,11 +1177,11 @@
 
     /**
      * Test Thread.yield does not make available the thread's parking permit.
      */
     @Test
-    void testYield4() throws Exception {
+    void testYield5() throws Exception {
         var thread = Thread.ofVirtual().start(() -> {
             Thread.yield();
             LockSupport.park();  // should park
         });
         try {
@@ -1969,10 +1999,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/TrackAllThreads.java b/test/jdk/java/lang/Thread/virtual/TrackAllThreads.java
--- /dev/null
+++ b/test/jdk/java/lang/Thread/virtual/TrackAllThreads.java
@@ -0,0 +1,135 @@
+/*
+ * 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
+ * @library /test/lib
+ * @run junit/othervm TrackAllThreads
+ */
+
+import java.lang.ref.WeakReference;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedTransferQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.LockSupport;
+
+import jdk.test.lib.util.ForceGC;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class TrackAllThreads {
+
+    /**
+     * Test that a virtual created directly with the Thread API, then parks, is not GC'ed.
+     */
+    @Test
+    void testRootContainer() {
+        Thread thread = Thread.ofVirtual().start(LockSupport::park);
+        var ref = new WeakReference<>(thread);
+        thread = null;
+        ForceGC.waitFor(() -> ref.refersTo(null), 2000);
+        thread = ref.get();
+        if (thread == null) {
+            fail("Thread has been GC'ed");
+        } else {
+            LockSupport.unpark(thread);
+        }
+    }
+
+    /**
+     * Test that a ThreadPoolExecutor using a virtual thread factory can be GC'ed after
+     * the last virtual thread terminates.
+     */
+    @Test
+    void testSharedContainer1() {
+        var queue = new LinkedTransferQueue<Runnable>();
+        ThreadFactory factory = Thread.ofVirtual().factory();
+        var executor = new ThreadPoolExecutor(0, 1, 1, TimeUnit.SECONDS, queue, factory);
+        executor.submit(() -> { });
+        var ref = new WeakReference<>(executor);
+        executor = null;
+        boolean cleared = ForceGC.waitFor(() -> ref.refersTo(null), Long.MAX_VALUE);
+        assertTrue(cleared);
+    }
+
+    /**
+     * Test that a ThreadPoolExecutor using a virtual thread factory is not GC'ed when
+     * a task (running in a virtual thread) has parked.
+     */
+    @Disabled
+    @Test
+    void testSharedContainer2() throws Exception {
+        var queue = new LinkedTransferQueue<Runnable>();
+        ThreadFactory factory = Thread.ofVirtual().factory();
+        var executor = new ThreadPoolExecutor(0, 1, 1, TimeUnit.SECONDS, queue, factory);
+        executor.execute(LockSupport::park);
+        var ref = new WeakReference<>(executor);
+        executor = null;
+        ForceGC.waitFor(() -> ref.refersTo(null), 2000);
+        executor = ref.get();
+        if (executor == null) {
+            fail("Executor has been GC'ed");
+        } else {
+            executor.shutdownNow();
+            executor.close();
+        }
+    }
+
+    /**
+     * Test that a ThreadPerTaskExecutor using a virtual thread factory can be GC'ed after
+     * the last virtual thread terminates.
+     */
+    @Test
+    void testThreadPerTaskExecutor1() {
+        var executor = Executors.newVirtualThreadPerTaskExecutor();
+        executor.submit(() -> { });
+        var ref = new WeakReference<>(executor);
+        executor = null;
+        boolean cleared = ForceGC.waitFor(() -> ref.refersTo(null), Long.MAX_VALUE);
+        assertTrue(cleared);
+    }
+
+    /**
+     * Test that a ThreadPerTaskExecutor using a virtual thread factory is not GC'ed when
+     * a task (running in a virtual thread) has parked.
+     */
+    @Disabled
+    @Test
+    void testThreadPerTaskExecutor2() throws Exception {
+        var executor = Executors.newVirtualThreadPerTaskExecutor();
+        executor.execute(LockSupport::park);
+        var ref = new WeakReference<>(executor);
+        executor = null;
+        ForceGC.waitFor(() -> ref.refersTo(null), 2000);
+        executor = ref.get();
+        if (executor == null) {
+            fail("Executor has been GC'ed");
+        } else {
+            executor.shutdownNow();
+            executor.close();
+        }
+    }
+}
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, 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,118 @@
+/*
+ * 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 os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @library /test/lib
+ * @run main/othervm LotsOfContendedMonitorEnter
+ */
+
+/*
+ * @test id=LM_LEGACY
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @library /test/lib
+ * @run main/othervm -XX:LockingMode=1 LotsOfContendedMonitorEnter
+ */
+
+/*
+ * @test id=LM_LIGHTWEIGHT
+ * @requires os.arch=="amd64" | os.arch=="x86_64" | os.arch=="aarch64"
+ * @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
+ * @run main/othervm/timeout=300 Skynet100kWithMonitors 50
+ */
+
+/*
+ * @test
+ * @requires vm.debug == true & vm.continuations
+ * @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/instrument/TraceUsageAgent.java b/test/jdk/java/lang/instrument/TraceUsageAgent.java
--- a/test/jdk/java/lang/instrument/TraceUsageAgent.java
+++ b/test/jdk/java/lang/instrument/TraceUsageAgent.java
@@ -45,11 +45,11 @@
                 case "addTransformer" -> {
                     var transformer = new ClassFileTransformer() { };
                     inst.addTransformer(transformer);
                 }
                 case "retransformClasses" -> {
-                    inst.retransformClasses(Object.class);
+                    inst.retransformClasses(Integer.class);
                 }
                 case "redefineModule" -> {
                     Module base = Object.class.getModule();
                     inst.redefineModule(base, Set.of(), Map.of(), Map.of(), Set.of(), Map.of());
                 }
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.Config;
+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.
-     */
-    @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++) {
-                scope.fork(() -> {
-                    tids.add(Thread.currentThread().threadId());
-                    return null;
-                });
-            }
-            scope.join();
-        }
-        assertEquals(100, tids.size());
-    }
-
-    /**
-     * Test that fork creates a new virtual thread for each task.
+     * Test that fork creates virtual threads when no ThreadFactory is configured.
      */
     @Test
     void testForkCreateVirtualThread() throws Exception {
         Set<Thread> threads = ConcurrentHashMap.newKeySet();
-        try (var scope = new StructuredTaskScope<Object>()) {
-            for (int i = 0; i < 100; i++) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            for (int i = 0; i < 50; i++) {
+                // runnable
+                scope.fork(() -> {
+                    threads.add(Thread.currentThread());
+                });
+
+                // callable
                 scope.fork(() -> {
                     threads.add(Thread.currentThread());
                     return null;
                 });
             }
@@ -137,114 +124,122 @@
         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";
             });
@@ -256,125 +251,194 @@
             // 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());
         }
     }
 
     /**
-     * Test fork after scope is shutdown.
+     * Test fork after task scope is cancelled. This test uses the cancel method to
+     * cancel execution explicitly.
      */
     @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 testForkAfterCancel1(ThreadFactory factory) throws Exception {
+        var countingThreadFactory = new CountingThreadFactory(factory);
+        var testJoiner = new CountingJoiner<String>();
+
+        try (var scope = StructuredTaskScope.open(testJoiner,
+                cf -> cf.withThreadFactory(countingThreadFactory))) {
+
+            scope.cancel();
+
+            // fork subtask
+            var subtask = scope.fork(() -> "foo");
+            
+            assertEquals(0, countingThreadFactory.threadCount());
+            assertEquals(1, testJoiner.onForkCount());
+            assertEquals(0, testJoiner.onCompleteCount());
+
             scope.join();
+
+            assertEquals(0, countingThreadFactory.threadCount());
+            assertEquals(1, testJoiner.onForkCount());
+            assertEquals(0, testJoiner.onCompleteCount());
+
             assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
-            assertThrows(IllegalStateException.class, subtask::get);
-            assertThrows(IllegalStateException.class, subtask::exception);
         }
-        assertFalse(executed.get());
     }
+    
 
     /**
-     * Test fork after scope is closed.
+     * Test fork after task scope is cancelled. This test uses a custom Joiner to
+     * cancel execution.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testForkAfterClose(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Object>(null, factory)) {
+    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(1, countingThreadFactory.threadCount());
+            assertEquals(2, testJoiner.onForkCount());
+            assertEquals(1, testJoiner.onCompleteCount());
+            assertEquals("foo", subtask1.get());
+            assertEquals(Subtask.State.UNAVAILABLE, subtask2.state());
+        }
+    }
+
+    /**
+     * Test fork after task scope is closed.
+     */
+    @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 when the ThreadFactory 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 repeated calls to join.
+     */
+    @Test
+    void testJoinAfterJoin() 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");
+
+            // each call to join should invoke Joiner::result
+            assertEquals("foo", scope.join());
+            assertEquals("bar", scope.join());
+            assertEquals("baz", scope.join());
+        }
+    }
+
+    /**
+     * Test join method is owner confined.
      */
     @ParameterizedTest
     @MethodSource("factories")
     void testJoinConfined(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<Boolean>()) {
-
-            // thread in scope cannot join
-            Subtask<Boolean> subtask = scope.fork(() -> {
-                assertThrows(WrongThreadException.class, () -> { scope.join(); });
-                return true;
-            });
-
-            scope.join();
-
-            assertTrue(subtask.get());
+        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)) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
+
             var latch = new CountDownLatch(1);
 
             Subtask<String> subtask = scope.fork(() -> {
                 latch.await();
                 return "foo";
@@ -384,11 +448,11 @@
             Thread.currentThread().interrupt();
             try {
                 scope.join();
                 fail("join did not throw");
             } catch (InterruptedException expected) {
-                assertFalse(Thread.interrupted());   // interrupt status should be clear
+                assertFalse(Thread.interrupted());   // interrupt status should be cleared
             } finally {
                 // let task continue
                 latch.countDown();
             }
 
@@ -402,11 +466,13 @@
      * 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();
                 return "foo";
             });
@@ -428,448 +494,260 @@
             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();
+    void testJoinWhenCancelled(ThreadFactory factory) throws Exception {
+        var countingThreadFactory = new CountingThreadFactory(factory);
+        var testJoiner = new CancelAfterOneJoiner<String>();
 
-            scope.join();
-
-            // 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);
+                scope.join();
+                fail();
+            } catch (ExecutionException e) {
+                assertTrue(e.getCause() instanceof TimeoutException);
             }
+            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(() -> {
-                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());
-                }
-            }
-        }
-    }
+    void testJoinWithTimeout3(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory)
+                        .withTimeout(Duration.ofSeconds(-1)))) {
 
-    /**
-     * Test joinUntil with a deadline that has already expired.
-     */
-    @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;
             });
 
-            // now
-            try {
-                scope.joinUntil(Instant.now());
-                fail("joinUntil did not throw");
-            } catch (TimeoutException expected) {
-                assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
-            }
-
-            // in the past
             try {
-                scope.joinUntil(Instant.now().minusSeconds(1));
-                fail("joinUntil did not throw");
-            } catch (TimeoutException expected) {
-                assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
+                scope.join();
+                fail();
+            } catch (ExecutionException e) {
+                assertTrue(e.getCause() instanceof TimeoutException);
             }
+            assertTrue(scope.isCancelled());
+            assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
         }
     }
 
     /**
-     * Test joinUntil with interrupt status set.
+     * Test that cancelling execution interrupts unfinished threads. This test uses the
+     * cancel method to cancel execution explicitly.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testInterruptJoinUntil1(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope<String>(null, factory)) {
-            var latch = new CountDownLatch(1);
+    void testCancelInterruptsThreads1(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
 
-            Subtask<String> subtask = scope.fork(() -> {
-                latch.await();
-                return "foo";
+            // fork subtask that runs for a long time
+            var started = new CountDownLatch(1);
+            var interrupted = new CountDownLatch(1);
+            var subtask = scope.fork(() -> {
+                started.countDown();
+                try {
+                    Thread.sleep(Duration.ofDays(1));
+                } catch (InterruptedException e) {
+                    interrupted.countDown();
+                }
             });
+            started.await();
 
-            // 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();
-            }
+            scope.cancel();
+
+            // subtask should be interrupted
+            interrupted.await();
 
-            // join should complete
             scope.join();
-            assertEquals("foo", subtask.get());
+            assertEquals(Subtask.State.UNAVAILABLE, subtask.state());
         }
     }
 
     /**
-     * Test interrupt of thread blocked in joinUntil.
+     * Test that cancelling execution interrupts unfinished threads. This test uses
+     * a custom Joiner to cancel execution.
      */
     @ParameterizedTest
     @MethodSource("factories")
-    void testInterruptJoinUntil2(ThreadFactory factory) throws Exception {
-        try (var scope = new StructuredTaskScope(null, factory)) {
-            var latch = new CountDownLatch(1);
+    void testCancelInterruptsThreads2(ThreadFactory factory) throws Exception {
+        var testJoiner = new CancelAfterOneJoiner<String>();
 
-            Subtask<String> subtask = scope.fork(() -> {
-                latch.await();
-                return "foo";
+        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();
 
-            // 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();
+            // fork subtask2, the scope should be cancelled when the subtask completes
+            var subtask2 = scope.fork(() -> "bar");
+            while (!scope.isCancelled()) {
+                Thread.sleep(20);
             }
 
-            // join should complete
+            // subtask1 should be interrupted
+            interrupted.await();
+
             scope.join();
-            assertEquals("foo", subtask.get());
+            assertEquals(Subtask.State.UNAVAILABLE, subtask1.state());
+            assertEquals("bar", subtask2.get());
         }
     }
 
     /**
-     * Test that shutdown interrupts unfinished subtasks.
+     * Test that timeout interrupts unfinished threads.
      */
     @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(() -> {
+    void testTimeoutInterruptsThreads(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll(),
+                cf -> cf.withThreadFactory(factory)
+                        .withTimeout(Duration.ofSeconds(2)))) {
+
+            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.set(true);
-                } finally {
-                    latch.countDown();
+                    interrupted.countDown();
                 }
                 return null;
             });
 
-            scope.shutdown();
-
-            // wait for task to complete
-            latch.await();
-            assertTrue(interrupted.get());
+            while (!scope.isCancelled()) {
+                Thread.sleep(50);
+            }
 
-            scope.join();
+            // if subtask started then it should be interrupted
+            if (started.get()) {
+                interrupted.await();
+            }
 
-            // subtask result/exception not available
+            try {
+                scope.join();
+                fail();
+            } catch (ExecutionException e) {
+                assertTrue(e.getCause() instanceof TimeoutException);
+            }
             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
     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)) {
-            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();
-
+        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);
@@ -885,89 +763,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 +857,224 @@
      * 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 cancel after task scope is cancelled.
      */
-    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
+    void testCancelAfterCancel() {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            scope.cancel();
         }
     }
 
     /**
-     * Test that handleComplete method is invoked for tasks that complete before shutdown.
+     * Test cancel after task scope is closed.
      */
-    @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
+    void testCancelAfterClose() {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            assertFalse(scope.isCancelled());
+            scope.cancel();
+            assertTrue(scope.isCancelled());
+            scope.cancel();
+            assertTrue(scope.isCancelled());
         }
     }
 
     /**
-     * Test that handleComplete method is not invoked for tasks that finish after shutdown
-     * or are forked after shutdown.
+     * Test cancel method is owner confined.
      */
     @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);
+    void testCancelConfined(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.<Boolean>awaitAll(),
+                cf -> cf.withThreadFactory(factory))) {
 
-            scope.join();
-
-            // handleComplete should not be called
-            for (int i = 0; i < 3; i++) {
-                assertEquals(0, scope.subtasks().size());
-                Thread.sleep(20);
+            // random thread cannot cancel scope
+            try (var pool = Executors.newCachedThreadPool(factory)) {
+                Future<Boolean> future = pool.submit(() -> {
+                    assertThrows(WrongThreadException.class, scope::cancel);
+                    return null;
+                });
+                future.get();
             }
 
-            assertEquals(Subtask.State.UNAVAILABLE, subtask1.state());
-            assertEquals(Subtask.State.UNAVAILABLE, subtask2.state());
-            assertEquals(Subtask.State.UNAVAILABLE, subtask3.state());
+            // subtask cannot cancel
+            Subtask<Boolean> subtask = scope.fork(() -> {
+                assertThrows(WrongThreadException.class, scope::cancel);
+                return true;
+            });
+            scope.join();
+            assertTrue(subtask.get());
         }
     }
 
     /**
-     * Test that the default handleComplete throws IllegalArgumentException if called
-     * with a running task.
+     * Test that isCancelled returns true after close.
      */
     @Test
-    void testHandleCompleteThrows() throws Exception {
-        class TestScope<T> extends StructuredTaskScope<T> {
-            protected void handleComplete(Subtask<? extends T> subtask) {
-                super.handleComplete(subtask);
-            }
-        }
-
-        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();
+    void testIsCancelledAfterClose() throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            assertFalse(scope.isCancelled());
+            scope.close();
+            assertTrue(scope.isCancelled());
         }
     }
 
     /**
-     * Test ensureOwnerAndJoined.
+     * Test Joiner.onFork throwing exception.
      */
-    @ParameterizedTest
-    @MethodSource("factories")
-    void testEnsureOwnerAndJoined(ThreadFactory factory) throws Exception {
-        class MyScope<T> extends StructuredTaskScope<T> {
-            MyScope(ThreadFactory factory) {
-                super(null, factory);
+    @Test
+    void testOnForkThrows() throws Exception {
+        var joiner = new Joiner<String, Void>() {
+            @Override
+            public boolean onFork(Subtask<? extends String> subtask) {
+                throw new FooException();
             }
-            void invokeEnsureOwnerAndJoined() {
-                super.ensureOwnerAndJoined();
+            @Override
+            public Void result() {
+                return null;
             }
+        };
+        try (var scope = StructuredTaskScope.open(joiner)) {
+            assertThrows(FooException.class, () -> scope.fork(() -> "foo"));
         }
+    }
 
-        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();
-                });
+    /**
+     * Test Joiner.onFork returning true to cancel execution.
+     */
+    @Test
+    void testOnForkCancelsExecution() throws Exception {
+        var joiner = new Joiner<String, Void>() {
+            @Override
+            public boolean onFork(Subtask<? extends String> subtask) {
                 return true;
-            });
-            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();
             }
+            @Override
+            public Void result() {
+                return null;
+            }
+        };
+        try (var scope = StructuredTaskScope.open(joiner)) {
+            assertFalse(scope.isCancelled());
+            scope.fork(() -> "foo");
+            assertTrue(scope.isCancelled());
+            scope.join();
         }
     }
 
     /**
-     * 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 +1082,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 +1106,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 +1130,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 +1186,508 @@
     /**
      * 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 (ExecutionException 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 (ExecutionException 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(ExecutionException.class, scope::join);
+            assertTrue(ex.getCause() instanceof FooException);
+        }
+    }
+
+    /**
+     * Test Joiner.awaitAllSuccessfulOrThrow() with no subtasks.
+     */
+    @Test
+    void testIgnoreSuccessfulOrThrow1() throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAllSuccessfulOrThrow())) {
+            var result = scope.join();
+            assertNull(result);
         }
     }
 
     /**
-     * Test ShutdownOnSuccess methods are confined to the owner.
+     * 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 testIgnoreSuccessfulOrThrow2(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 testIgnoreSuccessfulOrThrow3(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 (ExecutionException 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 testIgnoreAll1() 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 testIgnoreAll2(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 testIgnoreAll3(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.all(Predicate) with no subtasks.
+     */
+    @Test
+    void testAllWithPredicate1() throws Throwable {
+        try (var scope = StructuredTaskScope.open(Joiner.all(s -> false))) {
+            var subtasks = scope.join();
+            assertEquals(0, subtasks.count());
+        }
+    }
 
-            Throwable ex = scope.exception().orElse(null);
-            assertTrue(ex instanceof ArithmeticException);
+    /**
+     * Test Joiner.all(Predicate) with no cancellation.
+     */
+    @ParameterizedTest
+    @MethodSource("factories")
+    void testAllWithPredicate2(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>all(s -> false),
+                cf -> cf.withThreadFactory(factory))) {
 
-            ex = assertThrows(ExecutionException.class, () -> scope.throwIfFailed());
-            assertTrue(ex.getCause() instanceof ArithmeticException);
+            var subtask1 = scope.fork(() -> "foo");
+            var subtask2 = scope.fork(() -> { throw new FooException(); });
 
-            ex = assertThrows(FooException.class,
-                              () -> scope.throwIfFailed(e -> new FooException(e)));
-            assertTrue(ex.getCause() instanceof ArithmeticException);
+            var subtasks = scope.join().toList();
+            assertEquals(2, subtasks.size());
+
+            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.all(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 testAllWithPredicate3(ThreadFactory factory) throws Exception {
+        try (var scope = StructuredTaskScope.open(Joiner.<String>all(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.all(Predicate) with cancellation after serveral subtasks complete.
+     */
+    @ParameterizedTest
+    @MethodSource("factories")
+    void testAllWithPredicate4(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.all(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.all(null));
+
+        // fork
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
+            assertThrows(NullPointerException.class, () -> scope.fork((Callable<Object>) null));
+            assertThrows(NullPointerException.class, () -> scope.fork((Runnable) null));
         }
 
-        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));
+        // 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)));
+    }
+
+    /**
+     * 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 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 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();
+        }
+    }
+
+    /**
+     * 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;
 
@@ -53,11 +54,12 @@
     @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)) {
+            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();
@@ -72,13 +74,15 @@
     @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)) {
+            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();
@@ -97,17 +101,19 @@
     @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)) {
+            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)) {
+                        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>();
+                    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,11 +170,11 @@
      * 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>()) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
             ScopedValue.runWhere(name, "x", () -> {
                 assertThrows(StructureViolationException.class, scope::close);
             });
         }
     }
@@ -177,11 +183,11 @@
      * 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>()) {
+        try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
             ScopedValue.runWhere(name, "x", () -> {
                 assertThrows(StructureViolationException.class,
                         () -> scope.fork(() -> "foo"));
             });
         }
@@ -195,21 +201,21 @@
         ScopedValue<String> name1 = ScopedValue.newInstance();
         ScopedValue<String> name2 = ScopedValue.newInstance();
 
         // rebind
         ScopedValue.runWhere(name1, "x", () -> {
-            try (var scope = new StructuredTaskScope<String>()) {
+            try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
                 ScopedValue.runWhere(name1, "y", () -> {
                     assertThrows(StructureViolationException.class,
                             () -> scope.fork(() -> "foo"));
                 });
             }
         });
 
         // new binding
         ScopedValue.runWhere(name1, "x", () -> {
-            try (var scope = new StructuredTaskScope<String>()) {
+            try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
                 ScopedValue.runWhere(name2, "y", () -> {
                     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/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
@@ -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,20 +43,23 @@
 /**
 * @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;
@@ -274,10 +278,12 @@
         return "" + r;
     }
 
     @Test
     public void testPinnedMonitor() {
+        if (Platform.isX64() || Platform.isAArch64()) return;
+
         // Test pinning due to held monitor
         final AtomicReference<Continuation.Pinned> res = new AtomicReference<>();
 
         Continuation cont = new Continuation(FOO, ()-> {
             syncFoo(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
@@ -468,11 +468,11 @@
         int computed = computeResult();
         assert result == computed : "result: " + result + " expected: " + computed;
     }
 
     boolean shouldPin() {
-        return traceHas(Op.PIN::contains);
+        return traceHas(Op.PIN::contains) && !(Platform.isX64() || Platform.isAArch64());
     }
 
     void verifyPin(boolean yieldResult) {
         if (yieldResult) yields++;
         if (!yieldResult && traceHas(op -> Op.INTERPRETED.contains(op) && Op.REFLECTED.contains(op))) return;
diff a/test/langtools/ProblemList-Virtual.txt b/test/langtools/ProblemList-Virtual.txt
--- /dev/null
+++ b/test/langtools/ProblemList-Virtual.txt
@@ -0,0 +1,28 @@
+####
+## 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
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
@@ -61,10 +61,12 @@
 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/patterns/Exhaustiveness.java 					8326616    generic-all    intermittently timeout
 
+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);
+        }
+    }
+}
