8219143: jdb should support breakpoint thread filters
authorcjplummer
Mon, 25 Feb 2019 18:54:40 -0800
changeset 53918 616a32d6b463
parent 53917 1ee9149df76f
child 53919 554c3c813ad6
child 53938 c6b18dd94973
8219143: jdb should support breakpoint thread filters Summary: add thread filter to stop command. Reviewed-by: sspitsyn, amenkov
src/jdk.jdi/share/classes/com/sun/tools/example/debug/tty/BreakpointSpec.java
src/jdk.jdi/share/classes/com/sun/tools/example/debug/tty/Commands.java
src/jdk.jdi/share/classes/com/sun/tools/example/debug/tty/EventRequestSpecList.java
src/jdk.jdi/share/classes/com/sun/tools/example/debug/tty/TTYResources.java
test/jdk/com/sun/jdi/JdbStopThreadidTest.java
test/jdk/com/sun/jdi/lib/jdb/JdbCommand.java
--- a/src/jdk.jdi/share/classes/com/sun/tools/example/debug/tty/BreakpointSpec.java	Mon Feb 25 16:05:06 2019 -0800
+++ b/src/jdk.jdi/share/classes/com/sun/tools/example/debug/tty/BreakpointSpec.java	Mon Feb 25 18:54:40 2019 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1998, 2011, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1998, 2019, 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
@@ -44,20 +44,24 @@
     String methodId;
     List<String> methodArgs;
     int lineNumber;
+    ThreadReference threadFilter; /* Thread to break in. null if global breakpoint. */
+    public static final String locationTokenDelimiter = ":( \t\n\r";
 
-    BreakpointSpec(ReferenceTypeSpec refSpec, int lineNumber) {
+    BreakpointSpec(ReferenceTypeSpec refSpec, int lineNumber, ThreadReference threadFilter) {
         super(refSpec);
         this.methodId = null;
         this.methodArgs = null;
         this.lineNumber = lineNumber;
+        this.threadFilter = threadFilter;
     }
 
-    BreakpointSpec(ReferenceTypeSpec refSpec, String methodId,
+    BreakpointSpec(ReferenceTypeSpec refSpec, String methodId, ThreadReference threadFilter,
                    List<String> methodArgs) throws MalformedMemberNameException {
         super(refSpec);
         this.methodId = methodId;
         this.methodArgs = methodArgs;
         this.lineNumber = 0;
+        this.threadFilter = threadFilter;
         if (!isValidMethodName(methodId)) {
             throw new MalformedMemberNameException(methodId);
         }
@@ -78,8 +82,11 @@
             throw new InvalidTypeException();
         }
         EventRequestManager em = refType.virtualMachine().eventRequestManager();
-        EventRequest bp = em.createBreakpointRequest(location);
+        BreakpointRequest bp = em.createBreakpointRequest(location);
         bp.setSuspendPolicy(suspendPolicy);
+        if (threadFilter != null) {
+            bp.addThreadFilter(threadFilter);
+        }
         bp.enable();
         return bp;
     }
@@ -104,7 +111,8 @@
     public int hashCode() {
         return refSpec.hashCode() + lineNumber +
             ((methodId != null) ? methodId.hashCode() : 0) +
-            ((methodArgs != null) ? methodArgs.hashCode() : 0);
+            ((methodArgs != null) ? methodArgs.hashCode() : 0) +
+            ((threadFilter != null) ? threadFilter.hashCode() : 0);
     }
 
     @Override
@@ -118,6 +126,9 @@
                    ((methodArgs != null) ?
                         methodArgs.equals(breakpoint.methodArgs)
                       : methodArgs == breakpoint.methodArgs) &&
+                   ((threadFilter != null) ?
+                        threadFilter.equals(breakpoint.threadFilter)
+                      : threadFilter == breakpoint.threadFilter) &&
                    refSpec.equals(breakpoint.refSpec) &&
                    (lineNumber == breakpoint.lineNumber);
         } else {
--- a/src/jdk.jdi/share/classes/com/sun/tools/example/debug/tty/Commands.java	Mon Feb 25 16:05:06 2019 -0800
+++ b/src/jdk.jdi/share/classes/com/sun/tools/example/debug/tty/Commands.java	Mon Feb 25 18:54:40 2019 -0800
@@ -1037,16 +1037,16 @@
     }
 
 
-    private void printBreakpointCommandUsage(String atForm, String inForm) {
-        MessageOutput.println("printbreakpointcommandusage",
-                              new Object [] {atForm, inForm});
+    private void printBreakpointCommandUsage(String usageMessage) {
+        MessageOutput.println(usageMessage);
     }
 
-    protected BreakpointSpec parseBreakpointSpec(StringTokenizer t,
-                                             String atForm, String inForm) {
+    protected BreakpointSpec parseBreakpointSpec(StringTokenizer t, String next_token,
+                                                 ThreadReference threadFilter,
+                                                 String usageMessage) {
         BreakpointSpec breakpoint = null;
         try {
-            String token = t.nextToken(":( \t\n\r");
+            String token = next_token;
 
             // We can't use hasMoreTokens here because it will cause any leading
             // paren to be lost.
@@ -1064,16 +1064,24 @@
 
                 NumberFormat nf = NumberFormat.getNumberInstance();
                 nf.setParseIntegerOnly(true);
-                Number n = nf.parse(lineToken);
+                Number n;
+                try {
+                    n = nf.parse(lineToken);
+                } catch (java.text.ParseException pe) {
+                    MessageOutput.println("Invalid line number specified");
+                    printBreakpointCommandUsage(usageMessage);
+                    return null;
+                }
                 int lineNumber = n.intValue();
 
                 if (t.hasMoreTokens()) {
-                    printBreakpointCommandUsage(atForm, inForm);
+                    MessageOutput.println("Extra tokens after breakpoint location");
+                    printBreakpointCommandUsage(usageMessage);
                     return null;
                 }
                 try {
                     breakpoint = Env.specList.createBreakpoint(classId,
-                                                               lineNumber);
+                                                               lineNumber, threadFilter);
                 } catch (ClassNotFoundException exc) {
                     MessageOutput.println("is not a valid class name", classId);
                 }
@@ -1082,7 +1090,8 @@
                 int idot = token.lastIndexOf('.');
                 if ( (idot <= 0) ||                     /* No dot or dot in first char */
                      (idot >= token.length() - 1) ) { /* dot in last char */
-                    printBreakpointCommandUsage(atForm, inForm);
+                    MessageOutput.println("Invalid <class>.<method_name> specification");
+                    printBreakpointCommandUsage(usageMessage);
                     return null;
                 }
                 String methodName = token.substring(idot + 1);
@@ -1090,9 +1099,9 @@
                 List<String> argumentList = null;
                 if (rest != null) {
                     if (!rest.startsWith("(") || !rest.endsWith(")")) {
-                        MessageOutput.println("Invalid method specification:",
+                        MessageOutput.println("Invalid <method_name> specification:",
                                               methodName + rest);
-                        printBreakpointCommandUsage(atForm, inForm);
+                        printBreakpointCommandUsage(usageMessage);
                         return null;
                     }
                     // Trim the parens
@@ -1107,6 +1116,7 @@
                 try {
                     breakpoint = Env.specList.createBreakpoint(classId,
                                                                methodName,
+                                                               threadFilter,
                                                                argumentList);
                 } catch (MalformedMemberNameException exc) {
                     MessageOutput.println("is not a valid method name", methodName);
@@ -1115,7 +1125,7 @@
                 }
             }
         } catch (Exception e) {
-            printBreakpointCommandUsage(atForm, inForm);
+            printBreakpointCommandUsage(usageMessage);
             return null;
         }
         return breakpoint;
@@ -1145,33 +1155,74 @@
     }
 
     void commandStop(StringTokenizer t) {
-        String atIn;
         byte suspendPolicy = EventRequest.SUSPEND_ALL;
+        ThreadReference threadFilter = null;
 
-        if (t.hasMoreTokens()) {
-            atIn = t.nextToken();
-            if (atIn.equals("go") && t.hasMoreTokens()) {
-                suspendPolicy = EventRequest.SUSPEND_NONE;
-                atIn = t.nextToken();
-            } else if (atIn.equals("thread") && t.hasMoreTokens()) {
-                suspendPolicy = EventRequest.SUSPEND_EVENT_THREAD;
-                atIn = t.nextToken();
-            }
-        } else {
+        /*
+         * Allowed syntax:
+         *    stop [go|thread] [<thread_id>] <at|in> <location>
+         * If no options are given, the current list of breakpoints is printed.
+         * If "go" is specified, then immediately resume after stopping. No threads are suspended.
+         * If "thread" is specified, then only suspend the thread we stop in.
+         * If neither "go" nor "thread" are specified, then suspend all threads.
+         * If an integer <thread_id> is specified, then only stop in the specified thread.
+         * <location> can either be a line number or a method:
+         *    - <class id>:<line>
+         *    - <class id>.<method>[(argument_type,...)]
+         */
+
+        if (!t.hasMoreTokens()) {
             listBreakpoints();
             return;
         }
 
-        BreakpointSpec spec = parseBreakpointSpec(t, "stop at", "stop in");
-        if (spec != null) {
-            // Enforcement of "at" vs. "in". The distinction is really
-            // unnecessary and we should consider not checking for this
-            // (and making "at" and "in" optional).
-            if (atIn.equals("at") && spec.isMethodBreakpoint()) {
-                MessageOutput.println("Use stop at to set a breakpoint at a line number");
-                printBreakpointCommandUsage("stop at", "stop in");
+        String token = t.nextToken();
+
+        /* Check for "go" or "thread" modifiers. */
+        if (token.equals("go") && t.hasMoreTokens()) {
+            suspendPolicy = EventRequest.SUSPEND_NONE;
+            token = t.nextToken();
+        } else if (token.equals("thread") && t.hasMoreTokens()) {
+            suspendPolicy = EventRequest.SUSPEND_EVENT_THREAD;
+            token = t.nextToken();
+        }
+
+        /* Handle <thread_id> modifier. */
+        if (!token.equals("at") && !token.equals("in")) {
+            Long threadid;
+            try {
+                threadid = Long.decode(token);
+            } catch (NumberFormatException nfe) {
+                MessageOutput.println("Expected at, in, or an integer <thread_id>:", token);
+                printBreakpointCommandUsage("printstopcommandusage");
                 return;
             }
+            try {
+                ThreadInfo threadInfo = ThreadInfo.getThreadInfo(token);
+                if (threadInfo == null) {
+                    MessageOutput.println("Invalid <thread_id>:", token);
+                    return;
+                }
+                threadFilter = threadInfo.getThread();
+                token = t.nextToken(BreakpointSpec.locationTokenDelimiter);
+            } catch (VMNotConnectedException vmnce) {
+                MessageOutput.println("<thread_id> option not valid until the VM is started with the run command");
+                return;
+            }
+
+        }
+
+        /* Make sure "at" or "in" comes next. */
+        if (!token.equals("at") && !token.equals("in")) {
+            MessageOutput.println("Missing at or in");
+            printBreakpointCommandUsage("printstopcommandusage");
+            return;
+        }
+
+        token = t.nextToken(BreakpointSpec.locationTokenDelimiter);
+
+        BreakpointSpec spec = parseBreakpointSpec(t, token, threadFilter, "printstopcommandusage");
+        if (spec != null) {
             spec.suspendPolicy = suspendPolicy;
             resolveNow(spec);
         }
@@ -1183,7 +1234,8 @@
             return;
         }
 
-        BreakpointSpec spec = parseBreakpointSpec(t, "clear", "clear");
+        String token = t.nextToken(BreakpointSpec.locationTokenDelimiter);
+        BreakpointSpec spec = parseBreakpointSpec(t, token, null, "printclearcommandusage");
         if (spec != null) {
             if (Env.specList.delete(spec)) {
                 MessageOutput.println("Removed:", spec.toString());
--- a/src/jdk.jdi/share/classes/com/sun/tools/example/debug/tty/EventRequestSpecList.java	Mon Feb 25 16:05:06 2019 -0800
+++ b/src/jdk.jdi/share/classes/com/sun/tools/example/debug/tty/EventRequestSpecList.java	Mon Feb 25 18:54:40 2019 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1998, 2011, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1998, 2019, 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
@@ -36,6 +36,7 @@
 
 import com.sun.jdi.request.EventRequest;
 import com.sun.jdi.event.ClassPrepareEvent;
+import com.sun.jdi.ThreadReference;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -108,21 +109,21 @@
         }
     }
 
-    BreakpointSpec createBreakpoint(String classPattern, int line)
+    BreakpointSpec createBreakpoint(String classPattern, int line, ThreadReference threadFilter)
         throws ClassNotFoundException {
         ReferenceTypeSpec refSpec =
             new PatternReferenceTypeSpec(classPattern);
-        return new BreakpointSpec(refSpec, line);
+        return new BreakpointSpec(refSpec, line, threadFilter);
     }
 
     BreakpointSpec createBreakpoint(String classPattern,
-                                 String methodId,
+                                    String methodId, ThreadReference threadFilter,
                                     List<String> methodArgs)
                                 throws MalformedMemberNameException,
                                        ClassNotFoundException {
         ReferenceTypeSpec refSpec =
             new PatternReferenceTypeSpec(classPattern);
-        return new BreakpointSpec(refSpec, methodId, methodArgs);
+        return new BreakpointSpec(refSpec, methodId, threadFilter, methodArgs);
     }
 
     EventRequestSpec createExceptionCatch(String classPattern,
--- a/src/jdk.jdi/share/classes/com/sun/tools/example/debug/tty/TTYResources.java	Mon Feb 25 16:05:06 2019 -0800
+++ b/src/jdk.jdi/share/classes/com/sun/tools/example/debug/tty/TTYResources.java	Mon Feb 25 18:54:40 2019 -0800
@@ -120,12 +120,14 @@
         {"Exception occurred caught", "Exception occurred: {0} (to be caught at: {1})"},
         {"Exception occurred uncaught", "Exception occurred: {0} (uncaught)"},
         {"Exceptions caught:", "Break when these exceptions occur:"},
+        {"Expected at, in, or an integer <thread_id>:", "Expected \"at\", \"in\", or an integer <thread_id>: {0}"},
         {"expr is null", "{0} = null"},
         {"expr is value", "{0} = {1}"},
         {"expr is value <collected>", "  {0} = {1} <collected>"},
         {"Expression cannot be void", "Expression cannot be void"},
         {"Expression must evaluate to an object", "Expression must evaluate to an object"},
         {"extends:", "extends: {0}"},
+        {"Extra tokens after breakpoint location", "Extra tokens after breakpoint location"},
         {"Failed reading output", "Failed reading output of child java interpreter."},
         {"Fatal error", "Fatal error:"},
         {"Field access encountered before after", "Field ({0}) is {1}, will be {2}: "},
@@ -154,11 +156,14 @@
         {"Invalid connect type", "Invalid connect type"},
         {"Invalid consecutive invocations", "Invalid consecutive invocations"},
         {"Invalid exception object", "Invalid exception object"},
-        {"Invalid method specification:", "Invalid method specification: {0}"},
+        {"Invalid line number specified", "Invalid line number specified"},
+        {"Invalid <method_name> specification:", "Invalid <method_name> specification: {0}"},
         {"Invalid option on class command", "Invalid option on class command"},
         {"invalid option", "invalid option: {0}"},
         {"Invalid thread status.", "Invalid thread status."},
+        {"Invalid <thread_id>:", "Invalid <thread_id>: {0}"},
         {"Invalid transport name:", "Invalid transport name: {0}"},
+        {"Invalid <class>.<method_name> specification", "Invalid <class>.<method_name> specification"},
         {"I/O exception occurred:", "I/O Exception occurred: {0}"},
         {"is an ambiguous method name in", "\"{0}\" is an ambiguous method name in \"{1}\""},
         {"is an invalid line number for",  "{0,number,integer} is an invalid line number for {1}"},
@@ -191,6 +196,7 @@
         {"Method exitedValue:", "Method exited: return value = {0}, "},
         {"Method is overloaded; specify arguments", "Method {0} is overloaded; specify arguments"},
         {"minus version", "This is {0} version {1,number,integer}.{2,number,integer} (Java SE version {3})"},
+        {"Missing at or in", "Missing \"at\" or \"in\""},
         {"Monitor information for thread", "Monitor information for thread {0}:"},
         {"Monitor information for expr", "Monitor information for {0} ({1}):"},
         {"More than one class named", "More than one class named: ''{0}''"},
@@ -241,7 +247,18 @@
         {"Owned by:", "  Owned by: {0}, entry count: {1,number,integer}"},
         {"Owned monitor:", "  Owned monitor: {0}"},
         {"Parse exception:", "Parse Exception: {0}"},
-        {"printbreakpointcommandusage", "Usage: {0} <class>:<line_number> or\n       {1} <class>.<method_name>[(argument_type,...)]"},
+        {"printclearcommandusage", "Usage clear <class>:<line_number> or\n      clear <class>.<method_name>[(argument_type,...)]"},
+        {"printstopcommandusage",
+         "Usage: stop [go|thread] [<thread_id>] <at|in> <location>\n" +
+         "  If \"go\" is specified, immediately resume after stopping\n" +
+         "  If \"thread\" is specified, only suspend the thread we stop in\n" +
+         "  If neither \"go\" nor \"thread\" are specified, suspend all threads\n" +
+         "  If an integer <thread_id> is specified, only stop in the specified thread\n" +
+         "  \"at\" and \"in\" have the same meaning\n" +
+         "  <location> can either be a line number or a method:\n" +
+         "    <class_id>:<line_number>\n" +
+         "    <class_id>.<method>[(argument_type,...)]"
+        },
         {"Removed:", "Removed: {0}"},
         {"Requested stack frame is no longer active:", "Requested stack frame is no longer active: {0,number,integer}"},
         {"run <args> command is valid only with launched VMs", "'run <args>' command is valid only with launched VMs"},
@@ -292,6 +309,8 @@
         {"Thread not suspended", "Thread not suspended"},
         {"thread group number description name", "{0,number,integer}. {1} {2}"},
         {"Threadgroup name not specified.", "Threadgroup name not specified."},
+        {"<thread_id> option not valid until the VM is started with the run command",
+         "<thread_id> option not valid until the VM is started with the run command"},
         {"Threads must be suspended", "Threads must be suspended"},
         {"trace method exit in effect for", "trace method exit in effect for {0}"},
         {"trace method exits in effect", "trace method exits in effect"},
@@ -318,7 +337,6 @@
         {"Usage: unmonitor <monitor#>", "Usage: unmonitor <monitor#>"},
         {"Usage: up [n frames]", "Usage: up [n frames]"},
         {"Use java minus X to see", "Use 'java -X' to see the available non-standard options"},
-        {"Use stop at to set a breakpoint at a line number", "Use 'stop at' to set a breakpoint at a line number"},
         {"VM already running. use cont to continue after events.", "VM already running. Use 'cont' to continue after events."},
         {"VM Started:", "VM Started: "},
         {"vmstartexception", "VM start exception: {0}"},
@@ -357,9 +375,17 @@
              "threadgroups              -- list threadgroups\n" +
              "threadgroup <name>        -- set current threadgroup\n" +
              "\n" +
-             "stop in <class id>.<method>[(argument_type,...)]\n" +
-             "                          -- set a breakpoint in a method\n" +
-             "stop at <class id>:<line> -- set a breakpoint at a line\n" +
+             "stop [go|thread] [<thread_id>] <at|in> <location>\n" +
+             "                          -- set a breakpoint\n" +
+             "                          -- if no options are given, the current list of breakpoints is printed\n" +
+             "                          -- if \"go\" is specified, immediately resume after stopping\n" +
+             "                          -- if \"thread\" is specified, only suspend the thread we stop in\n" +
+             "                          -- if neither \"go\" nor \"thread\" are specified, suspend all threads\n" +
+             "                          -- if an integer <thread_id> is specified, only stop in the specified thread\n" +
+             "                          -- \"at\" and \"in\" have the same meaning\n" +
+             "                          -- <location> can either be a line number or a method:\n" +
+             "                          --   <class_id>:<line_number>\n" +
+             "                          --   <class_id>.<method>[(argument_type,...)]\n" +
              "clear <class id>.<method>[(argument_type,...)]\n" +
              "                          -- clear a breakpoint in a method\n" +
              "clear <class id>:<line>   -- clear a breakpoint at a line\n" +
@@ -412,7 +438,7 @@
              "<n> <command>             -- repeat command n times\n" +
              "# <command>               -- discard (no-op)\n" +
              "help (or ?)               -- list commands\n" +
-             "dbgtrace [flag]           -- same as dbgtrace command line option" +
+             "dbgtrace [flag]           -- same as dbgtrace command line option\n" +
              "version                   -- print version information\n" +
              "exit (or quit)            -- exit debugger\n" +
              "\n" +
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/com/sun/jdi/JdbStopThreadidTest.java	Mon Feb 25 18:54:40 2019 -0800
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2019, 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 8219143
+ * @summary Tests that using the "stop in" threadid option will properly cause the
+ * breakpoint to only be triggered when hit in the specified thread.
+ *
+ * @library /test/lib
+ * @run compile -g JdbStopThreadidTest.java
+ * @run main/othervm JdbStopThreadidTest
+ */
+
+import lib.jdb.Jdb;
+import lib.jdb.JdbCommand;
+import lib.jdb.JdbTest;
+
+import java.util.regex.*;
+
+class JdbStopThreadidTestTarg {
+    static Object lockObj = new Object();
+
+    public static void main(String[] args) {
+        test();
+    }
+
+    private static void test() {
+        JdbStopThreadidTestTarg test = new JdbStopThreadidTestTarg();
+        MyThread myThread1 = test.new MyThread("MYTHREAD-1");
+        MyThread myThread2 = test.new MyThread("MYTHREAD-2");
+        MyThread myThread3 = test.new MyThread("MYTHREAD-3");
+
+        synchronized (lockObj) {
+            myThread1.start();
+            myThread2.start();
+            myThread3.start();
+            // Wait for all threads to have started. Note they all block on lockObj after starting.
+            while (!myThread1.started || !myThread2.started || !myThread3.started) {
+                try {
+                    Thread.sleep(50);
+                } catch (InterruptedException e) {
+                }
+            }
+            // Stop here so the test can setup the breakpoint in MYTHREAD-2
+            brkMethod();
+        }
+
+        // Wait for all threads to finish before exiting
+        try {
+            myThread1.join();
+            myThread2.join();
+            myThread3.join();
+        } catch (InterruptedException e) {
+        }
+    }
+
+    static void brkMethod() {
+    }
+
+    public static void print(Object obj) {
+        System.out.println(obj);
+    }
+
+    class MyThread extends Thread {
+        volatile boolean started = false;
+
+        public MyThread(String name) {
+            super(name);
+        }
+
+        public void run() {
+            started = true;
+            synchronized (JdbStopThreadidTestTarg.lockObj) {
+            }
+            brkMethod();
+        }
+
+        void brkMethod() {
+        }
+    }
+}
+
+public class JdbStopThreadidTest extends JdbTest {
+    public static void main(String argv[]) {
+        new JdbStopThreadidTest().run();
+    }
+
+    private JdbStopThreadidTest() {
+        super(DEBUGGEE_CLASS);
+    }
+
+    private static final String DEBUGGEE_CLASS = JdbStopThreadidTestTarg.class.getName();
+    private static final String DEBUGGEE_THREAD_CLASS = JdbStopThreadidTestTarg.class.getName() + "$MyThread";
+    private static Pattern threadidPattern = Pattern.compile("MyThread\\)(\\S+)\\s+MYTHREAD-2");
+
+    @Override
+    protected void runCases() {
+        jdb.command(JdbCommand.stopIn(DEBUGGEE_CLASS, "brkMethod"));
+        jdb.command(JdbCommand.run().waitForPrompt("Breakpoint hit: \"thread=main\"", true));
+        jdb.command(JdbCommand.threads());
+
+        // Find the threadid for MYTHREAD-2 in the "threads" command output
+        String output = jdb.getJdbOutput();
+        Matcher m = threadidPattern.matcher(output);
+        String threadid = null;
+        if (m.find()) {
+            threadid = m.group(1);
+        } else {
+            throw new RuntimeException("FAILED: Did not match threadid pattern.");
+        }
+
+        // Setup a breakpoint in MYTHREAD-2.
+        jdb.command(JdbCommand.stopInThreadid(DEBUGGEE_THREAD_CLASS, "brkMethod", threadid));
+
+        // Continue until MYTHREAD-2 breakpoint is hit. If we hit any other breakpoint before
+        // then (we aren't suppose to), then this test will fail.
+        jdb.command(JdbCommand.cont().waitForPrompt("Breakpoint hit: \"thread=MYTHREAD-2\", \\S+MyThread.brkMethod", true));
+        // Continue until the application exits. Once again, hitting a breakpoint will cause
+        // a failure because we are not suppose to hit one.
+        jdb.command(JdbCommand.cont().waitForPrompt(Jdb.APPLICATION_EXIT, true));
+    }
+}
--- a/test/jdk/com/sun/jdi/lib/jdb/JdbCommand.java	Mon Feb 25 16:05:06 2019 -0800
+++ b/test/jdk/com/sun/jdi/lib/jdb/JdbCommand.java	Mon Feb 25 18:54:40 2019 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2002, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2002, 2019, 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
@@ -162,6 +162,9 @@
     public static JdbCommand stopIn(String targetClass, String methodName) {
         return new JdbCommand("stop in " + targetClass + "." + methodName);
     }
+    public static JdbCommand stopInThreadid(String targetClass, String methodName, String threadid) {
+        return new JdbCommand("stop " + threadid + " in " + targetClass + "." + methodName);
+    }
     public static JdbCommand thread(int threadNumber) {
         return new JdbCommand("thread " + threadNumber);
     }
@@ -226,6 +229,10 @@
         return new JdbCommand("methods " + classId);
     }
 
+    public static JdbCommand threads() {
+        return new JdbCommand("threads");
+    }
+
     // trace [go] methods [thread]
     //                           -- trace method entries and exits.
     //                           -- All threads are suspended unless 'go' is specified