异步调用--Future模式:返回伪造数据

一、Future模式的基本思想

        如果某个方法的执行过程非常耗时,并且我们又不着急要这个方法的返回结果。

        假设在单线程情况下,我们就必须要等待。

        这个时候,可以修改此耗时的方法,让其立即返回客户端一个伪造的数据

        并且,在这个耗时的方法中开启另外一条线程在后台慢慢计算

        主线程继续执行

        如果主线程需要用耗时方法的返回结果的时候,若子线程中返回结果没有计算完毕,那么主线程需要等待耗时方法计算完毕

        因此,此刻的主线程的状态为阻塞状态,需要等待子线程完成计算。

        所以,CountDownLatch这个类的缺点就很明显,如果子线程耗时过多,那么主线程也会一直等待,程序执行效率大大降低

       下面的例子中说明了Future模式的基本思想:开启另外一个线程,在后台计算结果,主线程立刻返回一个伪造的结果,不影响主线程执行其它任务。

è¿éåå¾çæè¿°

è¿éåå¾çæè¿°

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by jay.zhou on 2018/9/6.
 */
public class Client<T> {
    public Data<T> request(T param) {
        //立即构造一个 伪造的数据给主线程
        FutureData<T> futureData = new FutureData<T>();

        //同时开启一个子线程,去进行计算
        Runnable calculateTask = () -> {
            //构造真实的RealeData,这将会消耗一些时间
            RealData<T> realData = new RealData<>(param);
            //并且将计算结果装配到 客户端的伪造的FutureData中
            futureData.setRealData(realData);
        };
        new Thread(calculateTask).start();
        System.out.println("立刻返回伪造数据");
        //立即返回一个 伪造的数据
        return futureData;
    }

    //主函数测试,主函数其实就是主线程,可以理解为应用程序的主干线程
    public static void main(String[] args) {
        Client<String> client = new Client<>();
        Data data = client.request("异步调用--Future模式");
        System.out.println("主线程其它任务执行");
        //使用伪造结果做事情,伪造数据需要等到真实数据装配完毕才能调用。否则阻塞主线程。
        System.out.println(data.getResult());
    }

}

interface Data<T> {
    T getResult();
}

//返回给客户端的伪造的数据,内部拥有真实的计算出来的数据
class FutureData<T> implements Data<T> {
    //伪造的数据需要 装配一个真实的数据
    private RealData<T> realData;
    //开启的线程是否已经执行完毕
    private boolean isReady = false;
    //getResult()与setRealData()方法应该是同步的,原因参考getResult()方法注释
    private ReentrantLock lock = new ReentrantLock();
    //在设置realData完毕之后,唤醒需要用到realData的getResult方法的线程
    private Condition condition = lock.newCondition();

    //如果主线程需要获取计算结果,那么返回的这个结果应该是 RealData 计算的结果
    //如果RealData还没构造完毕,那么此方法应该阻塞,等待RealData构造完毕
    //所以,通过realData获取结果的getResult方法应该与 设置realData的setRealData方法同步
    //为了同步,此类应该增加同步锁
    @Override
    public T getResult() {
        //先进入同步状态
        lock.lock();
        //本方法的目标就是获取realData的值,所以要排除获取不到的情况
        while (!isReady) {
            try {
                condition.await();
            } catch (InterruptedException e) {
                lock.lock();
                e.printStackTrace();
            }
        }
        //如果装配好了,即isReady=true,释放同步锁
        lock.unlock();
        //返回执行结果
        return realData.getResult();
    }

    public void setRealData(RealData<T> realData) {
        //既然调用了setRealData方法,说明子线程已经构造好了realData对象
        lock.lock();
        //健壮性判断,不允许设置两次realData
        if (this.realData != null) {
            lock.unlock();
            return;
        }
        this.realData = realData;
        //装配成功
        isReady = true;
        //唤醒getResult方法中的需要此值的线程
        condition.signal();
        //释放同步锁
        lock.unlock();

    }
}

//真实的数据,构造函数模拟复杂的构造逻辑
class RealData<T> implements Data<T> {
    private T param;

    //真实返回结果的构造函数需要耗费一定的时间
    public RealData(T param) {
        this.param = param;
        //模拟构造花费5秒时间
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public T getResult() {
        return param;
    }
}

 

二、JDK对Future的支持


        java.util.concurrent.FutureTask<V> 类实现了 Runnable接口,Future接口,RunnableFuture接口。

        Future表示异步计算的结果。

        FutureTask类在计算完成后,可以通过get()方法获取其结果。若构造函数中接收的任务尚未完成,调用get()方法,将会出现线程阻塞,直到任务完成。

        FutureTask包装的是Callable或者Runnable,用的是构造函数接收的。因为FutureTask实现了Runnable接口,所以可以提交给ThreadPoolExecutor的execute(Runnable)执行

è¿éåå¾çæè¿°

 

       首先,创建一个任务,要么实现Runnable接口,要么实现Callable接口。

       再使用FutureTask进行封装。为什么构造函数接收Runnable的时候要加上值,因为Runnable()接口实现的run()方法没有返回值。而Callable接口实现的call()方法,是有返回值的,并且还能抛出异常。

       因为FutureTask实现了Runnable接口,所以可以提交给ThreadPoolExecutor的execute(Runnable)执行

       FutureTask的get()方法,获取的是Callable()接口实现call()方法的返回值。若没有取消任务,且任务call()任务执行较为耗时没有执行完毕,那么get()方法将会一直阻塞,直到任务结束。

       主线程中的ThreadPoolExecutor.execute(futureTask);将不会阻塞主线程直到主线程需要调用FutureTask的get()方法获取返回值的时候,才可能出现主线程阻塞

import java.util.concurrent.*;

/**
 * Created by jay.zhou on 2018/9/6.
 */
public class FutureTaskDemo {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        Callable<String> task = () -> {
            //模拟计算时间
            Thread.sleep(5000);
            return "Callable接口实现的返回结果";
        };
        //创建FutureTask来包装Callable接口实现
        FutureTask<String> futureTask = new FutureTask<>(task);
        //创建线程池准备执行任务
        ExecutorService service = Executors.newFixedThreadPool(1);
        //执行任务,线程池将会分配一个线程去执行指定的任务
        service.execute(futureTask);
        //主线程执行其它任务
        Thread.sleep(2000);
        System.out.println("主线程执行其它任务花费了2秒");
        //主线程需要子线程任务的结果
        String result = futureTask.get();
        System.out.println("FutureTask任务的执行结果是:"+result);
        //关闭线程池
        service.shutdown();
    }
}

       

 

三、Callable接口与Runnable接口的区别

    (1)Callable接口的方法是call(),Runnable接口的方法是run()

    (2)Callable的任务执行后可返回值,而Runnable的任务执行后没有返回值

    (3)Callable的任务执行中可以抛异常,而Runnable的任务不能抛出异常

posted @ 2022-07-17 12:15  小大宇  阅读(53)  评论(0)    收藏  举报