Java Threads, 2nd EditionBy Scott Oaks & Henry Wong
2nd Edition January 1999
1-56592-418-5, Order Number: 4185
336 pages, $34.95
Introduction to Threading
In this chapter:
This is a book about using threads in the Java programming language and the Java virtual machine. The topic of threads is very important in Java--so important that many features of a threaded system are built into the Java language itself, while other features of a threaded system are required by the Java virtual machine. Threading is an integral part of using Java.
The concept of threads is not a new one: for some time, many operating systems have had libraries that provide the C programmer with a mechanism to create threads. Other languages, such as Ada, have support for threads embedded into the language, much as support for threads is built into the Java language. Nonetheless, the topic of threads is usually considered a peripheral programming topic, one that's only needed in special programming cases.
With Java, things are different: it is impossible to write any but the simplest Java program without introducing the topic of threads. And the popularity of Java ensures that many developers who might never have considered learning about threading possibilities in a language like C or C++ need to become fluent in threaded programming.
We'll start by defining some terms used throughout this book. Many terms surrounding Java are used inconsistently in various sources; we'll endeavor to be consistent in our usage of these terms throughout the book.
- First is the term Java itself. As we know, Java started out as a programming language, and many people today think of Java as being simply a programming language. But Java is much more than just a programming language: it's also an API specification and a virtual machine specification. So when we say Java, we mean the entire Java platform: a programming language, an API, and a virtual machine specification that, taken together, define an entire programming and runtime environment. Often when we say Java, it's clear from context that we're talking specifically about the programming language, or parts of the Java API, or the virtual machine. The point to remember is that the threading features we discuss in this book derive their properties from all the components of the Java platform taken as a whole. While it's possible to take the Java programming language, directly compile it into assembly code, and run it outside of the virtual machine, such an executable may not necessarily behave the same as the programs we describe in this book.
- Virtual machine, interpreters, and browsers
- The Java virtual machine is another term for the Java interpreter, which is the code that ultimately runs Java programs by interpreting the intermediate byte-code format of the Java programming language. The Java interpreter actually comes in three popular forms: the interpreter for developers (called java) that runs programs via the command line or a file manager, the interpreter for end users (called jre) that is a subset of the developer environment and forms the basis of (among other things) the Java plug-in, and the interpreter that is built into many popular web browsers such as Netscape Navigator, Internet Explorer, HotJava?, and the appletviewer that comes with the Java Developer's Kit. All of these forms are simply implementations of the Java virtual machine, and we'll refer to the Java virtual machine when our discussion applies to any of them. When we use the term Java interpreter, we're talking specifically about the command-line, standalone version of the virtual machine (including those virtual machines that perform just-in-time compilation); when we use the term Java-enabled browser (or, more simply, browser), we're talking specifically about the virtual machine built into web browsers.
- For the most part, virtual machines are indistinguishable--at least in theory. In practice, there are a few important differences between implementations of virtual machines, and one of those differences comes in the world of threads. This difference is important in relatively few circumstances, and we'll discuss it in Chapter 6.
- Programs, applications, and applets
- This leads us to the terms that we'll use for things written in the Java language. Generically, we'll call such entities programs. But there are two types of programs a typical Java programmer might write: programs that can be run directly by the Java interpreter and programs designed to be run by a Java-enabled browser. Much of the time, the distinction between these two types of Java programs is not important, and in those cases, we'll refer to them as programs. But in those cases where the distinction is important, we'll use the term applets for programs running in the Java-enabled browser and the term applications for standalone Java programs. In terms of threads, the distinction between an applet and an application manifests itself only in Java's security model; we'll discuss the interaction between the security model and Java threads in Chapter 10.
This leaves us only one more term to define: what exactly is a thread? The term thread is shorthand for thread of control, and a thread of control is, at its simplest, a section of code executed independently of other threads of control within a single program.
Thread of Control
Thread of control sounds like a complicated technical term, but it's really a simple concept: it is the path taken by a program during execution. This determines what code will be executed: does the if block get executed, or does the else block? How many times does the while loop execute? If we were executing tasks from a "to do" list, much as a computer executes an application, what steps we perform and the order in which we perform them is our path of execution, the result of our thread of control.
Having multiple threads of control is like executing tasks from two lists. We are still doing the tasks on each "to do" list in the correct order, but when we get bored with the tasks on one of the lists, we switch lists with the intention of returning at some future time to the first list at the exact point where we left off.
Overview of Multitasking
We're all familiar with the use of multitasking operating systems to run multiple programs simultaneously. Each of these programs has at least one thread within it, so at some level, we're already comfortable with the notion of a thread in a single process. The single-threaded process has the following properties, which, as it turns out, are shared by all threads in a program with multiple threads as well:
- The process begins execution at a well-known point. In programming languages like C and C++ (not to mention Java itself), the thread begins execution at the first statement of the function or method called main().
- Execution of the statements follows in a completely ordered, predefined sequence for a given set of inputs. An individual process is single-minded in this regard: it simply executes the next statement in the program.
- While executing, the process has access to certain data. In Java, there are three types of data a process can access: local variables are accessed from the thread's stack, instance variables are accessed through object references, and static variables are accessed through class or object references.
Now consider what happens when you sit at your computer and start two single-threaded programs: a text editor, say, and a file manager. You now have two processes running on your computer; each process has a single thread with the properties just outlined. Each process does not necessarily know about the other process, although, depending on the operating system running on your computer, there are several ways in which the processes can send each other various messages. A common behavior is that you can drag a file icon from the file manager into the text editor in order to edit the file. Each process thus runs independently of the other, although they can cooperate if they so choose. The typical multitasking environment is shown in Figure 1-1.
Figure 1-1. Processes in a multitasking environment
From the point of view of the person using the computer, these processes often appear to execute simultaneously, although many variables can affect that appearance. These variables depend on the operating system: for example, a given operating system may not support multitasking at all, so that no two programs appear to execute simultaneously. Or the user may have decided that a particular process is more important than other processes and hence should always run, shutting out the other processes from running and again affecting the appearance of simultaneity.
Finally, the data contained within these two processes is, by default, separated: each has its own stack for local variables, and each has its own data area for objects and other data elements. Under many operating systems, the programmer can make arrangements so that the data objects reside in memory that can be shared between the processes, allowing both processes to access them.
Overview of Multithreading
All of this leads us to a common analogy: we can think of a thread just as we think of a process, and we can consider a program with multiple threads running within a single instance of the Java virtual machine just as we consider multiple processes within an operating system, as we show in Figure 1-2.
Figure 1-2. Multitasking versus threading
So it is that within a Java program, multiple threads have these properties:
- Each thread begins execution at a predefined, well-known location. For one of the threads in the program, that location is the main() method; for the rest of the threads, it is a particular location the programmer decides on when the code is written. Note that this is true of an applet as well, in which case the main() method was executed by the browser itself.
- Each thread executes code from its starting location in an ordered, predefined (for a given set of inputs) sequence. Threads are single-minded in their purpose, always simply executing the next statement in the sequence.
- Each thread executes its code independently of the other threads in the program. If the threads choose to cooperate with each other, there are a variety of mechanisms we will explore that allow that cooperation. Exploiting those methods of cooperation is the reason why programming with threads is such a useful technique, but that cooperation is completely optional, much as the user is never required to drag a file from the file manager into the text editor.
- The threads appear to have a certain degree of simultaneous execution. As we'll explore in Chapter 6, the degree of simultaneity depends on several factors--programming decisions about the relative importance of various threads as well as operating system support for various features. The potential for simultaneous execution is the key thing you must keep in mind when threading your code.
- The threads have access to various types of data. At this point, the analogy to multiple processes breaks down somewhat, depending on the type of data the Java program is attempting to access.
Each thread is separate, so that local variables in the methods that the thread is executing are separate for different threads. These local variables are completely private; there is no way for one thread to access the local variables of another thread. If two threads happen to execute the same method, each thread gets a separate copy of the local variables of that method. This is completely analogous to running two copies of the text editor: each process would have separate copies of the local variables.
Objects and their instance variables, on the other hand, can be shared between threads in a Java program, and sharing these objects between threads of a Java program is much easier than sharing data objects between processes in most operating systems. In fact, the ability to share data objects easily between threads is another reason why programming with threads is so useful. But Java threads cannot arbitrarily access each other's data objects: they need permission to access the objects, and one thread needs to pass the object reference to the other thread.
Static variables are the big exception to this analogy: they are automatically shared between all threads in a Java program.
Don't panic over this analogy: the fact that you'll be programming with threads in Java doesn't mean you'll necessarily be doing the system-level type of programming you'd need to perform if you were writing the multitasking operating system responsible for running multiple programs. The Java Thread API is designed to be simple and requires little specialized skill for most common tasks.
The notion of threading is so ingrained in Java that it's almost impossible to write even the simplest programs in Java without creating and using threads. And many of the classes in the Java API are already threaded, so that often you are using multiple threads without realizing it.
Historically, threading was first exploited to make certain programs easier to write: if a program can be split into separate tasks, it's often easier to program the algorithm as separate tasks or threads. Programs that fall into this category are typically specialized and deal with multiple independent tasks. The relative rareness of these types of programs makes threading in this category a specialized skill. Often, these programs were written as separate processes using operating-system-dependent communication tools such as signals and shared memory spaces to communicate between processes. This approach increased system complexity.
The popularity of threading increased when graphical interfaces became the standard for desktop computers because the threading system allowed the user to perceive better program performance. The introduction of threads into these platforms didn't make the programs any faster, but it did create an illusion of faster performance for the user, who now had a dedicated thread to service input or display output.
Recently, there's been a flurry of activity regarding a new use of threaded programs: to exploit the growing number of computers that have multiple processors. Programs that require a lot of CPU processing are natural candidates for this category, since a calculation that requires one hour on a single-processor machine could (at least theoretically) run in half an hour on a two-processor machine, or 15 minutes on a four-processor machine. All that is required is that the program be written to use multiple threads to perform the calculation.
While computers with multiple processors have been around for a long time, we're now seeing these machines become cheap enough to be very widely available. The advent of less expensive machines with multiple processors, and of operating systems that provide programmers with thread libraries to exploit those processors, has made threaded programming a hot topic, as developers move to extract every benefit from these new machines. Until Java, much of the interest in threading centered around using threads to take advantage of multiple processors on a single machine.
However, threading in Java often has nothing at all to do with multiprocessor machines and their capabilities; in fact, the first Java virtual machines were unable to take advantage of multiple processors on a machine, and many implementations of the virtual machine still follow that model. However, there are also implementations of the virtual machine that do take advantage of the multiple processors that the computer may have. A correctly written program running in one of those virtual machines on a computer with two processors may indeed take roughly half the time to execute that it would take on a computer with a single processor. If you're looking to use Java to have your program scale to many processors, that is indeed possible when you use the correct virtual machine. However, even if your Java program is destined to be run on a machine with a single CPU, threading is still very important.
The major reason threading is so important in Java is that Java has no concept of asynchronous behavior. This means that many of the programming techniques you've become accustomed to using in typical programs are not applicable in Java; instead, you must learn a new repertoire of threading techniques to handle these cases of asynchronous behavior.
This is not to say there aren't other times when threads are a handy programming technique in Java; certainly it's easy to use Java for a program that implements an algorithm that naturally lends itself to threading. And many Java programs implement multiple independent behaviors. The next few sections cover some of the circumstances in which Java threads are a required component of the program, due to the need for asynchronous behavior or to the elegance that threading lends to the problem.
In Java, as in most programming languages, when you try to get input from the user, you execute a read() method specifying the user's terminal (System.in in Java). When the program executes the read() method, the program will typically wait until the user types at least one character before it continues and executes the next statement. This type of I/O is called blocking I/O : the program blocks until some data is available to satisfy the read() method.
This type of behavior is often undesirable. If you're reading data from a network socket, that data is often not available when you want to read it: the data may have been delayed in transit over the network, or you may be reading from a network server that sends data only periodically. If the program blocks when it tries to read from the socket, then it's unable to do anything else until the data is actually available. If the program has a user interface that contains a button and the user presses the button while the program is executing the read() method, nothing will happen: the program will be unable to process the mouse events and execute the event-processing method associated with the button. This can be very frustrating for the user, who thinks the program has hung.
Traditionally, there are three techniques to cope with this situation:
- I/O multiplexing
- Developers often take all input sources and use a system call like select() to notify them when data is available from a particular source. This allows input to be handled much like an event from the user (in fact, many graphical toolkits use this method transparently to the user, who simply registers a callback function that is called whenever data is available from a particular source).
- Polling allows a developer to test if data is available from a particular source. If data is available, the data can be read and processed; if it is not, the program can perform another task. Polling can be done either explicitly--with a system call like poll()--or, in some systems, by making the read() function return an indication that no data is immediately available.
- A file descriptor representing an input source can often be set so that an asynchronous signal is delivered to the program when data is available on that input source. This signal interrupts the program, which processes the data and then returns to whatever task it had been doing.
In Java, none of these techniques is directly available. There is limited support for polling via the available() method of the FilterInputStream class, but this method does not have the rich semantics that polling typically has in most operating systems. To compensate for the lack of these features, a Java developer must set up a separate thread to read the data. This separate thread can block when data isn't available, and the other thread(s) in the Java program can process events from the user or perform other tasks.
While this issue of blocking I/O can conceivably occur with any data source, it occurs most frequently with network sockets. If you're used to programming sockets, you've probably used one of these techniques to read from a socket, but perhaps not to write to one. Many developers, used to programming on a local area network, are vaguely aware that writing to a socket may block, but it's a possibility that many of them ignore because it can only happen under certain circumstances, such as a backlog in getting data onto the network. This backlog rarely happens on a fast local area network, but if you're using Java to program sockets over the Internet, the chances of this backlog happening are greatly increased; hence the chance of blocking while attempting to write data onto the network is also increased. So in Java, you may need two threads to handle the socket: one to read from the socket and one to write to it.
Alarms and Timers
Traditional operating systems typically provide some sort of timer or alarm call: the program sets the timer and continues processing. When the timer expires, the program receives some sort of asynchronous signal that notifies the program of the timer's expiration.
In Java, the programmer must set up a separate thread to simulate a timer. This thread can sleep for the duration of a specified time interval and then notify other threads that the timer has expired.
A Java program is often called on to perform independent tasks. In the simplest case, a single applet may perform two independent animations for a web page. A more complex program would be a calculation server that performs calculations on behalf of several clients simultaneously. In either case, while it is possible to write a single-threaded program to perform the multiple tasks, it's easier and more elegant to place each task in its own thread.
The complete answer to the question "Why threads?" really lies in this category. As programmers, we're trained to think linearly and often fail to see simultaneous paths that our program might take. But there's no reason why processes that we've conventionally thought of in a single-threaded fashion need necessarily remain so: when the Save button in a word processor is pressed, we typically have to wait a few seconds until we can continue. Worse yet, the word processor may periodically perform an autosave, which invariably interrupts the flow of typing and disrupts the thought process. In a threaded word processor, the save operation would be in a separate thread so that it didn't interfere with the work flow. As you become accustomed to writing programs with multiple threads, you'll discover many circumstances in which adding a separate thread will make your algorithms more elegant and your programs better to use.
With the advent of virtual machines that can use multiple CPUs simultaneously, Java has become a useful platform for developing programs that use algorithms that can be parallelized. Any program that contains a loop is a candidate for being parallelized; that is, running one iteration of the loop on one CPU while another iteration of the loop is simultaneously running on another CPU. Dependencies between the data that each iteration of the loop needs may prohibit a particular loop from being parallelized, and there may be other reasons why a loop should not be parallelized. But for many programs with CPU-intensive loops, parallelizing the loop will greatly speed up the execution of the program when it is run on a machine with multiple processors.
Many languages have compilers that support automatic parallelization of loops; as yet, Java does not. But as we'll see in Chapter 9, parallelizing a loop by hand is often not a difficult task.
The idea of multiple threads of control within a single program may seem like a new and difficult concept, but it is not. All programs have at least one thread already, and multiple threads in a single program are not radically different from multiple programs within an operating system.
A Java program can contain many threads, all of which may be created without the explicit knowledge of the developer. For now, all you need to consider is that when you write a Java application, there is an initial thread that begins its operation by executing the main() method of your application. When you write a Java applet, there is a thread that is executing the callback methods (init(),
actionPerformed(), etc.) of your applet; we speak of this thread as the applet's thread. In either case, your program starts with what you can consider as a single thread. If you want to perform I/O (particularly if the I/O might block), start a timer, or do any other task in parallel with the initial thread, you must start a new thread to perform that task. In the next chapter, we'll examine how to do just that.
1. Though it's possible to write a single Java program so that it can be run both by the interpreter and by a browser, the distinction still applies at the time the program is actually run.
Back to: Java Threads, 2nd Edition
© 2001, O'Reilly & Associates, Inc.