Concurrent programming: asynchronous call to get return value

Time:2021-11-30

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

Runnable

When creating a thread, you cannew Thread(Runnable)Method, encapsulate the task code inRunnableofrun()Method, theRunnableSubmit as task toThread, or using a thread poolexecute(Runnable)Method 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 is 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 of all, we can’t modify itrun()Method allows it to have a return value, which violates the principle of interface implementation; We can do this in three steps:

  1. We can customize theRunnableDefine variables in and store 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 myRunnable = new MyRunnable<>();
        new Thread(myRunnable).start();
        System. Out. Println (localdatetime. Now() + "myrunnable startup ~");
        MyRunnable.Result result = myRunnable.getResult();
        System.out.println(LocalDateTime.now() + " " + result.getValue());
    }
}

class MyRunnable implements Runnable {
    //Use result as the storage variable of the return value, and use volatile decoration to prevent instruction rearrangement
    private volatile Result 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 returned result");
            } 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 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 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 is easy to make mistakes, resulting in thread safety problems. ThenJDKHas provided us with a special API to meet our requirements, which isCallable

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> 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 task = new FutureTask<>(new MyCallable(begin, end));
            tasks.add(task);
            new Thread(task).start();
        }
        
        for (FutureTask task : tasks) {
            //Get task processing results from task
            System.out.println(task.get());
        }
    }
}
class MyCallable implements Callable {
    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:

You can set theCallableObject encapsulated inFutureTaskObject, give it toThreadObject execution.

FutureTaskThe reason why it can beThreadParameter created becauseFutureTaskyesRunnableInterface.

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 implements RunnableFuture {
    //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 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 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, exceptget()Method also provides some other methods.

  • 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.

stayJDK8Introduced inCompletableFuture, future has been improved, which can be defined inCompletableFutureThe callback object is 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 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 ("main thread continues to execute");
        Thread.sleep(5000);
        System.out.println ("main thread execution ends");
    }
}

class MySupplier implements Supplier {
    @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 {
    @Override
    public void accept(Integer integer) {
        System. Out. Println ("execution result" + integer);
    }
}
//Callback function object when task execution is abnormal
class MyFunction implements Function {
    @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 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 ("execution result" + x);
        });
        future.exceptionally((type) -> {
            System. Out. Println ("execution exception" + type);
            return 0;
        });
        System.out.println ("main thread continues to execute");
        Thread.sleep(5000);
        System.out.println ("main thread execution ends");
    }
}

Through the example, we find thatCompletableFutureAdvantages:

  • 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

MultipleCompletableFutureIt can be executed serially. For example, the first task queries and the second task updates

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

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 oneFuture = CompletableFuture.supplyAsync(() -> 1234);
        //Second task
        CompletableFuture twoFuture = CompletableFuture.supplyAsync(() -> 5678);
		//Merge two tasks into one parallel task through anyof
        CompletableFuture anyFuture = CompletableFuture.anyOf(oneFuture, twoFuture);

        anyFuture.thenAccept(System.out::println);
        System.out.println ("main thread continues to execute");
        Thread.sleep(5000);
        System.out.println ("main thread execution ends");
    }
}

adoptanyOf()Multiple tasks can be achieved, and only one is successful,CompletableFutureOne moreallOf()Method implements the merge 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.