Monday, September 24, 2007

Parallel Computing in Java

I have been reading a bit about how to make our Java applications scalable. Besides standard performance techniques that one can apply to fine tune one's application, I was also trying to find out what to do if my Java application is performing at its best but it is not enough. What if one server is just not enough to perform a task in the time required by an SLA ? How to employ multiple machines to perform such a task? This is different from Clustering which has to be done at the application server level and seems more suitable for "load balancing" kind of requirement. It can be used to do multiple tasks at multiple places but cannot be used to do one task at multiple places.

Enter, Terracotta and GridGain.
While Terracotta allows you to make your objects shared accross JVM, GridGain seems to be a more pure parallel computing type of environment. Aparantly, both of these tools can be used to make your application take advantages of multiple machines.

The Approach
To understand how they work, I am going to implement at simple producer-consumer scenario where there are producers of "Jobs" and consumers that take up those Jobs. It is the standard multi-threaded producer-consumer scenario execpt that the we are going to have the consumers (and the producers as well if required) running on multiple machines instead of multiple threads on one machine. The idea is to employ multiple machines to do the jobs instead of multiple threads working on the same machine.

Let's see some code now ...

The basic producer consumer scenario -

First let's define what our producers and consumers will work on --

A Job :

//imports
public class Job {
int jobduration = (int) (Math.random()*5000);
public void run(){
try {
Thread.sleep(jobduration);
System.out.println("Job finished in " + jobduration + " millis.");
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}


Jobs - a container to hold all the jobs that need to be done :
//imports
public class Jobs {
//we can also use BlockingQueue and avoid writing our own synchronization logic
//Come to think of it, if we use BlockingQueue, we won't need Jobs class at all.
//But this is good for learning the Terracotta stuff.
private Queue list = new LinkedList();

public Job getJob() {
while(true)
{
synchronized(this)
{
try{
if(!list.isEmpty()) return list.remove();
else this.wait();
}catch(Exception e){
e.printStackTrace();
}
}
}
}

public void addJob(Job job) {
synchronized(this){
list.offer(job);
this.notifyAll();
}
}
}


Lets now look at the producer and the consumer code.

The producer :
//imports...
public class Producer extends Thread {
Jobs jobs;
public Producer(Jobs j){
jobs = j;
}

public void run(){
while(true){
try {
//sleep randomly for up to 5 seconds.
Thread.sleep((int) (Math.random()*5000));
jobs.addJob(new Job());
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
}


The consumer :
//imports...
public class Consumer extends Thread{
Jobs jobs;
public Consumer(Jobs j){
jobs = j;
}

public void run(){
while(true){
Job job = jobs.getJob();
if(job!=null) job.run();
}
}
}

In a regular, single JVM application, we would create a shared Jobs instance and create as many number of Producer and Consumer threads as we want passing them the same Jobs instance.

For example:
public class OldMain {
Jobs jobs = new Jobs();
public OldMain(){
new Producer(jobs).start();
new Consumer(jobs).start();
new Consumer(jobs).start();
}

public static void main(String[] args){
new OldMain();
}
}

Here, we are running two Consumers in the same JVM, which doesn't really add much value unless it is running on multiple CPUs. So, what we want to do is to run Consumers on multiple machines, while all picking up jobs from the same Jobs instances.

No comments: