站内搜索: 请输入搜索关键词
当前页面: 图书首页 > Java Threads, Third Edition

2.2 Creating a Thread - Java Threads, Third Edition

Previous Section  < Day Day Up >  Next Section

2.2 Creating a Thread

Threads 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 Architecture

Before 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).

Real-Life Race Conditions

In order to understand threaded programming fully, you must understand how threads run and are created (the topic of this chapter) as well as how they interact with data (the topic of the next chapter). Any interesting threaded program uses both features.

This means that a forward reference to some details (like the synchronized keyword) is unavoidable. This is the essence of a race condition: two things need to complete sequentially in order to end up in a coherent state.

This race condition also applies to Swing programming. We use Swing components in our examples because they make the applications more relevant and interesting. Swing components have some special thread programming considerations, as we'll see over the next few chapters, but we won't be able to explain them fully until we understand more about how multiple threads work.


2.2.2 The Thread Class

Now 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:


Thread name

The name of a thread is part of the information shown when a thread object is printed. Otherwise, it has no significance, so give your threads names that make sense to you when you see them printed. The default name for a thread is Thread-N, where N is a unique number.


Runnable target

We discuss runnables in depth later in this chapter. A runnable object is the list of instructions that the thread executes. By default, this is the information in the run() method of the thread itself. Note that the Thread class itself implements the Runnable interface.


Thread group

Thread groups are an advanced topic (see Chapter 13). For the vast majority of applications, thread groups are unimportant. By default, a thread is assigned to the same thread group as the thread that calls the constructor.


Stack size

Every thread has a stack where it stores temporary variables as it executes methods. Everything related to the stack size of a thread is platform-dependent: its default stack size, the range of legal values for the stack size, the optimal value for the stack size, and so on. Use of the stack size in portable programs is highly discouraged. For more information, see Chapter 13.

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
figs/jth3_0203.gif


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.

    Previous Section  < Day Day Up >  Next Section