【无标题】

2023-12-21 23:53:26

1.创建线程的3种方法

1.继承Thread类

class MyThread extends Thread {
	@Override
    public void run() {
        // 线程执行的代码
    }
}

// 创建并启动线程
MyThread myThread = new MyThread();
myThread.start();

继承Thread类, 并重写run方法, 然后创建该类的实例并调用start方法启动线程

2.实现Runnable接口

class MyRunnable implements Runnable {
	@Override
    public void run() {
        // 线程执行的代码
    }
}

// 创建并启动线程
Thread myThread = new Thread(new MyRunnable());
myThread.start();

实现Runnable接口, 并重写run方法, 创建Thread实例并传递Runnable实例,最后调用start方法

3.实现Callable接口, 并实现

class MyCallable implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        return 1;
    }
}

// 创建线程池, 并启动
MyCallable myCallable= new MyCallable();
ExecutorService executorService = Executors.newFixedThreadPool(10);
Future<Integer> future = executorService.submit(myCallable);

Lambda表达式
因为Runnable和Callable都是函数式接口, 所以都可以使用Lambda表达式进行简化

Runnable runable = () -> {
   // 无返回值
};

Callable callable = () -> {
    // 有返回值
    return 1;
};

总结:
上面三种创建线程的方式, 其中继承Thread类或者实现Runnable接口都可以创建线程, 但是它两有一个共同的问题: 没有返回值, 没有返回值, 就无法获取线程执行完的结果

JDK1.5新增了一个Callable接口来解决上面的问题, 但是Callable只能在线程池中提交任务使用

2.线程实现原理

继承Thread类和实现Runnable接口两种方式本身就是一种方式,通过创建Thread实例,然后调用start()方法来创建实例

Callable接口的方式实质上也是通过Thread类来实现的, 我们可以看一下ExecutorService的submit()方法

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

在这个方法中, 首先将Callable实例封装成一个FutureTask实例, FutureTask实现了RunnableFuture接口,而RunnableFuture又实现了Runnable接口,也就是说封装后的FutureTask仍然只是一个任务实例,此时与线程并没有任何关系,真正建立关系是在execute()方法中

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    ……
}

其中的addWorker方法, 该方法是创建一个线程

private boolean addWorker(Runnable firstTask, boolean core) {
    ……
    w = new Worker(firstTask);
    final Thread t = w.thread;
    ……
}

其中初始化Worker的方式如下

Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

public Thread newThread(Runnable r) {
    Thread t = new Thread(group, r,
                          namePrefix + threadNumber.getAndIncrement(),
                          0);
    if (t.isDaemon())
        t.setDaemon(false);
    if (t.getPriority() != Thread.NORM_PRIORITY)
        t.setPriority(Thread.NORM_PRIORITY);
    return t;
}

从上面对Callable的分析,我们可以得出结论,所有创建线程的方式都可以归结为一种方式,那就是创建Thread实例

3.Future

3.1 Future的使用

Future主要是配合Callable使用

Callable的call方法可以有返回值,可以声明抛出异常。和Callable配合的有一个Future类,通过Future可以了解任务执行情况,或者取消任务的执行,还可获取任务执行的结果,这些功能都是Runnable做不到的,Callable的功能要比Runnable强大。

Future<String> future =  Executors.newSingleThreadExecutor().submit(() -> {
   return "Task completed";
});

1.取消任务
用于取消任务的执行。 参数true表示中断执行任务的线程

boolean canceled = future.cancel(true);

2.检查任务是否完成
用于检查任务是否已经完成。如果任务已经完成,返回true;否则返回false。

if (future.isDone()) {
    // 任务已完成
} else {
    // 任务未完成
}

3.处理异常
get()方法用于获取异步任务的结果。如果任务已经完成,它会立即返回结果;否则,它会阻塞直到任务完成。

如果在执行过程中, 线程抛出异常, 使用 try-catch 块来处理异步任务中的异常。

try {
    String result = future.get();
    System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

3.2 Future的局限性

  • 阻塞等待: Future.get()方法是阻塞的,如果任务还没有完成,调用get() 会一直等待。这可能导致程序的响应性变差,特别是在需要等待很长时间的情况下。
  • 取消困难: Future 接口提供了cancel() 方法来取消任务,但这个方法的实现是可选的,而且并不是所有的Future实现都支持取消。如果任务已经开始执行或已经完成,那么取消可能会很困难。
  • 单一结果: Future只能表示单一的异步结果。如果需要处理多个并发任务的结果,可能需要使用更复杂的数据结构,比如CompletionService
  • 缺乏通知机制: Future缺乏内置的通知机制,不能直接注册回调函数,因此在任务完成时无法直接执行某些操作。这使得编写异步代码相对复杂。
  • 异常处理不直观: 异步任务中的异常处理比同步代码更加复杂。在Future 中,如果任务抛出异常,异常会被包装在ExecutionException中,需要在客户端代码中进行处理。
  • 不适合流式处理: Future接口本身并不提供对任务结果的流式处理能力。在需要对异步任务进行流式处理的情况下,可能需要结合其他的编程模型或使用更高级别的抽象,比如CompletableFuture。

4.CompletionService使用

Callable+Future虽然可以实现多个task并行执行,但是如果遇到前面的task执行较慢时需要阻塞等待前面的task执行完后面task才能取得结果。

而CompletionService的主要功能就是一边生成任务,一边获取任务的返回值。让两件事分开执行,任务之间不会互相阻塞,可以实现先执行完的先取结果,不再依赖任务顺序了。

案例: Future方式

public static void main(String[] args) {
    //    创建线程池
    ExecutorService executor = Executors.newFixedThreadPool(2);
    //    异步向电商S1询价
    Future<Integer> f1 = executor.submit(() -> getPriceByS1());
    //    异步向电商S2询价
    Future<Integer> f2 = executor.submit(() -> getPriceByS2());
    //    获取电商S1报价并异步保存
    executor.execute(() -> save(f1.get()));
    //    获取电商S2报价并异步保存
    executor.execute(() -> save(f2.get()));
}

在这个案例中, 如果获取S1报价的耗时很长, 那么即使获取S2报价的耗时很短, 也无法让保存S2报价的操作先执行, 因为此时主线程会阻塞在f1.get()操作上

案例: CompletionService方式

public static void main(String[] args) throws InterruptedException, ExecutionException {
    ExecutorService executor = Executors.newFixedThreadPool(10);
    //创建CompletionService
    CompletionService<Integer> cs = new ExecutorCompletionService<>(executor);
    //异步向电商S1询价
    cs.submit(() -> {
        Thread.sleep(2000);
        return  10;
    });
    //异步向电商S2询价
    cs.submit(() -> {
        return  20;
    });
    //将询价结果异步保存到数据库
    for (int i = 0; i < 2; i++) {
        Integer r = cs.take().get();
        System.out.println(r);
    }
}

CompletionService更适合处理一组任务, 可以将所有的任务提交到 CompletionService 中,然后按照它们完成的顺序逐个处理结果。

调用take()方法会阻塞等待下一个已完成的任务,这意味着你可以立即处理完成的任务,而不必等待所有任务都完成。

4.1 CompletionService相较于Future的优点

  • 按顺序获取任务完成的结果:
    CompletionService提供了take()和poll()方法,可以按照任务完成的顺序获取结果, 这样你就可以立即处理已经完成任务的结果. 而不用等所有任务都完成后才开始处理
  • 避免阻塞等待:
    CompletionService的take()方法会阻塞等待下一个已完成的任务,这意味着你可以立即处理完成的任务,而不必等待所有任务都完成。相比之下,Future.get() 方法是阻塞的,必须等待特定的Future对象的任务完成。
  • 方便处理一组任务:
    CompletionService更适合处理一组任务,你可以将所有的任务提交到CompletionService中,然后按照它们完成的顺序逐个处理结果。
  • 简化异常处理:
    在 CompletionService中,每个任务的结果都包装在Future对象中,如果任务抛出异常,你可以通过Future的get方法捕获ExecutionException,从而更容易地处理异常。在Future中,一个任务的异常可能会影响其他任务的执行,因为它们共享相同的 ExecutorService。
  • 更灵活的任务提交:
    CompletionService的submit方法接受Runnable或Callable,并返回包装结果的Future对象。这使得任务的提交更加灵活,你可以根据需要选择是否关心任务的结果。
  • 支持多个ExecutorService:
    CompletionService允许你使用不同的ExecutorService实例,这对于将不同类型的任务分配给不同的线程池是很有用的

4.2 CompletionService实现原理

CompletionService内部通过阻塞队列 + FutureTask, 阻塞队列的作用是为了存储那些已经执行完成的任务的Future对象, 它是按照完成先后顺序排序

5.CompletableFuture

CompletableFuture是对Future的扩展和增强, 它实现了Future接口, 并在此基础上进行了丰富的扩展,完美弥补了Future的局限性

文章来源:https://blog.csdn.net/qq_24099547/article/details/135127797
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。