6700889: Thread resume invalidates all stack frames, even from other threads
authorjjh
Wed, 09 Jul 2008 13:43:26 -0700
changeset 836 d81b1f62fb82
parent 831 50f701930577
child 837 7c6095e6a342
6700889: Thread resume invalidates all stack frames, even from other threads 6701700: MonitorInfo objects aren't invalidated when the owning thread is resumed Summary: Don't inform ThreadListeners for thread T1 when some other thread is resumed, and MonitoryIfoImpl must add itself as a ThreadListener Reviewed-by: dcubed
jdk/src/share/classes/com/sun/tools/jdi/MonitorInfoImpl.java
jdk/src/share/classes/com/sun/tools/jdi/ThreadReferenceImpl.java
jdk/src/share/classes/com/sun/tools/jdi/VMAction.java
jdk/src/share/classes/com/sun/tools/jdi/VMState.java
jdk/src/share/classes/com/sun/tools/jdi/VirtualMachineImpl.java
jdk/test/com/sun/jdi/MonitorFrameInfo.java
jdk/test/com/sun/jdi/ResumeOneThreadTest.java
--- a/jdk/src/share/classes/com/sun/tools/jdi/MonitorInfoImpl.java	Fri Jul 04 18:55:37 2008 +0200
+++ b/jdk/src/share/classes/com/sun/tools/jdi/MonitorInfoImpl.java	Wed Jul 09 13:43:26 2008 -0700
@@ -40,11 +40,12 @@
     int  stack_depth;
 
     MonitorInfoImpl(VirtualMachine vm, ObjectReference mon,
-                    ThreadReference thread, int dpth) {
+                    ThreadReferenceImpl thread, int dpth) {
         super(vm);
         this.monitor = mon;
         this.thread = thread;
         this.stack_depth = dpth;
+        thread.addListener(this);
     }
 
 
--- a/jdk/src/share/classes/com/sun/tools/jdi/ThreadReferenceImpl.java	Fri Jul 04 18:55:37 2008 +0200
+++ b/jdk/src/share/classes/com/sun/tools/jdi/ThreadReferenceImpl.java	Wed Jul 09 13:43:26 2008 -0700
@@ -35,12 +35,34 @@
     static final int SUSPEND_STATUS_SUSPENDED = 0x1;
     static final int SUSPEND_STATUS_BREAK = 0x2;
 
-    private ThreadGroupReference threadGroup;
     private int suspendedZombieCount = 0;
 
-    // This is cached only while the VM is suspended
-    private static class Cache extends ObjectReferenceImpl.Cache {
-        String name = null;
+    /*
+     * Some objects can only be created while a thread is suspended and are valid
+     * only while the thread remains suspended.  Examples are StackFrameImpl
+     * and MonitorInfoImpl.  When the thread resumes, these objects have to be
+     * marked as invalid so that their methods can throw
+     * InvalidStackFrameException if they are called.  To do this, such objects
+     * register themselves as listeners of the associated thread.  When the
+     * thread is resumed, its listeners are notified and mark themselves
+     * invalid.
+     * Also, note that ThreadReferenceImpl itself caches some info that
+     * is valid only as long as the thread is suspended.  When the thread
+     * is resumed, that cache must be purged.
+     * Lastly, note that ThreadReferenceImpl and its super, ObjectReferenceImpl
+     * cache some info that is only valid as long as the entire VM is suspended.
+     * If _any_ thread is resumed, this cache must be purged.  To handle this,
+     * both ThreadReferenceImpl and ObjectReferenceImpl register themselves as
+     * VMListeners so that they get notified when all threads are suspended and
+     * when any thread is resumed.
+     */
+
+    // This is cached for the life of the thread
+    private ThreadGroupReference threadGroup;
+
+    // This is cached only while this one thread is suspended.  Each time
+    // the thread is resumed, we clear this and start with a fresh one.
+    private static class LocalCache {
         JDWP.ThreadReference.Status status = null;
         List<StackFrame> frames = null;
         int framesStart = -1;
@@ -52,6 +74,17 @@
         boolean triedCurrentContended = false;
     }
 
+    private LocalCache localCache;
+
+    private void resetLocalCache() {
+        localCache = new LocalCache();
+    }
+
+    // This is cached only while all threads in the VM are suspended
+    // Yes, someone could change the name of a thread while it is suspended.
+    private static class Cache extends ObjectReferenceImpl.Cache {
+        String name = null;
+    }
     protected ObjectReferenceImpl.Cache newCache() {
         return new Cache();
     }
@@ -59,8 +92,10 @@
     // Listeners - synchronized on vm.state()
     private List<WeakReference<ThreadListener>> listeners = new ArrayList<WeakReference<ThreadListener>>();
 
+
     ThreadReferenceImpl(VirtualMachine aVm, long aRef) {
         super(aVm,aRef);
+        resetLocalCache();
         vm.state().addListener(this);
     }
 
@@ -72,10 +107,24 @@
      * VMListener implementation
      */
     public boolean vmNotSuspended(VMAction action) {
-        synchronized (vm.state()) {
-            processThreadAction(new ThreadAction(this,
-                                   ThreadAction.THREAD_RESUMABLE));
+        if (action.resumingThread() == null) {
+            // all threads are being resumed
+            synchronized (vm.state()) {
+                processThreadAction(new ThreadAction(this,
+                                            ThreadAction.THREAD_RESUMABLE));
+            }
+
         }
+
+        /*
+         * Othewise, only one thread is being resumed:
+         *   if it is us,
+         *      we have already done our processThreadAction to notify our
+         *      listeners when we processed the resume.
+         *   if it is not us,
+         *      we don't want to notify our listeners
+         *       because we are not being resumed.
+         */
         return super.vmNotSuspended(action);
     }
 
@@ -191,23 +240,19 @@
     }
 
     private JDWP.ThreadReference.Status jdwpStatus() {
-        JDWP.ThreadReference.Status status = null;
+        JDWP.ThreadReference.Status myStatus = localCache.status;
         try {
-            Cache local = (Cache)getCache();
-
-            if (local != null) {
-                status = local.status;
-            }
-            if (status == null) {
-                status = JDWP.ThreadReference.Status.process(vm, this);
-                if (local != null) {
-                    local.status = status;
+             if (myStatus == null) {
+                 myStatus = JDWP.ThreadReference.Status.process(vm, this);
+                if ((myStatus.suspendStatus & SUSPEND_STATUS_SUSPENDED) != 0) {
+                    // thread is suspended, we can cache the status.
+                    localCache.status = myStatus;
                 }
             }
-        } catch (JDWPException exc) {
+         } catch (JDWPException exc) {
             throw exc.toJDIException();
         }
-        return status;
+         return myStatus;
     }
 
     public int status() {
@@ -245,8 +290,7 @@
 
     public ThreadGroupReference threadGroup() {
         /*
-         * Thread group can't change, so it's cached more conventionally
-         * than other things in this class.
+         * Thread group can't change, so it's cached once and for all.
          */
         if (threadGroup == null) {
             try {
@@ -260,19 +304,10 @@
     }
 
     public int frameCount() throws IncompatibleThreadStateException  {
-        int frameCount = -1;
         try {
-            Cache local = (Cache)getCache();
-
-            if (local != null) {
-                frameCount = local.frameCount;
-            }
-            if (frameCount == -1) {
-                frameCount = JDWP.ThreadReference.FrameCount
+            if (localCache.frameCount == -1) {
+                localCache.frameCount = JDWP.ThreadReference.FrameCount
                                           .process(vm, this).frameCount;
-                if (local != null) {
-                    local.frameCount = frameCount;
-                }
             }
         } catch (JDWPException exc) {
             switch (exc.errorCode()) {
@@ -283,7 +318,7 @@
                 throw exc.toJDIException();
             }
         }
-        return frameCount;
+        return localCache.frameCount;
     }
 
     public List<StackFrame> frames() throws IncompatibleThreadStateException  {
@@ -297,23 +332,25 @@
 
     /**
      * Is the requested subrange within what has been retrieved?
-     * local is known to be non-null
+     * local is known to be non-null.  Should only be called from
+     * a sync method.
      */
-    private boolean isSubrange(Cache local,
-                               int start, int length, List frames) {
-        if (start < local.framesStart) {
+    private boolean isSubrange(LocalCache localCache,
+                               int start, int length) {
+        if (start < localCache.framesStart) {
             return false;
         }
         if (length == -1) {
-            return (local.framesLength == -1);
+            return (localCache.framesLength == -1);
         }
-        if (local.framesLength == -1) {
-            if ((start + length) > (local.framesStart + frames.size())) {
+        if (localCache.framesLength == -1) {
+            if ((start + length) > (localCache.framesStart +
+                                    localCache.frames.size())) {
                 throw new IndexOutOfBoundsException();
             }
             return true;
         }
-        return ((start + length) <= (local.framesStart + local.framesLength));
+        return ((start + length) <= (localCache.framesStart + localCache.framesLength));
     }
 
     public List<StackFrame> frames(int start, int length)
@@ -329,51 +366,42 @@
      * Private version of frames() allows "-1" to specify all
      * remaining frames.
      */
-    private List<StackFrame> privateFrames(int start, int length)
+    synchronized private List<StackFrame> privateFrames(int start, int length)
                               throws IncompatibleThreadStateException  {
-        List<StackFrame> frames = null;
+
+        // Lock must be held while creating stack frames so if that two threads
+        // do this at the same time, one won't clobber the subset created by the other.
+
         try {
-            Cache local = (Cache)getCache();
-
-            if (local != null) {
-                frames = local.frames;
-            }
-            if (frames == null || !isSubrange(local, start, length, frames)) {
+            if (localCache.frames == null || !isSubrange(localCache, start, length)) {
                 JDWP.ThreadReference.Frames.Frame[] jdwpFrames
                     = JDWP.ThreadReference.Frames.
-                          process(vm, this, start, length).frames;
+                    process(vm, this, start, length).frames;
                 int count = jdwpFrames.length;
-                frames = new ArrayList<StackFrame>(count);
+                localCache.frames = new ArrayList<StackFrame>(count);
 
-                // Lock must be held while creating stack frames.
-                // so that a resume will not resume a partially
-                // created stack.
-                synchronized (vm.state()) {
-                    for (int i = 0; i<count; i++) {
-                        if (jdwpFrames[i].location == null) {
-                            throw new InternalException("Invalid frame location");
-                        }
-                        StackFrame frame = new StackFrameImpl(vm, this,
-                                            jdwpFrames[i].frameID,
-                                            jdwpFrames[i].location);
-                        // Add to the frame list
-                        frames.add(frame);
+                for (int i = 0; i<count; i++) {
+                    if (jdwpFrames[i].location == null) {
+                        throw new InternalException("Invalid frame location");
                     }
+                    StackFrame frame = new StackFrameImpl(vm, this,
+                                                          jdwpFrames[i].frameID,
+                                                          jdwpFrames[i].location);
+                    // Add to the frame list
+                    localCache.frames.add(frame);
                 }
-                if (local != null) {
-                    local.frames = frames;
-                    local.framesStart = start;
-                    local.framesLength = length;
-                }
+                localCache.framesStart = start;
+                localCache.framesLength = length;
+                return Collections.unmodifiableList(localCache.frames);
             } else {
-                int fromIndex = start - local.framesStart;
+                int fromIndex = start - localCache.framesStart;
                 int toIndex;
                 if (length == -1) {
-                    toIndex = frames.size() - fromIndex;
+                    toIndex = localCache.frames.size() - fromIndex;
                 } else {
                     toIndex = fromIndex + length;
                 }
-                frames = frames.subList(fromIndex, toIndex);
+                return Collections.unmodifiableList(localCache.frames.subList(fromIndex, toIndex));
             }
         } catch (JDWPException exc) {
             switch (exc.errorCode()) {
@@ -384,28 +412,18 @@
                 throw exc.toJDIException();
             }
         }
-        return Collections.unmodifiableList(frames);
     }
 
     public List<ObjectReference> ownedMonitors()  throws IncompatibleThreadStateException  {
-        List<ObjectReference> monitors = null;
         try {
-            Cache local = (Cache)getCache();
-
-            if (local != null) {
-                monitors = local.ownedMonitors;
-            }
-            if (monitors == null) {
-                monitors = Arrays.asList(
+            if (localCache.ownedMonitors == null) {
+                localCache.ownedMonitors = Arrays.asList(
                                  (ObjectReference[])JDWP.ThreadReference.OwnedMonitors.
                                          process(vm, this).owned);
-                if (local != null) {
-                    local.ownedMonitors = monitors;
-                    if ((vm.traceFlags & vm.TRACE_OBJREFS) != 0) {
-                        vm.printTrace(description() +
-                                      " temporarily caching owned monitors"+
-                                      " (count = " + monitors.size() + ")");
-                    }
+                if ((vm.traceFlags & vm.TRACE_OBJREFS) != 0) {
+                    vm.printTrace(description() +
+                                  " temporarily caching owned monitors"+
+                                  " (count = " + localCache.ownedMonitors.size() + ")");
                 }
             }
         } catch (JDWPException exc) {
@@ -417,29 +435,22 @@
                 throw exc.toJDIException();
             }
         }
-        return monitors;
+        return localCache.ownedMonitors;
     }
 
     public ObjectReference currentContendedMonitor()
                               throws IncompatibleThreadStateException  {
-        ObjectReference monitor = null;
         try {
-            Cache local = (Cache)getCache();
-
-            if (local != null && local.triedCurrentContended) {
-                monitor = local.contendedMonitor;
-            } else {
-                monitor = JDWP.ThreadReference.CurrentContendedMonitor.
+            if (localCache.contendedMonitor == null &&
+                !localCache.triedCurrentContended) {
+                localCache.contendedMonitor = JDWP.ThreadReference.CurrentContendedMonitor.
                     process(vm, this).monitor;
-                if (local != null) {
-                    local.triedCurrentContended = true;
-                    local.contendedMonitor = monitor;
-                    if ((monitor != null) &&
-                        ((vm.traceFlags & vm.TRACE_OBJREFS) != 0)) {
-                        vm.printTrace(description() +
-                              " temporarily caching contended monitor"+
-                              " (id = " + monitor.uniqueID() + ")");
-                    }
+                localCache.triedCurrentContended = true;
+                if ((localCache.contendedMonitor != null) &&
+                    ((vm.traceFlags & vm.TRACE_OBJREFS) != 0)) {
+                    vm.printTrace(description() +
+                                  " temporarily caching contended monitor"+
+                                  " (id = " + localCache.contendedMonitor.uniqueID() + ")");
                 }
             }
         } catch (JDWPException exc) {
@@ -450,40 +461,31 @@
                 throw exc.toJDIException();
             }
         }
-        return monitor;
+        return localCache.contendedMonitor;
     }
 
     public List<MonitorInfo> ownedMonitorsAndFrames()  throws IncompatibleThreadStateException  {
-        List<MonitorInfo> monitors = null;
         try {
-            Cache local = (Cache)getCache();
-
-            if (local != null) {
-                monitors = local.ownedMonitorsInfo;
-            }
-            if (monitors == null) {
+            if (localCache.ownedMonitorsInfo == null) {
                 JDWP.ThreadReference.OwnedMonitorsStackDepthInfo.monitor[] minfo;
                 minfo = JDWP.ThreadReference.OwnedMonitorsStackDepthInfo.process(vm, this).owned;
 
-                monitors = new ArrayList<MonitorInfo>(minfo.length);
+                localCache.ownedMonitorsInfo = new ArrayList<MonitorInfo>(minfo.length);
 
                 for (int i=0; i < minfo.length; i++) {
                     JDWP.ThreadReference.OwnedMonitorsStackDepthInfo.monitor mi =
                                                                          minfo[i];
                     MonitorInfo mon = new MonitorInfoImpl(vm, minfo[i].monitor, this, minfo[i].stack_depth);
-                    monitors.add(mon);
+                    localCache.ownedMonitorsInfo.add(mon);
                 }
 
-                if (local != null) {
-                    local.ownedMonitorsInfo = monitors;
-                    if ((vm.traceFlags & vm.TRACE_OBJREFS) != 0) {
-                        vm.printTrace(description() +
-                                      " temporarily caching owned monitors"+
-                                      " (count = " + monitors.size() + ")");
+                if ((vm.traceFlags & vm.TRACE_OBJREFS) != 0) {
+                    vm.printTrace(description() +
+                                  " temporarily caching owned monitors"+
+                                  " (count = " + localCache.ownedMonitorsInfo.size() + ")");
                     }
                 }
 
-            }
         } catch (JDWPException exc) {
             switch (exc.errorCode()) {
             case JDWP.Error.THREAD_NOT_SUSPENDED:
@@ -493,7 +495,7 @@
                 throw exc.toJDIException();
             }
         }
-        return monitors;
+        return localCache.ownedMonitorsInfo;
     }
 
     public void popFrames(StackFrame frame) throws IncompatibleThreadStateException {
@@ -511,7 +513,7 @@
     }
 
     public void forceEarlyReturn(Value  returnValue) throws InvalidTypeException,
-                                             ClassNotLoadedException,
+                                                            ClassNotLoadedException,
                                              IncompatibleThreadStateException {
         if (!vm.canForceEarlyReturn()) {
             throw new UnsupportedOperationException(
@@ -604,6 +606,9 @@
                     iter.remove();
                 }
             }
+
+            // Discard our local cache
+            resetLocalCache();
         }
     }
 }
--- a/jdk/src/share/classes/com/sun/tools/jdi/VMAction.java	Fri Jul 04 18:55:37 2008 +0200
+++ b/jdk/src/share/classes/com/sun/tools/jdi/VMAction.java	Wed Jul 09 13:43:26 2008 -0700
@@ -38,10 +38,18 @@
     static final int VM_NOT_SUSPENDED = 2;
 
     int id;
+    ThreadReference resumingThread;
 
     VMAction(VirtualMachine vm, int id) {
+        this(vm, null, id);
+    }
+
+    // For id = VM_NOT_SUSPENDED, if resumingThread != null, then it is
+    // the only thread that is being resumed.
+     VMAction(VirtualMachine vm,  ThreadReference resumingThread, int id) {
         super(vm);
         this.id = id;
+        this.resumingThread = resumingThread;
     }
     VirtualMachine vm() {
         return (VirtualMachine)getSource();
@@ -49,4 +57,8 @@
     int id() {
         return id;
     }
+
+    ThreadReference resumingThread() {
+        return resumingThread;
+    }
 }
--- a/jdk/src/share/classes/com/sun/tools/jdi/VMState.java	Fri Jul 04 18:55:37 2008 +0200
+++ b/jdk/src/share/classes/com/sun/tools/jdi/VMState.java	Wed Jul 09 13:43:26 2008 -0700
@@ -116,16 +116,25 @@
     }
 
     /**
-     * Tell listeners to invalidate suspend-sensitive caches.
+     * All threads are resuming
      */
-    synchronized void thaw() {
+    void thaw() {
+        thaw(null);
+    }
+
+    /**
+     * Tell listeners to invalidate suspend-sensitive caches.
+     * If resumingThread != null, then only that thread is being
+     * resumed.
+     */
+    synchronized void thaw(ThreadReference resumingThread) {
         if (cache != null) {
             if ((vm.traceFlags & vm.TRACE_OBJREFS) != 0) {
                 vm.printTrace("Clearing VM suspended cache");
             }
             disableCache();
         }
-        processVMAction(new VMAction(vm, VMAction.VM_NOT_SUSPENDED));
+        processVMAction(new VMAction(vm, resumingThread, VMAction.VM_NOT_SUSPENDED));
     }
 
     private synchronized void processVMAction(VMAction action) {
--- a/jdk/src/share/classes/com/sun/tools/jdi/VirtualMachineImpl.java	Fri Jul 04 18:55:37 2008 +0200
+++ b/jdk/src/share/classes/com/sun/tools/jdi/VirtualMachineImpl.java	Wed Jul 09 13:43:26 2008 -0700
@@ -146,8 +146,9 @@
     public boolean threadResumable(ThreadAction action) {
         /*
          * If any thread is resumed, the VM is considered not suspended.
+         * Just one thread is being resumed so pass it to thaw.
          */
-        state.thaw();
+        state.thaw(action.thread());
         return true;
     }
 
--- a/jdk/test/com/sun/jdi/MonitorFrameInfo.java	Fri Jul 04 18:55:37 2008 +0200
+++ b/jdk/test/com/sun/jdi/MonitorFrameInfo.java	Wed Jul 09 13:43:26 2008 -0700
@@ -25,7 +25,8 @@
  *  @test
  *  @bug 6230699
  *  @summary Test ThreadReference.ownedMonitorsAndFrames()
- *
+ *  @bug 6701700
+ *  @summary MonitorInfo objects aren't invalidated when the owning thread is resumed
  *  @author Swamy Venkataramanappa
  *
  *  @run build TestScaffold VMConnection TargetListener TargetAdapter
@@ -100,15 +101,15 @@
 
         if (!mainThread.frame(0).location().method().name()
                         .equals("foo3")) {
-            failure("frame failed");
+            failure("FAILED: frame failed");
         }
 
         if (mainThread.frames().size() != (initialSize + 3)) {
-            failure("frames size failed");
+            failure("FAILED: frames size failed");
         }
 
         if (mainThread.frames().size() != mainThread.frameCount()) {
-            failure("frames size not equal to frameCount");
+            failure("FAILED: frames size not equal to frameCount");
         }
 
         /* Test monitor frame info.
@@ -119,13 +120,32 @@
             if (monitors.size() != expectedCount) {
                 failure("monitors count is not equal to expected count");
             }
+            MonitorInfo mon = null;
             for (int j=0; j < monitors.size(); j++) {
-                MonitorInfo mon  = (MonitorInfo)monitors.get(j);
+                mon  = (MonitorInfo)monitors.get(j);
                 System.out.println("Monitor obj " + mon.monitor() + "depth =" +mon.stackDepth());
                 if (mon.stackDepth() != expectedDepth[j]) {
-                    failure("monitor stack depth is not equal to expected depth");
+                    failure("FAILED: monitor stack depth is not equal to expected depth");
                 }
             }
+
+            // The last gotten monInfo is in mon.   When we resume the thread,
+            // it should become invalid.  We will step out of the top frame
+            // so that the frame depth in this mon object will no longer be correct.
+            // That is why the monInfo's have to become invalid when the thread is
+            // resumed.
+            stepOut(mainThread);
+            boolean ok = false;
+            try {
+                System.out.println("*** Saved Monitor obj " + mon.monitor() + "depth =" +mon.stackDepth());
+            } catch(InvalidStackFrameException ee) {
+                // ok
+                ok = true;
+                System.out.println("Got expected InvalidStackFrameException after a resume");
+            }
+            if (!ok) {
+                failure("FAILED: MonitorInfo object was not invalidated by a resume");
+            }
         } else {
             System.out.println("can not get monitors frame info");
         }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/com/sun/jdi/ResumeOneThreadTest.java	Wed Jul 09 13:43:26 2008 -0700
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2008 Sun Microsystems, Inc.  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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+/**
+ *  @test
+ *  @bug 6700889
+ *  @summary  Thread resume invalidates all stack frames, even from other threads
+ *
+ *  @author jjh
+ *
+ *  @run build TestScaffold VMConnection TargetListener TargetAdapter
+ *  @run compile -g ResumeOneThreadTest.java
+ *  @run main ResumeOneThreadTest
+ */
+import com.sun.jdi.*;
+import com.sun.jdi.event.*;
+import com.sun.jdi.request.*;
+
+import java.util.*;
+
+class ResumeOneThreadTarg extends Thread {
+    static String name1 = "Thread 1";
+    static String name2 = "Thread 2";
+
+    public ResumeOneThreadTarg(String name) {
+        super(name);
+    }
+
+    public static void main(String[] args) {
+        System.out.println("    Debuggee: Howdy!");
+        ResumeOneThreadTarg t1 = new ResumeOneThreadTarg(name1);
+        ResumeOneThreadTarg t2 = new ResumeOneThreadTarg(name2);
+
+        t1.start();
+        t2.start();
+    }
+
+    // This just starts two threads. Each runs to a bkpt.
+    public void run() {
+        if (getName().equals(name1)) {
+            run1();
+        } else {
+            run2();
+        }
+    }
+
+    public void bkpt1(String p1) {
+        System.out.println("    Debuggee: bkpt 1");
+    }
+
+    public void run1() {
+        bkpt1("Hello Alviso!");
+    }
+
+
+
+    public void bkpt2() {
+        System.out.println("    Debuggee: bkpt 2");
+    }
+
+    public void run2() {
+        bkpt2();
+    }
+}
+
+/********** test program **********/
+
+public class ResumeOneThreadTest extends TestScaffold {
+    ReferenceType targetClass;
+    ThreadReference mainThread;
+
+    BreakpointRequest request1;
+    BreakpointRequest request2;
+
+    ThreadReference thread1 = null;
+    ThreadReference thread2 = null;;
+    boolean theVMisDead = false;
+
+    ResumeOneThreadTest (String args[]) {
+        super(args);
+    }
+
+    public static void main(String[] args)      throws Exception {
+        new ResumeOneThreadTest(args).startTests();
+    }
+
+
+    synchronized public void breakpointReached(BreakpointEvent event) {
+        println("-- Got bkpt at: " + event.location());
+        ThreadReference eventThread = event.thread();
+
+        if (eventThread.name().equals(ResumeOneThreadTarg.name1)) {
+            thread1 = eventThread;
+        }
+
+        if (eventThread.name().equals(ResumeOneThreadTarg.name2)) {
+            thread2 = eventThread;
+        }
+    }
+
+    public void vmDied(VMDeathEvent event) {
+        theVMisDead = true;
+    }
+
+    synchronized public void eventSetComplete(EventSet set) {
+        if (theVMisDead) {
+            return;
+        }
+        if (thread1 == null || thread2 == null) {
+            // Don't do a set.resume(), just let the other thread
+            // keep running until it hits its bkpt.
+            return;
+        }
+
+        // Both threads are stopped at their bkpts.  Get a StackFrame from
+        // Thread 1 then resume Thread 2 and verify that the saved StackFrame is
+        // still valid.
+
+        // suspend everything.
+        println("-- All threads suspended");
+        vm().suspend();
+
+        StackFrame t1sf0 = null;
+        try {
+            t1sf0 = thread1.frame(0);
+        } catch (IncompatibleThreadStateException ee) {
+            failure("FAILED: Exception: " + ee);
+        }
+
+        println("-- t1sf0 args: " + t1sf0.getArgumentValues());
+
+        // Ok, we have a StackFrame for thread 1.  Resume just thread 2
+        // Note that thread 2 has been suspended twice - by the SUSPEND_ALL
+        // bkpt, and by the above vm().suspend(), so we have to resume
+        // it twice.
+        request2.disable();
+
+        thread2.resume();
+        thread2.resume();
+        println("-- Did Resume on thread 2");
+
+        // Can we get frames for thread1?
+        try {
+            StackFrame t1sf0_1 = thread1.frame(0);
+            if (!t1sf0.equals(t1sf0_1)) {
+                failure("FAILED: Got a different frame 0 for thread 1 after resuming thread 2");
+            }
+        } catch (IncompatibleThreadStateException ee) {
+            failure("FAILED: Could not get frames for thread 1: Exception: " + ee);
+        } catch (Exception ee) {
+            failure("FAILED: Could not get frames for thread 1: Exception: " + ee);
+        }
+
+
+        try {
+            println("-- t1sf0 args: " + t1sf0.getArgumentValues());
+        } catch (InvalidStackFrameException ee) {
+            // This is the failure.
+            failure("FAILED Got InvalidStackFrameException");
+            vm().dispose();
+            throw(ee);
+        }
+
+        // Let the debuggee finish
+        request1.disable();
+        thread1.resume();
+        vm().resume();
+        println("--------------");
+    }
+
+    /********** test core **********/
+
+    protected void runTests() throws Exception {
+
+        /*
+         * Get to the top of main()
+         * to determine targetClass and mainThread
+         */
+        BreakpointEvent bpe = startToMain("ResumeOneThreadTarg");
+        targetClass = bpe.location().declaringType();
+        mainThread = bpe.thread();
+        EventRequestManager erm = vm().eventRequestManager();
+        final Thread mainThread = Thread.currentThread();
+
+        /*
+         * Set event requests
+         */
+
+        Location loc1 = findMethod(targetClass, "bkpt1", "(Ljava/lang/String;)V").location();
+        request1 = erm.createBreakpointRequest(loc1);
+        request1.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
+        request1.enable();
+
+        Location loc2 = findMethod(targetClass, "bkpt2", "()V").location();
+        request2 = erm.createBreakpointRequest(loc2);
+        request2.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
+        request2.enable();
+
+        /*
+         * resume the target, listening for events
+         */
+        listenUntilVMDisconnect();
+        /*
+         * deal with results of test
+         * if anything has called failure("foo") testFailed will be true
+         */
+        if (!testFailed) {
+            println("ResumeOneThreadTest: passed");
+        } else {
+            throw new Exception("ResumeOneThreadTest: failed");
+        }
+    }
+}