Date: Wed, 25 Sep 1996 17:03:10 +0100 From: Richard Beton rdb@roke.co.uk Organization: Roke Manor Research Ltd To: occam-com@ukc.ac.uk Subject: Re: Java Workshop - Internal Visit Report Status: RO
Author: Richard Beton at roke-itn Date: 25/09/96 15:42 Subject: Java Threads Workshop ------------------------------- Message Contents ---------------------------
Here is a report on the key issues presented at the Java Threads Workshop in Canterbury, September 23-24 1996.
IMPORTANT NOTE: This report was written for internal use at Roke Manor Research Ltd and is copyright Roke Manor Research Ltd. It is made public by their permission, but Roke Manor accept no liability for any use to which it is put.
Although most of the issues discussed were directly concerned with programming multi-threaded Java appplications (and applets), the principles could equally be applied to C++ multi-threaded programming, or even programming with Posix Lightweight Threads on Unix etc.
Java threads have a lifecycle state machine involving 5 essential states:
[NotStarted] | | start | | | v -------------------> [Runnable] [Running] --------------+ <------------------- | ^ yield | | | | | | | | | | | | sleep | | suspend | stop | | | | | v v v | resume +------------------------- [Blocked] [Stopped]
This is a diagram with some of the possible transitions omitted for clarity. Runnable describes a thread which is on the execution queue, but is awaiting time-slice access to the processor. Otherwise, threads are essentially either Running or Blocked.
The suspend() and resume() methods were discussed. As a rule of thumb, they should never be used because they do not involve synchronisation and can easily lead to deadlock (a thread can be infinitely suspended).
'Synchronized' methods ensure exclusive execution by only one thread at a time. This enables the atomic modificaion of object state. This is secure for use by any number of threads. Every Java object has a monitor which provides this synchronisation between threads. Before accessing a synchronized method, a thread has to wait in a queue to access the monitor for that object, unless the monitor is free.
The wait() and notify() methods also use the monitor. They are called from within the synchronized block, i.e. after the monitor has been grabbed. wait() puts the calling thread on a standby queue. notify() moves the thread at the head of the standby queue (if any) to the back of the monitor queue. It is quite easy to write code where an unlucky thread moves from queue to queue and back again without ever getting the resource it is seeking - this is 'starvation' and is usually undesirable.
A major consequence of the use of monitors is that synchronised methods are difficult to understand, and impossible to understand in isolation. Consider the CubbyHole class in the Javasoft Java Tutorial:
class CubbyHole { private int contents; private boolean available = false; public synchronized int get() { while (available == false) { try { wait(); } catch (InterruptedException e) {} } available = false; notify(); return contents; } public synchronized void put(int value) ... body of put method }
CubbyHole stores an integer; access is via the get() and put() methods. Inspection of the get() method is not helpful. The while loop appears to be infinite, because 'available' is not changed within it. It is only possible to understand the get() method by also understanding the put() method. The 'available' flag is actually changed by a different thread whilst the getting thread is waiting, but it's not easy to see that is this case (I have deliberately not included the body of put() to emphasise the point - if you haven't seen the example before, you can only guess at how it might work).
This sort of coding is what makes multi-threaded programming hard to do, and even harder to verify and maintain. Therefore, if we do it this way, it's costing us MONEY!
The solution proposed at the workshop is to construct black-box passive communication objects to do all the difficult stuff. Our application consists only of active threads which communicate with each other using passive communication objects. The active objects need not (and should not) use the wait, notify, or synchronised keywords at all.
The passive objects require careful use of wait(), notify(), etc and are very difficult to write correctly. Fortunately, these difficult passive objects need only be implemented once. I have an example of one for anyone interested.
As an aside, note that this distinction between active and passive objects is reminiscent of the similar Shlaer-Mellor distinction, except that Shlaer-Mellor were only interested in passive data storage using intelligent store objects, and they were not concerned with communication between threads per se (theirs is an analysis methodology).
Note also that the passive communications objects are essentially similar to channels in the occam sense - not surprising, since the lecture presenters have been working with the theory behind channel communication for over a decade.
As an example, consider the following code fragments. Within some 'main()' method, two threads and a channel are instantiated and connected:
... blah Channel chan = new Channel(); WriterThread a = new WriterThread (chan); ReaderThread b = new ReaderThread (chan); ... more blah
The Channel has public methods void write(Object o) and Object read() So within the WriterThread, anything can be passed to the ReaderThread, for example a WriterThread method may contain:
... blah String string = "Hello World"; chan.write (string); ... more blah
and at the reading end, within some ReaderThread method:
... blah String tellMe = chan.read(); ... more blah
N.B. Java provides run-time type checking: an exception is raised if chan.read() does not receive a String (in this case). As per usual in Java, the instanceof operator would allow code to test the received type and cast it safely.
See me for further details of ready-made channel classes. Also I have plenty of information concerning the construction of programs which you can be sure will never deadlock. If you don't reason carefully about deadlock freedom, then your multi-threaded applications can, in all probability, deadlock at some time or other. There is now a substantial body of knowledge concerning how to prevent deadlock.
Java monitors in the current release (JDK1.0.2) are known to be dreadfully slow. This is likely to improve in the future, but how much it improves is impossible to predict. Occam compilers, such as the Kent University KROC compiler, produce code for which similar thread scheduling and synchronisation operates three orders of magnitude faster (yes, 1000 times!).
Rick
-- Richard Beton BSc MInstP CPhys Roke Manor Research, Romsey, Hampshire SO51 0ZN ------- Standard disclaimer about my own views etc ------- See http://www1.roke.co.uk/WHR/WHR.html