Exercise 4-1. Write a
Java program that takes a list of filenames on the command line and
prints out the number of lines in each file. The program should
create one thread for each file and use these threads to count the
lines in all the files at the same time. Use
java.io.LineNumberReader to help you count lines.
You'll probably want to define a
LineCounter class that extends
Thread or implements Runnable.
Now write a variant of your program that uses your
LineCounter class to read the files sequentially,
rather than at the same time. Compare the performance of the
multithreaded and single-threaded programs, using
System.currentTimeMills( ) to determine elapsed
time. Compare the performance of the two programs for two, five, and
ten files.
Exercise 4-2. The ThreadSafeIntList class of Example 4-2 is a simplified version of Example 2-7 with synchronized methods.
Another approach to synchronizing data structures is to write a
wrapper class with synchronized methods that delegate to an instance
of the original class. See, for example, the
synchronizedList( ) and related methods of
java.util.Collections. Modify the original
IntList class (Example 2-7) to
use this approach. Add a static factory method named
synchronizedIntList( ) that takes an
IntList as its argument and returns an
IntList object that has thread-safe behavior.
You'll need to implement the wrapper class as a
subclass of IntList, overriding its public methods
to make them synchronized and make them delegate to the specified
IntList instance.
Exercise 4-3. Example 4-4 demonstrates how deadlock can occur when
two threads each attempt to obtain a lock held by the other. Modify
the example to create deadlock among three threads, where each thread
is trying to acquire a lock held by one of the other threads.
Exercise 4-4. Example 4-4 uses the synchronized
statement to demonstrate deadlock. Write a similar program that
causes two threads to deadlock, but use
synchronized methods instead of the
synchronized statement. This sort of deadlock is a
little more subtle and harder to detect.
Exercise 4-5. Example 4-6 shows an implementation of the Java 1.3
java.util.Timer API. Java 1.2 introduced another
Timer class, the
javax.swing.Timer class, which has a similar
purpose but a different API. It invokes the actionPerformed(
) method of any number of registered
ActionListener objects one or more times after a
specified delay and at a specified interval. Read the documentation
for this Timer class, then create your own
implementation of it. If you've read Chapter 11, you know that the methods of event listeners,
such as the actionPerformed( ) method, are
supposed to be invoked only by the event dispatch thread. Therefore,
your implementation of the Timer class should not
invoke actionPerformed( ) directly, but should
instead use java.awt.EventQueue.invokeLater( ) or
javax.swing.SwingUtilities.invokeLater( ) to tell
the event dispatch thread to invoke the method.
Exercise 4-6. Once you have read Chapter 5, you may want to come
back to this chapter to try this exercise. The
Server class of Chapter 5 is a
multithreaded, multiservice network server. It demonstrates important
networking techniques, but it also makes heavy use of threads. One
particular feature of this program is that it creates a
ThreadGroup to contain all the threads it creates.
Modify Server so that in addition to creating this
one master thread group, it also creates a nested thread group for
each of the individual services it provides. Place the thread for
each individual client connection within the thread group for its
service. Also, modify the program so that each service can have a
thread priority specified and use this priority value when creating
connection threads. You will probably want to store the thread group
and priority for each service as fields of the nested
Listener class.