|
|
< Day Day Up > |
|
2.2 Creating a ThreadThreads can be created in two ways: using the Thread class and using the Runnable interface. The Runnable interface (generally) requires an instance of a thread, so we begin with the Thread class. In this section, we start developing a typing game. The idea of this game is that characters are displayed and the user must type the key corresponding to the character. Through the next few chapters, we add enough logic to score the user's accuracy and timing and provide enough feedback so that the user can improve her typing skills. For now, we are content to display a random character and display the character the user types in response. This application has two tasks: one task must continually display a random character and then pause for some random period of time. The second task must display characters typed on the keyboard. 2.2.1 The Example ArchitectureBefore we delve into the threading aspects of our code, let's look at a few utility classes used in this and subsequent examples. The typing game has two sources for characters: characters that the user types at the keyboard and characters that are randomly generated. Both sources of characters are represented by this interface: package javathreads.examples.ch02;
public interface CharacterSource {
public void addCharacterListener(CharacterListener cl);
public void removeCharacterListener(CharacterListener cl);
public void nextCharacter( );
}We want to use the standard Java pattern of event listeners to handle these characters: a listener can register with a particular source and be notified when a new character is available. That requires the typical set of Java classes for a listener pattern, starting with the listener interface: package javathreads.examples.ch02;
public interface CharacterListener {
public void newCharacter(CharacterEvent ce);
}The events themselves are objects of this class: package javathreads.examples.ch02;
public class CharacterEvent {
public CharacterSource source;
public int character;
public CharacterEvent(CharacterSource cs, int c) {
source = cs;
character = c;
}
}And finally, we need a helper class that fires the events when appropriate: package javathreads.examples.ch02;
import java.util.*;
public class CharacterEventHandler {
private Vector listeners = new Vector( );
public void addCharacterListener(CharacterListener cl) {
listeners.add(cl);
}
public void removeCharacterListener(CharacterListener cl) {
listeners.remove(cl);
}
public void fireNewCharacter(CharacterSource source, int c) {
CharacterEvent ce = new CharacterEvent(source, c);
CharacterListener[] cl = (CharacterListener[] )
listeners.toArray(new CharacterListener[0]);
for (int i = 0; i < cl.length; i++)
cl[i].newCharacter(ce);
}
}In our graphical display, one canvas registers to be notified when the user types a character; that canvas displays the character. A second canvas registers to be notified when a random character is generated; it displays the new characters as they are generated. We've chosen this design pattern since, in later examples, multiple objects will be interested in knowing when new characters are generated. Here's a utility class that can display a given character: package javathreads.examples.ch02;
import java.awt.*;
import javax.swing.*;
public class CharacterDisplayCanvas extends JComponent implements CharacterListener {
protected FontMetrics fm;
protected char[] tmpChar = new char[1];
protected int fontHeight;
public CharacterDisplayCanvas( ) {
setFont(new Font("Monospaced", Font.BOLD, 18));
fm = Toolkit.getDefaultToolkit( ).getFontMetrics(getFont( ));
fontHeight = fm.getHeight( );
}
public CharacterDisplayCanvas(CharacterSource cs) {
this( );
setCharacterSource(cs);
}
public void setCharacterSource(CharacterSource cs) {
cs.addCharacterListener(this);
}
public Dimension preferredSize( ) {
return new Dimension(fm.getMaxAscent( ) + 10,
fm.getMaxAdvance( ) + 10);
}
public synchronized void newCharacter(CharacterEvent ce) {
tmpChar[0] = (char) ce.character;
repaint( );
}
protected synchronized void paintComponent(Graphics gc) {
Dimension d = getSize( );
gc.clearRect(0, 0, d.width, d.height);
if (tmpChar[0] == 0)
return;
int charWidth = fm.charWidth((int) tmpChar[0]);
gc.drawChars(tmpChar, 0, 1,
(d.width - charWidth) / 2, fontHeight);
}
}Although this class has no references to threads, it still has thread-related issues: namely, we had to use the synchronized keyword for some of the methods. This is because of something known as a race condition (see Chapter 3). 2.2.2 The Thread ClassNow we can program our first task (and our first thread): a thread that periodically generates a random character. In Java, threads are represented by instances of the java.lang.Thread class. They are created just like any other Java object, but they contain a special method that tells the virtual machine to begin executing the code of the thread as a separate "list." Here's a partial API of the Thread class, showing its constructors and its execution-related methods: package java.lang;
public class Thread implements Runnable {
public Thread( );
public Thread(Runnable target);
public Thread(ThreadGroup group, Runnable target);
public Thread(String name);
public Thread(ThreadGroup group, String name);
public Thread(Runnable target, String name);
public Thread(ThreadGroup group, Runnable target, String name);
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize);
public void start( );
public void run( );
}As you see, threads are created with four pieces of information:
We can use these methods of the Thread class to create our first thread: package javathreads.examples.ch02.example2;
import java.util.*;
import javathreads.examples.ch02.*;
public class RandomCharacterGenerator extends Thread implements CharacterSource {
static char[] chars;
static String charArray = "abcdefghijklmnopqrstuvwxyz0123456789";
static {
chars = charArray.toCharArray( );
}
Random random;
CharacterEventHandler handler;
public RandomCharacterGenerator( ) {
random = new Random( );
handler = new CharacterEventHandler( );
}
public int getPauseTime( ) {
return (int) (Math.max(1000, 5000 * random.nextDouble( )));
}
public void addCharacterListener(CharacterListener cl) {
handler.addCharacterListener(cl);
}
public void removeCharacterListener(CharacterListener cl) {
handler.removeCharacterListener(cl);
}
public void nextCharacter( ) {
handler.fireNewCharacter(this,
(int) chars[random.nextInt(chars.length)]);
}
public void run( ) {
for (;;) {
nextCharacter( );
try {
Thread.sleep(getPauseTime( ));
} catch (InterruptedException ie) {
return;
}
}
}
}The first thing to note about this example is that it extends the Thread class. The class itself is constructed simply by calling its (only) constructor, and the actual list of instructions we want to execute is in the run() method. The run() method is a standard method of the Thread class; it is the place where the thread begins its execution. In a sense, the run() method is similar to the main() method of a standalone Java application: the main() method is where your first thread starts executing. Subsequent threads start executing with the run() method of the thread. Though some subtle differences between run() and main( ) exist, this is the best way to think of the relationship between them. So when the run() method of this class is eventually called, it fires off a new character to its listeners, sleeps for a random period of time between 1 and 5 seconds, and then repeats the process (forever, as the loop never terminates). The second task of our application is responsible for displaying the characters typed at the keyboard. It is also responsible for creating and starting our second thread. That code looks like this: package javathreads.examples.ch02.example2;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javathreads.examples.ch02.*;
public class SwingTypeTester extends JFrame implements CharacterSource {
protected RandomCharacterGenerator producer;
private CharacterDisplayCanvas displayCanvas;
private CharacterDisplayCanvas feedbackCanvas;
private JButton quitButton;
private JButton startButton;
private CharacterEventHandler handler;
public SwingTypeTester( ) {
initComponents( );
}
private void initComponents( ) {
handler = new CharacterEventHandler( );
displayCanvas = new CharacterDisplayCanvas( );
feedbackCanvas = new CharacterDisplayCanvas(this);
quitButton = new JButton( );
startButton = new JButton( );
add(displayCanvas, BorderLayout.NORTH);
add(feedbackCanvas, BorderLayout.CENTER);
JPanel p = new JPanel( );
startButton.setLabel("Start");
quitButton.setLabel("Quit");
p.add(startButton);
p.add(quitButton);
add(p, BorderLayout.SOUTH);
addWindowListener(new WindowAdapter( ) {
public void windowClosing(WindowEvent evt) {
quit( );
}
});
feedbackCanvas.addKeyListener(new KeyAdapter( ) {
public void keyPressed(KeyEvent ke) {
char c = ke.getKeyChar( );
if (c != KeyEvent.CHAR_UNDEFINED)
newCharacter((int) c);
}
});
startButton.addActionListener(new ActionListener( ) {
public void actionPerformed(ActionEvent evt) {
producer = new RandomCharacterGenerator( );
displayCanvas.setCharacterSource(producer);
producer.start( );
startButton.setEnabled(false);
feedbackCanvas.setEnabled(true);
feedbackCanvas.requestFocus( );
}
});
quitButton.addActionListener(new ActionListener( ) {
public void actionPerformed(ActionEvent evt) {
quit( );
}
});
pack( );
}
private void quit( ) {
System.exit(0);
}
public void addCharacterListener(CharacterListener cl) {
handler.addCharacterListener(cl);
}
public void removeCharacterListener(CharacterListener cl) {
handler.removeCharacterListener(cl);
}
public void newCharacter(int c) {
handler.fireNewCharacter(this, c);
}
public void nextCharacter( ) {
throw new IllegalStateException("We don't produce on demand");
}
public static void main(String args[]) {
new SwingTypeTester( ).show( );
}
}Most of this code is, of course, GUI code. The lines to note with respect to the Thread class are in the actionPerformed( ) method associated with the Start button. In the event callback, we construct a thread object (i.e., the instance of the RandomCharacterGenerator class) like any other Java object, and then we call the start() method on that object. Note that we did not call the RandomCharacterGenerator object's run() method. The start() method of the Thread class calls the run() method (see Section 2.3). Other threads are involved in this example, even though you don't see references to them. First, there is the main thread of the application. This thread starts when you begin execution of the program (i.e., when you type the java command). That thread calls the main() method of your application. The second thread of the application is the instance of the RandomCharacterGenerator class. It is created the first time the Start button is pressed. A third thread in the application is the event-processing thread. That thread is started by the Swing toolkit when the first GUI element of the application is created. That thread is significant to us because that's the thread that executes the actionPerformed() and keyPressed() methods of the application. There are many other threads in the virtual machine that we don't interact with; for now, we're concerned about the three threads we've just discussed. At this point, you can compile and run the application. Using our master ant script, execute this command: piccolo% ant ch2-ex2 The GUI window shown in Figure 2-3 appears. After you press the Start button, characters appear at random intervals in the top half of the window; as you type characters, they appear in the bottom half of the window. Figure 2-3. The SwingTypeTester window![]() At this point, we can't do much about scoring what the user types. That would require communication between the two threads of the program, which is the topic of the next chapter. However, we can clear up a few things in the display as we discuss how the RandomCharacterGenerator thread runs. |
|
|
< Day Day Up > |
|