Java: Selected Topics
Java now provides facilities that make it efficient in implementing
advanced applications, and even systems applications. It is not as
efficient as C, in some cases by a factor of 4 or 5. But in most cases,
with a little bit of care, much better is possible. Of course, to achieve
higher level of efficiency, especially dealing with concurrency and
networking, it is necessary to understand systems concepts as we
encountered using Unix and to know the
Java classes that express them. Here we touch on some of these classes.
Essentially all the concepts and facilities we encountered in Unix are
replicated in some form in Java. In fact Java now has classes,
for example thread pools, that represent good ways of
encapsulating system solution patterns, that can be adopted even when
programming in C.
For a deeper understanding of the way that Java achieves its high
performance you may look
here
The JVM and associated technologies have made a fundamental contribution
to our discipline since they provide an efficient virtual machine that
runs essentially everywhere. This machine could be used as target for new
languages
(for example Scala) or old languages (for example JPython), making them
portable by default.
Creating Threads
Threads can be defined by extending the Thread class or by
implementing the Runnable interface.
The former method is preferable if we plan to
extend methods of the Thread class in addition to the run method. The
latter method is preferable if that is not the case or we want to extend
some other class, not Thread.
Here is an example of extending the Thread
class, and here is the same behavior
implemented using the Runnable interface.
To extend Thread in general we do something like
class Foo extends Thread
{
.............
void run() {
// here is the code we want executed by the thread
}
.............
}
then we create an instance with
Thread x = new Foo();
and start its execution with
x.start();
which will start the new thread executing the run method.
To use Runnable we do something like
class Foo implements Runnable
{
.............
void run() {
// here is the code we want executed by the thread
}
.............
}
then we create a thread as follows
Foo y = new Foo();
Thread x = new Thread(y)
and again start its execution with
x.start();
which will start the new thread executing the run method of y.
public void interrupt() is used to interrupt a thread. It will cause the
InterruptedException or a ClosedByInterruptException if the thread was
blocked on wait, join, or sleep, or on an I/O operation. Otherwise, i.e. if the thread is running the
exception is not raised, the interrupt sets
the status of the thread to interrupted, which the thread can determine by calling the
isInterrupted() method, and the thread continues executing.
Traditional Synchronization
Traditionally synchronization in Java has been done using the
synchronized attribute in conjunction with methods or
blocks of code. Each Java object can be seen as a monitor with one
condition variable. Methods or code that are qualified with the synchonized
attribute on the same object will be executed in mutual exclusion. While
executing such code
one can relinquish control of the monitor and go to sleep by executing the
wait method, to be waken up later with a call to
notify or to notifyAll made by another
thread now executing synchronized code [notify will wake one waiting
thread, notifyAll will wake them all - but of course they will run only
one at a time].
Here is an implementation for the problem of Producers
communicating with Consumers though a Protected Bounded Buffer
using the traditional synchronized approach.
Simple Sockets for Client-Server Interaction
TCP Client-Server interaction is easily done.
On the server you create a ServerSocket (what we had called a listening socket)
ServerSocket listener = new ServerSocket(port);
then you accept a connection
Socket connected = listener.accept();
and establish the appropriate input and output streams on this socket
InputStreamReader reader = new InputStreamReader(connected.getInputStream());
OutputStreamWriter writer = new OutputStreamWriter(connected.getOutputStream());
Now you can read from reader and write to writer and then close the streams.
Here is a program that sends repeatedly buffers to a
server, and here is the server program that
sends back buffers. Both client and server print
out information on the data rate of the connection.
This program aims at sending/receiving as much data as possible using
traditional socket mechanisms.
Explicit Synchronization with Locks and Conditions
For greater flexibility Java now (Java 1.5 and later) makes available explict locks
(often called intrinsic locks)
and associated conditions.
The Lock interface is implemented by two locks,
ReentrantLock and
ReentrantReadWriteLock.
The word "reentrant" refers to the fact that these locks can be locked more than once
by the same thread, i.e. one thread can lock .. lock and then unlock .. unlock.
The word "ReadWrite" refers to a lock that implements the classic Readers and Writers
behavior (i.e. concurrent multiple readers).
The ReentrantLock has a multiplicity of methods, as you can see from its API.
Among them is public Condition newCondition() which returns a condition that will be
associated to this lock [remember our discussion of monitors and of Pthread mutexes
and conditions]. The basic methods of conditions are await, signal, and
signalAll, that
correspond directly to the wait, notify, and notifyAll which we saw with synchronized
objects.
Here is another version of the Producers and
Consumers problem using explicit locks and conditions.
LockFree Atomic Operations
Java supports the attribute volatile for variables. If we have said
volatile T x; //
It guaranties that if we write x, the write will go directly to memory, and if we
read, it will come directly from memory. Thus updates made by a thread will become
immediately visible to a concurrent thread. Beware that volatile applies to primitive
types. In the case of reference type, the volatile applies to the refernce, not to the
referenced object. Volatile does not give atomicity. In particular if we have said
volatile in x = 0;
..
x++;
the x++ operation may malfunction in a concurrent program because it is really 3
operations: read from x to a register, increment register, store to x.
For atomicity on simple variable Java gives us the package
java.util.concurrent.atomic. In it we find AtomicBoolean, AtomicInteger,
AtomicIntegerArray, AtomicLong, AtomicLongArray, AtomicReference,
AtomicReferenceArray, plus field updaters.
Let's look at AtomicInteger.
AtomicInteger x = new AtomicInteger(); // x initialez to 0
AtomicInteger y = new AtomicInteger(3);// y initialized to 3
int a = y.get(); // store in a value of y
int b = y.getAndSet(5); // b gets old value of y, 3, and y is set to 5
int c = x.addAndGet(6); // value of x is incremented by 6 and returned
// now x has value 6
int d = x.decrementAndSet(2)// value of x decremented by 2 and returned
// now x has value 4
boolean updated = x.compareAndSwap(4,11); // If the value of x is 4,
// change it to 11 and return true; otherwise
// x is unchanged and false is returned
x.set(21); // x is set to 21
All these operations are atomic and lock free. They are user mode operations, thus no
overhead for context switches to kernel.
Here is a threadsafe counter
class Counter {
private final AtomicInteger count = new AtomicInteger(0);
public int increment() {
return count.getAndIncrement();
}
}
AtomicIntegerArray and similar array types allow atomic operations on each element of the
array. AtomicIntegerFieldUpdater and similar field updaters allow atomic operations on
volatile int fields of a class.
Java NIO
Traditional Java IO works well when processing files
sequentially or communicating using blocking socket and associated threads.
For frequent random accesses to files or for communication on hundreds of
sockets it works less efficiently, and that is where the new Java IO, or
NIO, comes into play. Fundamental concepts in NIO are buffers, channels, selection keys,
and selectors. Selectors, selection keys, and channels allow us to reproduce the effect of
the select statement in Unix. Namely we can block waiting for events at a
variety of sources without needing to have a separate thread blocked
waiting for each individual event. Also, interruptible channels, when
closed will wake up any thread that might be blocked on that channel.
Also, if instead such a thread is interrupted, the channel is
automatically closed. Non-blocking IO is also supported.
Buffers
A Buffer can be created by allocation [allocate(int)], by wrapping an
existing array [wrap(byte[])], or by asking the system to allocate the
buffer outside of the JVM memory to reduce the number of copy
operations [allocateDirect(int)]. In the first two
cases the buffer will have a backing array, in the latter in may or not
do so [find out by calling hasArray()]. We can have buffers
for all primitive types except boolean. An extension of the ByteBuffer,
the MappedByteBuffer, is available to achieve the effect of Memory Mapped
IO. We will see examples of use of buffers when discussing channels.
For a buffer are defined the properties:
- capacity: the number of elements that it can contain.
- limit: it represents the high water mark allowed for
reading or writing to the buffer
- position: like the cursor in Unix files, the current
position where we can read or write
- mark: a position that can be remembered with the
mark() operation and later reestablished as current position with the
reset() call.
Among these properties holds the relation
0 <= mark <= position <= limit <= capacity
Reading and writing to/from a buffer is done using get and put operations,
which can take place relative to the current position, or at absolute
locations in the buffer identified by an index.
Initially position is 0, the limit is equal to capacity, and the mark is undefined.
The clear() operation will empty the content of the buffer, set limit to capacity,
position to 0 and mark is undefined. The flip() operation sets limit
to the current position, position to 0, and mark is undefined. It is used
to read out what was written into the buffer. The rewind() operation sets
the position to 0, the mark is undefined, and limit is unchanged. The
hasRemaining() operation
tells us if the position is less than limit (thus we can write/read something
to/from the current position).
Buffers are not threadsafe.
Channels, SelectionKeys, and Selectors
Channels, from the API: "A channel represents an open connection to an
entity such as a hardware device, a file, a network socket, or a program
component that is capable of performing one or more distinct I/O
operations, for example reading or writing. ... Channels are, in general,
intended to be safe for multithreaded access".
The channels of greatest interest are FileChannel, SocketChannel, and
ServerSocketChannel. They are all interruptible in the sense indicated
earler, that a thread blocked on this channel will receive a signal if the
channel is closed. Viceversa the channel will be closed if the thread
is interrupted. Only the latter two channels are selectable, i.e. can be
used in conjunction with a selector to achieve the effect of the Unix
select call.
Here is an example of use of ByteBuffer in
conjunction with channels.
And here is a trivial use of the
MappedByteBuffer concept.
A SelectionKey represents the registration of a selectable channel such as
SocketChannel or ServerSocketChannel, with a selector. A selection key
corresponds to one of possible operations applicable to a channel, connect,
accept, read, and write. That operation is specified when the selectable
channel is registered with a selector. For example, if server is a
ServerSocketChannel, selector is a Selector, and we want to register
with the selector the
server to wait on an accept operation, we say
server.register(selector, SelectionKey.OP_ACCEPT);
and if instead we want to register a SocketChannel client to wait on a
read operation, we say
client.register(selector, SelectionKey.OP_READ);
Before being registered with a selector the channels should be places in
non-blocking mode with
client.configureBlocking(false); and server.configureBlocking(false);
The selector had been created with
Selector selector = Selector.open();
and then, after the channels are registered, it blocks in the select call
until a significant event occurs:
selector.select();
When this call returns it means that one or more significant event among those that
were registered has occurred. We determine what are the ready channels (i.e. channels
where significant events have occurred) with
Set readyKeys = selector.selectedKeys();
Then we iterate thru the ready keys and carry out the corresponding
operations
Iterator iterator = readyKeys.iterator( );
while (iterator.hasNext( )) {
SelectionKey key = iterator.next( );
iterator.remove( );
try {
if (key.isAcceptable()) {
............
} else if (key.isReadable()) {
............
} else if (key.isWritable()) {
............
} else if (key.isConnectable()) {
............
}
} catch (Exception e) {
...........
}
Here are three programs that exemplify the use of buffers, channels,
selectors, and selectionkeys.
The first is an example from Java I/O, 2nd Edition by Elliote Rusty
Harold.
It is a concurrent server
that accepts connections and creates a thread to
write junk to the client until the client disconnects.
The second is a concurrent server from
Java NIO by Ron Hitchens that
echoes back all the data it receives using a selector to handle the concurrent
connections.
The third is also a concurrent
server again from Java NIO by Ron Hitchens
that combines the
use of a selector with a thread pool to handle the request.
It answers the question: the selector mechanisms seems ideal for a single
processor system since all is done in a single thread. But what about a
system with multiple processors? The answer is to let different threads
take care of different subsets of the ready selection keys.
Executors and Thread Pools
In last example of the previous section was used the class ThreadPool,
defined in the Java API, that represents the concept of a thread pool.
And here is code that demonstrates the related concept of
ExecutorService with a fixed size thread pool.
Java Remote Method Invocation (RMI)
Java provides an interface above the socket interface for communicating
between a client and a server, the Remote Method Invocation facility.
Here are pointers to a
tutorial and
examples