Programming Java threads in the real world, Part 9

More threads in an object-oriented world: Synchronous dispatchers, active objects, detangling console I/O

1 2 3 4 Page 3
Page 3 of 4

If the character passed to run through the Handler is a newline, the else clause on line 73 prints all the buffered characters to the console (along with the newline) and then destroys the buffer. That is, the users Map contains buffers only for those threads that are in the process of assembling a line. Once the line is flushed, the buffer is discarded.

I've also implemented the OutputStream's flush() (Listing 3, line 87) and close() (Listing 3, line 94) methods. Note that flush() flushes the partially assembled buffers for all threads to standard output.

The Test class (Listing 3, line 110) encapsulates a small test routine that creates two threads, each of which prints a message with random sleeps inserted between each character-write operation to make sure that the write requests won't be jumbled up.

Finally, a couple of style notes: First, nothing at all is synchronized (and nothing needs to be), because all write requests are executed serially on a single thread (the active object's event-loop thread). Second, whenever possible I've written code in terms of the most abstract class or interface available. For example, even though the list of actively writing threads is maintained in a HashMap, the actual reference (users [Listing 3, line 18]) is declared as a Map reference. This is just good object-oriented programming, but many Java programmers don't do it. By using the most abstract class possible, I can replace the HashMap with a different data structure (such as a TreeMap) at any time simply by changing the single new invocation on line 29. The rest of the code adapts automatically.

Listing 3: /src/com/holub/asynch/Console.java

001 002 package com.holub.asynch; 003 import java.io.*; 004 import java.util.*; 005 import com.holub.asynch.Active_object; 006 import com.holub.asynch.Mutex; 007 import com.holub.asynch.JDK_11_unloading_bug_fix; 008

/*********************************************************************** This file presents a console-output task that demonstrates how to use the Active_object class. The Console is an OutputStream that multiple threads can use to write to the console. Unlike a normal printstream(), the current class guarantees that lines print intact. (Characters from one line will not be inserted into another line.)

(c) 1999, Allen I. Holub. This code may not be distributed by yourself except in binary form, incorporated into a java .class file. You may use this code freely for personal purposes, but you may not incorporate it into any commercial product without my express written permission.

@author Allen I. Holub */

009 010 public class Console extends OutputStream 011 { 012 private Console() 013 { new JDK_11_unloading_bug_fix( Console.class ); 014 } 015 016 private static Active_object dispatcher = null; 017 private static Console the_console = null; 018 private static Map users = null; 019

/****************************************************************** A private constructor makes it impossible to create a Console using new. Use System.out() to get a reference to the Console. */

020 021 private Console(){} 022

/****************************************************************** The console is a "singleton" -- only one object is permitted to exist. The Console has a private constructor, so you cannot manufacture one with new. Get a reference to the one-and-only instance of the Console by calling Console.out().

@return a thread-safe OutputStream that you can wrap with any of the standard java.io decorators. This output stream buffers characters on a per-thread basis until a newline, sent by that thread, is encountered. The Console object then sends the entire line to the standard output as a single unit. */

023 024 public static final Console out() 025 { if( the_console == null ) 026 { synchronized( OutputStream.class ) 027 { if( the_console == null ) 028 { the_console = new Console(); 029 users = new HashMap(); 030 dispatcher = new Active_object(); 031 dispatcher.start(); 032 } 033 } 034 } 035 return the_console; 036 } 037

/******************************************************************* Shut down the Console in an orderly way. The Console uses a daemon thread to do its work, so it's not essential to shut it down explicitly, but you can call shut_down() to kill the thread in situations where you know that no more output is expected. Any characters that have been buffered, but not yet sent to the console, will be lost. You can actually call out() after shut_down(), but it's inefficient to do so. */

038 039 public static void shut_down() 040 { dispatcher.close(); 041 dispatcher = null; 042 the_console = null; 043 users = null; 044 } 045

/******************************************************************* This method overrides the OutputStream write(int) function. Use the inherited functions for all other OutputStream functionality. For a given thread, no output at all is flushed until a newline is encountered. (This behavior is accomplished using a hashtable, indexed by thread object, that contains a buffer of accumulated characters.) The entire line is flushed as a unit when the newline is encountered. Once the Console is closed, (see close), any requests to write characters are silently ignored. */

046 047 public void write(final int character) throws IOException 048 { if( character != 0 ) 049 dispatcher.dispatch( new Handler(character, Thread.currentThread()) ); 050 } 051

/******************************************************************* This class defines the request object that's sent to the Active_object. All the real work is done here. */

052 053 private class Handler implements Runnable 054 { 055 private int character; 056 private Object key; 057 058 Handler( int character, Object key ) 059 { this.character = character; 060 this.key = key; 061 } 062 063 public void run() 064 { List buffer = (List)( users.get(key) ); 065 066 if( character != '\n' ) 067 { if( buffer == null ) // first time this thread made request 068 { buffer = new Vector(); 069 users.put( key, buffer ); 070 } 071 buffer.add( new int[]{ character } ); 072 } 073 else 074 { if( buffer != null ) 075 { for( Iterator i = ((List)buffer).iterator(); i.hasNext() ; ) 076 { int c = ( (int[])( i.next() ) )[0]; 077 System.out.print( (char)c ); 078 } 079 users.remove( key ); 080 } 081 System.out.print( '\n' ); 082 } 083 } 084 } 085

/******************************************************************* This method overrides the OutputStream flush() method. All partially-buffered lines are printed. A newline is added automatically to the end of each text string. This method does not block. **/

086 087 public void flush() throws IOException 088 { Set keys = users.keySet(); 089 for( Iterator i = keys.iterator(); i.hasNext(); ) 090 dispatcher.dispatch( new Handler('\n', i.next()) ); 091 } 092

/******************************************************************* This method overrides the OutputStream close() method. Output is flushed (see flush). Subsequent output requests are silently ignored. **/

093 094 public void close() throws IOException 095 { flush(); 096 dispatcher.close(); // blocks until everything stops. 097 } 098

/******************************************************************* A convenience method, this method provides a simple way to print a string without having to wrap the Console.out() stream in a DataOutputStream. **/

099 100 public void println( final String s ) 101 { try 102 { for( int i = 0; i < s.length(); ++i ) 103 write( s.charAt(i) ); 104 write( '\n' ); 105 } 106 catch( IOException e ){ /*ignore it*/ } 107 } 108

/******************************************************************* A test class that prints two messages in parallel on two threads, with random sleeps between each character. */

109 110

static public class Test extends Thread

111 { 112

private String message;

113

private DataOutputStream data =

114 new DataOutputStream( Console.out() ); 115 116

public Test( String message )

117 { this.message = message; 118 } 119 120

public void run()

121 { try 122 { Random indeterminate_time = new Random(); 123 for(int count = 2; --count >= 0 ;) 124 { for( int i = 0; i < message.length(); ++i ) 125 { Console.out().write( message.charAt(i) ); 126 sleep( Math.abs(indeterminate_time.nextInt()) % 20 ); 127 } 128 129 Console.out().println( "(" + count + ")" ); 130 sleep( Math.abs(indeterminate_time.nextInt()) % 20 ); 131 132 data.writeChars( "[" + count + "]" ); 133 sleep( Math.abs(indeterminate_time.nextInt()) % 20 ); 134 135 Console.out().write('\n'); 136 } 137 } 138 catch( Exception e ) 139 { Console.out().println( e.toString() ); 140 } 141 } 142 143

static public void main( String[] args ) throws Exception

144 { 145 Thread t1 = new Test( "THIS MESSAGE IS FROM THREAD ONE" ); 146 Thread t2 = new Test( "this message is from thread two" ); 147 148 t1.start(); 149 t2.start(); 150 151 t1.join(); // Wait for everything to get enqueued 152 t2.join(); 153 154 Console.out().close(); // wait for everything to be printed 155 } 156 } 157 }

Wrapping things up, books, and JavaOne

So, that's it for threads. The nine parts of this series, when taken together, give you a pretty good introduction to real Java threading (as compared to the simplified version found in most books). I've covered everything from thread architectures to common problems in multithreaded systems to atomic-level synchronization classes to the architectural approach to threading discussed this month. The tools I've developed along the way provide a good foundation to a multithreaded toolbox that can make your life as a Java thread programmer much easer.

For those of you who would like to see all the material I've been discussing in one place, an expanded version of this threading series will turn into the book Taming Java Threads, to be published by Apress (see Resources).

I'll also be speaking about many of the thread-related topics I've discussed in this series at the upcoming JavaOne Worldwide Java Developer Conference on Tuesday, June 15, from 2:45 p.m.to 5:00 p.m. in Moscone Center's Hall E.

And now for something completely different...

Next month I plan to change the subject entirely, turning the discussion to object-oriented ways to implement user interfaces. Most of the RAD tools for Java programming (such as Symantec's Café, IBM's Visual Age, Microsoft's J++, and so on) produce code that isn't in the least bit object-oriented. Using these RAD tools cuts you off from many of the benefits of object-oriented design (like fast debugging and ease of maintenance). The tool-generated code is much harder to debug and maintain than that of a carefully crafted object-oriented system, and these tools really add time to the overall development cycle.

Java itself is so easy to program that by the time you've hacked up the machine-generated code to make it maintainable, you may as well have written it correctly to begin with. And if you don't take steps to make the machine-generated systems maintainable, you will loose enormous amounts of time down the road as the code has to be updated. To paraphrase Fred Brooks, author of the classic book The Mythical Man Month, there is no silver bullet that kills the werewolf of development time. Next month I'll explain this further by describing exactly what an object-oriented approach to UI design would look like. Subsequent columns will present a series of classes that will help you build true object-oriented interfaces using Java.

Related:
1 2 3 4 Page 3
Page 3 of 4