Increase Java Serialization Performance
Working with Java network IO is fairly straightforward. You use a combination of ServerSocket
InputStream
and OutputStream
objects, and then send data back and forth between two Java applications or Java Threads. The simplest way to send and receive a Java object is to use an ObjectOutputStream/ObjectInputStream
object pair, but is it the most efficient way? Let's experiment.
Java Network IO Summary
First, let's review the basics of Java network IO. To begin, you need to create a ServerSocket
that sits and waits for network clients to connect. A client can be any application, even a C++ application, so long as it connects on the correct IP address and port. Here's an example you can add to any Java code to create a connection listener (get the complete code listing here):
// Create a server socket in its own thread new Thread() { public void run() { try { ServerSocket clientConnect = new ServerSocket(8081); while ( true ) { Socket client = clientConnect.accept(); // blocks // A new client connected, create a listener for it Listener listener = new Listener(client); listeners.add(listener); } } catch ( Exception e ) { e.printStackTrace(); } } }.start();
In this code, a Thread
is created and started, and the work is done in the run()
method. The first step is to create a ServerSocket
on the localhost where 8081 is the port to listen on. You can use any port; I arbitrarily chose 8081. Next, a call to accept()
blocks and waits until a network client connects on the matching IP address and port. The return is a Socket
connection to the client that's used to communicate with the client. In this code, I instantiate a Listener
object, which is a class I wrote to encapsulate all network communication, and add it to a collection of listeners. This code is wrapped in a while
loop so that the ServerSocket
will always be there ready to accept each incoming connection. Next, let's dive into the actual network communication.
Using ObjectOutputStream
Before we look at the Listener
class (introduced in the code snippet above), let's look at the Sender
class I wrote that takes a Java object and sends it over the network to any clients connected:
public class Sender extends Thread { public Sender( ) { start(); } public void run() { try { Socket sender = new Socket("localhost", 8081); if ( sender != null && sender.isConnected() ) { ObjectOutputStream oos = new ObjectOutputStream( new BufferedOutputStream( sender.getOutputStream() )); Message msg = new Message(); msg.active = true; msg.userid = messages; msg.username = "User_" + messages; msg.data = this.toString(); msg.type = Message.MESSAGE_TYPE_USER; oos.writeObject(msg); oos.flush(); } } catch ( Exception e ) { e.printStackTrace(); } } }
The Sender
class extends Thread
and calls Thread.start()
in the constructor, which means each Sender
object instantiated will result in a new running thread. Within the run()
method, the first step is to create a connection to the server (the code that created the ServerSocket
above) on the write IP address and port. Next, the resulting Socket
's OutputStream
object is retrieved via a call to Socket.getOutputStream()
, and passed into the constructor of an ObjectOutputStream
object.
If you look closely, you'll see there's a BufferedOutputStream
object in there as well. Although it's not required, using buffered IO improves efficiency and performance, as an application can write to the underlying output stream without necessarily invoking a call to the underlying system for each byte written. Later, we'll examine the performance differences with and without it.
An object can be written by simply calling ObjectOutputStream.writeObject()
, followed by a call to flush()
to force the bytes to be sent out over the network. In the complete sample application, included below, the Sender
code will send 100,000 Message
objects — an arbitrary number — in order to measure the time it takes to send and receive them.
Using ObjectInputStream
On the flip side, the Listener
object uses ObjectInputStream
to listen and reconstruct Java Object
s as they arrive, as shown here:
public class Listener extends Thread { Socket client = null; public Listener( Socket client ) { this.client = client; start(); // start the Thread } public void run() { try { ObjectInputStream ois = new ObjectInputStream( new BufferedInputStream( client.getInputStream() )); Message msg = (Message)ois.readObject(); } catch ( Exception e ) { e.printStackTrace(); } } }
As with Sender
, this class extends Thread
, which results in a new running thread with each object instantiated. If you recall, this class was instantiated when a network client connects to the ServerSocket
, with the resulting Socket
connection passed into the constructor. Within the run()
method, the Socket
's InputStream
is retrieved, and passed into the constructor of ObjectInputStream
. Again, we use buffered IO (BufferedInputStream
in this case) for efficiency and performance. To read an Object
as it's sent, a call is made to ObjectInputStream.readObject()
, which blocks until a complete object arrives over the network.
This is very straightforward, and the actual code behind sending and receiving entire Java Object
s is minimal. In fact, most of the code in this example involves setting up the network connections. It's important to note that any Object
s sent via Java IO must be serializable, which is achieved by implementing the java.io.Serializable
interface. This interface doesn't contain any methods or fields; merely its presence is enough. Here's the Message
class used in this example:
public class Message implements java.io.Serializable { private static final long serialVersionUID = 1L; public static final int MESSAGE_TYPE_USER = 1; public static final int MESSAGE_TYPE_QUIT = 2; public Integer type; public String username; public Integer userid; public Boolean active; public String data; }