We have explored quite a few features of Java with the first three
versions of the
HelloJava application. But until now,
our application has been rather passive; it has waited patiently for
events to come its way and responded to the whims of the user. Now
our application is going to take some
initiative—HelloJava4
will blink! Here is
the code for our latest version:
//file: HelloJava4.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class HelloJava4 extends JComponent implements MouseMotionListener, ActionListener, Runnable { // Coordinates for the message int messageX = 125, messageY = 95; String theMessage; JButton theButton; int colorIndex; // Current index into someColors. static Color[] someColors = { Color.black, Color.red, Color.green, Color.blue, Color.magenta }; boolean blinkState; public HelloJava4(String message) { theMessage = message; theButton = new JButton("Change Color"); setLayout(new FlowLayout( )); add(theButton); theButton.addActionListener(this); addMouseMotionListener(this); Thread t = new Thread(this); t.start( ); } public void paintComponent(Graphics g) { g.setColor(blinkState ? getBackground() : currentColor( )); g.drawString(theMessage, messageX, messageY); } public void mouseDragged(MouseEvent e) { messageX = e.getX( ); messageY = e.getY( ); repaint( ); } public void mouseMoved(MouseEvent e) {} public void actionPerformed(ActionEvent e) { // Did somebody push our button? if (e.getSource( ) == theButton) changeColor( ); } synchronized private void changeColor( ) { // Change the index to the next color. if (++colorIndex == someColors.length) colorIndex = 0; setForeground(currentColor( )); // Use the new color. repaint( ); // Paint again so we can see the change. } synchronized private Color currentColor( ) { return someColors[colorIndex]; } public void run( ) { try { while(true) { blinkState = !blinkState; // Toggle blinkState. repaint( ); // Show the change. Thread.sleep(500); } } catch (InterruptedException ie) {} } public static void main(String[] args) { JFrame f = new JFrame("HelloJava4"); // Make the application exit when the window is closed. f.addWindowListener(new WindowAdapter( ) { public void windowClosing(WindowEvent we) { System.exit(0); } }); f.setSize(300, 300); f.getContentPane( ).add(new HelloJava4("Hello, Java!")); f.setVisible(true); } }
Compile and run this version of HelloJava just like the others. You’ll see that the text does in fact blink. Our apologies if you don’t like blinking text—we’re not overly fond of it either—but it does make for a simple, instructive example.
All the changes
we’ve made in HelloJava4
have to do with
setting up a
separate thread of execution to make the text blink. Java is a
multithreaded language, which means there can be
many threads running at the same time. A thread
is a separate flow of control within a program. Conceptually, threads
are similar to processes, except that unlike processes,
multiple threads share the same address space, which means that they
can share variables and methods (but also have their own local
variables). Threads are also quite lightweight in comparison to
processes, so it’s conceivable for a single application to be
running hundreds of threads concurrently.
Multithreading provides a way for an application to handle many different tasks at the same time. It’s easy to imagine multiple things going on at the same time in an application like a web browser. The user could be listening to an audio clip while scrolling an image; at the same time, the browser can be downloading an image. Multithreading is especially useful in GUI-based applications, as it improves the interactive performance of these applications.
Unfortunately for us, programming with multiple threads can be quite a headache. The difficulty lies in making sure routines are implemented so they can be run by multiple concurrent threads. If a routine changes the value of a state variable, for example, then only one thread should be executing the routine at a time. Later in this section, we’ll examine briefly the issue of coordinating multiple threads’ access to shared data. In other languages, synchronization of threads can be extremely complex and error-prone. You’ll see that Java gives you a few simple tools that help you deal with many of these problems. Java threads can be started, stopped, suspended, and prioritized. Threads are preemptive, so a higher priority thread can interrupt a lower priority thread when vying for processor time. See Chapter 8, for a complete discussion of threads.
The Java runtime system creates and manages a number of threads.
(Exactly how varies with the implementation.) We’ve already
mentioned the repaint thread, which manages repaint( )
requests and event processing for GUI components that
belong to the java.awt
and
javax.swing
packages. Our example applications
have done most of their work in one thread. Methods like
mouseDragged( )
and actionPerformed( )
are invoked by the windowing thread and run on its time.
Similarly, our constructor runs as part of the main application
thread. This means we are somewhat limited in the amount of
processing we do within these methods. If we were, for instance, to
go into an endless
loop in our constructor, our
application would never appear, as it would never finish
initializing. If we want an application to perform any extensive
processing, such as animation, a lengthy calculation, or
communication, we should create separate threads for these tasks.
As you might have guessed, threads are
created and controlled as Thread
objects. An
instance of the Thread
class corresponds to a
single thread. It contains methods to start, control, and stop the
thread’s execution. Our basic plan is to create a
Thread
object to handle our blinking code. We call
the Thread
’s start( )
method to begin execution. Once the thread starts, it continues to
run until we call the Thread
’s
interrupt( )
method to terminate it.
So how do we tell the thread which
method to run? Well, the Thread
object is rather
picky; it always expects to execute a method called run( )
to perform the action of the thread. The run( )
method can, however, with a little persuasion, be located
in any class we desire.
We specify the location of the run( )
method in
one of two ways. First, the Thread
class itself
has a method called run( )
. One way to execute
some Java code in a separate thread is to subclass
Thread
and
override
its run( )
method to do our bidding. Invoking the
start( )
method of the subclass object causes its
run( )
method to execute in a separate thread.
It’s not always desirable or possible to create a subclass of
Thread
to contain our run( )
method. The Thread
class has a constructor that
takes an object reference as its argument. If we create a
Thread
object using this constructor and call its
start( )
method, the Thread
executes the run( )
method of the argument object,
rather than its own. In order to accomplish this, Java needs a
guarantee that the object we are passing it does indeed contain a
compatible run( )
method. We already know how to
make such a guarantee: we use an interface. Java provides an
interface named Runnable
that must be implemented
by any class that wants to become a Thread
.
We’ve used the second technique
in the HelloJava4
example. To create a thread, a
HelloJava4
object passes itself
(this
) to the Thread
constructor. This means that HelloJava4
itself
must implement the Runnable
interface, by
implementing the run( )
method. This method is
called automatically when the runtime system needs to start the
thread.
We indicate that the class implements the interface in our class declaration:
public class HelloJava4 extends JComponent implements MouseMotionListener, ActionListener, Runnable {...}
At compile time, the Java compiler checks to make sure we abide by
this statement. We have carried through by adding an appropriate
run( )
method to HelloJava4
. It
takes no arguments and returns no value. Our run( )
method accomplishes blinking by changing the color of our
text a couple of times a second. It’s a very short routine, but
we’re going to delay looking at it until we tie up some loose
ends in dealing with the Thread
itself.
We
want the blinking to begin when the application starts. So
we’ll start the thread in the initialization code in
HelloJava4
’s constructor. It takes only two
lines:
Thread t = new Thread(this); t.start( );
First, the constructor creates a new instance of Thread
, passing it the object that contains the run( )
method to the constructor. Since
HelloJava4
itself contains our run( )
method, we pass the special variable
this
to the constructor. this
always refers to our object. After creating the new
Thread
, we call its start( )
method to begin execution. This, in turn, invokes
HelloJava4
’s run( )
method in a separate thread.
Our run( )
method
does its job by setting the
value of the variable
blinkState
. We have added
blinkState
, a boolean
value, to represent whether we are currently blinking on or off:
boolean blinkState;
A setColor( )
call has been added to our
paintComponent( )
method to handle blinking. When
blinkState
is true, the call to setColor( )
draws the text in the background color, making it
disappear:
g.setColor(blinkState ? getBackground() : currentColor( ));
Here we are being somewhat terse, using the C-like
ternary operator to return one of two
alternative color values based on the value of
blinkState
.
Finally, we come to the run( )
method itself:
public void run( ) { try { while(true) { blinkState = !blinkState; repaint( ); Thread.sleep(500); } } catch (InterruptedException ie) {} }
Basically, run( )
is an
infinite
while
loop. This means the method will run
continuously until the thread is terminated by a call to the
controlling Thread
object’s
interrupt( )
method.
The body of the loop does three things on each pass:
sleep( )
is a static method of the Thread
class. The method
can be invoked from anywhere and has the effect of putting the
current thread to sleep for the specified number of milliseconds. The
effect here is to give us approximately two blinks per second. The
try/catch
construct, described in the next
section, traps any errors in the call to the sleep( )
method of the Thread
class.
The
try/catch
statement in Java is used to handle
special conditions called
exceptions
. An exception is a
message that is sent, normally in response to an error, during the
execution of a statement or a method. When an exceptional condition
arises, an object is created that contains information about the
particular problem or condition. Exceptions act somewhat like events.
Java stops execution at the place where the exception occurred, and
the exception object is said to be
thrown by that
section of code. Like an event, an exception must be delivered
somewhere and handled. The section of code that receives the
exception object is said to
catch
the exception. An exception causes the execution of the instigating
section of code to stop abruptly and transfers control to the code
that receives the exception object.
The try/catch
construct allows you to catch
exceptions for a section of code. If an exception is caused by any
statement inside of a try
clause, Java attempts to
deliver the exception to the appropriate catch
clause. A
catch
clause looks
like a method declaration with one argument and no return type. If
Java finds a catch
clause with an argument type
that matches the type of the exception, that catch
clause is invoked. A try
clause can have multiple
catch
clauses with different argument types; Java
chooses the appropriate one in a way that is analogous to the
selection of overloaded methods. You can catch multiple types of
exceptions from a block of code. Depending on the type of exception
thrown, the appropriate catch
clause will be
executed.
If there is no try/catch
clause surrounding the
code, or a matching catch
clause is not found, the
exception is thrown up the call stack to the calling method. If the
exception is not caught there, it’s thrown up another level,
and so on until the exception is handled. This provides a very
flexible error-handling mechanism, so that exceptions in deeply
nested calls can bubble up to the surface of the call stack for
handling. As a programmer, you need to know what exceptions a
particular statement can generate, so
methods
in Java are required to declare the exceptions they can throw. If a
method doesn’t handle an exception itself, it must specify that
it can throw that exception, so that its calling method knows that it
may have to handle it. See Chapter 4, for a
complete discussion of exceptions and the
try/catch
clause.
So, why do we need a try/catch
clause in the
run( )
method? What kind of exception can
Thread
’s sleep( )
method
throw and why do we care about it, when we don’t seem to check
for exceptions anywhere else? Under some circumstances,
Thread
’s sleep( )
method
can throw an
InterruptedException
,
indicating that it was interrupted by another thread. Since the
run( )
method specified in the
Runnable
interface doesn’t declare it can
throw an InterruptedException
, we must catch it
ourselves, or the compiler will complain. The
try/catch
statement in our example has an empty
catch
clause, which means that it handles the
exception by ignoring it. In this case, our thread’s
functionality is so simple it doesn’t matter if it’s
interrupted. All of the other methods we have used either handle
their own exceptions or throw only general-purpose exceptions that
are assumed to be possible everywhere and don’t need to be
explicitly declared.
At any given time, there can be a number of threads running in the Java runtime system. Unless we explicitly coordinate them, these threads will be executing methods without any regard for what the other threads are doing. Problems can arise when these methods share the same data. If one method is changing the value of some variables at the same time that another method is reading these variables, it’s possible that the reading thread might catch things in the middle and get some variables with old values and some with new. Depending on the application, this situation could cause a critical error.
In our HelloJava examples, both our paintComponent( )
and mouseDragged( )
methods access the
messageX
and messageY
variables. Without knowing the implementation of our particular Java
environment, we have to assume that these methods could conceivably
be called by different threads and run concurrently.
paintComponent( )
could be called while
mouseDragged( )
is in the midst of updating
messageX
and messageY
. At that
point, the data is in an inconsistent state and if
paintComponent( )
gets lucky, it could get the new
x
value with the old y
value.
Fortunately, in this case, we probably would not even notice if this
were to happen in our application. We did, however, see another case,
in our changeColor( )
and currentColor( )
methods, where there is the potential for a more serious
“out of bounds” error.
The synchronized
modifier tells Java to acquire a
lock
for the class that
contains the method before executing that method. Only one method can
have the lock on a class at any given time, which means that only one
synchronized method in that class can be running at a time. This
allows a method to alter data and leave it in a consistent state
before a concurrently running method is allowed to access it. When
the method is done, it releases the lock on the class.
Unlike synchronization in other languages, the
synchronized
keyword in Java provides locking at
the language level. This means there is no way that you can forget to
unlock a class. Even if the method throws an exception or the thread
is terminated, Java will release the lock. This feature makes
programming with threads in Java much easier than in other languages.
See Chapter 8 for more details on coordinating
threads and shared data.
Whew! Now it’s time to say goodbye to HelloJava. We hope that you have developed a feel for the major features of the Java language, and that this will help you as you go on to explore the details of programming with Java.
Get Learning Java now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.