This article is the first in a four-part Java 101 series exploring Java threads. Although you might think threading in Java would be challenging to grasp, I intend to show you that threads are easy to understand. In this article, I introduce you to Java threads and runnables. In subsequent articles, we'll explore synchronization (via locks), synchronization problems (such as deadlock), the wait/notify mechanism, scheduling (with and without priority), thread interruption, timers, volatility, thread groups, and thread local variables.
Note that this article (part of the JavaWorld archives) was updated with new code listings and downloadable source code in May 2013.
Understanding Java threads - read the whole series
What is a thread?
Conceptually, the notion of a thread is not difficult to grasp: it's an independent path of execution through program code. When multiple threads execute, one thread's path through the same code usually differs from the others. For example, suppose one thread executes the byte code equivalent of an if-else statement's if
part, while another thread executes the byte code equivalent of the else
part. How does the JVM keep track of each thread's execution? The JVM gives each thread its own method-call stack. In addition to tracking the current byte code instruction, the method-call stack tracks local variables, parameters the JVM passes to a method, and the method's return value.
When multiple threads execute byte-code instruction sequences in the same program, that action is known as multithreading. Multithreading benefits a program in various ways:
- Multithreaded GUI (graphical user interface)-based programs remain responsive to users while performing other tasks, such as repaginating or printing a document.
- Threaded programs typically finish faster than their nonthreaded counterparts. This is especially true of threads running on a multiprocessor machine, where each thread has its own processor.
Java accomplishes multithreading through its java.lang.Thread
class. Each Thread
object describes a single thread of execution. That execution occurs in Thread
's run()
method. Because the default run()
method does nothing, you must subclass Thread
and override run()
to accomplish useful work. For a taste of threads and multithreading in the context of Thread
, examine Listing 1:
Listing 1. ThreadDemo.java
// ThreadDemo.java
class ThreadDemo
{
public static void main (String [] args)
{
MyThread mt = new MyThread ();
mt.start ();
for (int i = 0; i < 50; i++)
System.out.println ("i = " + i + ", i * i = " + i * i);
}
}
class MyThread extends Thread
{
public void run ()
{
for (int count = 1, row = 1; row < 20; row++, count++)
{
for (int i = 0; i < count; i++)
System.out.print ('*');
System.out.print ('\n');
}
}
}
Listing 1 presents source code to an application consisting of classes ThreadDemo
and MyThread
. Class ThreadDemo
drives the application by creating a MyThread
object, starting a thread that associates with that object, and executing some code to print a table of squares. In contrast, MyThread
overrides Thread
's run()
method to print (on the standard output stream) a right-angle triangle composed of asterisk characters.
Thread scheduling and the JVM
Most (if not all) JVM implementations use the underlying platform's threading capabilities. Because those capabilities are platform-specific, the order of your multithreaded programs' output might differ from the order of someone else's output. That difference results from scheduling, a topic I explore later in this series.
When you type java ThreadDemo
to run the application, the JVM creates a starting thread of execution, which executes the main()
method. By executing mt.start ();
, the starting thread tells the JVM to create a second thread of execution that executes the byte code instructions comprising the MyThread
object's run()
method. When the start()
method returns, the starting thread executes its for
loop to print a table of squares, while the new thread executes the run()
method to print the right-angle triangle.
What does the output look like? Run ThreadDemo
to find out. You will notice each thread's output tends to intersperse with the other's output. That results because both threads send their output to the same standard output stream.
The Thread class
To grow proficient at writing multithreaded code, you must first understand the various methods that make up the Thread
class. This section explores many of those methods. Specifically, you learn about methods for starting threads, naming threads, putting threads to sleep, determining whether a thread is alive, joining one thread to another thread, and enumerating all active threads in the current thread's thread group and subgroups. I also discuss Thread
's debugging aids and user threads versus daemon threads.
I'll present the remainder of Thread
's methods in subsequent articles, with the exception of Sun's deprecated methods.
Deprecated methods
Sun has deprecated a variety of Thread
methods, such as suspend()
and resume()
, because they can lock up your programs or damage objects. As a result, you should not call them in your code. Consult the SDK documentation for workarounds to those methods. I do not cover deprecated methods in this series.
Constructing threads
Thread
has eight constructors. The simplest are:
Thread()
, which creates aThread
object with a default nameThread(String name)
, which creates aThread
object with a name that thename
argument specifies
The next simplest constructors are Thread(Runnable target)
and Thread(Runnable target, String name)
. Apart from the Runnable
parameters, those constructors are identical to the aforementioned constructors. The difference: The Runnable
parameters identify objects outside Thread
that provide the run()
methods. (You learn about Runnable
later in this article.) The final four constructors resemble Thread(String name)
, Thread(Runnable target)
, and Thread(Runnable target, String name)
; however, the final constructors also include a ThreadGroup
argument for organizational purposes.
One of the final four constructors, Thread(ThreadGroup group, Runnable target, String name, long stackSize)
, is interesting in that it lets you specify the desired size of the thread's method-call stack. Being able to specify that size proves helpful in programs with methods that utilize recursion—an execution technique whereby a method repeatedly calls itself—to elegantly solve certain problems. By explicitly setting the stack size, you can sometimes prevent StackOverflowError
s. However, too large a size can result in OutOfMemoryError
s. Also, Sun regards the method-call stack's size as platform-dependent. Depending on the platform, the method-call stack's size might change. Therefore, think carefully about the ramifications to your program before writing code that calls Thread(ThreadGroup group, Runnable target, String name, long stackSize)
.
Start your vehicles
Threads resemble vehicles: they move programs from start to finish. Thread
and Thread
subclass objects are not threads. Instead, they describe a thread's attributes, such as its name, and contain code (via a run()
method) that the thread executes. When the time comes for a new thread to execute run()
, another thread calls the Thread
's or its subclass object's start()
method. For example, to start a second thread, the application's starting thread—which executes main()
—calls start()
. In response, the JVM's thread-handling code works with the platform to ensure the thread properly initializes and calls a Thread
's or its subclass object's run()
method.
Once start()
completes, multiple threads execute. Because we tend to think in a linear fashion, we often find it difficult to understand the concurrent (simultaneous) activity that occurs when two or more threads are running. Therefore, you should examine a chart that shows where a thread is executing (its position) versus time. The figure below presents such a chart.
The chart shows several significant time periods:
- The starting thread's initialization
- The moment that thread begins to execute
main()
- The moment that thread begins to execute
start()
- The moment
start()
creates a new thread and returns tomain()
- The new thread's initialization
- The moment the new thread begins to execute
run()
- The different moments each thread terminates
Note that the new thread's initialization, its execution of run()
, and its termination happen simultaneously with the starting thread's execution. Also note that after a thread calls start()
, subsequent calls to that method before the run()
method exits cause start()
to throw a java.lang.IllegalThreadStateException
object.
What's in a name?
During a debugging session, distinguishing one thread from another in a user-friendly fashion proves helpful. To differentiate among threads, Java associates a name with a thread. That name defaults to Thread
, a hyphen character, and a zero-based integer number. You can accept Java's default thread names or you can choose your own. To accommodate custom names, Thread
provides constructors that take name
arguments and a setName(String name)
method. Thread
also provides a getName()
method that returns the current name. Listing 2 demonstrates how to establish a custom name via the Thread(String name)
constructor and retrieve the current name in the run()
method by calling getName()
:
Listing 2. NameThatThread.java
// NameThatThread.java
class NameThatThread
{
public static void main (String [] args)
{
MyThread mt;
if (args.length == 0)
mt = new MyThread ();
else
mt = new MyThread (args [0]);
mt.start ();
}
}
class MyThread extends Thread
{
MyThread ()
{
// The compiler creates the byte code equivalent of super ();
}
MyThread (String name)
{
super (name); // Pass name to Thread superclass
}
public void run ()
{
System.out.println ("My name is: " + getName ());
}
}
You can pass an optional name argument to MyThread
on the command line. For example, java NameThatThread X
establishes X
as the thread's name. If you fail to specify a name, you'll see the following output:
My name is: Thread-1
If you prefer, you can change the super (name);
call in the MyThread (String name)
constructor to a call to setName (String name)
—as in setName (name);
. That latter method call achieves the same objective—establishing the thread's name—as super (name);
. I leave that as an exercise for you.
Naming main
Java assigns the name main
to the thread that runs the main()
method, the starting thread. You typically see that name in the Exception in thread "main"
message that the JVM's default exception handler prints when the starting thread throws an exception object.
To sleep or not to sleep
Later in this column, I will introduce you to animation— repeatedly drawing on one surface images that slightly differ from each other to achieve a movement illusion. To accomplish animation, a thread must pause during its display of two consecutive images. Calling Thread
's static sleep(long millis)
method forces a thread to pause for millis
milliseconds. Another thread could possibly interrupt the sleeping thread. If that happens, the sleeping thread awakes and throws an InterruptedException
object from the sleep(long millis)
method. As a result, code that calls sleep(long millis)
must appear within a try
block—or the code's method must include InterruptedException
in its throws
clause.
To demonstrate sleep(long millis)
, I've written a CalcPI1
application. That application starts a new thread that uses a mathematic algorithm to calculate the value of the mathematical constant pi. While the new thread calculates, the starting thread pauses for 10 milliseconds by calling sleep(long millis)
. After the starting thread awakes, it prints the pi value, which the new thread stores in variable pi
. Listing 3 presents CalcPI1
's source code:
Listing 3. CalcPI1.java
// CalcPI1.java
class CalcPI1
{
public static void main (String [] args)
{
MyThread mt = new MyThread ();
mt.start ();
try
{
Thread.sleep (10); // Sleep for 10 milliseconds
}
catch (InterruptedException e)
{
}
System.out.println ("pi = " + mt.pi);
}
}
class MyThread extends Thread
{
boolean negative = true;
double pi; // Initializes to 0.0, by default
public void run ()
{
for (int i = 3; i < 100000; i += 2)
{
if (negative)
pi -= (1.0 / i);
else
pi += (1.0 / i);
negative = !negative;
}
pi += 1.0;
pi *= 4.0;
System.out.println ("Finished calculating PI");
}
}
If you run this program, you will see output similar (but probably not identical) to the following:
pi = -0.2146197014017295
Finished calculating PI