JUC组件拓展

一、FutureTask

1. 概述
1)FutureTask这个组件是J.U.C里面的,但不是AQS的子类,但是这个类对线程处理的结果很值得我们学习和在项目中使用

2)在Java中一般通过继承Thread类或者实现Runnable接口这两种方式来创建多线程,但是这两种方式都有个缺陷,就是不能在执行完成后获取执行的结果,在Java 1.5之后提供了Callable和Future接口,通过它们就可以在任务执行完毕之后得到任务的执行结果
2. Callable 与 Runnable
1)Callable接口定义,运行Callable任务可以拿到一个Future对象,表示异步计算的结果

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

3)可以看到Callable是个泛型接口,泛型V就是要call()方法返回的类型。Callable接口和Runnable接口很像,都可以被另外一个线程执行,Callable功能更强大些,正如前面所说的,Runnable不会返回数据也不能抛出异常,而Callable可以有返回值与可以抛出异常
3. Future
1)Future接口代表异步计算的结果,通过Future接口提供的方法可以查看异步计算是否执行完成,或者等待执行结果并获取执行结果,同时还可以取消执行

2)也就是说Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。通常不能从线程中获得方法的返回值,这时Future就出场了,Future可以监控目标线程调用call()的情况。总结来说,Future可以得到线程任务方法的返回值

因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask
4. FutureTask
1)Future只是一个接口,不能直接用来创建对象,FutureTask是Future的实现类

2)FutureTask最终还是执行Callable类型的任务。如果在FutureTask构造函数中传入Runnable,会转换成Callable类型

3)FutureTask实际上实现了Runnable与Future接口,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

4)好处:假设有个很费时的逻辑需要计算,并且返回这个计算值,同时这个值又不是马上需要,那么就可以使用这个组合,用另外一个线程计算返回值,而当前线程在使用这个返回值之前,可以做其他的操作,等到需要这个返回值时,才通过Future得到

二、Fork/Join

在这里插入图片描述

1. 概述
1)Fork/Join框架是Java7提供的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架

2)它的思想与MapReduce类似,从字面上理解,Fork即把一个大任务,切割成若干个子任务并行执行,Join即把若干个子任务结果进行合并,最后得到大任务的结果,主要采取工作窃取算法

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

在这里插入图片描述

1)假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务

2)但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行

3)而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行

4)工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列
3. Fork/Join的局限性
对于Fork/Join框架而言,当一个任务正在等待它使用Join操作创建的子任务结束时,执行这个任务的工作线程,寻找其他并未被执行的任务,并开始执行,通过这种方式,线程充分利用它们的运行时间,来提高应用程序的性能。为了实现这个目标,Fork/Join框架执行的任务有一些局限性:

1)任务只能使用Fork、Join操作来作为同步机制,如果使用了其他同步机制,那他们在同步操作时,工作线程则不能执行其他任务。如:在框架的操作中,使任务进入睡眠,那么在这个睡眠期间内,正在执行这个任务的工作线程,将不会执行其他任务

2)所执行的任务,不应该执行IO操作,如读和写数据文件

3)任务不能抛出检查型异常,必须通过必要的代码处理它们
4. ForkJoinPool

在这里插入图片描述

1)ForkJoinPool中维护了一组WorkQueue,也就是工作队列,工作队列中又维护了一个工作线程ForkJoinWorkerThread与一组工作任务ForkJoinTask

2)WorkQueue是一个双端队列(Deque),即 Double Ended Queue ,Deque是一种具有队列和栈的性质的数据结构,双端队列中的元素可以从两端弹出,其限定插入和删除操作在表的两端进行

3)每个工作线程在运行中产生新的任务(通常是因为调用了fork())时,会放入工作队列的队尾,并且工作线程在处理自己的工作队列时,使用的是LIFO 方式,也就是说每次从队尾取出任务来执行

4)每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务(或是来自于刚刚提交到 pool 的任务,或是来自于其他工作线程的工作队列),窃取的任务位于其他线程的工作队列的队首,也就是说工作线程在窃取其他工作线程的任务时,使用的是 FIFO 方式

5)在遇到 join() 时,如果需要 join 的任务尚未完成,则会先处理其他任务,并等待其完成

6)在既没有自己的任务,也没有可以窃取的任务时,进入休眠

7)ForkJoinPool自身也拥有工作队列,这些工作队列的作用是用来接收由外部线程(非 ForkJoinThread 线程)提交过来的任务,而这些工作队列被称为 submitting queue 

5. ForkJoinTask
1)ForkJoinTask需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务

2)我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制,通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下两个子类:

	(1)RecursiveAction:用于没有返回结果的任务

	(2)RecursiveTask :用于有返回结果的任务。

3)fork() 做的工作只有一件事,既是把任务推入当前工作线程的工作队列里

4)join() 的工作则复杂得多,也是join() 可以使得线程免于被阻塞的原因

	(1)检查调用 join() 的线程是否是 ForkJoinThread 线程。如果不是(例如 main 线程),则阻塞当前线程,等待任务完成。如果是,则不阻塞

	(2)查看任务的完成状态,如果已经完成,直接返回结果

	(3)如果任务尚未完成,但处于自己的工作队列内,则完成它

	(4)如果任务已经被其他的工作线程偷走,则窃取这个小偷的工作队列内的任务(以 FIFO 方式),执行,以期帮助它早日完成欲 join 的任务

	(5)如果偷走任务的小偷也已经把自己的任务全部做完,正在等待需要 join 的任务时,则找到小偷的小偷,帮助它完成它的任务

	(6)递归地执行第5步	

三、BlockingQueue(阻塞队列)(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒)

在这里插入图片描述

1. 概述
1)BlockingQueue即为阻塞队列,是一个先进先出的队列,在某些情况下,对阻塞队列的访问可能会造成阻塞,被阻塞的情况主要有两种:

	(1)当队列满时,进行入队列操作。当一个线程试图对一个已经满了的队列进行入队操作时, 他将会阻塞,除非有另一个线程做了出队列的操作

	(2)当队列空时,进行出队列操作。当一个线程试图对一个空队列进行出队操作时,他也将会被阻塞,除非有另一个线程做了入队的操作

2)阻塞队列是线程安全的,主要用在生产者与消费者的场景。

3)上图就是线程生产和消费的场景,负责生产的线程不断的制造新对象并插入到阻塞队列中,直到达到队列的上限值,从而被阻塞,直到消费线程对队列进行消费。同理,负责消费的线程不断的从队列中消费对象,直到这个队列为空,这时消费线程将会被阻塞,除非队列中有新的队列被生产加入

4)BlockingQueue 是一个接口,继承自 Queue,所以其实现类也可以作为 Queue 的实现来使用,而 Queue 又继承自 Collection 接口

5)BlockingQueue 的实现都是线程安全的,但是批量的集合操作如 addAll, containsAll, retainAll 和 removeAll 不一定是原子操作。如 addAll(c) 有可能在添加了一些元素后中途抛出异常,此时 BlockingQueue 中已经添加了部分元素,这个是允许的,取决于具体的实现

6)它实现了 java.util.Collection 接口。例如,我们可以用 remove(x) 来删除任意一个元素,但是,这类操作通常并不高效,所以尽量只在少数的场合使用,比如一条消息已经入队,但是需要做取消操作的时候

7)BlockingQueue家庭中实现类主要有以下几个,常用的是ArrayBlockingQueue与LinkedBlockingQueue
2. ArrayBlockingQueue
1)介绍

	(1)有界的阻塞队列,内部实现是一个数组,有边界的意思是:容量是有限的,必须初始化时,指定它的容量大小,以先进先出的方式存储数据,最新插入的对象在尾部,最先移除的对象在头部

2)源码解析	

	(1)ArrayBlockingQueue 在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行。按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜

	(2)通过构造函数得知,参数fair控制对象的内部锁是否采用公平锁,默认采用非公平锁

	(3)items、takeIndex、putIndex、count等属性并没有使用volatile修饰,这是因为访问这些变量(通过方法获取)使用都是在锁块内,并不存在可见性问题,如size()

	(4)另外有个独占锁lock用来对出,入队操作加锁这导致同时只有一个线程可以访问入队出队
3. LinkedBlockingQueue
1)介绍

	(1)基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成)		

	(2)当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理

	(3)LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能

	(4)作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了

	(5)LinkedBlockingQueue是一个使用链表完成队列操作的阻塞队列。链表是单向链表,而不是双向链表

2)源码分析

	(1)通过其构造函数,得知其可以当做无界队列也可以当做有界队列来使用。

	(2)这里用了两把锁分别是takeLock与putLock、两个Condition分别是notEmpty与notFull,它们是这样搭配的:	

		a. 如果要获取(take)一个元素,需要获取 takeLock 锁,但是获取了锁还不够,如果队列此时为空,还需要队列不为空(notEmpty)这个条件(Condition)

		b. 如果要插入(put)一个元素,需要获取 putLock 锁,但是获取了锁还不够,如果队列此时已满,还需要队列不是满的(notFull)这个条件(Condition)

		c. 从上面的构造函数中,这里会初始化一个空的头结点,那么第一个元素入队的时候,队列中就会有两个元素。读取元素时,也总是获取头节点后面的一个节点。count 的计数值不包括这个头节点

3)与 ArrayBlockingQueue 对比

	(1)ArrayBlockingQueue是共享锁,粒度大,入队与出队的时候只能有1个被执行,不允许并行执行。LinkedBlockingQueue是独占锁,入队与出队是可以并行进行的。当然这里说的是读和写进行并行,两者的读读与写写是不能并行的。总结就是LinkedBlockingQueue可以并发读写

	(2)ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别
4. SynchronousQueue
1)介绍

	1)它是一个特殊的队列,它的名字其实就蕴含了它的特征 – - 同步的队列。为什么说是同步的呢?这里说的并不是多线程的并发问题,而是因为当一个线程往队列中写入一个元素时,写入操作不会立即返回,需要等待另一个线程来将这个元素拿走;同理,当一个读线程做读操作的时候,同样需要一个相匹配的写线程的写操作。这里的 Synchronous 指的就是读线程和写线程需要同步,一个读线程匹配一个写线程,同理一个写线程匹配一个读线程

	2)不像ArrayBlockingQueue、LinkedBlockingDeque之类的阻塞队列依赖AQS实现并发操作,SynchronousQueue直接使用CAS实现线程的安全访问

	3)SynchronousQueue 内部没有容量,但是由于一个插入操作总是对应一个移除操作,反过来同样需要满足。那么一个元素就不会再SynchronousQueue 里面长时间停留,一旦有了插入线程和移除线程,元素很快就从插入线程移交给移除线程。也就是说这更像是一种信道(管道),资源从一个方向快速传递到另一方 向。显然这是一种快速传递元素的方式,也就是说在这种情况下元素总是以最快的方式从插入着(生产者)传递给移除着(消费者),这在多任务队列中是最快处理任务的方式

	4)在线程池里的一个典型应用是Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收

参考网址

JAVA并发编程与高并发解决方案 - 并发编程 五 之 J.U.C组件拓展

注:文章是经过参考其他的文章然后自己整理出来的,有可能是小部分参考,也有可能是大部分参考,但绝对不是直接转载,觉得侵权了我会删,我只是把这个用于自己的笔记,顺便整理下知识的同时,能帮到一部分人。
ps : 有错误的还望各位大佬指正,小弟不胜感激

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值