并发与高并发(十四)J.U.C组件拓展
前言
J.U.C的拓展组件有哪些?分别在什么场景下使用?如何使用?
主体概要
- J.U.C-FutureTask
- J.U.C-ForkJoin
- J.U.C-BlockingQueue
主体内容
一、J.U.C-FutureTask
1.这里要介绍的第一个组件是FutureTask,这个组件是J.U.C里面的,但它不是AQS的子类。这个类对线程结果的处理值得我们去学习。在学习Java多线程的时候,一定接触过Thread,Runnable,一种是直接继承Thread,另一种就是实现Runnable,这两种方式有个共同的缺陷,那就是无法获得线程执行的结果。从Java1.5开始就提供了Callable、Future,通过他们可以在任务执行完毕之后得到任务执行的结果。以下将介绍Callable、Future、FutureTask三个类的使用方法。
2.Callable和Runnable接口对比。
(1)Runnable很简单,只有一个run()方法,任务在run中执行即可。
(2)Callable是一个泛型的接口,它里面有一个call()函数,call函数返回类型就是我们传进去的类型。Callable功能比Runnable更强大些,主要是其执行完毕有返回值,并且能够抛出异常。
3.Future接口。
对于我们具体的Runnable或者Callable的一个任务,他可以进行取消,查询的任务是否被取消,查询是否完成以及获取结果等等。通常线程都是异步进行的,所以不可能从别的线程中直接获得方法的返回值,这个时候Future就有作用了,Future可以监听目标线程调用call的情况,当调用Future的get方法是时候,就可以获得它的结果。通常这个时候线程可能不会直接完成,当前线程就开始阻塞,直到call方法结束,返回出结果,线程才继续执行。总结出一句话:Future它可以得到别的线程任务方法的返回值。
4.FutureTask类。
FutureTask类它的父类是RunnableFuture,而RunnableFuture继承了Runnable和Future这两个接口。由此可见,FutureTask也是执行callable类型的任务,如果构造函数参数是Runnable的话,他会转换成Callable类型。FutureTask实现了两个接口:Runnable和Future,所以它即可以作为Runnable,被线程执行,又可以作为Future,得到Callable的返回值。那么这个组合的使用有什么好处呢?假设有一个很费时的逻辑,需要计算并且返回出值,同时这个值又不是马上就需要得到,那么就可以使用这个组合。用另外一个线程获取返回值,而这个线程就可以在返回值出来之前做其他的操作,等到需要这个返回值的时候,再通过Future得到。
5.接下来,我们举一个例子来演示一下Future类的使用。(例子中已有注释),注重看一下它的输出结果顺序。
import lombok.extern.slf4j.Slf4j; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @Slf4j public class FutureExample { static class MyCallable implements Callable<String>{//定义一个类实现Callable接口 @Override public String call() throws Exception { log.info("do something in callable"); Thread.sleep(5000);//这里用线程睡了5秒代表它做了很多逻辑的操作 return "Done";//执行完毕让它返回一个“Done”完成 } } public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool();//声明一个线程池 Future<String> future=executorService.submit(new MyCallable());//让线程池直接提交这个任务,并且用Future接受这个结果,这里就是用future接受了另外一个线程的结果 log.info("do somethis in main");//代表主线程做了其他事情 Thread.sleep(1000);//这里也假设主线程睡了1秒代表作了1秒的逻辑处理 String result = future.get();//这时我再调用future的get方法获得之前执行的任务的结果,如果之前的方法一直没有结束,那么主线程就会一直阻塞在这里。 log.info("result:{}",result);//最后打印一波结果,看看它返回的东西 } }
结果发现resultDone在上次输出大概5秒后出现(注意红框中时间),意思就是call中任务到出结果需要理想状态下的5秒,我主线程只要是睡在5秒以内,就不会对输出done的时间造成影响。另外一方面,也验证了Future可以得到其他线程之前的执行结果,只要callable中任务没有完成,主线程将会一直阻塞在future.get():
6.接下来介绍一下FutureTask的用法,类似的我们还是采用上面例子的逻辑,稍稍改动一下。
import lombok.extern.slf4j.Slf4j; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; @Slf4j public class FutureTaskExample { public static void main(String[] args) throws Exception { FutureTask<String> futureTask = new FutureTask<String>(new Callable() { @Override public Object call() throws Exception { log.info("do something in callable"); Thread.sleep(5000);//这里用线程睡了5秒代表它做了很多逻辑的操作 return "Done";//执行完毕让它返回一个“Done”完成 } }); new Thread(futureTask).start();//这里直接启动一个线程去执行futureTask log.info("do something in main"); Thread.sleep(1000);//让主线程睡1秒模拟其做了1秒的其他任务 String result = futureTask.get(); log.info("result:{}",result); } }
结果的效果和上面例子一样,隔了5秒:
其实看起来FutureTask似乎更方面。深入看一下,它不同传入参数的方法。
/** * Creates a {@code FutureTask} that will, upon running, execute the * given {@code Callable}. * * @param callable the callable task * @throws NullPointerException if the callable is null */ public FutureTask(Callable<V> callable) { if (callable == null) throw new NullPointerException(); this.callable = callable; this.state = NEW; // ensure visibility of callable } /** * Creates a {@code FutureTask} that will, upon running, execute the * given {@code Runnable}, and arrange that {@code get} will return the * given result on successful completion. * * @param runnable the runnable task * @param result the result to return on successful completion. If * you don't need a particular result, consider using * constructions of the form: * {@code Future<?> f = new FutureTask<Void>(runnable, null)} * @throws NullPointerException if the runnable is null */ public FutureTask(Runnable runnable, V result) { this.callable = Executors.callable(runnable, result); this.state = NEW; // ensure visibility of callable }
它的参数不仅可以为Callable还可以为Runnable,并且可以指定返回值类型。FutureTask相当于把Future和Callable相关的东西结合了,因此今后比较建议使用FutureTask。
二、J.U.C-ForkJoin
1.ForkJoin是Java7提供的一个用于定型执行任务的框架,它是一个把大任务分割成小任务,最终汇总小任务结果为大任务结果的框架。它采用的是一个叫做工作窃取(work-stealing)的算法:指某个线程从其他队列里窃取任务来执行。这里有一个大概的流程图。
那么为什么需要使用工作窃取算法呢?
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
ForkJoin框架有一些局限性:
- 任务只能使用Fork和join操作来进行同步机制,如果使用了其他同步机制,那他们在同步操作时,工作线程就不能执行其他任务了,比如在Forkjoin框架中,你使任务进入了睡眠,那么在此睡眠期间内,正在执行该任务的工作线程将不会执行其他任务了。
- 我们所创建的任务不应该执行IO操作,如读写数据文件。
- 任务不能抛出检查异常,它必须通过必要的代码处理它们。
ForkJoin框架有两个核心类:ForkJoinPool和ForkJoinTask,其中,ForkJoinPool负责做实现,包括刚刚提到的工作窃取算法,它管理工作线程和提供任务的状态以及它们的执行信息。而ForkJoinTask主要提供任务中Fork和Join操作的机制。关于这个框架的大概介绍到此结束。
2.接下来,用代码来演示一波该框架的用法:计算1到100之和。
import lombok.extern.slf4j.Slf4j; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.Future; import java.util.concurrent.RecursiveTask; @Slf4j public class ForkJoinTaskExample extends RecursiveTask<Integer> {//这里需要继承一个类RecursiveTask,Recursive是递归的意思,返回值设为整型 public static final int threshold =2; private int start; private int end; public ForkJoinTaskExample(int start,int end){ this.start=start; this.end=end; } @Override protected Integer compute(){ int sum = 0; //如果任务足够小就计算任务 boolean canCompute = (end -start)<=threshold;//当end=1 start=2 相差1小于我们上面的threshold=2,说明任务足够小不必拆分 if(canCompute){ for(int i=start;i<=end;i++){ sum+=i; } }else{//当end=1 start=100 相差99大于我们上面的threshold=2,说明任务可以拆分进行 //如果任务大于阈值,就分裂成两个子任务计算 int middle =(start+end)/2;//首先从两端取中间值 ForkJoinTaskExample leftTask = new ForkJoinTaskExample(start,middle); ForkJoinTaskExample rightTask = new ForkJoinTaskExample(middle+1,end); //执行子任务 leftTask.fork(); rightTask.fork(); //等待任务执行结束合并其结果 int leftResult = leftTask.join(); int rightResult = rightTask.join(); //合并子任务结果 sum = leftResult+rightResult; } return sum; } public static void main(String[] args) { ForkJoinPool forkJoinPool = new ForkJoinPool(); //生成一个计算任务,计算1+2+3+4... ForkJoinTaskExample task = new ForkJoinTaskExample(1,100);//传入start=1 end=100相当于从1加到100 //执行一个子任务 Future<Integer> result = forkJoinPool.submit(task); try{ log.info("result:{}",result.get()); }catch (Exception e){ log.error("exeception",e); } } }
结果:
23:01:16.215 [main] INFO com.practice.aqs.ForkJoinTaskExample - result:5050 Process finished with exit code 0
三、J.U.C-BlockingQueue
1.BlockingQueue就是阻塞队列,从阻塞这个词看起来,在某些情况下,对阻塞队列的访问可能会造成阻塞。阻塞的情况主要有以下两种:第一种,当队列满了的时候,进行入队列操作;第二种,当队列空着的时候,进行出队列的操作。因此,当一个线程试图对一个满了的队列,进行入队列的操作的时候,它将会阻塞,除非有另一个线程进行了出队列的操作;同样的,当一个线程试图对一个空队列进行出队列操作时,它也将会被阻塞,除非有另一个线程进行入队列操作。通过这种情形,我们知道阻塞队列其实是线程安全的。阻塞队列通常应用于生产者、消费者场景。生产者线程不停生产,直到这个队列塞满,生产线程们阻塞,然后消费者线程消费了,生产者们才能继续生产。
2.BlockingQueue队列为我们提供了四套方法
操作类型 | 抛出异常(Throws Exception) | 返回特殊值(Special Value) | 阻塞线程(Blocks) | 超时(Times Out) |
---|---|---|---|---|
插入(Insert) | add(e) | offer(e) | put(e) | offer(e, time, unit) |
删除(Remove) | remove() | poll() | take() | poll(time, unit) |
读取/检查(Examine) | element() | peek() | / | / |
我们看下这四套方法各自的特点
- 抛出异常(Throws Exception):如果操作不能马上进行,就抛出异常。
- 返回特殊值(Special Value):如果操作不能马上进行,那么就返回一个特殊的值。
- 阻塞线程(Blocks):如果操作不能马上进行,操作会被阻塞。
- 超时(Times Out):如果操作不能马上进行,操作会被阻塞指定的时间,如果指定的时间超过,还没有执行,就会返回一个特殊值,一般是true或者false。
这么多方法就不一一演示了,这里先介绍一些类。
3.首先,介绍几个类
- ArrayBlockingQueue:一个有界的阻塞队列,它的容量是有限的。我们在其初始化的时候需要指定其容量大小,这个大小一般指定后就不能变了。ArrayBlockingQueue以先进先出的方式存储数据,最先插入的对象在尾部,最先移除的对象在头部。
- DelayQueue:它阻塞的是内部元素,DelayQueue中的元素必须实现一个接口,是J.U.C里的一个叫做Delay的接口,这个Delay接口继承了Comparable接口,这主要是因为DelayQueue中的元素需要进行排序。一般我们都是现在元素过期的优先级进行排序。这个DelayQueue应用场景还是比较多的,比如:定时关闭连接啊,缓存对象啊,超时处理啊。
- LinkedBlockingQueue:它的大小配置是可选的,如果初始化的是指定了一个大小,那么它就是有边界的,如果不指定,他就是无边界的。它的内部实现是一个链表,除了内部实现不一样,其他大多数和ArrayBlockingQueue一样。它也是以先进先出的方式存储数据。
- PriorityBlockingQueue:它是一个有带优先级的阻塞队列,他也是一个没有边界的队列,但是它是有排序规则的。需要注意的是,PriorityBlockingQueue是允许插入NULL的,所有插入PriorityBlockingQueue队列的对象必须实现Comparable接口。
- SynchronousQueue:这个队列内部仅允许容纳一个元素,当一个线程插入一个元素后就会被阻塞,除非这个元素被另一个线程取走。因此,我们又称之为同步队列,它是一个无界非缓存的队列。
以上简单的介绍,有关这几个类的详细例子等待笔者研究过后后续补充...
J.U.C思维导图如下