This issue presents tips, techniques, and sample code for the following topics:
Using Synchronized Statements
Suppose that you're experimenting with JavaTM
language threads, and you write a simple program such as the following:
public class sync extends Thread {
static int n = 1;
public void run()
{
for (int i = 1; i <= 10; i++) {
System.out.println(n);
n++;
}
}
public static void main(String args[])
{
Thread thr1 = new sync();
Thread thr2 = new sync();
thr1.start();
thr2.start();
}
}
When you run this program, you get output like the following:
1
2
3
4
5
6
7
8
9
9
10
11
13
14
15
16
17
18
19
20
Two instances of "9" occur, and none of "12" occur.
Your results may vary from this output, which is the whole point of this
example.
Such results are "impossible," because "n" is incremented
immediately after printing its value, isn't it? How can the same value be
printed twice, or another value omitted altogether?
The reason that this type of output can occur has to do with the nature
of thread programming. In the following sequence:
System.out.println(n);
n++;
with two threads executing, nothing says that these two statements
must be executed atomically (that is, without any interruption between
statements) within one thread, without the other thread gaining control.
For example, a sequence such as the following:
System.out.println(n);
System.out.println(n);
n++;
n++;
is quite possible, leading to the results show above. This problem
is basic to multithreaded programming, system-level programming in
operating systems, and so on.
To solve this problem, the example can be rewritten as follows:
static Object critsect = new Object();
public void run()
{
for (int i = 1; i <= 10; i++) {
synchronized (critsect) {
System.out.println(n);
n++;
}
}
}
This technique typically goes by the name of "critical sections,"
that is, regions of a program that can be safely executed by only one thread
at a time. In this example, a class variable named "critsect" is
set up, and used together with the "synchronized" statement to lock
the region of code within the { } against simultaneous access. This procedure
guarantees that the I/O statement and the following increment are performed
atomically for a particular thread, before another thread is allowed to
perform the same operations.
Also, another type of synchronization is the synchronized instance method.
In this method, the whole method is protected against simultaneous access,
based on obtaining a lock on the "this" reference to the particular
class instance being operated upon by the method.
Finally Clauses
You may have seen the try...catch statement used in exception handling.
With such a statement, a block of code is tried (executed), and any exceptions
thrown as a result are directed to the various catch clauses. This type of
statement also supports the use of a "finally" clause, which is a
block of code that is executed whether or not the try block completes normally,
results in an exception, or exits the containing method. A finally clause is
therefore useful as a cleanup mechanism, and try...finally can be used without
catch, and without specifically using exceptions.
To see how all this works, consider the construction of a simple method,
one that copies one file to another. The method should accept two
file names, and propagate out any IOException that occurs (rather than
trapping it internally). Here is some code that implements this idea,
along with a driver program:
import java.io.*;
public class FileCopy {
public static void copy(String from, String to)
throws IOException
{
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(from);
fos = new FileOutputStream(to);
final int BUFSIZ = 1024;
byte buf[] = new byte[BUFSIZ];
int len = 0;
while ((len = fis.read(buf)) > 0)
fos.write(buf, 0, len);
}
finally {
if (fis != null)
fis.close();
if (fos != null)
fos.close();
}
}
public static void main(String args[])
{
// do many invalid copy() operations
for (int i = 1; i <= 5000; i++) {
try {
copy("xxx.txt", ".");
}
catch (IOException e) {
}
}
// do a valid operation
try {
copy("xxx.txt", "yyy.txt");
}
catch (IOException e) {
System.err.println(e);
}
}
}
The driver exercises a particular aspect of this programming problem,
that of gracefully handling repeated errors. In this example, the error
is an invalid output file for the copy (an attempt is made to copy to
".").
The copy method does not require a try...catch statement, because it
propagates out the exception, and declares it in a throws clause. But
what happens if copy is repeatedly called, with an invalid output file
argument, after the FileInputStream for the input file has been
successfully established? What can happen is a resource leak, with
operating system file descriptors being allocated and eventually
depleted. This situation is not the same as a memory leak, but instead
simply reflects a failure to call close on the FileInputStream. Garbage
collection will result in the finalize method being called for
FileInputStream, and it calls close, but there's no guarantee that
garbage collection will be invoked in a timely way. So the second copy
invocation in the example above, one that is supplied valid arguments,
fails without the try...finally.
To fix this problem, a try...finally statement is used. No matter what
happens in copy, the finally clause will be invoked, and it will close any
open input stream. In this way, a finally clause can be used as a general
cleanup mechanism.
|