第19章 Future设计模式(Java高并发编程详解:多线程与系统设计)

1.先给你一张凭据

假设有个任务需要执行比较长的的时间,通常需要等待任务执行结束或者出错才能返回结果, 在此期间调用者只能陷入阻塞苦苦等待, 对此, Future设计模式提供了一种凭据式的解决方案。在我们日常生活中,关于凭据的使用非常多见,比如你去某西服手工作坊想订做一身合体修身的西服,西服的制作过程比较漫长,少则一个礼拜,多则一个月,你不可能一直待在原地等待, 一般来说作坊会为你开一个凭据, 此凭据就是Future, 在接下来的任意日子里你可以凭借此凭据到作坊获取西服。在本章中,我们将通过程序的方式实现Future设计模式, 让读者体会这种设计的好处。

2.Future设计模式实现

Future设计模式所涉及的关键接口和它们之间的关系UML图

2.1接口定义

1.Future接口设计

Future提供了获取计算结果和判断任务是否完成的两个接口, 其中获取计算结果将会导致调用阻塞(在任务还未完成的情况下),相关代码如所示

java 复制代码
public interface Future <T>{
    // 返回计算后的结果,该方法会陷入阻塞状态
    T get() throws InterruptedException;

    // 判断任务是否已经执行完成
    boolean done();
}
2.FutureService接口设计

FutureService主要用于提交任务, 提交的任务主要有两种, 第一种不需要返回值, 第二种则需要获得最终的计算结果。FutureService接口中提供了对FutureServiceImpl构建的工厂方法, JDK8中不仅支持default方法还支持静态方法, JDK 9甚至还支持接口私有方法。FutureService接口的设计代码如所示。

java 复制代码
public interface FutureService<IN, OUT> {
    // 提交不需要返回值的任务,Future.get方法返回的将会是null
    Future<?> submit(Runnable runnable);

    // 提交需要返回值的任务,其中Task接口代替了Runnable接口
    Future<OUT> submit(Task<IN,OUT> task, IN input);

    // 使用静态方法创建一个FutureService的实现
    static <T,R> FutureService<T,R> newService() {
        return new FutureServiceImpl<>();
    }

}
3.Task接口设计

Task接口主要是提供给调用者实现计算逻辑之用的, 可以接受一个参数并且返回最终的计算结果, 这一点非常类似于JDK 1.5中的Callable接口, Task接口的设计代码如所示。

java 复制代码
@FunctionalInterface
public interface Task<IN,OUT> {
    // 给定一个参数,经过计算返回结果
    OUT get(IN input);
}

2.2 程序实现

1.FutureTask

FutureTask是Future的一个实现,除了实现Future中定义的get() 以及done() 方法, 还额外增加了protected方法finish, 该方法主要用于接收任务被完成的通知, FutureTask接口的设计代码如所示。

java 复制代码
public class FutureTask <T> implements Future<T> {

    // 计算结果
    private T result;

    // 任务是否完成
    private boolean isDone = false;

    // 定义对象锁
    private final Object LOCK = new Object();

    @Override
    public T get() throws InterruptedException {
        synchronized (LOCK) {
            // 当任务还没有完成时,调用get方法会被挂起而进入堵塞
            while(!isDone) {
                LOCK.wait();
            }
        }
        // 返回最终计算结果
        return result;
    }

    protected void finish(T result) {
        synchronized (LOCK) {
            // balking设计模式
            if ( isDone )
                return;

            // 计算完成,为result指定结果,并且将isDone设为true,同事唤醒阻塞中的线程
            this.result = result;
            this.isDone = true;
            LOCK.notifyAll();
        }

    }

    // 返回当前任务是否已经完成
    @Override
    public boolean done() {
        return isDone;
    }
}

FutureTask中充分利用了线程间的通信wait和notifyAll, 当任务没有被完成之前通过get方法获取结果, 调用者会进入阻塞, 直到任务完成并接收到其他线程的唤醒信号, finish方法接收到了任务完成通知, 唤醒了因调用get而进入阻塞的线程。

2.FutureServiceImpl
java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

/*
FutureServiceImpl的主要作用在于当提交任务时创建一个新的线程来受理该任务,进而达到任务异步执行的效果
 */
public class FutureServiceImpl<IN, OUT> implements FutureService<IN, OUT>{

    // 为执行的线程指定名字前缀(再三强调,为线程起一个特殊的名字是一个非常好的编程习惯)
    private final static String FUTURE_THREAD_PREFIX = "FUTURE-";

    private final AtomicInteger nextCounter = new AtomicInteger(0);


    private String getNextName() {
        return FUTURE_THREAD_PREFIX + nextCounter.getAndIncrement();
    }


    @Override
    public Future<?> submit(Runnable runnable) {

        final FutureTask<Void> future = new FutureTask<>();

        new Thread(
                ()->{
                    runnable.run();
                    // 任务执行结束之后将null作为结果传给future
                    future.finish(null);
                },getNextName()
        ).start();

        return future;
    }

    @Override
    public Future<OUT> submit(Task<IN, OUT> task, IN input) {
        final FutureTask<OUT> future = new FutureTask<>();
        new Thread(
                () -> {
                    OUT result = task.get(input);
                    // 任务执行结束之后,将真实的结果通过finish方法传递给future
                    future.finish(result);
                }, getNextName()
        ).start();
        return future;
    }
}

3.Future的使用以及技巧总结

Future直译是"未来"的意思, 主要是将一些耗时的操作交给一个线程去执行, 从而达到异步的目的,提交线程在提交任务和获得计算结果的过程中可以进行其他的任务执行,而不至于傻傻等待结果的返回。

我们提供了两种任务的提交(无返回值和有返回值)方式,在这里分别对其进行测试。无返回值的任务提交测试如下:

java 复制代码
import java.util.concurrent.TimeUnit;

public class FutureServiceClient {
    public static void main(String[] args) throws InterruptedException {
        // 定义不需要返回值的FutureService
        FutureService<Void, Void> service = FutureService.newService();
        // submit方法为立即返回的方法
        Future<?> future = service.submit(
                ()-> {
                    try {
                        TimeUnit.SECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("I am finish done.");
                }
        );
        
        // get方法会使当前线程进入阻塞
        future.get();
    }
}

测试有返回值的

java 复制代码
import java.util.concurrent.TimeUnit;

public class FutureServiceClient1 {


    public static void main(String[] args) throws InterruptedException {
        // 定义有返回值的FutureService
        FutureService<String,Integer> service = FutureService.newService();
        // submit方法会立即返回
        
        Future future = service.submit( input -> {
                    try {
                        TimeUnit.SECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    return input.length();
                }, "Hello"
        );
        // get方法使当前线程进入阻塞,最周会返回计算的结果
        System.out.println(future.get());
    }
}

4.增强FutureService使其支持回调

使用任务完成时回调的机制可以让调用者不再进行显式地通过get的方式获得数据而导致进入阻塞, 可在提交任务的时候将回调接口一并注入, 在这里对FutureService接口稍作修改,修改代码如所示。

java 复制代码
   // 增加回调接口Callback,当任务执行结束之后,Callback会得到执行
    @Override
    public Future<OUT> submit(Task<IN, OUT> task, IN input, Callback<OUT> callback) {
        final FutureTask<OUT> future = new FutureTask<>();
        new Thread(
                () -> {
                    OUT result = task.get(input);
                    future.finish(result);
                    // 执行回调
                    if(null != callback)
                        callback.call(result);
                }
        , getNextName()).start();
        return future;
    }

修改后的submit方法, 增加了一个Callback参数, 主要用来接受并处理任务的计算结果, 当提交的任务执行完成之后, 会将结果传递给Callback接口进行进一步的执行, 这样在提交任务之后不再会因为通过get方法获得结果而陷入阻塞。

Callback接口非常简单, 非常类似于JDK 8中的Consumer函数式接口, Callback接口的代码如所示。

java 复制代码
@FunctionalInterface
public interface Callback <T>{
    // 任务完成后会调用该方法,其中T为任务执行后的结果
    void call(T t);
}

测试代码:

java 复制代码
import java.util.concurrent.TimeUnit;



public class FutureServiceClient2 {

public static void main(String[] args) throws InterruptedException {

// 定义不需要返回值的FutureService

FutureService<String, Integer> service = FutureService.newService();

// submit方法为立即返回的方法

Future<?> future = service.submit(

input-> {

try {

TimeUnit.SECONDS.sleep(10);

} catch (InterruptedException e) {

e.printStackTrace();

}

return input.length();

},"HelloWorld", System.out::println

);



// get方法会使当前线程进入阻塞

future.get();

}

}
相关推荐
martian665几秒前
【Java基础篇】——第4篇:Java常用类库与工具类
java·开发语言
荷碧TongZJ3 分钟前
Jupyter Notebook 6/7 设置代码补全
ide·python·jupyter
violin-wang25 分钟前
Intellij IDEA调整栈内存空间大小详细教程,添加参数-Xss....
java·ide·intellij-idea·xss·栈内存·栈空间
在下陈平安27 分钟前
java-LinkedList源码详解
java·开发语言
黑客老李29 分钟前
一次使用十六进制溢出绕过 WAF实现XSS的经历
java·运维·服务器·前端·sql·学习·xss
√尖尖角↑35 分钟前
力扣——【104. 二叉树的最大深度】
python·算法·leetcode
Mao.O40 分钟前
IDEA编写SpringBoot项目时使用Lombok报错“找不到符号”的原因和解决
java·spring boot·intellij-idea·lombok
muxue1782 小时前
数据结构:栈
java·开发语言·数据结构
小涵3 小时前
Python和JavaScript在字符串比较上的差异
开发语言·javascript·python
运维小文4 小时前
python文本处理-基础篇
开发语言·python·正则表达式