private String generateThreadStack(ThreadInfo info) {
    // This is annoying, but the to string method on info sucks.
    StringBuilder result = new StringBuilder();
    result.append(info.getThreadName()).append(" ID=0x")
        .append(Long.toHexString(info.getThreadId())).append("(").append(info.getThreadId())
        .append(") state=").append(info.getThreadState());


    if (info.getLockInfo() != null) {
      result.append("\n\twaiting to lock <" + info.getLockInfo() + ">");
    }
    for (StackTraceElement element : info.getStackTrace()) {
      result.append("\n\tat " + element);
      for (MonitorInfo monitor : info.getLockedMonitors()) {
        if (element.equals(monitor.getLockedStackFrame())) {
          result.append("\n\tlocked <" + monitor + ">");
        }
      }
    }

    if (info.getLockedSynchronizers().length > 0) {
      result.append("\nLocked synchronizers:");
      for (LockInfo sync : info.getLockedSynchronizers()) {
        result.append(
            "\n" + sync.getClassName() + "@" + Integer.toHexString(sync.getIdentityHashCode()));

      }
    }

    return result.toString();
  }
This class is serializable version of the java 1.6 ThreadInfo class. It also holds a locality
field to identify the VM where the thread exists.
geode-core/src/main/java/org/apache/geode/distributed/internal/deadlock/LocalThread.java