JAVA并发编程与高并发解决方案 - 并发编程 二 之 线程安全性、安全发布对象

本文详细介绍了JAVA并发编程中J.U.C组件的重要组成部分,如FutureTask、Fork/Join框架的工作原理和使用案例。FutureTask允许在任务完成后获取执行结果,而Fork/Join框架通过工作窃取算法实现高效并行计算。此外,文章还探讨了阻塞队列BlockingQueue的多种实现,如ArrayBlockingQueue和LinkedBlockingQueue的特性与应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JAVA并发编程与高并发解决方案 - 并发编程 五

版本 作者 内容
2018.7.4 chuIllusions J.U.C组件拓展

J.U.C 组件拓展

FutureTask
Introduction

  FutureTask这个组件是J.U.C里面的,但不是AQS的子类,但是这个类对线程处理的结果很值得我们学习和在项目中使用。
  在Java中一般通过继承Thread类或者实现Runnable接口这两种方式来创建多线程,但是这两种方式都有个缺陷,就是不能在执行完成后获取执行的结果,在Java 1.5之后提供了Callable和Future接口,通过它们就可以在任务执行完毕之后得到任务的执行结果。

Callable 与 Runnable

Callable接口定义,运行Callable任务可以拿到一个Future对象,表示异步计算的结果。

@FunctionalInterface
public interface Callable<V> {
   
   
    /**
     * 计算结果或失败时扔出异常
     * @since 1.5
     * @return 计算结果
     * @throws 计算失败扔出异常
     */
    V call() throws Exception;
}

Runnable接口定义,由于run()方法返回值为void类型,所以在执行完任务之后无法返回任何结果。

@FunctionalInterface
public interface Runnable {
   
   
    /**
     * 当一个对象实现<code>Runnable</code>接口创建一个线程,这个对象通过覆写
     * run方法处理线程逻辑,并且Thread类启动该线程,执行Runnable处理线程逻辑
     * @since 1.0
     */
    public abstract void run();
}

 &emsp可以看到Callable是个泛型接口,泛型V就是要call()方法返回的类型。Callable接口和Runnable接口很像,都可以被另外一个线程执行,Callable功能更强大些,正如前面所说的,Runnable不会返回数据也不能抛出异常,而Callable可以有返回值与可以抛出异常。

Future

  Future接口代表异步计算的结果,通过Future接口提供的方法可以查看异步计算是否执行完成,或者等待执行结果并获取执行结果,同时还可以取消执行。也就是说Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。通常不能从线程中获得方法的返回值,这时Future就出场了,Future可以监控目标线程调用call()的情况。总结来说,Future可以得到线程任务方法的返回值。

public interface Future<V> {
   
   
    /*
     * 取消异步任务的执行。
     * 如果异步任务已经完成或者已经被取消,或者由于某些原因不能取消,则会返回false;
     * 如果任务还没有被执行,则会返回true并且异步任务不会被执行;
     * 如果任务已经开始执行了但是还没有执行完成:
     *      若mayInterruptIfRunning为true,则会立即中断执行任务的线程并返回true
     *      若mayInterruptIfRunning为false,则会返回true且不会中断任务执行线程
     */
    boolean cancel(boolean mayInterruptIfRunning);

    /*
     * 判断任务是否被取消,如果任务在结束(正常执行结束或者执行异常结束)前被取消则返回true,否则返回false。
     */
    boolean isCancelled();

    /*
     * 判断任务是否已经完成,如果完成则返回true,否则返回false。需要注意的是:任务执行过程中发生异常、任务被取消也属于任务已完成,也会返回true。
     */
    boolean isDone();

    /*
     * 获取任务执行结果:
     *      如果任务还没完成则会阻塞等待直到任务执行完成
     *      如果任务被取消则会抛出CancellationException异常
     *      如果任务执行过程发生异常则会抛出ExecutionException异常
     *      如果阻塞等待过程中被中断则会抛出InterruptedException异常
     */
    V get() throws InterruptedException, ExecutionException;

    /*
     * 带超时时间的get()版本,如果阻塞等待过程中超时则会抛出TimeoutException异常。
     */
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;

}

  因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask

FutureTask

  Future只是一个接口,不能直接用来创建对象,FutureTaskFuture的实现类。

public interface RunnableFuture<V> extends Runnable, Future<V> {}

public class FutureTask<V> implements RunnableFuture<V> {
   
   
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }
}

  从上面两个类结构,可以得知FutureTask最终还是执行Callable类型的任务。如果在FutureTask构造函数中传入Runnable,会转换成Callable类型。

  FutureTask实际上实现了RunnableFuture接口,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。好处:假设有个很费时的逻辑需要计算,并且返回这个计算值,同时这个值又不是马上需要,那么就可以使用这个组合,用另外一个线程计算返回值,而当前线程在使用这个返回值之前,可以做其他的操作,等到需要这个返回值时,才通过Future得到。

案例
@Slf4j
public class FutureExample {
   
   

    /**
     * Callable任务
     */
    static class MyCallable implements Callable<String> {

        @Override
        public String call() throws Exception {
            log.info("do something in callable");
            Thread.sleep(5000);
            return "Done";
        }
    }

    public static void main(String[] args) throws Exception {
        //1.生成线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //线程池提交Callable任务,并且得到Future
        Future<String> future = executorService.submit(new MyCallable());
        log.info("do something in main");
        Thread.sleep(1000);
        //调用Future.get()时,如果任务线程还未执行完毕,则会一直阻塞在此,等待线程任务完成,然后拿到结果
        String result = future.get();
        log.info("result:{}", result);
    }
}

以上Future与以下FutureTask要实现的效果是一样的。

@Slf4j
public class FutureTaskExample {
   
   

    public static void main(String[] args) throws Exception {
        FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                log.info("do something in callable");
                Thread.sleep(5000);
                return "Done";
            }
        });

        new Thread(futureTask).start();
        log.info("do something in main");
        // 1. 调用isDone()判断任务是否结束
        if(!futureTask.isDone()) {
            System.out.println("Task is not done");
            try {
                //阻塞主线程一秒钟
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        String result = futureTask.get();
        log.info("result:{}", result);
    }
}

参考深入学习 FutureTask

Fork/Join
Introduction

  Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。它的思想与MapReduce类似,从字面上理解,Fork即把一个大任务,切割成若干个子任务并行执行,Join即把若干个子任务结果进行合并,最后得到大任务的结果,主要采取工作窃取算法。
  工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。
Fork/Join框架流程图
  假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

  工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。

  对于Fork/Join框架而言,当一个任务正在等待它使用Join操作创建的子任务结束时,执行这个任务的工作线程,寻找其他并未被执行的任务,并开始执行,通过这种方式,线程充分利用它们的运行时间,来提高应用程序的性能。为了实现这个目标,Fork/Join框架执行的任务有一些局限性:
1. 任务只能使用Fork、Join操作来作为同步机制,如果使用了其他同步机制,那他们在同步操作时,工作线程则不能执行其他任务。如:在框架的操作中,使任务进入睡眠,那么在这个睡眠期间内,正在执行这个任务的工作线程,将不会执行其他任务
2. 所执行的任务,不应该执行IO操作,如读和写数据文件
3. 任务不能抛出检查型异常,必须通过必要的代码处理它们

核心是两个类:ForkJoinTaskForkJoinPool。Pool主要负责实现,包括上面所介绍的工作窃取算法,管理工作线程和提供关于任务的状态以及它们的执行信息;Task主要提供在任务中,执行Fork与Join操作的机制。

引用[并行流与串行流 Fork/Join框架的一张图来说明过程

fork/join过程图

Example

我们先来看一下Fork/Join框架的演示:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值