|
||||||||||||||
![]() public void run()
{
while (true)
{
try
{
sleep(100);
synchronized(this)
{
while(suspended)
wait();
}
}
catch(InterruptedException e) {
System.err.println("Interrupted");
}
t.setText(Integer.toString(count++));
}
}
}
public
void init() {
Container
cp = getContentPane();
cp.setLayout(new
FlowLayout());
cp.add(t);
suspend.addActionListener(
new
ActionListener() {
public
void
actionPerformed(ActionEvent e) {
ss.fauxSuspend();
}
});
cp.add(suspend);
resume.addActionListener(
new
ActionListener() {
public
void
actionPerformed(ActionEvent e) {
ss.fauxResume();
}
});
cp.add(resume);
}
public
static void main(String[] args) {
Console.run(new
Suspend(), 300, 100);
}
}
///:~
876
Thinking
in Java
![]() The
flag suspended
inside
Suspendable
is
used to turn suspension
on
and
off. To suspend, the flag is
set to true
by
calling fauxSuspend(
)
and
this is detected inside
run( ).
The wait(
),
as described earlier in
this
chapter,
must be synchronized
so
that it has the object
lock. In
fauxResume(
),
the suspended
flag
is set to false
and
notify( )
is
called--since
this wakes up wait(
) inside
a synchronized
clause
the
fauxResume(
) method
must also be synchronized
so
that it acquires
the
lock before calling
notify( )
(thus
the lock is available for
the wait(
)
to
wake up with). If you follow
the style shown in this
program you can
avoid
using suspend(
) and
resume(
).
The
destroy(
) method
of Thread
has
never been implemented; it's
like
a
suspend(
) that
cannot resume, so it has the
same deadlock issues
as
suspend(
).
However, this is not a
deprecated method and it
might be
implemented
in a future version of Java
(after 2) for special
situations in
which
the risk of a deadlock is
acceptable.
You
might wonder why these
methods, now deprecated,
were included in
Java
in the first place. It seems
an admission of a rather
significant
mistake
to simply remove them
outright (and pokes yet
another hole in
the
arguments for Java's
exceptional design and
infallibility touted by
Sun
marketing
people). The heartening part
about the change is that it
clearly
indicates
that the technical people
and not the marketing
people are
running
the show--they discovered a
problem and they are
fixing it. I find
this
much more promising and
hopeful than leaving the
problem in
because
"fixing it would admit an
error." It means that Java
will continue
to
improve, even if it means a
little discomfort on the
part of Java
programmers.
I'd rather deal with
the discomfort than watch
the language
stagnate.
Priorities
The
priority
of
a thread tells the scheduler
how important this thread
is.
If
there are a number of
threads blocked and waiting
to be run, the
scheduler
will run the one
with the highest priority
first. However, this
doesn't
mean that threads with
lower priority don't get
run (that is,
you
can't
get deadlocked because of
priorities). Lower priority
threads just
tend
to run less often.
Chapter
14: Multiple
Threads
877
![]() Although
priorities are interesting to
know about and to play
with, in
practice
you almost never need to
set priorities yourself. So
feel free to
skip
the rest of this section if
priorities aren't interesting to
you.
Reading
and setting priorities
You
can read the priority of a
thread with getPriority(
) and
change it
with
setPriority(
).
The form of the prior
"counter" examples can
be
used
to show the effect of
changing the priorities. In
this applet you'll
see
that
the counters slow down as
the associated threads have
their priorities
lowered:
//:
c14:Counter5.java
//
Adjusting the priorities of threads.
//
<applet code=Counter5 width=450
height=600>
//
</applet>
import
javax.swing.*;
import
java.awt.*;
import
java.awt.event.*;
import
com.bruceeckel.swing.*;
class
Ticker2 extends Thread {
private
JButton
b
= new JButton("Toggle"),
incPriority
= new JButton("up"),
decPriority
= new JButton("down");
private
JTextField
t
= new JTextField(10),
pr
= new JTextField(3); // Display priority
private
int count = 0;
private
boolean runFlag = true;
public
Ticker2(Container c) {
b.addActionListener(new
ToggleL());
incPriority.addActionListener(new
UpL());
decPriority.addActionListener(new
DownL());
JPanel
p = new JPanel();
p.add(t);
p.add(pr);
p.add(b);
p.add(incPriority);
p.add(decPriority);
878
Thinking
in Java
![]() c.add(p);
}
class
ToggleL implements ActionListener {
public
void actionPerformed(ActionEvent e) {
runFlag
= !runFlag;
}
}
class
UpL implements ActionListener {
public
void actionPerformed(ActionEvent e) {
int
newPriority = getPriority() + 1;
if(newPriority
> Thread.MAX_PRIORITY)
newPriority
= Thread.MAX_PRIORITY;
setPriority(newPriority);
}
}
class
DownL implements ActionListener {
public
void actionPerformed(ActionEvent e) {
int
newPriority = getPriority() - 1;
if(newPriority
< Thread.MIN_PRIORITY)
newPriority
= Thread.MIN_PRIORITY;
setPriority(newPriority);
}
}
public
void run() {
while
(true) {
if(runFlag)
{
t.setText(Integer.toString(count++));
pr.setText(
Integer.toString(getPriority()));
}
yield();
}
}
}
public
class Counter5 extends JApplet {
private
JButton
start
= new JButton("Start"),
upMax
= new JButton("Inc Max Priority"),
downMax
= new JButton("Dec Max Priority");
private
boolean started = false;
Chapter
14: Multiple
Threads
879
![]() private
static final int SIZE = 10;
private
Ticker2[] s = new Ticker2[SIZE];
private
JTextField mp = new JTextField(3);
public
void init() {
Container
cp = getContentPane();
cp.setLayout(new
FlowLayout());
for(int
i = 0; i < s.length; i++)
s[i]
= new Ticker2(cp);
cp.add(new
JLabel(
"MAX_PRIORITY
= " + Thread.MAX_PRIORITY));
cp.add(new
JLabel("MIN_PRIORITY = "
+
Thread.MIN_PRIORITY));
cp.add(new
JLabel("Group Max Priority = "));
cp.add(mp);
cp.add(start);
cp.add(upMax);
cp.add(downMax);
start.addActionListener(new
StartL());
upMax.addActionListener(new
UpMaxL());
downMax.addActionListener(new
DownMaxL());
showMaxPriority();
//
Recursively display parent thread groups:
ThreadGroup
parent =
s[0].getThreadGroup().getParent();
while(parent
!= null) {
cp.add(new
Label(
"Parent
threadgroup max priority = "
+
parent.getMaxPriority()));
parent
= parent.getParent();
}
}
public
void showMaxPriority() {
mp.setText(Integer.toString(
s[0].getThreadGroup().getMaxPriority()));
}
class
StartL implements ActionListener {
public
void actionPerformed(ActionEvent e) {
if(!started)
{
started
= true;
for(int
i = 0; i < s.length; i++)
s[i].start();
880
Thinking
in Java
![]() }
}
}
class
UpMaxL implements ActionListener {
public
void actionPerformed(ActionEvent e) {
int
maxp =
s[0].getThreadGroup().getMaxPriority();
if(++maxp
> Thread.MAX_PRIORITY)
maxp
= Thread.MAX_PRIORITY;
s[0].getThreadGroup().setMaxPriority(maxp);
showMaxPriority();
}
}
class
DownMaxL implements ActionListener {
public
void actionPerformed(ActionEvent e) {
int
maxp =
s[0].getThreadGroup().getMaxPriority();
if(--maxp
< Thread.MIN_PRIORITY)
maxp
= Thread.MIN_PRIORITY;
s[0].getThreadGroup().setMaxPriority(maxp);
showMaxPriority();
}
}
public
static void main(String[] args) {
Console.run(new
Counter5(), 450, 600);
}
}
///:~
Ticker2
follows
the form established earlier
in this chapter, but
there's
an
extra JTextField
for
displaying the priority of
the thread and
two
more
buttons for incrementing and
decrementing the
priority.
Also
notice the use of yield(
),
which voluntarily hands
control back to
the
scheduler. Without this the
multithreading mechanism still
works, but
you'll
notice it runs slowly (try
removing the call to
yield( )
to
see this).
You
could also call sleep(
),
but then the rate of
counting would be
controlled
by the sleep(
) duration
instead of the
priority.
The
init( )
in
Counter5
creates
an array of ten Ticker2s;
their buttons
and
fields are placed on the
form by the Ticker2
constructor.
Counter5
adds
buttons to start everything up as
well as increment and
decrement
Chapter
14: Multiple
Threads
881
![]() the
maximum priority of the
thread group. In addition,
there are labels
that
display the maximum and
minimum priorities possible
for a thread
and
a JTextField
to
show the thread group's
maximum priority.
(The
next
section will describe thread
groups.) Finally, the
priorities of the
parent
thread groups are also
displayed as labels.
When
you press an "up" or "down"
button, that Ticker2's
priority is
fetched
and incremented or decremented
accordingly.
When
you run this program,
you'll notice several
things. First of all,
the
thread
group's default priority is
five. Even if you decrement
the
maximum
priority below five before
starting the threads (or
before
creating
the threads, which requires
a code change), each thread
will have
a
default priority of
five.
The
simple test is to take one
counter and decrement its
priority to one,
and
observe that it counts much
slower. But now try to
increment it again.
You
can get it back up to the
thread group's priority, but
no higher. Now
decrement
the thread group's priority
a couple of times. The
thread
priorities
are unchanged, but if you
try to modify them either up
or down
you'll
see that they'll
automatically pop to the
priority of the thread
group.
Also,
new threads will still be
given a default priority,
even if that's higher
than
the group priority. (Thus
the group priority is not a
way to prevent
new
threads from having higher
priorities than existing
ones.)
Finally,
try to increment the group
maximum priority. It can't be
done.
You
can only reduce thread
group maximum priorities,
not increase them.
Thread
groups
All
threads belong to a thread
group. This can be either
the default thread
group
or a group you explicitly
specify when you create
the thread. At
creation,
the thread is bound to a
group and cannot change to a
different
group.
Each application has at
least one thread that
belongs to the system
thread
group. If you create more
threads without specifying a
group, they
will
also belong to the system
thread group.
Thread
groups must also belong to
other thread groups. The
thread group
that
a new one belongs to must be
specified in the constructor. If
you
create
a thread group without
specifying a thread group
for it to belong to,
882
Thinking
in Java
![]() it
will be placed under the
system thread group. Thus,
all thread groups in
your
application will ultimately
have the system thread
group as the
parent.
The
reason for the existence of
thread groups is hard to
determine from
the
literature, which tends to be
confusing on this subject.
It's often cited
as
"security reasons." According to
Arnold & Gosling,2 "Threads
within a
thread
group can modify the
other threads in the group,
including any
farther
down the hierarchy. A thread
cannot modify threads
outside of its
own
group or contained groups."
It's hard to know what
"modify" is
supposed
to mean here. The following
example shows a thread in a
"leaf"
subgroup
modifying the priorities of
all the threads in its
tree of thread
groups
as well as calling a method
for all the threads in
its tree.
//:
c14:TestAccess.java
//
How threads can access other threads
//
in a parent thread group.
public
class TestAccess {
public
static void main(String[] args) {
ThreadGroup
x
= new ThreadGroup("x"),
y
= new ThreadGroup(x, "y"),
z
= new ThreadGroup(y, "z");
Thread
one
= new TestThread1(x, "one"),
two
= new TestThread2(z, "two");
}
}
class
TestThread1 extends Thread {
private
int i;
TestThread1(ThreadGroup
g, String name) {
super(g,
name);
}
void
f() {
2
The
Java Programming Language,
by Ken Arnold and James
Gosling, Addison-Wesley
1996
pp 179.
Chapter
14: Multiple
Threads
883
![]() i++;
// modify this thread
System.out.println(getName()
+ " f()");
}
}
class
TestThread2 extends TestThread1 {
TestThread2(ThreadGroup
g, String name) {
super(g,
name);
start();
}
public
void run() {
ThreadGroup
g =
getThreadGroup().getParent().getParent();
g.list();
Thread[]
gAll = new Thread[g.activeCount()];
g.enumerate(gAll);
for(int
i = 0; i < gAll.length; i++) {
gAll[i].setPriority(Thread.MIN_PRIORITY);
((TestThread1)gAll[i]).f();
}
g.list();
}
}
///:~
In
main(
),
several ThreadGroups
are created, leafing off
from each
other:
x has
no argument but its name (a
String),
so it is automatically
placed
in the "system" thread
group, while y
is
under x
and
z is
under y.
Note
that initialization happens in
textual order so this code
is legal.
Two
threads are created and
placed in different thread
groups.
TestThread1
doesn't
have a run(
) method
but it does have an
f( ) that
modifies
the thread and prints
something so you can see it
was called.
TestThread2
is
a subclass of TestThread1
and
its run(
) is
fairly
elaborate.
It first gets the thread
group of the current thread,
then moves
up
the heritage tree by two
levels using getParent(
).
(This is contrived
since
I purposely place the
TestThread2
object
two levels down in
the
hierarchy.)
At this point, an array of
references to Threads
is created
using
the method activeCount(
) to
ask how many threads
are in this
thread
group and all the
child thread groups. The
enumerate( )
method
places
references to all of these
threads in the array
gAll,
then I simply
884
Thinking
in Java
![]() move
through the entire array
calling the f(
) method
for each thread, as
well
as modifying the priority.
Thus, a thread in a "leaf"
thread group
modifies
threads in parent thread
groups.
The
debugging method list(
) prints
all the information about a
thread
group
to standard output and is
helpful when investigating
thread group
behavior.
Here's the output of the
program:
java.lang.ThreadGroup[name=x,maxpri=10]
Thread[one,5,x]
java.lang.ThreadGroup[name=y,maxpri=10]
java.lang.ThreadGroup[name=z,maxpri=10]
Thread[two,5,z]
one
f()
two
f()
java.lang.ThreadGroup[name=x,maxpri=10]
Thread[one,1,x]
java.lang.ThreadGroup[name=y,maxpri=10]
java.lang.ThreadGroup[name=z,maxpri=10]
Thread[two,1,z]
Not
only does list(
) print
the class name of ThreadGroup
or
Thread,
but
it also prints the thread
group name and its
maximum priority. For
threads,
the thread name is printed,
followed by the thread
priority and
the
group that it belongs to.
Note that list(
) indents
the threads and
thread
groups to indicate that they
are children of the
unindented thread
group.
You
can see that f(
) is
called by the TestThread2
run( ) method, so
it's
obvious
that all threads in a group
are vulnerable. However, you
can
access
only the threads that
branch off from your
own system
thread
group
tree, and perhaps this is
what is meant by "safety."
You cannot
access
anyone else's system thread
group tree.
Controlling
thread groups
Putting
aside the safety issue,
one thing thread groups
seem to be useful
for
is control: you can perform
certain operations on an entire
thread
group
with a single command. The
following example demonstrates
this,
and
the restrictions on priorities
within thread groups. The
commented
numbers
in parentheses provide a reference to
compare to the
output.
Chapter
14: Multiple
Threads
885
![]() //:
c14:ThreadGroup1.java
//
How thread groups control priorities
//
of the threads inside them.
public
class ThreadGroup1 {
public
static void main(String[] args) {
//
Get the system thread & print its Info:
ThreadGroup
sys =
Thread.currentThread().getThreadGroup();
sys.list();
// (1)
//
Reduce the system thread group priority:
sys.setMaxPriority(Thread.MAX_PRIORITY
- 1);
//
Increase the main thread priority:
Thread
curr = Thread.currentThread();
curr.setPriority(curr.getPriority()
+ 1);
sys.list();
// (2)
//
Attempt to set a new group to the max:
ThreadGroup
g1 = new ThreadGroup("g1");
g1.setMaxPriority(Thread.MAX_PRIORITY);
//
Attempt to set a new thread to the max:
Thread
t = new Thread(g1, "A");
t.setPriority(Thread.MAX_PRIORITY);
g1.list();
// (3)
//
Reduce g1's max priority, then attempt
//
to increase it:
g1.setMaxPriority(Thread.MAX_PRIORITY
- 2);
g1.setMaxPriority(Thread.MAX_PRIORITY);
g1.list();
// (4)
//
Attempt to set a new thread to the max:
t
= new Thread(g1, "B");
t.setPriority(Thread.MAX_PRIORITY);
g1.list();
// (5)
//
Lower the max priority below the default
//
thread priority:
g1.setMaxPriority(Thread.MIN_PRIORITY
+ 2);
//
Look at a new thread's priority before
//
and after changing it:
t
= new Thread(g1, "C");
g1.list();
// (6)
t.setPriority(t.getPriority()
-1);
g1.list();
// (7)
886
Thinking
in Java
![]() //
Make g2 a child Threadgroup of g1 and
//
try to increase its priority:
ThreadGroup
g2 = new ThreadGroup(g1, "g2");
g2.list();
// (8)
g2.setMaxPriority(Thread.MAX_PRIORITY);
g2.list();
// (9)
//
Add a bunch of new threads to g2:
for
(int i = 0; i < 5; i++)
new
Thread(g2, Integer.toString(i));
//
Show information about all
threadgroups
//
and threads:
sys.list();
// (10)
System.out.println("Starting
all threads:");
Thread[]
all = new Thread[sys.activeCount()];
sys.enumerate(all);
for(int
i = 0; i < all.length; i++)
if(!all[i].isAlive())
all[i].start();
//
Suspends & Stops all threads in
//
this group and its subgroups:
System.out.println("All
threads started");
sys.suspend();
// Deprecated in Java 2
//
Never gets here...
System.out.println("All
threads suspended");
sys.stop();
// Deprecated in Java 2
System.out.println("All
threads stopped");
}
}
///:~
The
output that follows has
been edited to allow it to
fit on the page
(the
java.lang.
has
been removed) and to add
numbers to correspond to
the
commented
numbers in the listing
above.
(1)
ThreadGroup[name=system,maxpri=10]
Thread[main,5,system]
(2)
ThreadGroup[name=system,maxpri=9]
Thread[main,6,system]
(3)
ThreadGroup[name=g1,maxpri=9]
Thread[A,9,g1]
(4)
ThreadGroup[name=g1,maxpri=8]
Thread[A,9,g1]
Chapter
14: Multiple
Threads
887
![]() (5)
ThreadGroup[name=g1,maxpri=8]
Thread[A,9,g1]
Thread[B,8,g1]
(6)
ThreadGroup[name=g1,maxpri=3]
Thread[A,9,g1]
Thread[B,8,g1]
Thread[C,6,g1]
(7)
ThreadGroup[name=g1,maxpri=3]
Thread[A,9,g1]
Thread[B,8,g1]
Thread[C,3,g1]
(8)
ThreadGroup[name=g2,maxpri=3]
(9)
ThreadGroup[name=g2,maxpri=3]
(10)ThreadGroup[name=system,maxpri=9]
Thread[main,6,system]
ThreadGroup[name=g1,maxpri=3]
Thread[A,9,g1]
Thread[B,8,g1]
Thread[C,3,g1]
ThreadGroup[name=g2,maxpri=3]
Thread[0,6,g2]
Thread[1,6,g2]
Thread[2,6,g2]
Thread[3,6,g2]
Thread[4,6,g2]
Starting
all threads:
All
threads started
All
programs have at least one
thread running, and the
first action in
main(
) is
to call the static
method
of Thread
called
currentThread(
).
From this thread, the
thread group is produced
and
list(
) is
called for the result.
The output is:
(1)
ThreadGroup[name=system,maxpri=10]
Thread[main,5,system]
You
can see that the
name of the main thread
group is system,
and the
name
of the main thread is
main,
and it belongs to the
system
thread
group.
The
second exercise shows that
the system
group's
maximum priority
can
be reduced and the main thread
can have its priority
increased:
888
Thinking
in Java
![]() (2)
ThreadGroup[name=system,maxpri=9]
Thread[main,6,system]
The
third exercise creates a new
thread group, g1,
which automatically
belongs
to the system
thread
group since it isn't
otherwise specified. A
new
thread A
is
placed in g1.
After attempting to set this
group's
maximum
priority to the highest
level and A's
priority to the highest
level,
the
result is:
(3)
ThreadGroup[name=g1,maxpri=9]
Thread[A,9,g1]
Thus,
it's not possible to change
the thread group's maximum
priority to
be
higher than its parent
thread group.
The
fourth exercise reduces
g1's
maximum priority by two and
then tries
to
increase it up to Thread.MAX_PRIORITY.
The result is:
(4)
ThreadGroup[name=g1,maxpri=8]
Thread[A,9,g1]
You
can see that the
increase in maximum priority
didn't work. You
can
only
decrease a thread group's
maximum priority, not
increase it. Also,
notice
that thread A's
priority didn't change, and
now it is higher than
the
thread
group's maximum priority.
Changing a thread group's
maximum
priority
doesn't affect existing
threads.
The
fifth exercise attempts to
set a new thread to maximum
priority:
(5)
ThreadGroup[name=g1,maxpri=8]
Thread[A,9,g1]
Thread[B,8,g1]
The
new thread cannot be changed
to anything higher than the
maximum
thread
group priority.
The
default thread priority for
this program is six; that's
the priority a new
thread
will be created at and where
it will stay if you don't
manipulate the
priority.
Exercise 6 lowers the
maximum thread group
priority below the
default
thread priority to see what
happens when you create a
new thread
under
this condition:
(6)
ThreadGroup[name=g1,maxpri=3]
Chapter
14: Multiple
Threads
889
![]() Thread[A,9,g1]
Thread[B,8,g1]
Thread[C,6,g1]
Even
though the maximum priority
of the thread group is
three, the new
thread
is still created using the
default priority of six.
Thus, maximum
thread
group priority does not
affect default priority. (In
fact, there
appears
to be no way to set the
default priority for new
threads.)
After
changing the priority,
attempting to decrement it by one,
the result
is:
(7)
ThreadGroup[name=g1,maxpri=3]
Thread[A,9,g1]
Thread[B,8,g1]
Thread[C,3,g1]
Only
when you attempt to change
the priority is the thread
group's
maximum
priority enforced.
A
similar experiment is performed in
(8) and (9), in which a
new thread
group
g2 is
created as a child of g1
and
its maximum priority is
changed.
You
can see that it's
impossible for g2's
maximum to go higher than
g1's:
(8)
ThreadGroup[name=g2,maxpri=3]
(9)
ThreadGroup[name=g2,maxpri=3]
Also
notice that g2
is
automatically set to the
thread group maximum
priority
of g1
as
g2 is
created.
After
all of these experiments,
the entire system of thread
groups and
threads
is printed:
(10)ThreadGroup[name=system,maxpri=9]
Thread[main,6,system]
ThreadGroup[name=g1,maxpri=3]
Thread[A,9,g1]
Thread[B,8,g1]
Thread[C,3,g1]
ThreadGroup[name=g2,maxpri=3]
Thread[0,6,g2]
Thread[1,6,g2]
Thread[2,6,g2]
890
Thinking
in Java
![]() Thread[3,6,g2]
Thread[4,6,g2]
So
because of the rules of
thread groups, a child group
must always have a
maximum
priority that's less than or
equal to its parent's
maximum
priority.
The
last part of this program
demonstrates methods for an
entire group of
threads.
First the program moves
through the entire tree of
threads and
starts
each one that hasn't
been started. For drama,
the system
group
is
then
suspended and finally
stopped. (Although it's
interesting to see
that
suspend(
) and
stop( )
work
on entire thread groups, you
should keep
in
mind that these methods
are deprecated in Java 2.)
But when you
suspend
the system
group
you also suspend the
main thread
and the
whole
program shuts down, so it
never gets to the point
where the threads
are
stopped. Actually, if you do
stop the main
thread
it throws a
ThreadDeath
exception,
so this is not a typical
thing to do. Since
ThreadGroup
is
inherited from Object,
which
contains the wait(
)
method,
you can also choose to
suspend the program for
any number of
seconds
by calling wait(seconds
* 1000). This
must acquire the
lock
inside
a synchronized block, of
course.
The
ThreadGroup
class
also has suspend(
) and
resume( )
methods
so
you can stop and
start an entire thread group
and all of its threads
and
subgroups
with a single command.
(Again, suspend(
) and
resume(
)
are
deprecated in Java
2.)
Thread
groups can seem a bit
mysterious at first, but
keep in mind that
you
probably won't be using them
directly very often.
Runnable
revisited
Earlier
in this chapter, I suggested
that you think carefully
before making
an
applet or main Frame
as
an implementation of Runnable.
Of course,
if
you must inherit from a
class and
you
want to add threading
behavior to
the
class, Runnable
is
the correct solution. The
final example in this
chapter
exploits this by making a
Runnable
JPanel class
that paints
different
colors on itself. This
application is set up to take
values from the
command
line to determine how big
the grid of colors is and
how long to
Chapter
14: Multiple
Threads
891
![]() sleep(
) between
color changes. By playing
with these values
you'll
discover
some interesting and
possibly inexplicable features of
threads:
//:
c14:ColorBoxes.java
//
Using the Runnable interface.
//
<applet code=ColorBoxes width=500
height=400>
//
<param name=grid value="12">
//
<param name=pause value="50">
//
</applet>
import
javax.swing.*;
import
java.awt.*;
import
java.awt.event.*;
import
com.bruceeckel.swing.*;
class
CBox extends JPanel implements Runnable {
private
Thread t;
private
int pause;
private
static final Color[] colors = {
Color.black,
Color.blue, Color.cyan,
Color.darkGray,
Color.gray, Color.green,
Color.lightGray,
Color.magenta,
Color.orange,
Color.pink, Color.red,
Color.white,
Color.yellow
};
private
Color cColor = newColor();
private
static final Color newColor() {
return
colors[
(int)(Math.random()
* colors.length)
];
}
public
void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(cColor);
Dimension
s = getSize();
g.fillRect(0,
0, s.width, s.height);
}
public
CBox(int pause) {
this.pause
= pause;
t
= new Thread(this);
t.start();
}
892
Thinking
in Java
![]() public
void run() {
while(true)
{
cColor
= newColor();
repaint();
try
{
t.sleep(pause);
}
catch(InterruptedException e) {
System.err.println("Interrupted");
}
}
}
}
public
class ColorBoxes extends JApplet {
private
boolean isApplet = true;
private
int grid = 12;
private
int pause = 50;
public
void init() {
//
Get parameters from Web page:
if
(isApplet) {
String
gsize = getParameter("grid");
if(gsize
!= null)
grid
= Integer.parseInt(gsize);
String
pse = getParameter("pause");
if(pse
!= null)
pause
= Integer.parseInt(pse);
}
Container
cp = getContentPane();
cp.setLayout(new
GridLayout(grid, grid));
for
(int i = 0; i < grid * grid; i++)
cp.add(new
CBox(pause));
}
public
static void main(String[] args) {
ColorBoxes
applet = new ColorBoxes();
applet.isApplet
= false;
if(args.length
> 0)
applet.grid
= Integer.parseInt(args[0]);
if(args.length
> 1)
applet.pause
= Integer.parseInt(args[1]);
Console.run(applet,
500, 400);
}
Chapter
14: Multiple
Threads
893
![]() }
///:~
ColorBoxes
is
the usual applet/application
with an init(
) that
sets up
the
GUI. This sets up the
GridLayout
so
that it has grid
cells
in each
dimension.
Then it adds the appropriate
number of CBox
objects
to fill
the
grid, passing the pause
value
to each one. In main(
) you
can see
how
pause
and
grid have
default values that can be
changed if you pass
in
command-line arguments, or by using
applet parameters.
CBox
is
where all the work
takes place. This is
inherited from JPanel
and
it implements the Runnable
interface
so each JPanel
can
also be a
Thread.
Remember that when you
implement Runnable,
you don't
make
a Thread
object,
just a class that has a
run( )
method.
Thus, you
must
explicitly create a Thread
object
and hand the Runnable
object
to
the
constructor, then call
start( )
(this
happens in the constructor).
In
CBox
this
thread is called t.
Notice
the array colors,
which is an enumeration of all
the colors in class
Color.
This is used in newColor(
) to
produce a randomly
selected
color.
The current cell color is
cColor.
paintComponent(
) is
quite simple--it just sets
the color to cColor
and
fills
the entire JPanel
with
that color.
In
run( ),
you see the infinite
loop that sets the
cColor
to
a new random
color
and then calls repaint( )
to
show it. Then the
thread goes to
sleep(
) for
the amount of time specified
on the command line.
Precisely
because this design is
flexible and threading is
tied to each
JPanel
element,
you can experiment by making
as many threads as
you
want.
(In reality, there is a
restriction imposed by the
number of threads
your
JVM can comfortably
handle.)
This
program also makes an
interesting benchmark, since it
can show
dramatic
performance differences between
one JVM threading
implementation
and another.
Too
many threads
At
some point, you'll find
that ColorBoxes
bogs
down. On my machine,
this
occurred somewhere after a 10 x 10
grid. Why does this
happen?
894
Thinking
in Java
![]() You're
naturally suspicious that
Swing might have something
to do with
it,
so here's an example that
tests that premise by making
fewer threads.
The
following code is reorganized so
that an ArrayList
implements
Runnable
and
that ArrayList
holds
a number of color blocks
and
randomly
chooses ones to update. Then
a number of these ArrayList
objects
are created, depending
roughly on the grid
dimension you choose.
As
a result, you have far
fewer threads than color
blocks, so if there's a
speedup
we'll know it was because
there were too many
threads in the
previous
example:
//:
c14:ColorBoxes2.java
//
Balancing thread use.
//
<applet code=ColorBoxes2 width=600
height=500>
//
<param name=grid value="12">
//
<param name=pause value="50">
//
</applet>
import
javax.swing.*;
import
java.awt.*;
import
java.awt.event.*;
import
java.util.*;
import
com.bruceeckel.swing.*;
class
CBox2 extends JPanel {
private
static final Color[] colors = {
Color.black,
Color.blue, Color.cyan,
Color.darkGray,
Color.gray, Color.green,
Color.lightGray,
Color.magenta,
Color.orange,
Color.pink, Color.red,
Color.white,
Color.yellow
};
private
Color cColor = newColor();
private
static final Color newColor() {
return
colors[
(int)(Math.random()
* colors.length)
];
}
void
nextColor() {
cColor
= newColor();
repaint();
}
public
void paintComponent(Graphics g) {
Chapter
14: Multiple
Threads
895
![]() super.paintComponent(g);
g.setColor(cColor);
Dimension
s = getSize();
g.fillRect(0,
0, s.width, s.height);
}
}
class
CBoxList
extends
ArrayList implements Runnable {
private
Thread t;
private
int pause;
public
CBoxList(int pause) {
this.pause
= pause;
t
= new Thread(this);
}
public
void go() { t.start(); }
public
void run() {
while(true)
{
int
i = (int)(Math.random() * size());
((CBox2)get(i)).nextColor();
try
{
t.sleep(pause);
}
catch(InterruptedException e) {
System.err.println("Interrupted");
}
}
}
public
Object last() { return get(size() - 1);}
}
public
class ColorBoxes2 extends JApplet {
private
boolean isApplet = true;
private
int grid = 12;
//
Shorter default pause than ColorBoxes:
private
int pause = 50;
private
CBoxList[] v;
public
void init() {
//
Get parameters from Web page:
if
(isApplet) {
String
gsize = getParameter("grid");
if(gsize
!= null)
896
Thinking
in Java
![]() grid
= Integer.parseInt(gsize);
String
pse = getParameter("pause");
if(pse
!= null)
pause
= Integer.parseInt(pse);
}
Container
cp = getContentPane();
cp.setLayout(new
GridLayout(grid, grid));
v
= new CBoxList[grid];
for(int
i = 0; i < grid; i++)
v[i]
= new CBoxList(pause);
for
(int i = 0; i < grid * grid; i++) {
v[i
% grid].add(new CBox2());
cp.add((CBox2)v[i
% grid].last());
}
for(int
i = 0; i < grid; i++)
v[i].go();
}
public
static void main(String[] args) {
ColorBoxes2
applet = new ColorBoxes2();
applet.isApplet
= false;
if(args.length
> 0)
applet.grid
= Integer.parseInt(args[0]);
if(args.length
> 1)
applet.pause
= Integer.parseInt(args[1]);
Console.run(applet,
500, 400);
}
}
///:~
In
ColorBoxes2
an
array of CBoxList
is
created and initialized to
hold
grid
CBoxLists, each of
which knows how long to
sleep. An equal
number
of CBox2
objects
is then added to each
CBoxList,
and each list
is
told to go(
),
which starts its
thread.
CBox2
is
similar to CBox:
it paints itself with a
randomly chosen
color.
But
that's all
a
CBox2
does.
All of the threading has
been moved into
CBoxList.
The
CBoxList
could
also have inherited
Thread
and
had a member
object
of type ArrayList.
That design has the
advantage that the add(
)
and
get( )
methods
could then be given specific
argument and return
value
types instead of generic
Objects.
(Their names could also
be
Chapter
14: Multiple
Threads
897
![]() changed
to something shorter.) However,
the design used here
seemed at
first
glance to require less code.
In addition, it automatically retains
all
the
other behaviors of an ArrayList.
With all the casting
and parentheses
necessary
for get(
),
this might not be the
case as your body of
code
grows.
As
before, when you implement
Runnable
you
don't get all of
the
equipment
that comes with Thread,
so you have to create a new
Thread
and
hand yourself to its
constructor in order to have
something to
start(
),
as you can see in the
CBoxList
constructor
and in go(
).
The
run(
) method
simply chooses a random
element number within the
list
and
calls nextColor(
) for
that element to cause it to
choose a new
randomly
selected color.
Upon
running this program, you
see that it does indeed
run faster and
respond
more quickly (for instance,
when you interrupt it, it
stops more
quickly),
and it doesn't seem to bog
down as much at higher grid
sizes.
Thus,
a new factor is added into
the threading equation: you
must watch
to
see that you don't
have "too many threads"
(whatever that turns out
to
mean
for your particular program
and platform--here, the
slowdown in
ColorBoxes
appears
to be caused by the fact
that there's only one
thread
that
is responsible for all
painting, and it gets bogged
down by too many
requests).
If you have too many
threads, you must try to
use techniques
like
the one above to "balance"
the number of threads in
your program. If
you
see performance problems in a
multithreaded program you
now have
a
number of issues to
examine:
1.
Do
you have enough calls to
sleep(
),
yield( ),
and/or
wait(
)?
2.
Are
calls to sleep(
) long
enough?
3.
Are
you running too many
threads?
4.
Have
you tried different
platforms and JVMs?
Issues
like this are one
reason that multithreaded
programming is often
considered
an art.
898
Thinking
in Java
![]() Summary
It
is vital to learn when to
use multithreading and when
to avoid it. The
main
reason to use it is 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) or be
more
convenient
for the user. The
classic example of resource
balancing is using
the
CPU during I/O waits.
The classic example of user
convenience is
monitoring
a "stop" button during long
downloads.
The
main drawbacks to multithreading
are:
1.
Slowdown
while waiting for shared
resources
2.
Additional
CPU overhead required to
manage threads
3.
Unrewarded
complexity, such as the
silly idea of having a
separate
thread
to update each element of an
array
4.
Pathologies
including starving, racing,
and deadlock
An
additional advantage to threads is
that they substitute
"light"
execution
context switches (of the
order of 100 instructions)
for "heavy"
process
context switches (of the
order of 1000s 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. On the
other
hand,
a process change--the heavy
context switch--must exchange
the full
memory
space.
Threading
is like stepping into an
entirely new world and
learning a whole
new
programming language, or at least a
new set of language
concepts.
With
the appearance of thread
support in most microcomputer
operating
systems,
extensions for threads have
also been appearing in
programming
languages
or libraries. In all cases,
thread programming (1)
seems
mysterious
and requires a shift in the
way you think about
programming;
and
(2) looks similar to thread
support in other languages, so
when you
understand
threads, you understand a
common tongue. And
although
support
for threads can make
Java seem like a more
complicated
language,
don't blame Java. Threads
are tricky.
Chapter
14: Multiple
Threads
899
![]() One
of the biggest difficulties
with threads occurs because
more than one
thread
might be sharing a resource--such as
the memory in an
object--
and
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 a helpful 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
problem--at
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 point a
large number of
threads
seems to become unwieldy.
This critical point is not
in the many
thousands
as it might be with objects,
but rather in the low
hundreds,
sometimes
less than 100. 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 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. Of course, 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.
It
takes extra thought to
realize how messy things
can get.
One
thing you might notice
missing in this chapter is an
animation
example,
which is one of the most
popular things to do with
applets.
However,
a complete solution (with
sound) to this problem comes
with
the
Java JDK (available at
java.sun.com)
in the demo section. In
addition,
we
can expect better animation
support to become part of
future versions
of
Java, while completely
different non-Java, non-programming
solutions
to
animation for the Web
are appearing that will
probably be superior to
traditional
approaches. For explanations
about how Java
animation
works,
see Core
Java 2 by Horstmann
& Cornell, Prentice-Hall, 1997.
For
more
advanced discussions of threading,
see Concurrent
Programming
900
Thinking
in Java
![]() in
Java by Doug
Lea, Addison-Wesley, 1997, or
Java
Threads by Oaks
&
Wong,
O'Reilly, 1997.
Exercises
Solutions
to selected exercises can be
found in the electronic
document The
Thinking in Java
Annotated
Solution Guide, available
for a small fee from
.
1.
Inherit
a class from Thread
and
override the run(
) method.
Inside
run( ),
print a message, and then
call sleep(
).
Repeat this
three
times, then return from
run( ).
Put a start-up message
in
the
constructor and override
finalize(
) to
print a shut-down
message.
Make a separate thread class
that calls System.gc(
)
and
System.runFinalization(
) inside
run( ),
printing a
message
as it does so. Make several
thread objects of both
types
and
run them to see what
happens.
2.
Modify
Sharing2.java
to
add a synchronized
block
inside the
run(
) method
of TwoCounter
instead
of synchronizing the
entire
run( )
method.
3.
Create
two Thread
subclasses,
one with a run(
) that
starts up,
captures
the reference of the second
Thread
object
and then calls
wait(
).
The other class' run( )
should
call notifyAll(
) for
the
first
thread after some number of
seconds have passed, so the
first
thread
can print a message.
4.
In
Counter5.java
inside
Ticker2,
remove the yield(
) and
explain
the results. Replace the
yield( )
with
a sleep(
) and
explain
the results.
5.
In
ThreadGroup1.java,
replace the call to
sys.suspend(
) with
a
call to wait(
) for
the thread group, causing it
to wait for two
seconds.
For this to work correctly
you must acquire the
lock for
sys
inside
a synchronized
block.
6.
Change
Daemons.java
so
that main(
) has
a sleep(
) instead
of
a
readLine(
).
Experiment with different
sleep times to see
what
happens.
Chapter
14: Multiple
Threads
901
![]() 7.
In
Chapter 8, locate the
GreenhouseControls.java
example,
which
consists of three files. In
Event.java,
the class Event
is
based
on watching the time. Change
Event
so
that it is a Thread,
and
change the rest of the
design so that it works with
this new
Thread-based
Event.
8.
Modify
Exercise 7 so that the
java.util.Timer
class
found in JDK
1.3
is used to run the
system.
9.
Starting
with SineWave.java
from
Chapter 13, create a
program
(an
applet/application using the
Console
class)
that draws an
animated
sine wave that appears to
scrolls past the
viewing
window
like an oscilloscope, driving
the animation with a
Thread.
The speed of the animation
should be controlled with
a
java.swing.JSlider
control.
10.
Modify
Exercise 9 so that multiple
sine wave panels are
created
within
the application. The number
of sine wave panels should
be
controlled
by HTML tags or command-line
parameters.
11.
Modify
Exercise 9 so that the
java.swing.Timer
class
is used to
drive
the animation. Note the
difference between this
and
java.util.Timer.
902
Thinking
in Java
![]() 15:
Distributed
Computing
Historically,
programming across multiple
machines has
been
error-prone, difficult, and
complex.
The
programmer had to know many
details about the network
and
sometimes
even the hardware. You
usually needed to understand
the
various
"layers" of the networking
protocol, and there were a
lot of
different
functions in each different
networking library concerned
with
connecting,
packing, and unpacking
blocks of information; shipping
those
blocks
back and forth; and
handshaking. It was a daunting
task.
However,
the basic idea of
distributed computing is not so
difficult, and is
abstracted
very nicely in the Java
libraries. You want
to:
!
Get
some information from that
machine over there and
move it to
this
machine here, or vice versa.
This is accomplished with
basic
network
programming.
!
Connect to a
database, which may live
across a network. This
is
accomplished
with Java
DataBase Connectivity (JDBC),
which is
an
abstraction away from the
messy, platform-specific details
of
SQL
(the structured
query language used
for most database
transactions).
!
Provide
services via a Web server.
This is accomplished with
Java's
servlets
and
Java
Server Pages (JSPs).
!
Execute
methods on Java objects that
live on remote
machines
transparently,
as if those objects were
resident on local
machines.
This
is accomplished with Java's
Remote
Method Invocation
(RMI).
903
![]() !
Use
code written in other
languages, running on
other
architectures.
This is accomplished using
the Common
Object
Request
Broker Architecture (CORBA),
which is directly
supported
by Java.
!
Isolate
business logic from
connectivity issues,
especially
connections
with databases including
transaction management
and
security. This is accomplished
using Enterprise
JavaBeans
(EJBs).
EJBs are not actually a
distributed architecture, but
the
resulting
applications are usually
used in a networked
client-
server
system.
!
Easily,
dynamically, add and remove
devices from a
network
representing
a local system. This is
accomplished with Java's
Jini.
Each
topic will be given a light
introduction in this chapter.
Please note
that
each subject is voluminous
and by itself the subject of
entire books,
so
this chapter is only meant
to familiarize you with the
topics, not make
you
an expert (however, you can
go a long way with the
information
presented
here on network programming,
servlets and JSPs).
Network
programming
One
of Java's great strengths is
painless networking. The
Java network
library
designers have made it quite
similar to reading and
writing files,
except
that the "file" exists on a
remote machine and the
remote machine
can
decide exactly what it wants
to do about the information
you're
requesting
or sending. As much as possible,
the underlying details
of
networking
have been abstracted away
and taken care of within
the JVM
and
local machine installation of
Java. The programming model
you use is
that
of a file; in fact, you
actually wrap the network
connection (a
"socket")
with stream objects, so you
end up using the same
method calls
as
you do with all other
streams. In addition, Java's
built-in
multithreading
is exceptionally handy when
dealing with another
networking
issue: handling multiple
connections at once.
This
section introduces Java's
networking support using
easy-to-
understand
examples.
904
Thinking
in Java
![]() Identifying
a machine
Of
course, in order to tell one
machine from another and to
make sure
that
you are connected with a
particular machine, there
must be some way
of
uniquely identifying machines on a
network. Early networks
were
satisfied
to provide unique names for
machines within the local
network.
However,
Java works within the
Internet, which requires a
way to
uniquely
identify a machine from all
the others in
the world. This
is
accomplished
with the IP (Internet
Protocol) address which can
exist in
two
forms:
1.
The
familiar DNS (Domain
Name System) form. My
domain name
is
bruceeckel.com,
and if I have a computer
called Opus
in
my
domain,
its domain name would be
Opus.bruceeckel.com.
This
is
exactly the kind of name
that you use when
you send email to
people,
and is often incorporated
into a World Wide Web
address.
2.
Alternatively,
you can use the
"dotted quad" form, which is
four
numbers
separated by dots, such as
123.255.28.120.
In
both cases, the IP address
is represented internally as a 32-bit
number1
(so
each of the quad numbers
cannot exceed 255), and
you can get a
special
Java object to represent
this number from either of
the forms
above
by using the static
InetAddress.getByName( ) method
that's in
java.net.
The result is an object of
type InetAddress
that
you can use to
build
a "socket," as you will see
later.
As
a simple example of using
InetAddress.getByName(
),
consider
what
happens if you have a
dial-up Internet service
provider (ISP). Each
time
you dial up, you
are assigned a temporary IP
address. But while
you're
connected, your IP address
has the same validity as
any other IP
address
on the Internet. If someone
connects to your machine
using your
IP
address then they can
connect to a Web server or
FTP server that
you
have
running on your machine. Of
course, they need to know
your IP
1
This means a
maximum of just over four billion
numbers, which is rapidly running
out.
The
new standard for IP
addresses will use a 128-bit number,
which should produce
enough
unique IP addresses for the foreseeable
future.
Chapter
15: Distributed
Computing
905
![]() address,
and since a new one is
assigned each time you
dial up, how
can
you
find out what it
is?
The
following program uses
InetAddress.getByName(
) to
produce
your
IP address. To use it, you
must know the name of
your computer. On
Windows
95/98, go to "Settings," "Control
Panel," "Network," and
then
select
the "Identification" tab.
"Computer name" is the name
to put on the
command
line.
//:
c15:WhoAmI.java
//
Finds out your network address when
//
you're connected to the Internet.
import
java.net.*;
public
class WhoAmI {
public
static void main(String[] args)
throws
Exception {
if(args.length
!= 1) {
System.err.println(
"Usage:
WhoAmI MachineName");
System.exit(1);
}
InetAddress
a =
InetAddress.getByName(args[0]);
System.out.println(a);
}
}
///:~
In
this case, the machine is
called "peppy." So, once
I've connected to my
ISP
I run the program:
java
WhoAmI peppy
I
get back a message like
this (of course, the
address is different
each
time):
peppy/199.190.87.75
If
I tell my friend this
address and I have a Web
server running on my
computer,
he can connect to it by going to
the URL http://199.190.87.75
(only
as long as I continue to stay
connected during that
session). This can
906
Thinking
in Java
![]() sometimes
be a handy way to distribute
information to someone else,
or
to
test out a Web site
configuration before posting it to a
"real" server.
Servers
and clients
The
whole point of a network is to
allow two machines to
connect and talk
to
each other. Once the
two machines have found
each other they
can
have
a nice, two-way conversation.
But how do they find
each other? It's
like
getting lost in an amusement
park: one machine has to
stay in one
place
and listen while the
other machine says, "Hey,
where are you?"
The
machine that "stays in one
place" is called the
server,
and the one
that
seeks is called the
client.
This distinction is important
only while the
client
is trying to connect to the
server. Once they've
connected, it
becomes
a two-way communication process
and it doesn't matter
anymore
that one happened to take
the role of server and
the other
happened
to take the role of the
client.
So
the job of the server is to
listen for a connection, and
that's performed
by
the special server object
that you create. The
job of the client is to try
to
make
a connection to a server, and
this is performed by the
special client
object
you create. Once the
connection is made, you'll
see that at both
server
and client ends, the
connection is magically turned
into an I/O
stream
object, and from then on
you can treat the
connection as if you
were
reading from and writing to
a file. Thus, after the
connection is made
you
will just use the
familiar I/O commands from
Chapter 11. This is
one
of
the nice features of Java
networking.
Testing
programs without a
network
For
many reasons, you might
not have a client machine, a
server machine,
and
a network available to test
your programs. You might be
performing
exercises
in a classroom situation, or you
could be writing programs
that
aren't
yet stable enough to put
onto the network. The
creators of the
Internet
Protocol were aware of this
issue, and they created a
special
address
called localhost
to
be the "local loopback" IP
address for testing
without
a network. The generic way
to produce this address in
Java is:
InetAddress
addr = InetAddress.getByName(null);
Chapter
15: Distributed
Computing
907
![]() If
you hand getByName(
) a
null,
it defaults to using the
localhost.
The
InetAddress
is
what you use to refer to
the particular machine,
and
you
must produce this before
you can go any further.
You can't
manipulate
the contents of an InetAddress
(but
you can print them
out,
as
you'll see in the next
example). The only way
you can create an
InetAddress
is
through one of that class's
overloaded static
member
methods
getByName( )
(which
is what you'll usually
use),
getAllByName(
),
or getLocalHost(
).
You
can also produce the
local loopback address by
handing it the string
localhost:
InetAddress.getByName("localhost");
(assuming
"localhost" is configured in your
machine's "hosts" table), or
by
using
its dotted quad form to
name the reserved IP number
for the
loopback:
InetAddress.getByName("127.0.0.1");
All
three forms produce the
same result.
Port:
a unique place
within
the machine
An
IP address isn't enough to
identify a unique server,
since many servers
can
exist on one machine. Each
IP machine also contains
ports,
and when
you're
setting up a client or a server
you must choose a port
where both
client
and server agree to connect;
if you're meeting someone,
the IP
address
is the neighborhood and the
port is the bar.
The
port is not a physical
location in a machine, but a
software
abstraction
(mainly for bookkeeping
purposes). The client
program knows
how
to connect to the machine
via its IP address, but
how does it connect
to
a desired service (potentially
one of many on that
machine)? That's
where
the port numbers come in as
a second level of addressing.
The idea
is
that if you ask for a
particular port, you're
requesting the service
that's
associated
with the port number.
The time of day is a simple
example of a
service.
Typically, each service is
associated with a unique
port number on
a
given server machine. It's
up to the client to know
ahead of time which
port
number the desired service
is running on.
908
Thinking
in Java
![]() The
system services reserve the
use of ports 1 through 1024,
so you
shouldn't
use those or any other
port that you know to be in
use. The first
choice
for examples in this book
will be port 8080 (in
memory of the
venerable
old 8-bit Intel 8080
chip in my first computer, a
CP/M
machine).
Sockets
The
socket
is
the software abstraction
used to represent the
"terminals" of
a
connection between two
machines. For a given
connection, there's a
socket
on each machine, and you
can imagine a hypothetical
"cable"
running
between the two machines
with each end of the
"cable" plugged
into
a socket. Of course, the
physical hardware and
cabling between
machines
is completely unknown. The
whole point of the
abstraction is
that
we don't have to know more
than is necessary.
In
Java, you create a socket to
make the connection to the
other machine,
then
you get an InputStream
and
OutputStream
(or,
with the
appropriate
converters, Reader
and
Writer)
from the socket in order
to
be
able to treat the connection
as an I/O stream object.
There are two
stream-based
socket classes: a ServerSocket
that
a server uses to
"listen"
for incoming connections and
a Socket
that
a client uses in
order
to
initiate a connection. Once a
client makes a socket
connection, the
ServerSocket
returns
(via the accept(
) method)
a corresponding
Socket
through
which communications will
take place on the server
side.
From
then on, you have a
true Socket
to
Socket
connection
and you
treat
both ends the same
way because they are
the
same. At this point,
you
use the methods getInputStream(
) and
getOutputStream(
) to
produce
the corresponding InputStream
and
OutputStream
objects
from
each Socket.
These must be wrapped inside
buffers and
formatting
classes
just like any other
stream object described in
Chapter 11.
The
use of the term ServerSocket
would
seem to be another example
of
a
confusing naming scheme in
the Java libraries. You
might think
ServerSocket
would
be better named "ServerConnector" or
something
without
the word "Socket" in it.
You might also think
that ServerSocket
and
Socket
should
both be inherited from some
common base class.
Indeed,
the two classes do have
several methods in common,
but not
enough
to give them a common base
class. Instead, ServerSocket's
job
Chapter
15: Distributed
Computing
909
![]() is
to wait until some other
machine connects to it, then
to return an actual
Socket.
This is why ServerSocket
seems
to be a bit misnamed, since
its
job
isn't really to be a socket
but instead to make a
Socket
object
when
someone
else connects to it.
However,
the ServerSocket
does
create a physical "server" or
listening
socket
on the host machine. This
socket listens for incoming
connections
and
then returns an "established"
socket (with the local
and remote
endpoints
defined) via the accept( )
method.
The confusing part is
that
both
of these sockets (listening
and established) are
associated with the
same
server socket. The listening
socket can accept only
new connection
requests
and not data packets. So
while ServerSocket
doesn't
make
much
sense programmatically, it does
"physically."
When
you create a ServerSocket,
you give it only a port
number. You
don't
have to give it an IP address
because it's already on the
machine it
represents.
When you create a Socket,
however, you must give
both the
IP
address and the port
number where you're trying
to connect.
(However,
the Socket
that
comes back from ServerSocket.accept(
)
already
contains all this
information.)
A
simple server and
client
This
example makes the simplest
use of servers and clients
using sockets.
All
the server does is wait
for a connection, then uses
the Socket
produced
by that connection to create an
InputStream
and
OutputStream.
These are converted to a
Reader
and
a Writer,
then
wrapped
in a BufferedReader
and
a PrintWriter.
After that,
everything
it reads from the BufferedReader
it
echoes to the
PrintWriter
until
it receives the line "END,"
at which time it closes
the
connection.
The
client makes the connection
to the server, then creates
an
OutputStream
and
performs the same wrapping
as in the server.
Lines
of
text are sent through
the resulting PrintWriter.
The client also
creates
an InputStream
(again,
with appropriate conversions
and
wrapping)
to hear what the server is
saying (which, in this case,
is just the
words
echoed back).
910
Thinking
in Java
![]() Both
the server and client
use the same port
number and the client
uses
the
local loopback address to
connect to the server on the
same machine
so
you don't have to test it
over a network. (For some
configurations, you
might
need to be connected
to
a network for the programs
to work, even if
you
aren't communicating
over
that network.)
Here
is the server:
//:
c15:JabberServer.java
//
Very simple server that just
//
echoes whatever the client sends.
import
java.io.*;
import
java.net.*;
public
class JabberServer {
//
Choose a port outside of the range 1-1024:
public
static final int PORT = 8080;
public
static void main(String[] args)
throws
IOException {
ServerSocket
s = new ServerSocket(PORT);
System.out.println("Started:
" + s);
try
{
//
Blocks until a connection occurs:
Socket
socket = s.accept();
try
{
System.out.println(
"Connection
accepted: "+ socket);
BufferedReader
in =
new
BufferedReader(
new
InputStreamReader(
socket.getInputStream()));
//
Output is automatically flushed
//
by PrintWriter:
PrintWriter
out =
new
PrintWriter(
new
BufferedWriter(
new
OutputStreamWriter(
socket.getOutputStream())),true);
while
(true) {
String
str = in.readLine();
if
(str.equals("END")) break;
Chapter
15: Distributed
Computing
911
![]() System.out.println("Echoing:
" + str);
out.println(str);
}
//
Always close the two sockets...
}
finally {
System.out.println("closing...");
socket.close();
}
}
finally {
s.close();
}
}
}
///:~
You
can see that the
ServerSocket
just
needs a port number, not an
IP
address
(since it's running on
this
machine!).
When you call accept(
),
the
method blocks
until
some client tries to connect
to it. That is, it's
there
waiting
for a connection, but other
processes can run (see
Chapter 14).
When
a connection is made, accept(
) returns
with a Socket
object
representing
that connection.
The
responsibility for cleaning up
the sockets is crafted
carefully here. If
the
ServerSocket
constructor
fails, the program just
quits (notice we
must
assume that the constructor
for ServerSocket
doesn't
leave any
open
network sockets lying around
if it fails). For this case,
main(
)
throws
IOException so a try block
is not necessary. If
the
ServerSocket
constructor
is successful then all other
method calls must
be
guarded in a try-finally
block
to ensure that, no matter
how the block
is
left, the ServerSocket
is
properly closed.
The
same logic is used for
the Socket
returned
by accept(
).
If accept(
)
fails,
then we must assume that
the Socket
doesn't
exist or hold any
resources,
so it doesn't need to be cleaned
up. If it's successful,
however,
the
following statements must be in a
try-finally
block
so that if they fail
the
Socket
will
still be cleaned up. Care is
required here because
sockets
use
important nonmemory resources, so
you must be diligent in
order to
clean
them up (since there is no
destructor in Java to do it for
you).
912
Thinking
in Java
![]() Both
the ServerSocket
and
the Socket
produced
by accept(
) are
printed
to System.out.
This means that their
toString(
) methods
are
automatically
called. These
produce:
ServerSocket[addr=0.0.0.0,PORT=0,localport=8080]
Socket[addr=127.0.0.1,PORT=1077,localport=8080]
Shortly,
you'll see how these
fit together with what
the client is doing.
The
next part of the program
looks just like opening
files for reading
and
writing
except that the InputStream
and
OutputStream
are
created
from
the Socket
object.
Both the InputStream
and
OutputStream
objects
are converted to Reader
and
Writer
objects
using the
"converter"
classes InputStreamReader
and
OutputStreamWriter,
respectively.
You could also have
used the Java 1.0
InputStream
and
OutputStream
classes
directly, but with output
there's a distinct
advantage
to using the Writer
approach.
This appears with
PrintWriter,
which has an overloaded
constructor that takes a
second
argument,
a boolean
flag
that indicates whether to
automatically flush
the
output at the end of each
println( )
(but
not
print(
))
statement.
Every
time you write to out,
its buffer must be flushed
so the information
goes
out over the network.
Flushing is important for
this particular
example
because the client and
server each wait for a
line from the
other
party
before proceeding. If flushing
doesn't occur, the
information will not
be
put onto the network
until the buffer is full,
which causes lots of
problems
in this example.
When
writing network programs you
need to be careful about
using
automatic
flushing. Every time you
flush the buffer a packet
must be
created
and sent. In this case,
that's exactly what we want,
since if the
packet
containing the line isn't
sent then the handshaking
back and forth
between
server and client will
stop. Put another way,
the end of a line is
the
end of a message. But in
many cases, messages aren't
delimited by
lines
so it's much more efficient
to not use auto flushing
and instead let
the
built-in buffering decide
when to build and send a
packet. This way,
larger
packets can be sent and
the process will be
faster.
Note
that, like virtually all
streams you open, these
are buffered. There's
an
exercise at the end of this
chapter to show you what
happens if you
don't
buffer the streams (things
get slow).
Chapter
15: Distributed
Computing
913
![]() The
infinite while
loop
reads lines from the
BufferedReader
in and
writes
information to System.out
and
to the PrintWriter
out.
Note
that
in and
out could
be any streams, they just
happen to be connected to
the
network.
When
the client sends the
line consisting of "END,"
the program breaks
out
of the loop and closes
the Socket.
Here's
the client:
//:
c15:JabberClient.java
//
Very simple client that just sends
//
lines to the server and reads lines
//
that the server sends.
import
java.net.*;
import
java.io.*;
public
class JabberClient {
public
static void main(String[] args)
throws
IOException {
//
Passing null to getByName() produces the
//
special "Local Loopback" IP address, for
//
testing on one machine w/o a network:
InetAddress
addr =
InetAddress.getByName(null);
//
Alternatively, you can use
//
the address or name:
//
InetAddress addr =
//
InetAddress.getByName("127.0.0.1");
//
InetAddress addr =
//
InetAddress.getByName("localhost");
System.out.println("addr
= " + addr);
Socket
socket =
new
Socket(addr, JabberServer.PORT);
//
Guard everything in a try-finally to make
//
sure that the socket is closed:
try
{
System.out.println("socket
= " + socket);
BufferedReader
in =
new
BufferedReader(
new
InputStreamReader(
914
Thinking
in Java
![]() socket.getInputStream()));
//
Output is automatically flushed
//
by PrintWriter:
PrintWriter
out =
new
PrintWriter(
new
BufferedWriter(
new
OutputStreamWriter(
socket.getOutputStream())),true);
for(int
i = 0; i < 10; i ++) {
out.println("howdy
" + i);
String
str = in.readLine();
System.out.println(str);
}
out.println("END");
}
finally {
System.out.println("closing...");
socket.close();
}
}
}
///:~
In
main( )
you
can see all three
ways to produce the
InetAddress
of
the
local
loopback IP address: using
null,
localhost,
or the explicit
reserved
address
127.0.0.1.
Of course, if you want to
connect to a machine
across
a
network you substitute that
machine's IP address. When
the
InetAddress
addr is
printed (via the automatic
call to its toString(
)
method)
the result is:
localhost/127.0.0.1
By
handing getByName(
) a
null,
it defaulted to finding the
localhost,
and
that produced the special
address 127.0.0.1.
Note
that the Socket
called
socket
is
created with both
the
InetAddress
and
the port number. To
understand what it means
when
you
print one of these Socket
objects,
remember that an
Internet
connection
is determined uniquely by these
four pieces of data:
clientHost,
clientPortNumber,
serverHost,
and
serverPortNumber.
When the server comes
up, it takes up its
assigned
port
(8080) on the localhost
(127.0.0.1). When the client
comes up, it is
allocated
to the next available port
on its machine, 1077 in this
case,
Chapter
15: Distributed
Computing
915
![]() which
also happens to be on the
same machine (127.0.0.1) as
the server.
Now,
in order for data to move
between the client and
server, each side
has
to know where to send it.
Therefore, during the
process of connecting
to
the "known" server, the
client sends a "return
address" so the
server
knows
where to send its data.
This is what you see in
the example output
for
the server side:
Socket[addr=127.0.0.1,port=1077,localport=8080]
This
means that the server
just accepted a connection
from 127.0.0.1 on
port
1077 while listening on its
local port (8080). On the
client side:
Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]
which
means that the client
made a connection to 127.0.0.1 on
port 8080
using
the local port
1077.
You'll
notice that every time
you start up the client
anew, the local
port
number
is incremented. It starts at 1025
(one past the reserved
block of
ports)
and keeps going up until
you reboot the machine, at
which point it
starts
at 1025 again. (On UNIX
machines, once the upper
limit of the
socket
range is reached, the
numbers will wrap around to
the lowest
available
number again.)
Once
the Socket
object
has been created, the
process of turning it into
a
BufferedReader
and
PrintWriter
is
the same as in the server
(again,
in
both cases you start
with a Socket).
Here, the client initiates
the
conversation
by sending the string
"howdy" followed by a number.
Note
that
the buffer must again be
flushed (which happens
automatically via
the
second argument to the
PrintWriter
constructor).
If the buffer isn't
flushed,
the whole conversation will
hang because the initial
"howdy" will
never
get sent (the buffer
isn't full enough to cause
the send to happen
automatically).
Each line that is sent
back from the server is
written to
System.out
to
verify that everything is
working correctly. To
terminate
the
conversation, the agreed-upon
"END" is sent. If the client
simply
hangs
up, then the server
throws an exception.
You
can see that the
same care is taken here to
ensure that the
network
resources
represented by the Socket
are
properly cleaned up, using
a
try-finally
block.
916
Thinking
in Java
![]() Sockets
produce a "dedicated" connection
that persists until it is
explicitly
disconnected.
(The dedicated connection
can still be
disconnected
unexplicitly
if one side, or an intermediary
link, of the
connection
crashes.)
This means the two
parties are locked in
communication and the
connection
is constantly open. This
seems like a logical
approach to
networking,
but it puts an extra load on
the network. Later in this
chapter
you'll
see a different approach to
networking, in which the
connections
are
only temporary.
Serving
multiple clients
The
JabberServer
works,
but it can handle only
one client at a time.
In
a
typical server, you'll want
to be able to deal with many
clients at once.
The
answer is multithreading, and in
languages that don't
directly support
multithreading
this means all sorts of
complications. In Chapter 14
you
saw
that multithreading in Java is
about as simple as
possible,
considering
that multithreading is a rather
complex topic.
Because
threading
in Java is reasonably straightforward,
making a server that
handles
multiple clients is relatively
easy.
The
basic scheme is to make a
single ServerSocket
in
the server and
call
accept(
) to
wait for a new connection.
When accept(
) returns,
you take
the
resulting Socket
and
use it to create a new
thread whose job is
to
serve
that particular client. Then
you call accept(
) again
to wait for a
new
client.
In
the following server code,
you can see that it
looks similar to the
JabberServer.java
example
except that all of the
operations to serve a
particular
client have been moved
inside a separate thread
class:
//:
c15:MultiJabberServer.java
//
A server that uses multithreading
//
to handle any number of clients.
import
java.io.*;
import
java.net.*;
class
ServeOneJabber extends Thread {
private
Socket socket;
private
BufferedReader in;
private
PrintWriter out;
Chapter
15: Distributed
Computing
917
![]() public
ServeOneJabber(Socket s)
throws
IOException {
socket
= s;
in
=
new
BufferedReader(
new
InputStreamReader(
socket.getInputStream()));
//
Enable auto-flush:
out
=
new
PrintWriter(
new
BufferedWriter(
new
OutputStreamWriter(
socket.getOutputStream())),
true);
//
If any of the above calls throw an
//
exception, the caller is responsible for
//
closing the socket. Otherwise the thread
//
will close it.
start();
// Calls run()
}
public
void run() {
try
{
while
(true) {
String
str = in.readLine();
if
(str.equals("END")) break;
System.out.println("Echoing:
" + str);
out.println(str);
}
System.out.println("closing...");
}
catch(IOException e) {
System.err.println("IO
Exception");
}
finally {
try
{
socket.close();
}
catch(IOException e) {
System.err.println("Socket
not closed");
}
}
}
}
public
class MultiJabberServer {
918
Thinking
in Java
![]() static
final int PORT = 8080;
public
static void main(String[] args)
throws
IOException {
ServerSocket
s = new ServerSocket(PORT);
System.out.println("Server
Started");
try
{
while(true)
{
//
Blocks until a connection occurs:
Socket
socket = s.accept();
try
{
new
ServeOneJabber(socket);
}
catch(IOException e) {
//
If it fails, close the socket,
//
otherwise the thread will close it:
socket.close();
}
}
}
finally {
s.close();
}
}
}
///:~
The
ServeOneJabber
thread
takes the Socket
object
that's produced
by
accept( )
in
main( )
every
time a new client makes a
connection.
Then,
as before, it creates a BufferedReader
and
auto-flushed
PrintWriter
object
using the Socket.
Finally, it calls the
special
Thread
method
start(
),
which performs thread
initialization and
then
calls
run( ).
This performs the same
kind of action as in the
previous
example:
reading something from the
socket and then echoing it
back
until
it reads the special "END"
signal.
The
responsibility for cleaning up
the socket must again be
carefully
designed.
In this case, the socket is
created outside of
the
ServeOneJabber
so
the responsibility can be
shared. If the
ServeOneJabber
constructor
fails, it will just throw
the exception to the
caller,
who will then clean up
the thread. But if the
constructor succeeds,
then
the ServeOneJabber
object
takes over responsibility
for cleaning
up
the thread, in its run( ).
Chapter
15: Distributed
Computing
919
![]() Notice
the simplicity of the
MultiJabberServer.
As before, a
ServerSocket
is
created and accept(
) is
called to allow a new
connection.
But this time, the
return value of accept(
) (a
Socket)
is
passed
to the constructor for
ServeOneJabber,
which
creates a new
thread
to handle that connection.
When the connection is
terminated, the
thread
simply goes away.
If
the creation of the
ServerSocket
fails,
the exception is again
thrown
through
main(
).
But if the creation
succeeds, the outer
try-finally
guarantees
its cleanup. The inner
try-catch
guards
only against the
failure
of the ServeOneJabber
constructor;
if the constructor
succeeds,
then
the ServeOneJabber
thread
will close the associated
socket.
To
test that the server
really does handle multiple
clients, the
following
program
creates many clients (using
threads) that connect to the
same
server.
The maximum number of
threads allowed is determined by
the
final
int MAX_THREADS.
//:
c15:MultiJabberClient.java
//
Client that tests the MultiJabberServer
//
by starting up multiple clients.
import
java.net.*;
import
java.io.*;
class
JabberClientThread extends Thread {
private
Socket socket;
private
BufferedReader in;
private
PrintWriter out;
private
static int counter = 0;
private
int id = counter++;
private
static int threadcount = 0;
public
static int threadCount() {
return
threadcount;
}
public
JabberClientThread(InetAddress addr) {
System.out.println("Making
client " + id);
threadcount++;
try
{
socket
=
new
Socket(addr,
MultiJabberServer.PORT);
}
catch(IOException e) {
920
Thinking
in Java
![]() System.err.println("Socket
failed");
//
If the creation of the socket fails,
//
nothing needs to be cleaned up.
}
try
{
in
=
new
BufferedReader(
new
InputStreamReader(
socket.getInputStream()));
//
Enable auto-flush:
out
=
new
PrintWriter(
new
BufferedWriter(
new
OutputStreamWriter(
socket.getOutputStream())),
true);
start();
}
catch(IOException e) {
//
The socket should be closed on any
//
failures other than the socket
//
constructor:
try
{
socket.close();
}
catch(IOException e2) {
System.err.println("Socket
not closed");
}
}
//
Otherwise the socket will be closed by
//
the run() method of the thread.
}
public
void run() {
try
{
for(int
i = 0; i < 25; i++) {
out.println("Client
" + id + ": " + i);
String
str = in.readLine();
System.out.println(str);
}
out.println("END");
}
catch(IOException e) {
System.err.println("IO
Exception");
}
finally {
//
Always close it:
Chapter
15: Distributed
Computing
921
![]() try
{
socket.close();
}
catch(IOException e) {
System.err.println("Socket
not closed");
}
threadcount--;
// Ending this thread
}
}
}
public
class MultiJabberClient {
static
final int MAX_THREADS = 40;
public
static void main(String[] args)
throws
IOException, InterruptedException
{
InetAddress
addr =
InetAddress.getByName(null);
while(true)
{
if(JabberClientThread.threadCount()
<
MAX_THREADS)
new
JabberClientThread(addr);
Thread.currentThread().sleep(100);
}
}
}
///:~
The
JabberClientThread
constructor
takes an InetAddress
and
uses
it
to open a Socket.
You're probably starting to
see the pattern:
the
Socket
is
always used to create some
kind of Reader
and/or
Writer
(or
InputStream
and/or
OutputStream)
object, which is the only
way
that
the Socket
can
be used. (You can, of
course, write a class or two
to
automate
this process instead of
doing all the typing if it
becomes
painful.)
Again, start(
) performs
thread initialization and
calls run(
).
Here,
messages are sent to the
server and information from
the server is
echoed
to the screen. However, the
thread has a limited
lifetime and
eventually
completes. Note that the
socket is cleaned up if the
constructor
fails
after the socket is created
but before the constructor
completes.
Otherwise
the responsibility for
calling close(
) for
the socket is
relegated
to
the run(
) method.
The
threadcount
keeps
track of how many JabberClientThread
objects
currently exist. It is incremented as
part of the constructor
and
922
Thinking
in Java
![]() decremented
as run(
) exits
(which means the thread is
terminating). In
MultiJabberClient.main(
), you
can see that the
number of threads is
tested,
and if there are too
many, no more are created.
Then the method
sleeps.
This way, some threads
will eventually terminate
and more can be
created.
You can experiment with
MAX_THREADS
to
see where your
particular
system begins to have
trouble with too many
connections.
Datagrams
The
examples you've seen so far
use the Transmission
Control Protocol
(TCP,
also known as stream-based
sockets), which is
designed for
ultimate
reliability and guarantees
that the data will
get there. It allows
retransmission
of lost data, it provides
multiple paths through
different
routers
in case one goes down,
and bytes are delivered in
the order they
are
sent. All this control
and reliability comes at a
cost: TCP has a
high
overhead.
There's
a second protocol, called
User
Datagram Protocol (UDP),
which
doesn't
guarantee that the packets
will be delivered and
doesn't guarantee
that
they will arrive in the
order they were sent.
It's called an
"unreliable
protocol"
(TCP is a "reliable protocol"),
which sounds bad, but
because it's
much
faster it can be useful.
There are some applications,
such as an
audio
signal, in which it isn't so
critical if a few packets
are dropped here
or
there but speed is vital. Or
consider a time-of-day server,
where it
really
doesn't matter if one of the
messages is lost. Also, some
applications
might
be able to fire off a UDP
message to a server and can
then assume,
if
there is no response in a reasonable
period of time, that the
message
was
lost.
Typically,
you'll do most of your
direct network programming
with TCP,
and
only occasionally will you
use UDP. There's a more
complete
treatment
of UDP, including an example, in
the first edition of this
book
(available
on the CD ROM bound into
this book, or as a free
download
from
).
Using
URLs from within an applet
It's
possible for an applet to
cause the display of any
URL through the
Web
browser the applet is
running within. You can do
this with the
following
line:
Chapter
15: Distributed
Computing
923
![]() getAppletContext().showDocument(u);
in
which u
is
the URL
object.
Here's a simple example that
redirects you
to
another Web page. Although
you're just redirected to an
HTML page,
you
could also redirect to the
output of a CGI program.
//:
c15:ShowHTML.java
//
<applet code=ShowHTML width=100
height=50>
//
</applet>
import
javax.swing.*;
import
java.awt.*;
import
java.awt.event.*;
import
java.net.*;
import
java.io.*;
import
com.bruceeckel.swing.*;
public
class ShowHTML extends JApplet {
JButton
send = new JButton("Go");
JLabel
l = new JLabel();
public
void init() {
Container
cp = getContentPane();
cp.setLayout(new
FlowLayout());
send.addActionListener(new
Al());
cp.add(send);
cp.add(l);
}
class
Al implements ActionListener {
public
void actionPerformed(ActionEvent ae) {
try
{
//
This could be a CGI program instead of
//
an HTML page.
URL
u = new URL(getDocumentBase(),
"FetcherFrame.html");
//
Display the output of the URL using
//
the Web browser, as an ordinary page:
getAppletContext().showDocument(u);
}
catch(Exception e) {
l.setText(e.toString());
}
}
}
924
Thinking
in Java
![]() public
static void main(String[] args) {
Console.run(new
ShowHTML(), 100, 50);
}
}
///:~
The
beauty of the URL
class
is how much it shields you
from. You can
connect
to Web servers without
knowing much at all about
what's going
on
under the covers.
Reading
a file from the
server
A
variation on the above
program reads a file located
on the server. In
this
case,
the file is specified by the
client:
//:
c15:Fetcher.java
//
<applet code=Fetcher width=500
height=300>
//
</applet>
import
javax.swing.*;
import
java.awt.*;
import
java.awt.event.*;
import
java.net.*;
import
java.io.*;
import
com.bruceeckel.swing.*;
public
class Fetcher extends JApplet {
JButton
fetchIt= new JButton("Fetch the Data");
JTextField
f =
new
JTextField("Fetcher.java", 20);
JTextArea
t = new JTextArea(10,40);
public
void init() {
Container
cp = getContentPane();
cp.setLayout(new
FlowLayout());
fetchIt.addActionListener(new
FetchL());
cp.add(new
JScrollPane(t));
cp.add(f);
cp.add(fetchIt);
}
public
class FetchL implements ActionListener {
public
void actionPerformed(ActionEvent e) {
try
{
URL
url = new URL(getDocumentBase(),
f.getText());
t.setText(url
+ "\n");
Chapter
15: Distributed
Computing
925
![]() InputStream
is = url.openStream();
BufferedReader
in = new BufferedReader(
new
InputStreamReader(is));
String
line;
while
((line = in.readLine()) != null)
t.append(line
+ "\n");
}
catch(Exception ex) {
t.append(ex.toString());
}
}
}
public
static void main(String[] args) {
Console.run(new
Fetcher(), 500, 300);
}
}
///:~
The
creation of the URL
object
is similar to the previous
example--
getDocumentBase(
) is
the starting point as
before, but this time
the
name
of the file is read from
the JTextField.
Once the URL
object
is
created,
its String
version
is placed in the JTextArea
so
we can see what
it
looks like. Then an
InputStream
is
procured from the URL,
which in
this
case will simply produce a
stream of the characters in
the file. After
converting
to a Reader
and
buffering, each line is read
and appended to
the
JTextArea.
Note that the JTextArea
has
been placed inside a
JScrollPane
so
that scrolling is handled
automatically.
More
to networking
There's
actually a lot more to
networking than can be
covered in this
introductory
treatment. Java networking
also provides fairly
extensive
support
for URLs, including protocol
handlers for different types
of
content
that can be discovered at an
Internet site. You can
find other Java
networking
features fully and carefully
described in Java
Network
Programming
by
Elliotte Rusty Harold
(O'Reilly, 1997).
926
Thinking
in Java
![]() Java
Database
Connectivity
(JDBC)
It
has been estimated that
half of all software
development involves
client/server
operations. A great promise of
Java has been the
ability to
build
platform-independent client/server
database applications. This
has
come
to fruition with Java
DataBase Connectivity
(JDBC).
One
of the major problems with
databases has been the
feature wars
between
the database companies.
There is a "standard"
database
language,
Structured Query Language
(SQL-92), but you must
usually
know
which database vendor you're
working with despite the
standard.
JDBC
is designed to be platform-independent, so
you don't need to
worry
about
the database you're using
while you're programming.
However, it's
still
possible to make vendor-specific
calls from JDBC so you
aren't
restricted
from doing what you
must.
One
place where programmers may
need to use SQL type
names is in the
SQL
TABLE CREATE statement
when they are creating a
new database
table
and defining the SQL
type for each column.
Unfortunately there
are
significant
variations between SQL types
supported by different
database
products.
Different databases that
support SQL types with
the same
semantics
and structure may give
those types different names.
Most
major
databases support an SQL
data type for large
binary values: in
Oracle
this type is called a LONG
RAW,
Sybase calls it IMAGE,
Informix
calls
it BYTE,
and DB2 calls it LONG
VARCHAR FOR BIT DATA.
Therefore,
if database portability is a goal
you should try to use
only
generic
SQL type identifiers.
Portability
is an issue when writing for
a book where readers may
be
testing
the examples with all
kinds of unknown data
stores. I have tried
to
write
these examples to be as portable as
possible. You should also
notice
that
the database-specific code
has been isolated in order
to centralize any
changes
that you may need to
perform to get the examples
operational in
your
environment.
Chapter
15: Distributed
Computing
927
![]() JDBC,
like many of the APIs in
Java, is designed for
simplicity. The
method
calls you make correspond to
the logical operations you'd
think of
doing
when gathering data from a
database: connect to the
database,
create
a statement and execute the
query, and look at the
result set.
To
allow this platform
independence, JDBC provides a
driver
manager
that
dynamically maintains all
the driver objects that
your database
queries
will need. So if you have
three different kinds of
vendor databases
to
connect to, you'll need
three different driver
objects. The driver
objects
register
themselves with the driver
manager at the time of
loading, and
you
can force the loading
using Class.forName(
).
To
open a database, you must
create a "database URL" that
specifies:
1.
That
you're using JDBC with
"jdbc."
2.
The
"subprotocol": the name of
the driver or the name of
a
database
connectivity mechanism. Since
the design of JDBC
was
inspired
by ODBC, the first
subprotocol available is the
"jdbc-odbc
bridge,"
specified by "odbc."
3.
The
database identifier. This
varies with the database
driver used,
but
it generally provides a logical
name that is mapped by
the
database
administration software to a physical
directory where the
database
tables are located. For
your database identifier to
have
any
meaning, you must register
the name using your
database
administration
software. (The process of
registration varies
from
platform
to platform.)
All
this information is combined
into one string, the
"database URL." For
example,
to connect through the ODBC
subprotocol to a database
identified
as "people," the database
URL could be:
String
dbUrl = "jdbc:odbc:people";
If
you're connecting across a
network, the database URL
will contain the
connection
information identifying the
remote machine and can
become a
bit
intimidating. Here is an example of a
CloudScape database
being
called
from a remote client
utilizing RMI:
jdbc:rmi://192.168.170.27:1099/jdbc:cloudscape:db
928
Thinking
in Java
![]() This
database URL is really two
jdbc calls in one. The
first part
"jdbc:rmi://192.168.170.27:1099/" uses
RMI to make the
connection
to the remote database
engine listening on port
1099 at IP
Address
192.168.170.27. The second
part of the URL,
"jdbc:cloudscape:db" conveys
the more typical settings
using the
subprotocol
and database name but
this will only happen
after the first
section
has made the connection
via RMI to the remote
machine.
When
you're ready to connect to
the database, call the
static
method
DriverManager.getConnection(
) and
pass it the database URL,
the
user
name, and a password to get
into the database. You
get back a
Connection
object
that you can then
use to query and manipulate
the
database.
The
following example opens a
database of contact information
and looks
for
a person's last name as
given on the command line.
It selects only the
names
of people that have email
addresses, then prints out
all the ones
that
match the given last
name:
//:
c15:jdbc:Lookup.java
//
Looks up email addresses in a
//
local database using JDBC.
import
java.sql.*;
public
class Lookup {
public
static void main(String[] args)
throws
SQLException, ClassNotFoundException
{
String
dbUrl = "jdbc:odbc:people";
String
user = "";
String
password = "";
//
Load the driver (registers itself)
Class.forName(
"sun.jdbc.odbc.JdbcOdbcDriver");
Connection
c = DriverManager.getConnection(
dbUrl,
user, password);
Statement
s = c.createStatement();
//
SQL code:
ResultSet
r =
s.executeQuery(
"SELECT
FIRST, LAST, EMAIL " +
"FROM
people.csv people " +
Chapter
15: Distributed
Computing
929
![]() "WHERE
" +
"(LAST='"
+ args[0] + "') " +
"
AND (EMAIL Is Not Null) " +
"ORDER
BY FIRST");
while(r.next())
{
//
Capitalization doesn't matter:
System.out.println(
r.getString("Last")
+ ", "
+
r.getString("fIRST")
+
": " + r.getString("EMAIL") );
}
s.close();
// Also closes ResultSet
}
}
///:~
You
can see the creation of
the database URL as
previously described. In
this
example, there is no password
protection on the database so
the user
name
and password are empty
strings.
Once
the connection is made with
DriverManager.getConnection(
),
you
can use the resulting
Connection
object
to create a Statement
object
using the createStatement(
) method.
With the resulting
Statement,
you can call executeQuery(
),
passing in a string
containing
an SQL-92 standard SQL
statement. (You'll see
shortly how
you
can generate this statement
automatically, so you don't
have to know
much
about SQL.)
The
executeQuery(
) method
returns a ResultSet
object,
which is an
iterator:
the next(
) method
moves the iterator to the
next record in the
statement,
or returns false
if
the end of the result
set has been
reached.
You'll
always get a ResultSet
object
back from executeQuery(
) even
if
a
query results in an empty
set (that is, an exception
is not thrown). Note
that
you must call next( )
once
before trying to read any
record data. If
the
result set is empty, this
first call to next(
) will
return false.
For each
record
in the result set, you
can select the fields
using (among other
approaches)
the field name as a string.
Also note that the
capitalization of
the
field name is ignored--it
doesn't matter with an SQL
database. You
determine
the type you'll get
back by calling getInt(
),
getString(
),
getFloat(
),
etc. At this point, you've
got your database data in
Java
930
Thinking
in Java
![]() native
format and can do whatever
you want with it using
ordinary Java
code.
Getting
the example to work
With
JDBC, understanding the code
is relatively simple. The
confusing
part
is making it work on your
particular system. The
reason this is
confusing
is that it requires you to
figure out how to get
your JDBC driver
to
load properly, and how to
set up a database using your
database
administration
software.
Of
course, this process can
vary radically from machine
to machine, but
the
process I used to make it
work under 32-bit Windows
might give you
clues
to help you attack your
own situation.
Step
1: Find the JDBC
Driver
The
program above contains the
statement:
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
This
implies a directory structure,
which is deceiving. With
this particular
installation
of JDK 1.1, there was no
file called JdbcOdbcDriver.class,
so
if you looked at this
example and went searching
for it you'd be
frustrated.
Other published examples use
a pseudo name, such
as
"myDriver.ClassName,"
which is less than helpful.
In fact, the load
statement
above for the jdbc-odbc
driver (the only one
that actually comes
with
the JDK) appears in only a
few places in the online
documentation
(in
particular, a page labeled
"JDBC-ODBC Bridge Driver"). If
the load
statement
above doesn't work, then
the name might have
been changed as
part
of a Java version change, so
you should hunt through
the
documentation
again.
If
the load statement is wrong,
you'll get an exception at
this point. To test
whether
your driver load statement
is working correctly, comment
out the
code
after the statement and up
to the catch
clause;
if the program
throws
no exceptions it means that
the driver is loading
properly.
Chapter
15: Distributed
Computing
931
![]() Step
2: Configure the
database
Again,
this is specific to 32-bit
Windows; you might need to
do some
research
to figure it out for your
own platform.
First,
open the control panel.
You might find two
icons that say
"ODBC."
You
must use the one
that says "32bit ODBC,"
since the other one is
for
backward
compatibility with 16-bit
ODBC software and will
produce no
results
for JDBC. When you
open the "32bit ODBC"
icon, you'll see a
tabbed
dialog with a number of
tabs, including "User DSN,"
"System
DSN,"
"File DSN," etc., in which
"DSN" means "Data Source
Name." It
turns
out that for the
JDBC-ODBC bridge, the only
place where it's
important
to set up your database is
"System DSN," but you'll
also want to
test
your configuration and
create queries, and for
that you'll also need
to
set
up your database in "File
DSN." This will allow
the Microsoft Query
tool
(that comes with Microsoft
Office) to find the
database. Note that
other
query tools are also
available from other
vendors.
The
most interesting database is
one that you're already
using. Standard
ODBC
supports a number of different
file formats including
such
venerable
workhorses as DBase. However, it
also includes the
simple
"comma-separated
ASCII" format, which
virtually every data tool
has the
ability
to write. In my case, I just
took my "people" database
that I've been
maintaining
for years using various
contact-management tools
and
exported
it as a comma-separated ASCII file
(these typically have
an
extension
of .csv).
In the "System DSN" section
I chose "Add," chose
the
text
driver to handle my comma-separated
ASCII file, and then
un-
checked
"use current directory" to
allow me to specify the
directory where
I
exported the data
file.
You'll
notice when you do this
that you don't actually
specify a file, only
a
directory.
That's because a database is
typically represented as a
collection
of
files under a single
directory (although it could be
represented in other
forms
as well). Each file usually
contains a single table, and
the SQL
statements
can produce results that
are culled from multiple
tables in the
database
(this is called a join).
A database that contains
only a single table
(like
my "people" database) is usually
called a flat-file
database.
Most
problems
that go beyond the simple
storage and retrieval of
data generally
932
Thinking
in Java
![]() require
multiple tables that must be
related by joins to produce
the
desired
results, and these are
called relational
databases.
Step
3: Test the
configuration
To
test the configuration
you'll need a way to
discover whether the
database
is visible from a program
that queries it. Of course,
you can
simply
run the JDBC program
example above, up to and
including the
statement:
Connection
c = DriverManager.getConnection(
dbUrl,
user, password);
If
an exception is thrown, your
configuration was
incorrect.
However,
it's useful to get a
query-generation tool involved at
this point. I
used
Microsoft Query that came
with Microsoft Office, but
you might
prefer
something else. The query
tool must know where
the database is,
and
Microsoft Query required
that I go to the ODBC
Administrator's "File
DSN"
tab and add a new
entry there, again
specifying the text driver
and
the
directory where my database
lives. You can name
the entry anything
you
want, but it's helpful to
use the same name
you used in "System
DSN."
Once
you've done this, you
will see that your
database is available
when
you
create a new query using
your query tool.
Step
4: Generate your SQL
query
The
query that I created using
Microsoft Query not only
showed me that
my
database was there and in
good order, but it also
automatically created
the
SQL code that I needed to
insert into my Java program.
I wanted a
query
that would search for
records that had the
last name that was
typed
on
the command line when
starting the Java program.
So as a starting
point,
I searched for a specific
last name, "Eckel." I also
wanted to display
only
those names that had
email addresses associated
with them. The
steps
I took to create this query
were:
1.
Start
a new query and use
the Query Wizard. Select
the "people"
database.
(This is the equivalent of
opening the database
connection
using the appropriate
database URL.)
Chapter
15: Distributed
Computing
933
![]() 2.
Select
the "people" table within
the database. From within
the
table,
choose the columns FIRST,
LAST, and EMAIL.
3.
Under
"Filter Data," choose LAST
and select "equals" with
an
argument
of "Eckel." Click the "And"
radio button.
4.
Choose
EMAIL and select "Is
not Null."
5.
Under
"Sort By," choose
FIRST.
The
result of this query will
show you whether you're
getting what you
want.
Now
you can press the
SQL button and without
any research on your
part,
up
will pop the correct
SQL code, ready for
you to cut and paste.
For this
query,
it looked like this:
SELECT
people.FIRST, people.LAST,
people.EMAIL
FROM
people.csv people
WHERE
(people.LAST='Eckel') AND
(people.EMAIL
Is Not Null)
ORDER
BY people.FIRST
Especially
with more complicated
queries it's easy to get
things wrong, but
by
using a query tool you
can interactively test your
queries and
automatically
generate the correct code.
It's hard to argue the
case for
doing
this by hand.
Step
5: Modify and paste in your
query
You'll
notice that the code
above looks different from
what's used in the
program.
That's because the query
tool uses full qualification
for all of the
names,
even when there's only
one table involved. (When
more than one
table
is involved, the qualification
prevents collisions between
columns
from
different tables that have
the same names.) Since
this query involves
only
one table, you can
optionally remove the
"people" qualifier
from
most
of the names, like
this:
SELECT
FIRST, LAST, EMAIL
FROM
people.csv people
WHERE
(LAST='Eckel') AND
(EMAIL
Is Not Null)
934
Thinking
in Java
![]() ORDER
BY FIRST
In
addition, you don't want
this program to be hard
coded to look for
only
one
name. Instead, it should
hunt for the name
given as the command-
line
argument. Making these
changes and turning the
SQL statement into
a
dynamically-created String
produces:
"SELECT
FIRST, LAST, EMAIL " +
"FROM
people.csv people " +
"WHERE
" +
"(LAST='"
+ args[0] + "') " +
"
AND (EMAIL Is Not Null) " +
"ORDER
BY FIRST");
SQL
has another way to insert
names into a query called
stored
procedures,
which is used for speed.
But for much of your
database
experimentation
and for your first
cut, building your own
query strings in
Java
is fine.
You
can see from this
example that by using the
tools currently
available--
in
particular the query-building
tool--database programming with
SQL
and
JDBC can be quite
straightforward.
A
GUI version of the lookup
program
It's
more useful to leave the
lookup program running all
the time and
simply
switch to it and type in a
name whenever you want to
look
someone
up. The following program
creates the lookup program
as an
application/applet,
and it also adds name
completion so the data
will
show
up without forcing you to
type the entire last
name:
//:
c15:jdbc:VLookup.java
//
GUI version of Lookup.java.
//
<applet code=VLookup
//
width=500 height=200></applet>
import
javax.swing.*;
import
java.awt.*;
import
java.awt.event.*;
import
javax.swing.event.*;
import
java.sql.*;
Chapter
15: Distributed
Computing
935
![]() import
com.bruceeckel.swing.*;
public
class VLookup extends JApplet {
String
dbUrl = "jdbc:odbc:people";
String
user = "";
String
password = "";
Statement
s;
JTextField
searchFor = new JTextField(20);
JLabel
completion =
new
JLabel("
");
JTextArea
results = new JTextArea(40, 20);
public
void init() {
searchFor.getDocument().addDocumentListener(
new
SearchL());
JPanel
p = new JPanel();
p.add(new
Label("Last name to search for:"));
p.add(searchFor);
p.add(completion);
Container
cp = getContentPane();
cp.add(p,
BorderLayout.NORTH);
cp.add(results,
BorderLayout.CENTER);
try
{
//
Load the driver (registers itself)
Class.forName(
"sun.jdbc.odbc.JdbcOdbcDriver");
Connection
c = DriverManager.getConnection(
dbUrl,
user, password);
s
= c.createStatement();
}
catch(Exception e) {
results.setText(e.toString());
}
}
class
SearchL implements DocumentListener {
public
void changedUpdate(DocumentEvent e){}
public
void insertUpdate(DocumentEvent e){
textValueChanged();
}
public
void removeUpdate(DocumentEvent e){
textValueChanged();
}
}
936
Thinking
in Java
![]() public
void textValueChanged() {
ResultSet
r;
if(searchFor.getText().length()
== 0) {
completion.setText("");
results.setText("");
return;
}
try
{
//
Name completion:
r
= s.executeQuery(
"SELECT
LAST FROM people.csv people " +
"WHERE
(LAST Like '" +
searchFor.getText()
+
"%')
ORDER BY LAST");
if(r.next())
completion.setText(
r.getString("last"));
r
= s.executeQuery(
"SELECT
FIRST, LAST, EMAIL " +
"FROM
people.csv people " +
"WHERE
(LAST='" +
completion.getText()
+
"')
AND (EMAIL Is Not Null) " +
"ORDER
BY FIRST");
}
catch(Exception e) {
results.setText(
searchFor.getText()
+ "\n");
results.append(e.toString());
return;
}
results.setText("");
try
{
while(r.next())
{
results.append(
r.getString("Last")
+ ", "
+
r.getString("fIRST") +
":
" + r.getString("EMAIL") + "\n");
}
}
catch(Exception e) {
results.setText(e.toString());
}
Chapter
15: Distributed
Computing
937
![]() }
public
static void main(String[] args) {
Console.run(new
VLookup(), 500, 200);
}
}
///:~
Much
of the database logic is the
same, but you can
see that a
DocumentListener
is
added to listen to the
JTextField
(see
the
javax.swing.JTextField
entry
in the Java HTML
documentation from
java.sun.com
for
details), so that whenever
you type a new character
it
first
tries to do a name completion by
looking up the last name in
the
database
and using the first
one that shows up.
(It places it in the
completion
JLabel, and
uses that as the lookup
text.) This way, as
soon
as
you've typed enough
characters for the program
to uniquely find the
name
you're looking for, you
can stop.
Why
the JDBC API
seems
so complex
When
you browse the online
documentation for JDBC it
can seem
daunting.
In particular, in the DatabaseMetaData
interface--which
is
just
huge, contrary to most of
the interfaces you see in
Java--there are
methods
such as dataDefinitionCausesTransactionCommit(
),
getMaxColumnNameLength(
),
getMaxStatementLength(
),
storesMixedCaseQuotedIdentifiers(
),
supportsANSI92IntermediateSQL(
),
supportsLimitedOuterJoins(
),
and so on. What's this
all about?
As
mentioned earlier, databases
have seemed from their
inception to be in
a
constant state of turmoil,
primarily because the demand
for database
applications,
and thus database tools, is
so great. Only recently has
there
been
any convergence on the
common language of SQL (and
there are
plenty
of other database languages in
common use). But even
with an SQL
"standard"
there are so many variations
on that theme that JDBC
must
provide
the large DatabaseMetaData
interface
so that your code
can
discover
the capabilities of the
particular "standard" SQL
database that
it's
currently connected to. In
short, you can write
simple, transportable
SQL,
but if you want to optimize
speed your coding will
multiply
938
Thinking
in Java
![]() tremendously
as you investigate the
capabilities of a particular
vendor's
database.
This,
of course, is not Java's
fault. The discrepancies
between database
products
are just something that
JDBC tries to help
compensate for. But
bear
in mind that your life
will be easier if you can
either write generic
queries
and not worry quite as
much about performance, or,
if you must
tune
for performance, know the
platform you're writing for
so you don't
need
to write all that
investigation code.
A
more sophisticated example
A
more interesting
example2 involves a multitable
database that resides
on
a server. Here, the database
is meant to provide a repository
for
community
activities and to allow
people to sign up for these
events, so it
is
called the Community
Interests Database (CID).
This example will
only
provide
an overview of the database
and its implementation, and
is not
intended
to be an in-depth tutorial on database
development. There
are
numerous
books, seminars, and
software packages that will
help you in
the
design and development of a
database.
In
addition, this example
presumes the prior
installation of an SQL
database
on a server (although it could
also be run on a local
machine),
and
the interrogation and
discovery of an appropriate JDBC
driver for
that
database. Several free SQL
databases are available, and
some are
even
automatically installed with
various flavors of Linux.
You are
responsible
for making the choice of
database and locating the
JDBC
driver;
the example here is based on
an SQL database system
called
"Cloudscape."
To
keep changes in the
connection information simple,
the database
driver,
database URL, user name,
and password are placed in a
separate
class:
//:
c15:jdbc:CIDConnect.java
//
Database connection information for
//
the community interests database (CID).
2
Created by Dave
Bartlett.
Chapter
15: Distributed
Computing
939
![]() public
class CIDConnect {
//
All the information specific to
CloudScape:
public
static String dbDriver =
"COM.cloudscape.core.JDBCDriver";
public
static String dbURL =
"jdbc:cloudscape:d:/docs/_work/JSapienDB";
public
static String user = "";
public
static String password = "";
}
///:~
In
this example, there is no
password protection on the
database so the
user
name and password are
empty strings.
The
database consists of a set of
tables that have a structure
as shown
here:
EVTMEMS
EVENTS
MEM_ID
EVT_ID
EVT_ID
TITLE
(IE)
MEM_ORD
TYPE
LOC_ID
PRICE
DATETIME
MEMBERS
MEM_ID
LOCATIONS
MEM_UNAME
(AK)
LOC_ID
MEM_LNAME
(IE)
NAME
(IE)
MEM_FNAME
(IE)
CONTACT
ADDRESS
ADDRESS
CITY
CITY
STATE
STATE
ZIP
ZIP
PHONE
PHONE
EMAIL
DIRECTIONS
"Members"
contains community member
information, "Events"
and
"Locations"
contain information about
the activities and where
they take
place,
and "Evtmems" connects
events and members that
would like to
attend
that event. You can
see that a data member in
one table produces a
key
in another table.
940
Thinking
in Java
![]() The
following class contains the
SQL strings that will
create these
database
tables (refer to an SQL
guide for an explanation of
the SQL
code):
//:
c15:jdbc:CIDSQL.java
//
SQL strings to create the tables for the CID.
public
class CIDSQL {
public
static String[] sql = {
//
Create the MEMBERS table:
"drop
table MEMBERS",
"create
table MEMBERS " +
"(MEM_ID
INTEGER primary key, " +
"MEM_UNAME
VARCHAR(12) not null unique, "+
"MEM_LNAME
VARCHAR(40), " +
"MEM_FNAME
VARCHAR(20), " +
"ADDRESS
VARCHAR(40), " +
"CITY
VARCHAR(20), " +
"STATE
CHAR(4), " +
"ZIP
CHAR(5), " +
"PHONE
CHAR(12), " +
"EMAIL
VARCHAR(30))",
"create
unique index " +
"LNAME_IDX
on MEMBERS(MEM_LNAME)",
//
Create the EVENTS table
"drop
table EVENTS",
"create
table EVENTS " +
"(EVT_ID
INTEGER primary key, " +
"EVT_TITLE
VARCHAR(30) not null, " +
"EVT_TYPE
VARCHAR(20), " +
"LOC_ID
INTEGER, " +
"PRICE
DECIMAL, " +
"DATETIME
TIMESTAMP)",
"create
unique index " +
"TITLE_IDX
on EVENTS(EVT_TITLE)",
//
Create the EVTMEMS table
"drop
table EVTMEMS",
"create
table EVTMEMS " +
"(MEM_ID
INTEGER not null, " +
"EVT_ID
INTEGER not null, " +
"MEM_ORD
INTEGER)",
Chapter
15: Distributed
Computing
941
![]() "create
unique index " +
"EVTMEM_IDX
on EVTMEMS(MEM_ID, EVT_ID)",
//
Create the LOCATIONS table
"drop
table LOCATIONS",
"create
table LOCATIONS " +
"(LOC_ID
INTEGER primary key, " +
"LOC_NAME
VARCHAR(30) not null, " +
"CONTACT
VARCHAR(50), " +
"ADDRESS
VARCHAR(40), " +
"CITY
VARCHAR(20), " +
"STATE
VARCHAR(4), " +
"ZIP
VARCHAR(5), " +
"PHONE
CHAR(12), " +
"DIRECTIONS
VARCHAR(4096))",
"create
unique index " +
"NAME_IDX
on LOCATIONS(LOC_NAME)",
};
}
///:~
The
following program uses the
CIDConnect
and
CIDSQL
information
to
load the JDBC driver,
make a connection to the
database, and then
create
the table structure
diagrammed above. To connect
with the
database,
you call the static
method
DriverManager.getConnection(
),
passing it the database URL,
the
user
name, and a password to get
into the database. You
get back a
Connection
object
that you can use to
query and manipulate
the
database.
Once the connection is made
you can simply push
the SQL to
the
database, in this case by
marching through the
CIDSQL
array.
However,
the first time this
program is run, the "drop
table" command
will
fail, causing an exception,
which is caught, reported,
and then
ignored.
The reason for the
"drop table" command is to
allow easy
experimentation:
you can modify the
SQL that defines the
tables and then
rerun
the program, causing the
old tables to be replaced by
the new.
In
this example, it makes sense
to let the exceptions be
thrown out to the
console:
//:
c15:jdbc:CIDCreateTables.java
//
Creates database tables for the
//
community interests database.
import
java.sql.*;
942
Thinking
in Java
![]() public
class CIDCreateTables {
public
static void main(String[] args)
throws
SQLException,
ClassNotFoundException,
IllegalAccessException
{
//
Load the driver (registers itself)
Class.forName(CIDConnect.dbDriver);
Connection
c = DriverManager.getConnection(
CIDConnect.dbURL,
CIDConnect.user,
CIDConnect.password);
Statement
s = c.createStatement();
for(int
i = 0; i < CIDSQL.sql.length; i++) {
System.out.println(CIDSQL.sql[i]);
try
{
s.executeUpdate(CIDSQL.sql[i]);
}
catch(SQLException sqlEx) {
System.err.println(
"Probably
a 'drop table' failed");
}
}
s.close();
c.close();
}
}
///:~
Note
that all changes in the
database can be controlled by
changing
Strings
in the CIDSQL
table,
without modifying CIDCreateTables.
executeUpdate(
) will
usually return the number of
rows that were
affected
by the SQL statement.
executeUpdate(
) is
more commonly
used
to execute INSERT,
UPDATE,
or DELETE statements
that modify one
or
more rows. For statements
such as CREATE TABLE,
DROP TABLE,
and
CREATE
INDEX,
executeUpdate(
) always
returns zero.
To
test the database, it is
loaded with some sample
data. This requires a
series
of INSERTs followed by
a SELECT to produce
result set. To make
additions
and changes to the test
data easy, the test
data is set up as a
two-dimensional
array of Objects,
and the executeInsert(
) method
can
then use the information in
one row of the table to
create the
appropriate
SQL command.
Chapter
15: Distributed
Computing
943
![]() //:
c15:jdbc:LoadDB.java
//
Loads and tests the database.
import
java.sql.*;
class
TestSet {
Object[][]
data = {
{
"MEMBERS", new Integer(1),
"dbartlett",
"Bartlett", "David",
"123
Mockingbird Lane",
"Gettysburg",
"PA", "19312",
"123.456.7890",
"bart@you.net" },
{
"MEMBERS", new Integer(2),
"beckel",
"Eckel", "Bruce",
"123
Over Rainbow Lane",
"Crested
Butte", "CO", "81224",
"123.456.7890",
"beckel@you.net" },
{
"MEMBERS", new Integer(3),
"rcastaneda",
"Castaneda", "Robert",
"123
Downunder Lane",
"Sydney",
"NSW", "12345",
"123.456.7890",
"rcastaneda@you.net" },
{
"LOCATIONS", new Integer(1),
"Center
for Arts",
"Betty
Wright", "123 Elk Ave.",
"Crested
Butte", "CO", "81224",
"123.456.7890",
"Go
this way then that." },
{
"LOCATIONS", new Integer(2),
"Witts
End Conference Center",
"John
Wittig", "123 Music Drive",
"Zoneville",
"PA", "19123",
"123.456.7890",
"Go
that way then this." },
{
"EVENTS", new Integer(1),
"Project
Management Myths",
"Software
Development",
new
Integer(1), new Float(2.50),
"2000-07-17
19:30:00" },
{
"EVENTS", new Integer(2),
"Life
of the Crested Dog",
"Archeology",
944
Thinking
in Java
![]() new
Integer(2), new Float(0.00),
"2000-07-19
19:00:00" },
//
Match some people with events
{
"EVTMEMS",
new
Integer(1), // Dave is going to
new
Integer(1), // the Software event.
new
Integer(0) },
{
"EVTMEMS",
new
Integer(2), // Bruce is going to
new
Integer(2), // the Archeology event.
new
Integer(0) },
{
"EVTMEMS",
new
Integer(3), // Robert is going to
new
Integer(1), // the Software event.
new
Integer(1) },
{
"EVTMEMS",
new
Integer(3), // ... and
new
Integer(2), // the Archeology event.
new
Integer(1) },
};
//
Use the default data set:
public
TestSet() {}
//
Use a different data set:
public
TestSet(Object[][] dat) { data = dat; }
}
public
class LoadDB {
Statement
statement;
Connection
connection;
TestSet
tset;
public
LoadDB(TestSet t) throws SQLException
{
tset
= t;
try
{
//
Load the driver (registers itself)
Class.forName(CIDConnect.dbDriver);
}
catch(java.lang.ClassNotFoundException e) {
e.printStackTrace(System.err);
}
connection
= DriverManager.getConnection(
CIDConnect.dbURL,
CIDConnect.user,
CIDConnect.password);
Chapter
15: Distributed
Computing
945
![]() statement
= connection.createStatement();
}
public
void cleanup() throws SQLException {
statement.close();
connection.close();
}
public
void executeInsert(Object[] data) {
String
sql = "insert into "
+
data[0] + " values(";
for(int
i = 1; i < data.length; i++) {
if(data[i]
instanceof String)
sql
+= "'" + data[i] + "'";
else
sql
+= data[i];
if(i
< data.length - 1)
sql
+= ", ";
}
sql
+= ')';
System.out.println(sql);
try
{
statement.executeUpdate(sql);
}
catch(SQLException sqlEx) {
System.err.println("Insert
failed.");
while
(sqlEx != null) {
System.err.println(sqlEx.toString());
sqlEx
= sqlEx.getNextException();
}
}
}
public
void load() {
for(int
i = 0; i< tset.data.length; i++)
executeInsert(tset.data[i]);
}
//
Throw exceptions out to console:
public
static void main(String[] args)
throws
SQLException {
LoadDB
db = new LoadDB(new TestSet());
db.load();
try
{
//
Get a ResultSet from the loaded database:
ResultSet
rs = db.statement.executeQuery(
946
Thinking
in Java
![]() "select
" +
"e.EVT_TITLE,
m.MEM_LNAME, m.MEM_FNAME "+
"from
EVENTS e, MEMBERS m, EVTMEMS em " +
"where
em.EVT_ID = 2 " +
"and
e.EVT_ID = em.EVT_ID " +
"and
m.MEM_ID = em.MEM_ID");
while
(rs.next())
System.out.println(
rs.getString(1)
+ " " +
rs.getString(2)
+ ", " +
rs.getString(3));
}
finally {
db.cleanup();
}
}
}
///:~
The
TestSet
class
contains a default set of
data that is produced if
you
use
the default constructor;
however, you can also
create a TestSet
object
using
an alternate data set with
the second constructor. The
set of data is
held
in a two-dimensional array of Object
because
it can be any type,
including
String
or
numerical types. The
executeInsert(
) method
uses
RTTI
to distinguish between String
data
(which must be quoted)
and
non-String
data
as it builds the SQL command
from the data.
After
printing
this command to the console,
executeUpdate(
) is
used to send
it
to the database.
The
constructor for LoadDB
makes
the connection, and
load( )
steps
through
the data and calls
executeInsert(
) for
each record. cleanup(
)
closes
the statement and the
connection; to guarantee that
this is called, it
is
placed inside a finally
clause.
Once
the database is loaded, an
executeQuery(
) statement
produces a
sample
result set. Since the
query combines several
tables, it is an
example
of a join.
There
is more JDBC information
available in the electronic
documents
that
come as part of the Java
distribution from Sun. In
addition, you can
find
more in the book JDBC
Database Access with Java
(Hamilton,
Cattel,
and Fisher, Addison-Wesley,
1997). Other JDBC books
appear
regularly.
Chapter
15: Distributed
Computing
947
![]() Servlets
Client
access from the Internet or
corporate intranets is a sure
way to
allow
many users to access data
and resources easily3.
This type of access
is
based on clients using the
World Wide Web standards of
Hypertext
Markup
Language (HTML) and
Hypertext Transfer Protocol
(HTTP). The
Servlet
API set abstracts a common
solution framework for
responding to
HTTP
requests.
Traditionally,
the way to handle a problem
such as allowing an
Internet
client
to update a database is to create an
HTML page with text
fields and
a
"submit" button. The user
types the appropriate
information into the
text
fields and presses the
"submit" button. The data is
submitted along
with
a URL that tells the
server what to do with the
data by specifying
the
location
of a Common Gateway Interface
(CGI) program that the
server
runs,
providing the program with
the data as it is invoked.
The CGI
program
is typically written in Perl,
Python, C, C++, or any
language that
can
read from standard input
and write to standard
output. That's all
that
is
provided by the Web server:
the CGI program is invoked,
and standard
streams
(or, optionally for input,
an environment variable) are
used for
input
and output. The CGI program
is responsible for everything
else.
First
it looks at the data and
decides whether the format
is correct. If not,
the
CGI program must produce
HTML to describe the
problem; this page
is
handed to the Web server
(via standard output from
the CGI program),
which
sends it back to the user.
The user must usually
back up a page and
try
again. If the data is
correct, the CGI program
processes the data in
an
appropriate
way, perhaps adding it to a
database. It must then
produce an
appropriate
HTML page for the
Web server to return to the
user.
It
would be ideal to go to a completely
Java-based solution to
this
problem--an
applet on the client side to
validate and send the
data, and a
servlet
on the server side to
receive and process the
data. Unfortunately,
although
applets are a proven
technology with plenty of
support, they
have
been problematic to use on
the Web because you
cannot rely on a
3
Dave Bartlett was
instrumental in the development of this
material, and also the
JSP
section.
948
Thinking
in Java
![]() particular
version of Java being
available on a client's Web
browser; in
fact,
you can't rely on a Web
browser supporting Java at
all! In an
intranet,
you can require that
certain support be available,
which allows a
lot
more flexibility in what you
can do, but on the
Web the safest
approach
is
to handle all the processing
on the server side and
deliver plain HTML
to
the client. That way, no
client will be denied the
use of your site
because
they do not have the
proper software
installed.
Because
servlets provide an excellent
solution for
server-side
programming
support, they are one of
the most popular reasons
for
moving
to Java. Not only do they
provide a framework that
replaces CGI
programming
(and eliminates a number of
thorny CGI problems), but
all
your
code has the platform
portability gained from
using Java, and
you
have
access to all the Java
APIs (except, of course, the
ones that produce
GUIs,
like Swing).
The
basic servlet
The
architecture of the servlet
API is that of a classic
service provider with
a
service(
) method
through which all client
requests will be sent by
the
servlet
container software, and life
cycle methods init(
) and
destroy(
),
which
are called only when
the servlet is loaded and
unloaded (this
happens
rarely).
public
interface Servlet {
public
void init(ServletConfig config)
throws
ServletException;
public
ServletConfig getServletConfig();
public
void service(ServletRequest req,
ServletResponse
res)
throws
ServletException, IOException;
public
String getServletInfo();
public
void destroy();
}
getServletConfig(
)'s
sole purpose is to return a
ServletConfig
object
that
contains initialization and
startup parameters for this
servlet.
getServletInfo(
) returns
a string containing information
about the
servlet,
such as author, version, and
copyright.
Chapter
15: Distributed
Computing
949
![]() The
GenericServlet
class
is a shell implementation of this
interface and
is
typically not used. The
HttpServlet
class
is an extension of
GenericServlet
and
is designed specifically to handle
the HTTP
protocol--
HttpServlet
is
the one that you'll
use most of the
time.
The
most convenient attribute of
the servlet API is the
auxiliary objects
that
come along with the
HttpServlet class to support
it. If you look at
the
service(
) method
in the Servlet
interface,
you'll see it has
two
parameters:
ServletRequest
and
ServletResponse.
With the
HttpServlet
class
these two object are
extended for HTTP:
HttpServletRequest
and
HttpServletResponse.
Here's a simple
example
that shows the use of
HttpServletResponse:
//:
c15:servlets:ServletsRule.java
import
javax.servlet.*;
import
javax.servlet.http.*;
import
java.io.*;
public
class ServletsRule extends HttpServlet
{
int
i = 0; // Servlet "persistence"
public
void service(HttpServletRequest req,
HttpServletResponse
res) throws IOException {
res.setContentType("text/html");
PrintWriter
out = res.getWriter();
out.print("<HEAD><TITLE>");
out.print("A
server-side strategy");
out.print("</TITLE></HEAD><BODY>");
out.print("<h1>Servlets
Rule! " + i++);
out.print("</h1></BODY>");
out.close();
}
}
///:~
ServletsRule
is
about as simple as a servlet
can get. The servlet
is
initialized
only once by calling its
init( )
method,
on loading the
servlet
after
the servlet container is
first booted up. When a
client makes a
request
to a URL that happens to
represent a servlet, the
servlet container
intercepts
this request and makes a
call to the service(
) method,
after
setting
up the HttpServletRequest
and
HttpServletResponse
objects.
950
Thinking
in Java
![]() The
main responsibility of the
service(
) method
is to interact with
the
HTTP
request that the client
has sent, and to build an
HTTP response
based
on the attributes contained
within the request.
ServletsRule
only
manipulates
the response object without
looking at what the client
may
have
sent.
After
setting the content type of
the response (which must
always be done
before
the Writer
or
OutputStream
is
procured), the getWriter(
)
method
of the response object
produces a PrintWriter
object,
which is
used
for writing character-based
response data
(alternatively,
getOutputStream(
) produces
an OutputStream,
used for binary
response,
which is only utilized in
more specialized
solutions).
The
rest of the program simply
sends HTML back to the
client (it's
assumed
you understand HTML, so that
part is not explained) as
a
sequence
of Strings.
However, notice the
inclusion of the "hit
counter"
represented
by the variable i.
This is automatically converted to a
String
in
the print(
) statement.
When
you run the program,
you'll notice that the
value of i
is
retained
between
requests to the servlet.
This is an essential property of
servlets:
since
only one servlet of a
particular class is loaded
into the container,
and
it
is never unloaded (unless
the servlet container is
terminated, which is
something
that only normally happens
if you reboot the server
computer),
any
fields of that servlet class
effectively become persistent
objects! This
means
that you can effortlessly
maintain values between
servlet requests,
whereas
with CGI you had to write
values to disk in order to
preserve
them,
which required a fair amount
of fooling around to get it
right, and
resulted
in a non-cross-platform solution.
Of
course, sometimes the Web
server, and thus the
servlet container,
must
be rebooted as part of maintenance or
during a power failure.
To
avoid
losing any persistent
information, the servlet's
init( )
and
destroy(
) methods
are automatically called
whenever the servlet
is
loaded
or unloaded, giving you the
opportunity to save data
during
shutdown,
and restore it after
rebooting. The servlet
container calls the
destroy(
) method
as it is terminating itself, so you
always get an
opportunity
to save valuable data as
long as the server machine
is
configured
in an intelligent way.
Chapter
15: Distributed
Computing
951
![]() There's
one other issue when
using HttpServlet.
This class provides
doGet(
) and
doPost( )
methods
that differentiate between a CGI
"GET"
submission
from the client, and a CGI
"POST." GET and POST
vary only
in
the details of the way
that they submit the
data, which is
something
that
I personally would prefer to
ignore. However, most
published
information
that I've seen seems to
favor the creation of
separate
doGet(
) and
doPost( )
methods
instead of a single generic
service(
)
method,
which handles both cases.
This favoritism seems quite
common,
but
I've never seen it explained
in a fashion that leads me to
believe that
it's
anything more than inertia
from CGI programmers who are
used to
paying
attention to whether a GET or
POST is being used. So in
the spirit
of
"doing the simplest thing
that could possibly
work,"4 I will just use
the
service(
) method
in these examples, and let
it care about GETs
vs.
POSTs.
However, keep in mind that I
might have missed something
and
so
there may in fact be a good
reason to use doGet(
) and
doPost(
)
instead.
Whenever
a form is submitted to a servlet,
the HttpServletRequest
comes
preloaded with all the
form data, stored as
key-value pairs. If
you
know
the names of the fields,
you can just use
them directly with
the
getParameter(
) method
to look up the values. You
can also get an
Enumeration
(the
old form of the Iterator)
to the field names, as
is
shown
in the following example.
This example also
demonstrates how a
single
servlet can be used to
produce the page that
contains the form,
and
to
respond to the page (a
better solution will be seen
later, with JSPs). If
the
Enumeration
is
empty, there are no fields;
this means no form
was
submitted.
In this case, the form is
produced, and the submit
button will
re-call
the same servlet. If fields
do exist, however, they are
displayed.
//:
c15:servlets:EchoForm.java
//
Dumps the name-value pairs of any HTML form
import
javax.servlet.*;
import
javax.servlet.http.*;
import
java.io.*;
import
java.util.*;
4
A primary tenet of
Extreme Programming (XP). See
www.xprogramming.com.
952
Thinking
in Java
![]() public
class EchoForm extends HttpServlet {
public
void service(HttpServletRequest req,
HttpServletResponse
res) throws IOException {
res.setContentType("text/html");
PrintWriter
out = res.getWriter();
Enumeration
flds = req.getParameterNames();
if(!flds.hasMoreElements())
{
//
No form submitted -- create one:
out.print("<html>");
out.print("<form
method=\"POST\"" +
"
action=\"EchoForm\">");
for(int
i = 0; i < 10; i++)
out.print("<b>Field"
+ i + "</b> " +
"<input
type=\"text\""+
"
size=\"20\" name=\"Field" + i +
"\"
value=\"Value" + i +
"\"><br>");
out.print("<INPUT
TYPE=submit name=submit"+
"
Value=\"Submit\"></form></html>");
}
else {
out.print("<h1>Your
form contained:</h1>");
while(flds.hasMoreElements())
{
String
field= (String)flds.nextElement();
String
value= req.getParameter(field);
out.print(field
+ " = " + value+ "<br>");
}
}
out.close();
}
}
///:~
One
drawback you'll notice here
is that Java does not
seem to be designed
with
string processing in mind--the
formatting of the return
page is
painful
because of line breaks,
escaping quote marks, and
the "+" signs
necessary
to build String
objects.
With a larger HTML page it
becomes
unreasonable
to code it directly into
Java. One solution is to
keep the page
as
a separate text file, then
open it and hand it to the
Web server. If you
have
to perform any kind of
substitution to the contents of
the page, it's
not
much better since Java
has treated string
processing so poorly. In
these
cases you're probably better
off using a more appropriate
solution
(Python
would be my choice; there's a
version that embeds itself
in Java
called
JPython) to generate the
response page.
Chapter
15: Distributed
Computing
953
![]() Servlets
and multithreading
The
servlet container has a pool
of threads that it will
dispatch to handle
client
requests. It is quite likely
that two clients arriving at
the same time
could
be processing through your
service(
) at
the same time.
Therefore
the
service(
) method
must written in a thread-safe
manner. Any access
to
common resources (files,
databases) will need to be
guarded by using
the
synchronized
keyword.
The
following simple example
puts a synchronized
clause
around the
thread's
sleep( )
method.
This will block all
other threads until
the
| ||||||||||||||