这不就是多线程ThreadPoolExecutor和阻塞队列吗
无处不在的线程,多线程,阻塞队列,并发
编程世界无新鲜事,看你FQ翻得厉不厉害
场景:现在的软件开发迭代速度(一周一更新,甚至一天一发布)真是太快了,今天进行软件更新的时候,看到了有趣的现象,这不就是线程池,ThreadPoolExecutor,阻塞队列,任务(下载和安装)最好的案例嘛!经常看到很多博文在写多线程,并发,队列,却举不出现实生活的场景例子,都在背书吗(天下文章一大抄,看你会抄不会抄)。
现象图示:
我开启了全部更新38个要更新的app,可最多时看到了3个在同时下载,剩下的下载任务在排队(队列),安装过程中,明明已经下载了多个app,可同一时刻只有一个在安装,其他下载好的app也在排队
0. 线程thread
说起线程,不得不提起进程,
线程,还真不好下定义,你要问十个人就会有十种答案,我就当线程就是一个可以执行的任务程序(比如上面图片里的下载和安装)。java里线程主要通过继承java.lang.Thread类或实现java.lang.Runnable接口,其实Thread也是实现了Runnable接口的类,所有,线程还是围绕着java.lang.Thread类扩展包装,比如下面要要说的线程池。核心类如下
一个线程创建之后,总是处于其生命周期的4个状态之一中。线程的状态表明此线 程当前正在进行的活动,而线程的状态是可以通过程序来进行控制的,就是说,可以对线程进行操作来改变状态。这些操作包括启动(start)、终止(stop)、睡眠(sleep)、挂起 (suspend)、恢复(resume)、等待(wait)和通知(notify)。每一个操作都对应了一个方法,这些方法是由java.lang提供的。
线程状态在Java中是通过一个Thread的内部枚举State标识的。
创建状态(Thread.State.NEW)
如果创建了一个线程而没有启动它,那么,此线程就处于创建状态。比如,下述语句执行 以后,使系统有了一个处于创建状态的线程myThread:
Thread t= new ThreadClass();
其中,ThreadClass()是Thread的子类,而Thread是由java.lang提供的。
处于创建状态的线程还没有获得应有的资源,所以,这是一个空的线程。线程只有通过启动后,系统才会为它分配资源。对处于创建状态的线程可以进行两种操作:一是启动 (start)操作,使其进入可运行状态,二是终止(stop)操作,使其进入消亡状态。如果进入到消 亡状态,那么,此后这个线程就不能进入其他状态,也就是说,它不再存在了。
start方法是对应启动操作的方法,其具体功能是为线程分配必要的系统资源;将线程设置为可运行状态,从而可以使系统调度这个线程。
通过调用t.start()启动一个线程,使该线程进入可运行(Thread.State.RUNNABLE)的状态。
由JVM的决定去调度(Scheduler) 在可运行状态(Runnable)下的线程,使该线程处于运行 (Running) 状态,由于JVM的调度会出现不可控性,即不是优先级高的先被调用,可能先调用,也可能后调用的的情况。运行状态(Running)下,调用礼让yield()方法,可以使线程回到可运行状态(Runnable)下,再次JVM的调度(并不依赖优先级)。
线程执行完毕或异常退出会进入终止状态(Thread.State.TERMINATED)。
其余的还有几个状态:
Thread.State.BLOCKED
受阻塞并且正在等待监视器锁的某一线程的线程状态。处于受阻塞状态的某一线程正在等待监视器锁,以便进入一个同步的块/方法,或者在调用 Object.wait 之后再次进入同步的块/方法。
Thread.State.WAITING
某一等待线程的线程状态。某一线程因为调用下列方法之一而处于等待状态:
不带超时值的 Object.wait
不带超时值的 Thread.join
LockSupport.park
处于等待状态的线程正等待另一个线程,以执行特定操作。 例如,已经在某一对象上调用了 Object.wait() 的线程正等待另一个线程,以便在该对象上调用 Object.notify() 或 Object.notifyAll()。已经调用了 Thread.join() 的线程正在等待指定线程终止。
TIMED_WAITING具有指定等待时间的某一等待线程的线程状态。某一线程因为调用以下带有指定正等待时间的方法之一而处于定时等待状态:
Thread.sleep
带有超时值的 Object.wait
带有超时值的 Thread.join
LockSupport.parkNanos
LockSupport.parkUntil
谨记: 在给定时间点上,一个线程只会处于一种状态,状态转换图
线程优先级
java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
static int MAX_PRIORITY
线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY
线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY
分配给线程的默认优先级,取值为5。
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
每个线程都有默认的优先级,主线程的默认优先级为Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
1. 阻塞队列
BlockingQueue队列是一种数据结构,它有两个基本操作:在队列尾部加人一个元素,和从队列头部移除一个元素就是说,队列以一种先进先出的方式管理数据,如果你试图向一个已经满了的阻塞队列中添加一个元素或者是从一个空的阻塞队列中移除一个元素,将导致线程阻塞。
在多线程进行合作时,阻塞队列是很有用的工具。工作者线程可以定期地把中间结果存到阻塞队列中而其他工作者线程把中间结果取出并在将来修改它们。队列会自动平衡负载。如果第一个线程集运行得比第二个慢,则第二个线程集在等待结果时就会阻塞。如果第一个线程集运行得快,那么它将等待第二个线程集赶上来。
而BlockingQueue队列也是一组数据集合,它继承了Queue接口,而Queue接口继承了Collection接口。
阻塞队列提供的相关操作和特点
在java包"java.util.concurrent"中提供了若干种队列,大神给你写好了
ArrayBlockingQueue |
一个由数组结构组成的有界阻塞队列 |
LinkedBlockingQueue | 一个由链表结构组成的有界阻塞队列 |
PriorityBlockingQueue | 一个支持优先级排序的无界阻塞队列 |
DelayQueue | 一个使用优先级队列实现的无界阻塞队列 |
SynchronousQueue |
一个不存储元素的阻塞队列 |
LinkedTransferQueue |
一个由链表结构组成的无界阻塞队列 |
LinkedBlockingDeque |
一个由链表结构组成的双向阻塞队列 |
ArrayBlockingQueue
底层用数组实现的有界阻塞队列,默认情况下不保证线程公平的访问队列(按照阻塞的先后顺序访问队列),队列可用的时候,阻塞的线程都可以争夺队列的访问资格,当然也可以使用以下的构造方法创建一个公平的阻塞队列。ArrayBlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(10, true)。
伪代码:
(其实就是通过将ReentrantLock设置为true来 达到这种公平性的:即等待时间最长的线程会先操作)。用ReentrantLock condition 实现阻塞。
有界就是队列的长度有限制,例如数组队列,在构建的时候就指定了长度。无界就是可以无限地添加。
LinkedBlockingQueue
底层基于链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。这个队列的实现原理和ArrayBlockingQueue实现基本相同。也是采用ReentrantLock 控制并发,不同的是它使用两个独占锁来控制消费和生产。即用takeLock和putlock,这样的好处是消费者和生产者可以并发执行,对吞吐量有提升。
PriorityBlockingQueue
PriorityBlockingQueue是一个带优先级的队列,而不是先进先出队列。元素按优先级顺序被移除,该队列也没有上限(PriorityBlockingQueue是对 PriorityQueue的再次包装,是基于堆数据结构的,而PriorityQueue是没有容量限制的,与ArrayList一样,所以在优先阻塞 队列上put时是不会受阻的。虽然此队列逻辑上是无界的,但是由于资源被耗尽,所以试图执行添加操作可能会导致 OutOfMemoryError),但是如果队列为空,那么取元素的操作take就会阻塞,所以它的检索操作take是受阻的。也是用ReentrantLock控制并发。
DelayQueue
DelayQueue是在PriorityQueue基础上实现的,底层也是数组构造方法,是一个存放Delayed 元素的无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且poll将返回null。当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于或等于零的值时,则出现期满,poll就移除这个元素了。此队列不允许使用 null 元素。
SynchronousQueue
一个没有容量的队列 ,不会存储数据,每执行一次put就要执行一次take,否则就会阻塞。未使用锁。通过cas实现,吞吐量异常高。内部采用的就是ArrayBlockingQueue的阻塞队列,所以在功能上完全可以用ArrayBlockingQueue替换,但是SynchronousQueue是轻量级的,SynchronousQueue不具有任何内部容量,我们可以用来在线程间安全的交换单一元素。所以功能比较单一,优势就在于轻量。
LinkedBlockingDeque
LinkedBlockingDeque是双向链表实现的双向并发阻塞队列。该阻塞队列同时支持FIFO和FILO两种操作方式,即可以从队列的头和尾同时操作(插入/删除);并且,该阻塞队列是支持线程安全,当多线程竞争同一个资源时,某线程获取到该资源之后,其它线程需要阻塞等待。此外,LinkedBlockingDeque还是可选容量的(防止过度膨胀),即可以指定队列的容量。如果不指定,默认容量大小等于Integer.MAX_VALUE。
LinkedTransferQueue
jdk7才提供这个类,这个类实现了TransferQueue接口,也是基于链表的,对于所有给定的生产者都是先入先出的。与其他阻塞队列的区别是:其他阻塞队列,生产者生产数据,如果队列没有满,放下数据就走,消费者获取数据,看到有数据获取数据就走。而LinkedTransferQueue生产者放数据的时候,如果此时消费者没有获取,则需阻塞等待直到有消费者过来获取数据。有点类似SynchronousQueue,但是LinkedTransferQueue是被设计有容量的。LinkedTransferQueue 通过使用CAS来实现并发控制,是一个无界的安全队列。其长度可以无限延伸,当然带来的问题也是显而易见的。
2. 线程池ThreadPool
线程池,可以理解为存放线程的容器。
既然可以通过new出线程,那为什么要线程池呢,因为有以下优点:
(1)重用存在的线程,减少对象创建、消亡的开销,性能佳。
(2)可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
(3)提供定时执行、定期执行、单线程、并发数控制等功能。
而单独建立线程(特别是项目组开发人员多的时候,各创建各自的线程),却有以下缺点:
(1) 每次new Thread新建对象性能差。因为每次都会创建一个对象。这是既耗时又消耗资源的。
(2) 线程缺乏统一管理,可能会造成自锁,或者是内存溢出。
(3)缺乏更多功能,如定时执行、定期执行、线程中断。
Java通过Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
不过这几种创建线程池方便,可隐藏了细节也不好,既然四种创建线程池最后是通过java.util.concurrent.ThreadPoolExecutor类构造,不如直接考察它,代码如下:
请务必搞懂方方法里的参数:
(1)int corePoolSize(核心线程数):
线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,核心线程默认情况下会一直存活在线程池中;如果设置了 allowCoreThreadTimeOut 为 true,那么核心线程如果不干活的话,超过一定时间,就会被销毁掉。
(2)int maximumPoolSize(线程池能容纳的最大线程数量):
线程总数 = 核心线程数 + 非核心线程数。
(3)long keepAliveTime(非核心线程空闲存活时长):
非核心线程空闲时长超过该时长将会被回收
(4)TimeUnit unit 空闲线程的存活时间
在这里表示的是时间的单位,比如说秒。
(5)BlockingQueue workQueue(任务队列),就上面说的几种队列:
当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。常用的workQueue类型:
ArrayBlockingQueue:这里表示接到新任务,如果没有达到核心线程数,则新建核心线程执行任务,如果达到了,则入队等候,如果队列已满,则新建非核心线程执行任务,又如果总线程数到了 maximumPoolSize,并且队列也满了,则发生错误。
LinkedBlockingQueue:这里表示接到新任务,如果当前线程数小于核心线程数,则新建核心线程处理任务;如果当前线程数等于核心线程数,则进入队列等待。
DelayQueue:这里表示接到新任务,先入队,达到了指定的延时时间,才执行任务。
SynchornousQueue:这里表示接到新任务,直接交给线程处理,如果其他的线程都在工作,那就创建一个新的线程来处理这个任务。
(6).ThreadFactory threadFactory(线程工厂):
用来创建线程池中的线程。
(7).RejectedExecutionHandler handler(拒绝策略):
指的之超过了maximumPoolSize,无法再处理新的任务,就会直接拒绝,提供了以下 4 种策略:
AbortPolicy:默认策略,在拒绝任务时,会抛出RejectedExecutionException。
CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。
DiscardOldestPolicy:该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。
DiscardPolicy:该策略默默的丢弃无法处理的任务,不予任何处理
附图两张:
找到ThreadPoolExecutor和BlockingQueue了吗
3. 进入正题:下载和安装
普及了0,1,2后,开始说正事,怕直接说多线程并发,队列,任务,不好接受。
最后代码如下:
测试下载安装(可以自定义线程池执行器,自定义工厂,自定义策略)
执行效果图
总结:好好学习,天天向上,写也很累(有时候构思一篇文章两三个小时很快就过去了,甚至一下午),也是思考的过程,看别人的文章像过天书一样和听别人讲座,容易忘记(可以问问你的朋友同学同事等),要自己汇总,记忆会更深刻。
有时间了接着拆分!!!
扩展阅读参考:
0. https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html
1. 能有比官方更权威的吗 https://docs.oracle.com/javase/tutorial/essential/concurrency/runthread.html
2. Java Concurrency in Practice http://jcip.net/, 国内已有翻译版
3. http://gee.cs.oswego.edu/ 你们用的并发包java.util.concurrent(简称JUC),便出于他和其他人之手,经常看到国内的码农在拼命研究并发源码(也有研究spring源码的),不知道到什么程度了,了解他这个人吗。。。担心就算看懂了代码(我想只要是个码农都能看得懂),也只是看懂了代码,了解他的思想吗,他当时是如何筹划构思出来的,别人的技术,能学通50%(毕竟人家花了二十年循序渐进才做出来的库和框架比如spring,http://gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html,是经验和阅历的成果),能灵活运用就算成功
******************************************************************************************************
4. Java阻塞队列实现原理分析 https://developer.51cto.com/art/201704/536821.htm
5. 聊聊并发(七)——Java 中的阻塞队列 https://www.infoq.cn/article/java-blocking-queue/
6. Java线程池架构原理和源码解析(ThreadPoolExecutor)https://mp.weixin.qq.com/s?__biz=MjM5NTg2NTU0Ng==&mid=214688037&idx=6&sn=d1c989e7f539732cda5ceaa6cabd8b29
7. Java线程池使用说明 https://mp.weixin.qq.com/s?__biz=MzIyNjA1MjAyNg==&mid=212879024&idx=1&sn=a05bf0b28846850a0730e5844f95126d
8. Java线程池---Executor框架源码深度解析https://mp.weixin.qq.com/s?__biz=MzUyNDk2MTE1Mg==&mid=2247483663&idx=1&sn=cd57cf503c31eb6e4a423173520081f7
9. 深入源码分析Java线程池的实现原理https://mp.weixin.qq.com/s/-89-CcDnSLBYy3THmcLEdQ
10. 深度解读 Java 线程池设计思想及源码实现 https://mp.weixin.qq.com/s?__biz=MzUxNDA1NDI3OA==&mid=2247486041&idx=1&sn=2bc12fd0b57bedb84eb11aca8a574306
11. 探索Java 同步机制 https://www.ibm.com/developerworks/cn/java/j-lo-synchronized/