/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package java.lang; import java.util.Collections; import java.util.IdentityHashMap; import java.util.Map; import org.apache.harmony.lang.RuntimePermissionCollection; import org.jikesrvm.VM; import org.jikesrvm.classloader.RVMClass; import org.jikesrvm.scheduler.RVMThread; /** * This class must be implemented by the VM vendor. The documented methods must * be implemented to support other provided class implementations in this * package. A Thread is a unit of concurrent execution in Java. It has its own * call stack for methods being called and their parameters. Threads in the same * VM interact and synchronize by the use of shared Objects and monitors * associated with these objects. Synchronized methods and part of the API in * Object also allow Threads to cooperate. When a Java program starts executing * there is an implicit Thread (called "main") which is automatically created by * the VM. This Thread belongs to a ThreadGroup (also called "main") which is * automatically created by the bootstrap sequence by the VM as well. * * @see java.lang.Object * @see java.lang.ThreadGroup */ public class Thread implements Runnable { private final RVMThread vmThread; private long stacksize; /** * A representation of a thread's state. A given thread may only be in one * state at a time. * * @since 1.5 */ public enum State { /** * The thread is blocked and waiting for a lock. */ BLOCKED, /** * The thread has been created, but has never been started. */ NEW, /** * The thread may be run. */ RUNNABLE, /** * The thread has been terminated. */ TERMINATED, /** * The thread is waiting for a specified amount of time. */ TIMED_WAITING, /** * The thread is waiting. */ WAITING } /** * <p> * The maximum priority value allowed for a thread. * </p> */ public final static int MAX_PRIORITY = 10; /** * <p> * The minimum priority value allowed for a thread. * </p> */ public final static int MIN_PRIORITY = 1; /** * <p> * The normal (default) priority value assigned to threads. * </p> */ public final static int NORM_PRIORITY = 5; /** * This map is used to provide <code>ThreadLocal</code> functionality. * Maps <code>ThreadLocal</code> object to value. Lazy initialization is * used to avoid circular dependance. */ private Map<ThreadLocal<Object>, Object> localValues = null; /** * Synchronization is done using internal lock. */ private Object lock = new Object(); /** * This thread's context class loader */ private ClassLoader contextClassLoader; /** * Uncaught exception handler for this thread */ private UncaughtExceptionHandler exceptionHandler = null; /** * This thread's thread group */ private ThreadGroup group; /** * System thread group for keeping helper threads. */ private static ThreadGroup systemThreadGroup = null; /** * Main thread group. */ private static ThreadGroup mainThreadGroup = null; Object slot1; Object slot2; Object slot3; private Runnable action; private Runnable runnable; /** * Construct a wrapper for a given RVMThread */ Thread(RVMThread vmt, String name) { this(vmt, null, null, name, 0); } /** * Constructs a new Thread with no runnable object and a newly generated * name. The new Thread will belong to the same ThreadGroup as the Thread * calling this constructor. * * @see java.lang.ThreadGroup */ public Thread() { this(null, null, null, "TODO", 0); } /** * Constructs a new Thread with a runnable object and a newly generated * name. The new Thread will belong to the same ThreadGroup as the Thread * calling this constructor. * * @param runnable a java.lang.Runnable whose method <code>run</code> will * be executed by the new Thread * @see java.lang.ThreadGroup * @see java.lang.Runnable */ public Thread(Runnable runnable) { this(null, runnable, "TODO", 0); } /** * Constructs a new Thread with a runnable object and name provided. The new * Thread will belong to the same ThreadGroup as the Thread calling this * constructor. * * @param runnable a java.lang.Runnable whose method <code>run</code> will * be executed by the new Thread * @param threadName Name for the Thread being created * @see java.lang.ThreadGroup * @see java.lang.Runnable */ public Thread(Runnable runnable, String threadName) { this(null, null, runnable, threadName, 0); } /** * Constructs a new Thread with no runnable object and the name provided. * The new Thread will belong to the same ThreadGroup as the Thread calling * this constructor. * * @param threadName Name for the Thread being created * @see java.lang.ThreadGroup * @see java.lang.Runnable */ public Thread(String threadName) { this(null, null, null, threadName, 0); } /** * Constructs a new Thread with a runnable object and a newly generated * name. The new Thread will belong to the ThreadGroup passed as parameter. * * @param group ThreadGroup to which the new Thread will belong * @param runnable a java.lang.Runnable whose method <code>run</code> will * be executed by the new Thread * @throws SecurityException if <code>group.checkAccess()</code> fails * with a SecurityException * @throws IllegalThreadStateException if <code>group.destroy()</code> has * already been done * @see java.lang.ThreadGroup * @see java.lang.Runnable * @see java.lang.SecurityException * @see java.lang.SecurityManager */ public Thread(ThreadGroup group, Runnable runnable) { this(null, group, runnable, "TODO", 0); } /** * Constructs a new Thread with a runnable object, the given name and * belonging to the ThreadGroup passed as parameter. * * @param group ThreadGroup to which the new Thread will belong * @param runnable a java.lang.Runnable whose method <code>run</code> will * be executed by the new Thread * @param threadName Name for the Thread being created * @param stack Platform dependent stack size * @throws SecurityException if <code>group.checkAccess()</code> fails * with a SecurityException * @throws IllegalThreadStateException if <code>group.destroy()</code> has * already been done * @see java.lang.ThreadGroup * @see java.lang.Runnable * @see java.lang.SecurityException * @see java.lang.SecurityManager */ public Thread(ThreadGroup group, Runnable runnable, String threadName, long stack) { this(null, group, runnable, threadName, stack); } private Thread(RVMThread vmt, ThreadGroup group, Runnable runnable, String threadName, long stack){ if (group == null) { if (systemThreadGroup == null) { // This is main thread. systemThreadGroup = new ThreadGroup(); mainThreadGroup = new ThreadGroup(systemThreadGroup, "main"); group = mainThreadGroup; } else { group = mainThreadGroup; } } if (vmt == null) { vmThread = new org.jikesrvm.scheduler.RVMThread(this, stacksize, threadName, false, NORM_PRIORITY); } else { vmThread = vmt; } stacksize = stack; this.runnable = runnable; this.group = group; } /** * Constructs a new Thread with a runnable object, the given name and * belonging to the ThreadGroup passed as parameter. * * @param group ThreadGroup to which the new Thread will belong * @param runnable a java.lang.Runnable whose method <code>run</code> will * be executed by the new Thread * @param threadName Name for the Thread being created * @throws SecurityException if <code>group.checkAccess()</code> fails * with a SecurityException * @throws IllegalThreadStateException if <code>group.destroy()</code> has * already been done * @see java.lang.ThreadGroup * @see java.lang.Runnable * @see java.lang.SecurityException * @see java.lang.SecurityManager */ public Thread(ThreadGroup group, Runnable runnable, String threadName) { this(null, group, runnable, threadName, 0); } /** * Constructs a new Thread with no runnable object, the given name and * belonging to the ThreadGroup passed as parameter. * * @param group ThreadGroup to which the new Thread will belong * @param threadName Name for the Thread being created * @throws SecurityException if <code>group.checkAccess()</code> fails * with a SecurityException * @throws IllegalThreadStateException if <code>group.destroy()</code> has * already been done * @see java.lang.ThreadGroup * @see java.lang.SecurityException * @see java.lang.SecurityManager */ public Thread(ThreadGroup group, String threadName) { this(null, group, null, threadName, 0); } /** * Set the action to be executed when interruption, which is probably be * used to implement the interruptible channel. The action is null by * default. And if this method is invoked by passing in a non-null value, * this action's run() method will be invoked in <code>interrupt()</code>. * * @param action the action to be executed when interruption */ @SuppressWarnings("unused") private void setInterruptAction(Runnable action) { this.action = action; } /** * Returns the number of active threads in the running thread's ThreadGroup * * @return Number of Threads */ public static int activeCount() { RVMThread.dumpStack(); throw new Error("TODO"); } /** * This method is used for operations that require approval from a * SecurityManager. If there's none installed, this method is a no-op. If * there's a SecurityManager installed , * {@link SecurityManager#checkAccess(Thread)} is called for that * SecurityManager. * * @see java.lang.SecurityException * @see java.lang.SecurityManager */ public final void checkAccess() { RVMThread.dumpStack(); throw new Error("TODO"); } /** * Returns the number of stack frames in this thread. * * @return Number of stack frames * @deprecated The results of this call were never well defined. To make * things worse, it would depend if the Thread was suspended or * not, and suspend was deprecated too. */ @Deprecated public int countStackFrames() { return vmThread.countStackFrames(); } /** * Answers the instance of Thread that corresponds to the running Thread * which calls this method. * * @return a java.lang.Thread corresponding to the code that called * <code>currentThread()</code> */ public static Thread currentThread() { return RVMThread.getCurrentThread().getJavaLangThread(); } /** * Destroys the receiver without any monitor cleanup. Not implemented. * * @deprecated Not implemented. */ @Deprecated public void destroy() { RVMThread.dumpStack(); throw new Error("TODO"); } /** * Prints a text representation of the stack for this Thread. * */ public static void dumpStack() { RVMThread.dumpStack(); throw new Error("TODO"); } /** * Copies an array with all Threads which are in the same ThreadGroup as the * receiver - and subgroups - into the array <code>threads</code> passed * as parameter. If the array passed as parameter is too small no exception * is thrown - the extra elements are simply not copied. * * @param threads array into which the Threads will be copied * @return How many Threads were copied over * @throws SecurityException if the installed SecurityManager fails * {@link SecurityManager#checkAccess(Thread)} * @see java.lang.SecurityException * @see java.lang.SecurityManager */ public static int enumerate(Thread[] threads) { RVMThread.dumpStack(); throw new Error("TODO"); } /** * <p> * Returns the stack traces of all the currently live threads. * </p> * <p> * The <code>RuntimePermission("getStackTrace")</code> and * <code>RuntimePermission("modifyThreadGroup")</code> are checked before * returning a result. * </p> * * @return A Map of current Threads to StackTraceElement arrays. * @throws SecurityException if the current SecurityManager fails the * {@link SecurityManager#checkPermission(java.security.Permission)} * call. * @since 1.5 */ public static Map<Thread, StackTraceElement[]> getAllStackTraces() { RVMThread.dumpStack(); throw new Error("TODO"); } /** * Returns the context ClassLoader for the receiver. * * @return ClassLoader The context ClassLoader * @see java.lang.ClassLoader * @see #getContextClassLoader() */ public ClassLoader getContextClassLoader() { synchronized (lock) { // First, if the conditions // 1) there is a security manager // 2) the caller's class loader is not null // 3) the caller's class loader is not the same as or an // ancestor of contextClassLoader // are satisfied we should perform a security check. SecurityManager securityManager = System.getSecurityManager(); if (securityManager != null) { //the first condition is satisfied ClassLoader callerClassLoader = RVMClass.getClassLoaderFromStackFrame(1); if (callerClassLoader != null) { //the second condition is satisfied ClassLoader classLoader = contextClassLoader; while (classLoader != null) { if (classLoader == callerClassLoader) { //the third condition is not satisfied return contextClassLoader; } classLoader = classLoader.getParent(); } //the third condition is satisfied securityManager.checkPermission(RuntimePermissionCollection.GET_CLASS_LOADER_PERMISSION); } } return contextClassLoader; } } /** * <p> * Returns the default exception handler that's executed when uncaught * exception terminates a thread. * </p> * * @return An {@link UncaughtExceptionHandler} or <code>null</code> if * none exists. * @since 1.5 */ public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() { RVMThread.dumpStack(); throw new Error("TODO"); } /** * <p> * Returns the thread's identifier. The ID is a positive <code>long</code> * generated on thread creation, is unique to the thread and doesn't change * during the life of the thread; the ID may be reused after the thread has * been terminated. * </p> * * @return The thread's ID. * @since 1.5 */ public long getId() { RVMThread.dumpStack(); throw new Error("TODO"); } /** * Answers the name of the receiver. * * @return the receiver's name (a java.lang.String) */ public final String getName() { return vmThread.getName(); } /** * Answers the priority of the receiver. * * @return the receiver's priority (an <code>int</code>) * @see Thread#setPriority */ public final int getPriority() { return vmThread.getPriority(); } /** * <p> * Returns the current stack trace of the thread. * </p> * <p> * The <code>RuntimePermission("getStackTrace")</code> is checked before * returning a result. * </p> * * @return An array of StackTraceElements. * @throws SecurityException if the current SecurityManager fails the * {@link SecurityManager#checkPermission(java.security.Permission)} * call. * @since 1.5 */ public StackTraceElement[] getStackTrace() { RVMThread.dumpStack(); throw new Error("TODO"); } /** * <p> * Returns the current state of the thread for monitoring purposes. * </p> * * @return A State value. * @since 1.5 */ public State getState() { return vmThread.getState(); } /** * Answers the ThreadGroup to which the receiver belongs * * @return the receiver's ThreadGroup */ public final ThreadGroup getThreadGroup() { return group; } /** * A sample implementation of this method is provided by the reference * implementation. It must be included, as it is called by ThreadLocal.get() * and InheritableThreadLocal.get(). Return the value associated with the * ThreadLocal in the receiver * * @param local ThreadLocal to perform the lookup * @return the value of the ThreadLocal * @see #setThreadLocal */ Object getThreadLocal(ThreadLocal<Object> local) { Object value; if (localValues == null) { localValues = new IdentityHashMap<ThreadLocal<Object>, Object>(); value = local.initialValue(); localValues.put(local, value); return value; } value = localValues.get(local); if (value != null) { return value; } else { if (localValues.containsKey(local)) { return null; } else { value = local.initialValue(); localValues.put(local, value); return value; } } } /** * A sample implementation of this method is provided by the reference * implementation. It must be included, as it is called by ThreadLocal.set() * and InheritableThreadLocal.set(). Set the value associated with the * ThreadLocal in the receiver to be <code>value</code>. * * @param local ThreadLocal to set * @param value new value for the ThreadLocal * @see #getThreadLocal */ void setThreadLocal(ThreadLocal<Object> local, Object value) { if (localValues == null) { localValues = new IdentityHashMap<ThreadLocal<Object>, Object>(); } localValues.put(local, value); } /** * <p> * Returns the thread's uncaught exception handler. If not explicitly set, * then the ThreadGroup's handler is returned. If the thread is terminated, * then <code>null</code> is returned. * </p> * * @return An UncaughtExceptionHandler instance or <code>null</code>. * @since 1.5 */ public UncaughtExceptionHandler getUncaughtExceptionHandler() { if (exceptionHandler != null) { return exceptionHandler; } return getThreadGroup(); } /** * Posts an interrupt request to the receiver. * * @throws SecurityException if <code>group.checkAccess()</code> fails * with a SecurityException * @see java.lang.SecurityException * @see java.lang.SecurityManager * @see Thread#interrupted * @see Thread#isInterrupted */ public void interrupt() { if (action != null) { action.run(); } vmThread.interrupt(); } /** * Answers a <code>boolean</code> indicating whether the current Thread ( * <code>currentThread()</code>) has a pending interrupt request ( * <code>true</code>) or not (<code>false</code>). It also has the * side-effect of clearing the flag. * * @return a <code>boolean</code> * @see Thread#currentThread * @see Thread#interrupt * @see Thread#isInterrupted */ public static boolean interrupted() { RVMThread current = RVMThread.getCurrentThread(); if (current.isInterrupted()) { current.clearInterrupted(); return true; } return false; } /** * Answers <code>true</code> if the receiver has already been started and * still runs code (hasn't died yet). Answers <code>false</code> either if * the receiver hasn't been started yet or if it has already started and run * to completion and died. * * @return a <code>boolean</code> * @see Thread#start */ public final boolean isAlive() { return vmThread.isAlive(); } /** * Answers a <code>boolean</code> indicating whether the receiver is a * daemon Thread (<code>true</code>) or not (<code>false</code>) A * daemon Thread only runs as long as there are non-daemon Threads running. * When the last non-daemon Thread ends, the whole program ends no matter if * it had daemon Threads still running or not. * * @return a <code>boolean</code> * @see Thread#setDaemon */ public final boolean isDaemon() { return vmThread.isDaemonThread(); } /** * Answers a <code>boolean</code> indicating whether the receiver has a * pending interrupt request (<code>true</code>) or not ( * <code>false</code>) * * @return a <code>boolean</code> * @see Thread#interrupt * @see Thread#interrupted */ public boolean isInterrupted() { return vmThread.isInterrupted(); } /** * Blocks the current Thread (<code>Thread.currentThread()</code>) until * the receiver finishes its execution and dies. * * @throws InterruptedException if <code>interrupt()</code> was called for * the receiver while it was in the <code>join()</code> call * @see Object#notifyAll * @see java.lang.ThreadDeath */ public final void join() throws InterruptedException { vmThread.join(0,0); } /** * Blocks the current Thread (<code>Thread.currentThread()</code>) until * the receiver finishes its execution and dies or the specified timeout * expires, whatever happens first. * * @param timeoutInMilliseconds The maximum time to wait (in milliseconds). * @throws InterruptedException if <code>interrupt()</code> was called for * the receiver while it was in the <code>join()</code> call * @see Object#notifyAll * @see java.lang.ThreadDeath */ public final void join(long timeoutInMilliseconds) throws InterruptedException { vmThread.join(timeoutInMilliseconds,0); } /** * Blocks the current Thread (<code>Thread.currentThread()</code>) until * the receiver finishes its execution and dies or the specified timeout * expires, whatever happens first. * * @param timeoutInMilliseconds The maximum time to wait (in milliseconds). * @param nanos Extra nanosecond precision * @throws InterruptedException if <code>interrupt()</code> was called for * the receiver while it was in the <code>join()</code> call * @see Object#notifyAll * @see java.lang.ThreadDeath */ public final void join(long timeoutInMilliseconds, int nanos) throws InterruptedException { vmThread.join(timeoutInMilliseconds,nanos); } /** * This is a no-op if the receiver was never suspended, or suspended and * already resumed. If the receiver is suspended, however, makes it resume * to the point where it was when it was suspended. * * @throws SecurityException if <code>checkAccess()</code> fails with a * SecurityException * @see Thread#suspend() * @deprecated Used with deprecated method Thread.suspend(). */ @Deprecated public final synchronized void resume() { vmThread.resume(); } /** * Calls the <code>run()</code> method of the Runnable object the receiver * holds. If no Runnable is set, does nothing. * * @see Thread#start */ public void run() { if (runnable != null) { runnable.run(); } } /** * Set the context ClassLoader for the receiver. * * @param cl The context ClassLoader * @see java.lang.ClassLoader * @see #getContextClassLoader() */ public void setContextClassLoader(ClassLoader cl) { synchronized (lock) { SecurityManager securityManager = System.getSecurityManager(); if (securityManager != null) { securityManager.checkPermission(RuntimePermissionCollection.SET_CONTEXT_CLASS_LOADER_PERMISSION); } contextClassLoader = cl; } } /** * Set if the receiver is a daemon Thread or not. This can only be done * before the Thread starts running. * * @param isDaemon A boolean indicating if the Thread should be daemon or * not * @throws SecurityException if <code>checkAccess()</code> fails with a * SecurityException * @see Thread#isDaemon */ public final void setDaemon(boolean isDaemon) { vmThread.makeDaemon(isDaemon); } /** * <p> * Sets the default uncaught exception handler. * </p> * <p> * The <code>RuntimePermission("setDefaultUncaughtExceptionHandler")</code> * is checked prior to setting the handler. * </p> * * @param handler The handler to set or <code>null</code>. * @throws SecurityException if the current SecurityManager fails the * checkPermission call. * @since 1.5 */ public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler handler) { RVMThread.dumpStack(); throw new Error("TODO"); } /** * Sets the name of the receiver. * * @param threadName new name for the Thread * @throws SecurityException if <code>checkAccess()</code> fails with a * SecurityException * @see Thread#getName */ public final void setName(String threadName) { vmThread.setName(threadName); } /** * Sets the priority of the receiver. Note that the final priority set may * not be the parameter that was passed - it will depend on the receiver's * ThreadGroup. The priority cannot be set to be higher than the receiver's * ThreadGroup's maxPriority(). * * @param priority new priority for the Thread * @throws SecurityException if <code>checkAccess()</code> fails with a * SecurityException * @throws IllegalArgumentException if the new priority is greater than * Thread.MAX_PRIORITY or less than Thread.MIN_PRIORITY * @see Thread#getPriority */ public final void setPriority(int priority) { vmThread.setPriority(priority); } /** * <p> * Sets the default uncaught exception handler. * </p> * * @param handler The handler to set or <code>null</code>. * @throws SecurityException if the current SecurityManager fails the * checkAccess call. * @since 1.5 */ public void setUncaughtExceptionHandler(UncaughtExceptionHandler handler) { RVMThread.dumpStack(); throw new Error("TODO"); } /** * Causes the thread which sent this message to sleep an interval of time * (given in milliseconds). The precision is not guaranteed - the Thread may * sleep more or less than requested. * * @param time The time to sleep in milliseconds. * @throws InterruptedException if <code>interrupt()</code> was called for * this Thread while it was sleeping * @see Thread#interrupt() */ public static void sleep(long time) throws InterruptedException { RVMThread.sleep(time, 0); } /** * Causes the thread which sent this message to sleep an interval of time * (given in milliseconds). The precision is not guaranteed - the Thread may * sleep more or less than requested. * * @param time The time to sleep in milliseconds. * @param nanos Extra nanosecond precision * @throws InterruptedException if <code>interrupt()</code> was called for * this Thread while it was sleeping * @see Thread#interrupt() */ public static void sleep(long time, int nanos) throws InterruptedException { RVMThread.sleep(time, nanos); } /** * Starts the new Thread of execution. The <code>run()</code> method of * the receiver will be called by the receiver Thread itself (and not the * Thread calling <code>start()</code>). * * @throws IllegalThreadStateException Unspecified in the Java language * specification * @see Thread#run */ public void start() { vmThread.start(); } /** * Requests the receiver Thread to stop and throw ThreadDeath. The Thread is * resumed if it was suspended and awakened if it was sleeping, so that it * can proceed to throw ThreadDeath. * * @throws SecurityException if <code>checkAccess()</code> fails with a * SecurityException * @deprecated */ @Deprecated public final void stop() { vmThread.stop(new ThreadDeath()); } /** * Requests the receiver Thread to stop and throw the * <code>throwable()</code>. The Thread is resumed if it was suspended * and awakened if it was sleeping, so that it can proceed to throw the * <code>throwable()</code>. * * @param throwable Throwable object to be thrown by the Thread * @throws SecurityException if <code>checkAccess()</code> fails with a * SecurityException * @throws NullPointerException if <code>throwable()</code> is * <code>null</code> * @deprecated */ @Deprecated public final void stop(Throwable throwable) { vmThread.stop(throwable); } /** * This is a no-op if the receiver is suspended. If the receiver * <code>isAlive()</code> however, suspended it until * <code>resume()</code> is sent to it. Suspend requests are not queued, * which means that N requests are equivalent to just one - only one resume * request is needed in this case. * * @throws SecurityException if <code>checkAccess()</code> fails with a * SecurityException * @see Thread#resume() * @deprecated May cause deadlocks. */ @Deprecated public final synchronized void suspend() { vmThread.suspend(); } /** * Answers a string containing a concise, human-readable description of the * receiver. * * @return a printable representation for the receiver. */ @Override public String toString() { return vmThread.toString(); } /** * Causes the thread which sent this message to yield execution to another * Thread that is ready to run. The actual scheduling is * implementation-dependent. * */ public static void yield() { RVMThread.yield(); } /** * Returns whether the current thread has a monitor lock on the specified * object. * * @param object the object to test for the monitor lock * @return true when the current thread has a monitor lock on the specified * object */ public static boolean holdsLock(Object object) { return RVMThread.getCurrentThread().holdsLock(object); } /** * Implemented by objects that want to handle cases where a thread is being * terminated by an uncaught exception. Upon such termination, the handler * is notified of the terminating thread and causal exception. If there is * no explicit handler set then the thread's group is the default handler. */ public static interface UncaughtExceptionHandler { /** * The thread is being terminated by an uncaught exception. Further * exceptions thrown in this method are prevent the remainder of the * method from executing, but are otherwise ignored. * * @param thread the thread that has an uncaught exception * @param ex the exception that was thrown */ void uncaughtException(Thread thread, Throwable ex); } }