Concurrent programming: asynchronous call to get return value

Keywords: Java Concurrent Programming

Hello, I'm Xiao Hei, a migrant worker who lives on the Internet.

Runnable

When creating a Thread, you can use the new Thread(Runnable) method to encapsulate the task code in the run() method of Runnable, submit the Runnable to the Thread as a task, or use the execute(Runnable) method of the Thread pool for processing.

public class RunnableDemo {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.submit(new MyRunnable());
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("runnable Executing");
    }
}

Runnable problem

If you have seen or written Runnable related code before, you will certainly see that Runnable cannot obtain task execution results, which is the problem of Runnable. Can you transform it to meet the use of Runnable and obtain task execution results? The answer is yes, but it will be more troublesome.

First, we cannot modify the run() method to have a return value, which violates the principle of interface implementation; We can do this in three steps:

  1. We can define variables in the user-defined Runnable to store the calculation results;
  2. Provide external methods so that external can obtain results through methods;
  3. Before the end of task execution, if the external wants to obtain the result, block it;

If you have read my previous articles, I believe that the function is not complex. For the specific implementation, you can see my following code.

public class RunnableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyRunnable<String> myRunnable = new MyRunnable<>();
        new Thread(myRunnable).start();
        System.out.println(LocalDateTime.now() + " myRunnable start-up~");
        MyRunnable.Result<String> result = myRunnable.getResult();
        System.out.println(LocalDateTime.now() + " " + result.getValue());
    }
}

class MyRunnable<T> implements Runnable {
    // Use result as the storage variable of the return value, and use volatile decoration to prevent instruction rearrangement
    private volatile Result<T> result;

    @Override
    public void run() {
        // In this process, the result will be assigned to ensure that the external thread cannot obtain it during assignment, so the lock is added
        synchronized (this) {
            try {
                TimeUnit.SECONDS.sleep(2);
                System.out.println(LocalDateTime.now() + " run Method is executing");
                result = new Result("This is the result returned");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // Wake up the waiting thread after assignment
                this.notifyAll();
            }
        }
    }
	// Method is locked, and only one thread can obtain it
    public synchronized Result<T> getResult() throws InterruptedException {
		// Loop check whether the result has been assigned
        while (result == null) {
            // If there is no assignment, wait
            this.wait();
        }
        return result;
    }
	// Wrap the result with an inner class instead of directly using T as the return result
    // It can support the case that the return value is equal to null
    static class Result<T> {
        T value;
        public Result(T value) {
            this.value = value;
        }
        public T getValue() {
            return value;
        }
    }
}

From the running results, we can see that the return results of Runnable can be obtained in the main thread.

The above code seems to meet our requirements functionally, but there are many problems of concurrency, which is not recommended in actual development. In our actual work scenario, there are many such situations. We can't customize one set every time, and it's easy to make mistakes, resulting in thread safety problems. In JDK, we have provided us with a special API to meet our requirements, which is Callable.

Callable

We use Callable to complete the accumulation function of 100-100 million mentioned above.

public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Long max = 100_000_000L;
        Long avgCount = max % 3 == 0 ? max / 3 : max / 3 + 1;
        // Store results in FutureTask
        List<FutureTask<Long>> tasks = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            Long begin = 1 + avgCount * i;
            Long end = 1 + avgCount * (i + 1);
            if (end > max) {
                end = max;
            }
            FutureTask<Long> task = new FutureTask<>(new MyCallable(begin, end));
            tasks.add(task);
            new Thread(task).start();
        }
        
        for (FutureTask<Long> task : tasks) {
            // Get task processing results from task
            System.out.println(task.get());
        }
    }
}
class MyCallable implements Callable<Long> {
    private final Long min;
    private final Long max;
    public MyCallable(Long min, Long max) {
        this.min = min;
        this.max = max;
    }
    @Override
    public Long call() {
        System.out.println("min:" + min + ",max:" + max);
        Long sum = 0L;
        for (Long i = min; i < max; i++) {
            sum = sum + i;
        }
        // Calculation results can be returned
        return sum;
    }
}

Operation results:

When creating a Thread, you can encapsulate the Callable object in the FutureTask object and hand it over to the Thread object for execution.

FutureTask can be created as a parameter of Thread because FutureTask is an implementation class of Runnable interface.

Since FutureTask is also an implementation class of the Runnable interface, there must also be a run() method. Let's see how to have a return value through the source code.

First, there is the following information in FutureTask.

public class FutureTask<V> implements RunnableFuture<V> {
    // Status of the task
    private volatile int state;
    private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;

    // Specific task object
    private Callable<V> callable;
    // The exception object returned when the task returns the result or exception
    private Object outcome; 
    // Currently running thread
    private volatile Thread runner;
	// 
    private volatile WaitNode waiters;
    private static final sun.misc.Unsafe UNSAFE;
    private static final long stateOffset;
    private static final long runnerOffset;
    private static final long waitersOffset;
}
public void run() {
    // Verification of task status
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                // Execute the call method of callable to obtain the result
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                // If there is an exception, set the return value to ex
                setException(ex);
            }
            // If there is no exception in the execution process, set the result
            if (ran)
                set(result);
        }
    } finally {
        runner = null;
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

The core logic in this method is to execute the call() method of callable, assign the result, and encapsulate the exception if there is an exception.

Then let's look at how the get method gets the result.

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        // There will be a jam waiting here
        s = awaitDone(false, 0L);
    // Return results
    return report(s);
}
private V report(int s) throws ExecutionException {
    Object x = outcome;
    if (s == NORMAL)
        return (V)x;
    if (s >= CANCELLED)
        // An exception will be thrown if the status is abnormal
        throw new CancellationException();
    throw new ExecutionException((Throwable)x);
}

In FutureTask, there are some other methods besides get() method.

  • get(timeout,unit): get the result, but only wait for the specified time;

  • cancel(boolean mayInterruptIfRunning): cancels the current task;

  • isDone(): judge whether the task has been completed.

CompletableFuture

When using FutureTask to complete asynchronous tasks and obtain results through the get() method, the thread that obtains the results will enter blocking waiting. This method is not the most ideal state.

In JDK8, completabilefuture is introduced to improve the Future. When defining the completabilefuture, the callback object can be passed in. When the task is completed or abnormal, it will be called back automatically.

public class CompletableFutureDemo {
    public static void main(String[] args) throws InterruptedException {
        // Supplier object passed in when creating completabilefuture
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(new MySupplier());
        //When the execution is successful
        future.thenAccept(new MyConsumer());
        // Execution exception
        future.exceptionally(new MyFunction());
        // The main task can continue to be processed without waiting for the task to be executed
        System.out.println("The main thread continues execution");
        Thread.sleep(5000);
        System.out.println("End of main thread execution");
    }
}

class MySupplier implements Supplier<Integer> {
    @Override
    public Integer get() {
        try {
            // Task sleep 3s
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return 3 + 2;
    }
}
// Callback the Consumer object when the task is completed
class MyConsumer implements Consumer<Integer> {
    @Override
    public void accept(Integer integer) {
        System.out.println("results of enforcement" + integer);
    }
}
// Callback Function object when task execution is abnormal
class MyFunction implements Function<Throwable, Integer> {
    @Override
    public Integer apply(Throwable type) {
        System.out.println("Execution exception" + type);
        return 0;
    }
}

The above code can be simplified by lambda expressions.

public class CompletableFutureDemo {
    public static void main(String[] args) throws InterruptedException {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            try {
                // Task sleep 3s
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return 3 + 2;
        });
        //When the execution is successful
        future.thenAccept((x) -> {
            System.out.println("results of enforcement" + x);
        });
        future.exceptionally((type) -> {
            System.out.println("Execution exception" + type);
            return 0;
        });
        System.out.println("The main thread continues execution");
        Thread.sleep(5000);
        System.out.println("End of main thread execution");
    }
}

Through the example, we find the advantages of completable future:

  • When the asynchronous task ends, it will automatically call back the method of an object;
  • When an asynchronous task makes an error, it will automatically call back the method of an object;
  • After setting the callback, the main thread no longer cares about the execution of asynchronous tasks.

Of course, these advantages are not enough to reflect the powerful and more powerful functions of completable future.

Serial execution

Multiple completable future can be executed serially. For example, the first task is queried first, and the second task is updated

public class CompletableFutureDemo {
    public static void main(String[] args) throws InterruptedException {
        // First task
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 1234);
        // Second task
        CompletableFuture<Integer> secondFuture = future.thenApplyAsync((num) -> {
            System.out.println("num:" + num);
            return num + 100;
        });
        secondFuture.thenAccept(System.out::println);
        System.out.println("The main thread continues execution");
        Thread.sleep(5000);
        System.out.println("End of main thread execution");
    }
}

Parallel tasks

In addition to serialization, completable future also supports parallel processing.

public class CompletableFutureDemo {
    public static void main(String[] args) throws InterruptedException {
        // First task
        CompletableFuture<Integer> oneFuture = CompletableFuture.supplyAsync(() -> 1234);
        // Second task
        CompletableFuture<Integer> twoFuture = CompletableFuture.supplyAsync(() -> 5678);
		// Merge two tasks into one parallel task through anyOf
        CompletableFuture<Object> anyFuture = CompletableFuture.anyOf(oneFuture, twoFuture);

        anyFuture.thenAccept(System.out::println);
        System.out.println("The main thread continues execution");
        Thread.sleep(5000);
        System.out.println("End of main thread execution");
    }
}

Multiple tasks can be realized through anyOf(). Only one task is successful. Completabilefuture also has an allOf() method to realize the merged task after multiple tasks must be successful.

Summary

The asynchronous thread implemented by the Runnable interface cannot return the result of task operation by default. Of course, it can be returned through modification, but it is too complex to be modified;

The Callable interface and FutureTask can meet the return of asynchronous task results, but there is a problem. The main thread will block waiting when it cannot obtain the results;

Completabilefuture is enhanced. It only needs to specify the callback object at the end of task execution or exception, which will be executed automatically after completion, and supports advanced methods such as serial, parallel and execution after multiple tasks are executed.

The above is the whole content of this issue. I'll see you next time. If you think it's useful, pay attention.

Posted by satyricon on Sun, 19 Sep 2021 23:21:32 -0700