Summary
It is vital to learn when to use concurrency and when to avoid it. The main reasons to use it are: to manage a number of tasks whose intermingling will make more efficient use of the computer (including the ability to transparently distribute the tasks across multiple CPUs), allow better code organization, or be more convenient for the user. The classic example of resource balancing is to use the CPU during I/O waits. The classic example of user convenience is to monitor a "stop" button during long downloads.
An additional advantage to threads is that they provide "light" execution context switches (on the order of 100 instructions) rather than "heavy" process context switches (thousands of instructions). Since all threads in a given process share the same memory space, a light context switch changes only program execution and local variables. A process change the heavy context switchmust exchange the full memory space.
The main drawbacks to multithreading are:
Slowdown occurs while waiting for shared resources.
Additional CPU overhead is required to manage threads.
Unrewarded complexity arises from poor design decisions.
Opportunities are created for pathologies such as starving, racing, deadlock, and livelock.
Inconsistenciesoccur across platforms. For instance, while developing some of the examples for this book, I discovered race conditions that quickly appeared on some computers but that wouldn't appear on others. If you developed a program on the latter, you might get badly surprised when you distribute it.
One of the biggest difficulties with threads occurs because more than one thread might be sharing a resourcesuch as the memory in an objectand you must make sure that multiple threads don't try to read and change that resource at the same time. This requires judicious use of the synchronized keyword, which is an essential tool, but must be understood thoroughly because it can quietly introduce deadlock situations.
In addition, there's a certain art to the application of threads. Java is designed to allow you to create as many objects as you need to solve your problemat least in theory. (Creating millions of objects for an engineering finite-element analysis, for example, might not be practical in Java.) However, it seems that there is an upper bound to the number of threads you'll want to create, because at some number, threads seem to become balky. This critical point can be hard to detect, and will often depend on the OS and JVM; it could be less than a hundred or in the thousands. As you often create only a handful of threads to solve a problem, this is typically not much of a limit; yet in a more general design it becomes a constraint.
A significant nonintuitive issue in threading is that, because of thread scheduling, you can typically make your applications run faster by inserting calls to yield( ) or even sleep( ) inside run( )'s main loop. This definitely makes it feel like an art, in particular when the longer delays seem to speed up performance. The reason this happens is that shorter delays can cause the end-of-sleep( ) scheduler interrupt to happen before the running thread is ready to go to sleep, forcing the scheduler to stop it and restart it later so it can finish what it was doing and then go to sleep. The extra context switches can end up slowing things down, and the use of yield( ) or sleep( ) may prevent the extra switches. It takes extra thought to realize how messy things can get.
For more advanced discussions of threading, see Concurrent Programming in Java, 2nd Edition, by Doug Lea, Addison-Wesley, 2000.