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
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 theread()
function return an indication that no data is immediately available.- Signals
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.
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.
Get Java Threads, Second Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.