This four-part tutorial introduces multithreading in Java 5.0. You'll learn about the new thread-state model and the priority-based thread scheduler. We'll demonstrate how to create and execute threads by using the new classes Executors and ExecutorService from the java.util.concurrent package. You'll also learn how to create thread pools, which enable threads to be reused after they complete their tasks. This tutorial is intended for students who are already familiar with Java and for Java developers.
[Note: This series of four tutorials (Part 1, Part 2, Part 3, Part 4) is an excerpt (Sections 23.1-23.4) of Chapter 23, Multithreading, from our textbook Java How to Program, 6/e. These tutorials may refer to other chapters or sections of the book that are not included here. Permission Information: Deitel, Harvey M. and Paul J., JAVA HOW TO PROGRAM, ©2005, pp.1053-1058. Electronically reproduced by permission of Pearson Education, Inc., Upper Saddle River, New Jersey.]
It would be nice if we could perform one action at a time and perform it well, but that is usually difficult to do. The human body performs a great variety of operations in parallel—or, as we will say throughout this chapter, concurrently. Respiration, blood circulation, digestion, thinking and walking, for example, can occur concurrently. All the senses—sight, touch, smell, taste and hearing—can be employed at once. Computers, too, can perform operations concurrently. It is common for personal computers to compile a program, send a file to a printer and receive electronic mail messages over a network concurrently. Only computers that have multiple processors can truly execute operations concurrently. Operating systems on single-processor computers use various techniques to simulate concurrency, but on such computers only a single operation can execute at once.
Most programming languages do not enable programmers to specify concurrent activities. Rather, the languages generally provide control statements that only enable programmers to perform one action at a time, proceeding to the next action after the previous one has finished. Historically, concurrency has been implemented with operating system primitives available only to experienced systems programmers.
The Ada programming language, developed by the United States Department of Defense, made concurrency primitives widely available to defense contractors building military command-and-control systems. However, Ada has not been widely used in academia and commercial industry.
Java makes concurrency available to the applications programmer through its APIs. The programmer specifies that applications contain threads of execution, where each thread designates a portion of a program that may execute concurrently with other threads. This capability, called multithreading, gives the Java programmer powerful capabilities not available in the core C and C++ languages on which Java is based.
|Performance Tip 23.1|
|A problem with single-threaded applications is that lengthy activities must complete before other activities can begin. In a multithreaded application, threads can be distributed across multiple processors (if they are available) so that multiple tasks are performed concurrently and the application can operate more efficiently. Multithreading can also increase performance on single-processor systems that simulate concurrency—when one thread cannot proceed, another can use the processor.|
|Portability Tip 23.1|
|Unlike languages that do not have built-in multithreading capabilities (such as C and C++) and must therefore make nonportable calls to operating system multithreading primitives, Java includes multithreading primitives as part of the language itself and as part of its libraries. This facilitates manipulating threads in a portable manner across platforms.|
Another example of multithreading is Java’s garbage collection. C and C++ require the programmer to reclaim dynamically allocated memory explicitly. Java provides a garbage-collector thread that reclaims memory which is no longer needed.
Writing multithreaded programs can be tricky. Although the human mind can perform functions concurrently, people find it difficult to jump between parallel trains of thought. To see why multithreading can be difficult to program and understand, try the following experiment: Open three books to page 1, and try reading the books concurrently. Read a few words from the first book, then read a few words from the second book, then read a few words from the third book, then loop back and read the next few words from the first book, and so on. After this experiment, you will appreciate the challenges of multithreading—switching between books, reading briefly, remembering your place in each book, moving the book you are reading closer so you can see it and pushing books you are not reading aside—and, amid all this chaos, trying to comprehend the content of the books!
Programming concurrent applications is a difficult and error-prone undertaking. Even some of the simplest concurrent applications are beyond the capability of beginner programmers. If you find that you must use synchronization in a program, you should follow some simple guidelines. First, use existing classes from the Java API (such as the ArrayBlockingQueue class discussed in Section 23.9, Producer/Consumer Relationship: ArrayBlockingQueue) that manage synchronization for you. The classes in the Java API have been fully tested and debugged and help you avoid common traps and pitfalls. Second, if you find that you need more custom functionality than that provided in the Java APIs, you should use the synchronized keyword and Object methods wait, notify and notifyAll (discussed in Section 23.12, Monitors and Monitor Locks). Finally, if you need even more complex capabilities, then you should use the Lock and Condition interfaces that are introduced in Section 23.5, Thread Synchronization).
The Lock and Condition interfaces are advanced tools and should be used only by advanced programmers who are familiar with the common traps and pitfalls of concurrent programming with synchronization. We explain these topics in this chapter for a number of reasons. For one, they provide a solid basis for understanding how concurrent applications synchronize access to shared memory. Even, if an application does not use these tools explicitly, the concepts are still important to understand. We have also discussed these topics to highlight the new concurrency features introduced by J2SE 5.0. Finally, by showing you the complexity involved in using these low-level features, we hope to impress upon you the importance of using prepackaged concurrent capabilities whenever possible.