Book Home Java Distributed Computing Search this book

4.2. Making a Thread

The choice between extending the Thread class or implementing the Runnable interface with your application objects is sometimes not an obvious one. It's also usually not very important. Essentially, the difference between the two classes is that a Thread is supposed to represent how a thread of control runs (its priority level, the name for the thread), and a Runnable defines what a thread runs. In both cases, defining a subclass usually involves implementing the run() method to do whatever work you want done in the separate thread of control.

Most of the time we want to specify what runs in a thread, so in most cases you may want to implement the Runnable interface. With a Runnable subclass, you can use the same object with different types of Thread subclasses, depending on the application. You might use your implementation of Runnable inside a standard Thread in one case, and in another you might run it in a subclass of Thread that sends a notice across the network when it's started.

On the other hand, directly extending Thread can make your classes slightly easier to use. You just create one of your Thread subclasses and run it, instead of creating a Runnable subclass, putting into another Thread, and running it. Also, if your application objects are subclasses of Thread, then you can access them directly by asking the system for the current thread, or the threads in the current thread group, etc. Then you can cast the object to its subclass and call specialized methods on it, maybe to ask it how far it's gotten on whatever task you gave it.

In the next sections we'll look at how to both implement Runnable and extend Thread to make an object that executes in an independent thread. We'll return to our Solver example, making it usable in a multithreaded agent within a distributed system. The examples in this section will use fairly basic network communications, based on sockets and I/O streams, but the concepts extend pretty easily to distributed object scenarios.

4.2.1. Implementing Runnable

Suppose we wanted to make an implementation of our Solver interface (from Example 4-1) that was runnable within a thread. We may want to wrap the solver with a multithreaded server so that multiple clients can submit ProblemSets. In this case, there isn't really a compelling reason to extend the Thread class with the functionality of our Solver, since we don't have any special requirements on how the thread is run. So we would probably choose to implement the Runnable interface with the RunnableSolver class shown in Example 4-1.

Example 4-1. A Solver Runnable Within a Thread

package dcj.examples;

import java.lang.Runnable;
import java.io.*;

//
// RunnableSolver - An implementation of Solver that can be used as the
//                  the body of a Thread.
//

public class RunnableSolver implements Runnable, Solver {
  // Protected implementation variables
  protected ProblemSet currProblem = null;
  protected OutputStream clientOut = null; // Destination for solutions
  protected InputStream clientIn = null;   // Source of problems
  
  // Constructors
  public RunnableSolver(InputStream cin, OutputStream cout) {
    super();
    clientIn = cin;
    clientOut = cout;
  }
  
  public boolean Solve() {
    boolean success = true;
    SimpleCmdInputStream sin = new SimpleCmdInputStream(clientIn);
    String inStr = null;
    try {
      System.out.println("Reading from client...");
      inStr = sin.readString();
    }
    catch (IOException e) {
      System.out.println("Error reading data from client.");
      return false;
    }
    
    if (inStr.compareTo("problem") == 0) {
      try {
        inStr = sin.readString();
      }
      catch (IOException e) {
        System.out.println("Error reading data from client.");
        return false;
      }
            
      System.out.println("Got \"" + inStr + "\" from client.");
      double problem = Double.valueOf(inStr).doubleValue();
      ProblemSet p = new ProblemSet();
      p.Value(problem);
      success = Solve(p);
    }
    else {
      System.out.println("Error reading problem from client.");
      return false;
    }
    
    return success;
  }
  
  public boolean Solve(ProblemSet s) {
    boolean success = true;
    
    if (s == null) {
      System.out.println("No problem to solve.");
      return false;
    }
    
    System.out.println("Problem value = " + s.Value());
    
    // Solve problem here...
    try {
      s.Solution(Math.sqrt(s.Value()));
    }
    catch (ArithmeticException e) {
      System.out.println("Badly-formed problem.");
      success = false;
    }
    
    System.out.println("Problem solution = " + s.Solution());
    System.out.println("Sending solution to output...");
    
    // Write the solution to the designated output destination
    try {
      DataOutputStream dout = new DataOutputStream(clientOut);
      dout.writeChars("solution=" + s.Solution() + "\n");
    }
    catch (IOException e) {
      System.out.println("Error writing results to output.");
      success = false;
    }
    
    return success;
  }
  
  public boolean Problem(ProblemSet s) {
    currProblem = s;
    return true;
  }
  
  public boolean Iterations(int dummy) {
    // Not used on this solver
    return false; 
  }
  
  public void PrintResults(OutputStream os) {
    PrintStream pos = new PrintStream(os);
    pos.println("Problem solution: " + currProblem.Solution());
  }
  
  public void run() {
    Solve();
  }
}

Here are the critical features to note about the RunnableSolver in Example 4-1:

Constructor with input/output stream arguments

The constructor defined for RunnableSolver takes an InputStream and an OutputStream as arguments. These will be used by the solver to read the problem to be solved and to write out the results of the solver. The input/output streams could be attached to an active agent/client over a socket or pipe, or they might be connected to static data source/destinations like files, databases, etc.

Implementations of Solve() methods from Solver interface

The RunnableSolver implementation of Solve() first attempts to read the problem to be solved from its input stream. If successful, it calls the overridden Solve() method with the ProblemSet as the argument. The Solve(ProblemSet) implementation solves the problem, then writes the results to the solver's output stream.

Implementation of run() method from Runnable

The RunnableSolver's run() method simply calls Solve() to solve the current problem.

All together, the RunnableSolver class provides a Solver that can be created with connections to just about any kind of "client," and then wrapped with a Thread and run. The run() method calls Solve(), which reads the problem from the client, solves it, and writes the result to the client.

To demonstrate its use in action, Example 4-2 shows a RunnableSolveServer class that extends our SimpleServer class from Chapter 1, "Introduction". The RunnableSolverServer accepts connections from remote clients, and assigns a RunnableSolver to solve each client's problem. It creates a solver with the input and output streams from the socket connection to the client, then wraps the solver in a thread and starts the thread.

Example 4-2. A Server for the Runnable Solver

package dcj.examples;

import java.io.*;
import java.net.*;

class RunnableSolverServer extends SimpleServer {
  public RunnableSolverServer() { super(3000); }
  public RunnableSolverServer(int port) { super(port); }
  
  public static void main(String argv[]) {
    int port = 3000;
    if (argv.length > 0) {
      try {
        port = Integer.parseInt(argv[0]);
      }
      catch (NumberFormatException e) {
        System.err.println("Bad port number given.");
        System.err.println("   Using default port.");
      }
    }
    
    RunnableSolverServer server = new RunnableSolverServer(port);
    System.out.println("RunnableSolverServer running on port " + port);
    server.run();
  }

  // Override SimpleServer's serviceClient() method to spawn Solver threads
  // on each client connection.
  public void serviceClient(Socket clientConn) {
    InputStream inStream = null;
    OutputStream outStream = null;
    
    try {
      inStream = clientConn.getInputStream();
      outStream = clientConn.getOutputStream();
    }
    catch (IOException e) {
      System.out.println(
        "RunnableSolverServer: Error getting I/O streams.");
      System.exit(1);
    }

    RunnableSolver s = new RunnableSolver(inStream, outStream);
    Thread t = new Thread(s);
    t.start();
  }
}

Example 4-3 shows RunnableSolverClient, a sample client to the RunnableSolverServer. It simply makes a socket connection to the RunnableSolverServer's host and port, writes the problem to the socket's output stream, and waits for the answer on the input stream.

Example 4-3. A Client for the Runnable Solver

package dcj.examples;

import java.lang.*;
import java.net.*;
import java.io.*;

public class RunnableSolverClient extends SimpleClient {
  ProblemSet problem;
  
  public RunnableSolverClient(String host, int port, double pval) {
    super(host, port);
    problem = new ProblemSet();
    problem.Value(pval);
  }

  public static void main(String argv[]) {
    if (argv.length < 3) {
      System.out.println(
         "Usage: java RunnableSolverClient [host] [port] [problem]");
      System.exit(1);
    }
    
    String host = argv[0];
    int port = 3000;
    double pval = 0;
    try {
      port = Integer.parseInt(argv[1]);
      pval = Double.valueOf(argv[2]).doubleValue();
    }
    catch (NumberFormatException e) {
      System.err.println("Bad port number or problem value given.");
    }
    
    RunnableSolverClient client = 
      new RunnableSolverClient(host, port, pval);
    System.out.println("Attaching client to " + host + ":" + port + "...");
    client.run();
  }
  
  public void run() {
    try {
      OutputStreamWriter wout =
        new OutputStreamWriter(serverConn.getOutputStream());
      BufferedReader rin = new BufferedReader(
        new InputStreamReader(serverConn.getInputStream()));

      // Send a problem...
      wout.write("problem " + problem.Value() + " ");
      // ...and read the solution
      String result = rin.readLine();
    }
    catch (IOException e) {
      System.out.println("RunnableSolverClient: " + e);
      System.exit(1);
    }
  }
}

We've reused some classes from Chapter 1, "Introduction" to implement our RunnableSolverServer and RunnableSolverClient. The RunnableSolverServer is an extension of our SimpleServer, which simply overrides the serviceClient() method to attach a RunnableSolver to the client socket. The RunnableSolverClient is an extension of the SimpleClient. This allows us to use the constructor of SimpleClient to establish the socket connection to the server. All we need to do is provide a new implementation of the main() method that accepts an additional argument (the problem to be solved), and override the run() method from SimpleClient to do the required communication with the server.

4.2.2. Extending Thread

Making a Solver subclass that extends Thread requires just a few minor changes to our Runnable version. The same run() method can be used on our Thread subclass as on the RunnableSolver, but in this case it's overriding the run() from Thread rather than from Runnable.

To make our multithreaded server work with the Thread-derived Solver, we only have to change its serviceClient() implementation slightly. Rather than creating a RunnableSolver and wrapping a thread around it, a Thread-derived Solver acts as both the Solver and the thread, so we only need to create one for the incoming client, then start() it:

ThreadSolver ts = new ThreadSolver(inStream, outStream);
ts.start();

Our client will work with the Thread-derived Solver without changes. It just wants to connect to a Solver over a socket--it doesn't care if the Solver is running as a Thread, or running inside another Thread.



Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.