views:

264

answers:

4

We have a set of actions or "Jobs" which we'd like to happen one at a time (not concurrently). Ie: Job A can't happen while B is happening, and you can't have two C jobs running at the same time.

In the event that a thread attempts to run a job concurrently they should get an error. We shouldn't just queue up the request.

Some jobs happen asynchronously where the user makes request, we return a status message and id, and then process job asynchronously on server.

We're looking for advice on how to handle this scenario.

One option is to lock on a shared object:

public class Global {
  public static final Object lock = new Object();
}

public class JobA {
    public void go() {
        synchronized(Global.lock) {
            //Do A stuff
        }
    }
}

public class JobB {
    public void go() {
        synchronized(Global.lock) {
            //Do B stuff
        }
    }
}

Problems with this:

  1. We would queue up concurrent requests and not return an error message (which we want to do)
  2. If JobA wants to write message to a queue to process asynchronously, how can we be assured that when the message is read from the queue by another method in JobA that the method will be able to acquire the Global.lock lock before another instance of JobA starts?

Any suggestions on a better approach?

+8  A: 

Have you considered using a SingleThreadExecutor ? As its name suggests, it uses a single thread to execute Callables (similar to Runnables - see the doc) and thus satisfies your requirement for synchronous processing. Using the java.util.concurrent classes available will simplify your threading code enormously.

To handle the scenario when submitting a job when another job is running, and generating an error, set a bounded queue and a rejection handler. See this page and section 8.3.2/8.3.3 specifically for details on ThreadPoolExecutor.setRejectedExecutionHandler().

Brian Agnew
+1 for encouraging use of the standard libraries in the JDK.
Andy Gherna
Using SingleThreadExecutor doesn't satisfy the requirement for an error when job is submitted while another is still running. It will just block and execute when the first job finishes.
Mike Q
Then used a bounded queue with max length 1 and configured to throw an exception rather than block.
hbunny
Maybe you could use a ThreadPoolExecutor, with corePoolSize=1, maximumPoolSize=1 and workQueue=new ArrayBlockingQueue(1).
Jerome
Specifically, set the rejection policy to `AbortPolicy` (http://java.sun.com/javase/6/docs/api/java/util/concurrent/ThreadPoolExecutor.AbortPolicy.html)
erickson
+6  A: 

Sounds like what you need is an exclusive lock where you can avoid blocking when trying to acquire the lock.

Fortunately the Lock interface has a tryLock method so you can do the following.

final Lock lock = new ReentrantLock();

....

void attemptJob( Runnable runnable )
{
    if (!lock.tryLock())
        throw new ConcurrentJobException();

    try
    {
       runnable.run();
    }
    finally
    {
       lock.unlock();
    }
}

If you want to run async then you should delegate the Runnable (or Callable if you prefer) to an ExecutorService and, when it completes, release the lock.

EDIT: e.g. (plus some more final declarations)

final Lock lock = new ReentrantLock();
final ExecutorService service = Executors.newSingleThreadExecutor();

....

void attemptJob( final Runnable runnable )
{
    if (!lock.tryLock())
        throw new ConcurrentJobException();

    service.execute( new Runnable()
    {
        public void run()
        {
            try
            {
                runnable.run();
            }
            finally
            {
                lock.unlock();
            } 
        }
    });
}

Don't forget to shutdown() the ExecutorService when you are done with it.

Mike Q
Thanks. Could you provide more details on async using `ExecutorService`?
Marcus
Sure, updated above with a simple example
Mike Q
A: 

Have a look at Spring Batch. I think it can help you.

duffymo
A: 

Another possibility is to use a configured ThreadPoolExecutor. I believe this will work but strongly encourage readers to test it for themselves.

Executor executor = new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>());

The above is approx what newSingleThreadExecutor does in the Executors class.

To satisfy the "error on concurrent task" requirement I think you can just change the queue implementation to a SynchronousQueue.

Executor executor = new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new SynchronousQueue<Runnable>());

This should allow one task to run and if a second if submitted before the first finishes the ThreadPoolExecutor RejectedExecutionHandler will get called, which by default throws a RejectedExecutionException.

Mike Q