Control invocation of a method call after new object creation

Viewed 83

I have a scheduled method call which on the scheduled time calls the following method:

private void doSomething(Map<String, String> someArguments) throws CustomException {
  MyEnum runType = getRunType(someArguments);
  switch (runType) {
        case FRUIT:
             new FruitClass().workNow();
             break;

        case VEGETABLE:
             new VegetableClass().workNow();
             break;

        default:
            // log that the type is not known 
      }
   }

The method signature of workNow is like:

workNow() throws CustomException

workNow method runs for several minutes and does some work. My issue is, when one workNow for FRUIT (or VEGETABLE) is going on and another invoke happens with same type (FRUIT for example), it creates a new FruitClass instance and starts executing its workNow parallelly.

How do I control this behavior? I want the second invocation through second object to wait until first workNow through first object is not complete.

To clarify:

  • Parallel invocation of FRUIT and VEGETABLE is fine. I want to control parallel invocation of same type. Two or more FRUIT or two or more VEGETABLE.

  • I can not make FruitClass and VegetableClass as singletons. I need some wrapping code around new to work how I want.

3 Answers

Do the synchronisation on a class object, and this will be enough to avoid creation of another class until finished:

private void doSomething(Map<String, String> someArguments) {
    MyEnum runType = getRunType(someArguments);
    switch (runType) {
        case FRUIT:
            synchronized (FruitClass.class){
                new FruitClass().workNow();
            }
            break;

        case VEGETABLE:
            synchronized (VegetableClass.class){
                new VegetableClass().workNow();
            }
            break;

        default:
            // log that the type is not known 
    }
}

synchronized on class object uses the class instance as a monitor. Class object is actually a singleton (the object representing the class metadata at runtime), and only one thread can be in this block.

Couple of solutions, I could think of :

Solution-1

static final String FRUIT = "FRUIT";
static final String VEGETABLE = "VEGETABLE";

private void doSomething(Map<String, String> someArguments) {
    MyEnum runType = getRunType(someArguments);
        switch (runType) {
            case FRUIT:
                synchronized (FRUIT){
                    new FruitClass().workNow();
                }
                break;

            case VEGETABLE:
                synchronized (VEGETABLE){
                    new VegetableClass().workNow();
                }
                break;

            default:
                // log that the type is not known 
        }
}

This might be better than using class objects, since they would be heavier and consume memory.

Solution-2

This is an enhancement to the Solution-1, incase there are multiple cases and class level Strings are not desired.

private void doSomething(Map<String, String> someArguments) {
    MyEnum runType = getRunType(someArguments);
    synchronized(runType.toString().intern()) {//This prevents 2 FRUITs or 2 VEGETABLEs from entering
        switch (runType) {
            case FRUIT:
                    new FruitClass().workNow();
                break;

            case VEGETABLE:
                    new VegetableClass().workNow();
                break;

            default:
                // log that the type is not known 
        }
    }
}

Both are tested in a slightly different example, but make the point.

There are surely many ways to address this. I believe the simplest is to use single-thread pools for each type of task:

//one pool per runType
private final ExecutorService fruitService = Executors.newSingleThreadExecutor();
private final ExecutorService vegService = Executors.newSingleThreadExecutor();

And then:

private void doSomething(Map<String, String> someArguments) {
    MyEnum runType = getRunType(someArguments);

    CompletableFuture<Void> result;

    switch (runType) {
    case FRUIT:
        result = CompletableFuture.runAsync(() -> 
                new FruitClass().workNow(), fruitService)
                .exceptionally((exception) -> {
                    if (exception instanceof CustomException) {
                        System.out.println("Failed with custom exception...");
                    }

                    return null; // returning Void
                });
        break;

    case VEGETABLE:
        result = CompletableFuture.runAsync(() -> 
                new VegetableClass().workNow(), vegService)
                .exceptionally((exception) -> {
                    if (exception instanceof CustomException) {
                        System.out.println("Failed with custom exception...");
                    }

                    return null; // returning Void
                });

        break;

    default:
        throw new RuntimeException();
    }

    result.join();
}

This simply forces concurrent calls to wait for resources and 2 tasks of the same type won't run concurrently.

It offers the additional benefit of asynchronous execution, although you can explicitly block to wait for results if needed.

Related