《java并发编程的艺术》迷你书.pdf

上传人:椰子壳 文档编号:3331442 上传时间:2019-08-13 格式:PDF 页数:63 大小:3.40MB
返回 下载 相关 举报
《java并发编程的艺术》迷你书.pdf_第1页
第1页 / 共63页
《java并发编程的艺术》迷你书.pdf_第2页
第2页 / 共63页
《java并发编程的艺术》迷你书.pdf_第3页
第3页 / 共63页
《java并发编程的艺术》迷你书.pdf_第4页
第4页 / 共63页
《java并发编程的艺术》迷你书.pdf_第5页
第5页 / 共63页
点击查看更多>>
资源描述

《《java并发编程的艺术》迷你书.pdf》由会员分享,可在线阅读,更多相关《《java并发编程的艺术》迷你书.pdf(63页珍藏版)》请在三一文库上搜索。

1、 2 目录 推荐序 . 4 第九章 Java 并发容器和框架 6 1.1 ConcurrentHashMap 的分析和使用 6 1.1.1 为什么要使用 ConcurrentHashMap . 6 1.1.2 ConcurrentHashMap 的锁分段技术 7 1.1.3 ConcurrentHashMap 的结构 7 1.1.4 ConcurrentHashMap 的初始化 9 1.1.5 定位 Segment 11 1.1.6 ConcurrentHashMap 的 get 操作 . 12 1.1.7 ConcurrentHashMap 的 put 操作 . 12 1.1.8 Concu

2、rrentHashMap 的 size 操作 13 1.2 ConcurrentLinkedQueue 的分析和使用 13 1.2.1 ConcurrentLinkedQueue 的介绍 14 1.2.2 ConcurrentLinkedQueue 的结构 14 1.2.3 入队列 . 15 1.2.4 出队列 . 18 1.3 Java 线程池的分析和使用 19 1.3.1 为什么需要使用线程池 . 19 1.3.2 线程池的创建 . 20 1.3.3 线程池的饱和策略 . 21 1.3.4 向线程池提交任务 . 21 1.3.5 线程池的关闭 . 22 1.3.6 线程池的分析 . 22

3、1.3.7 合理的配置线程池 . 24 1.3.8 线程池的监控 . 25 1.4 Java 中的阻塞队列 26 1.4.1 什么是阻塞队列? . 26 1.4.2 Java 里的阻塞队列 27 1.4.3 ArrayBlockingQueue . 27 1.4.4 LinkedBlockingQueue . 28 1.4.5 PriorityBlockingQueue 28 1.4.6 DelayQueue . 28 1.4.7 SynchronousQueue 30 1.4.8 LinkedTransferQueue 31 1.4.9 LinkedBlockingDeque . 31 1.

4、4.10 阻塞队列的实现原理 . 32 1.5 Fork/Join 框架 35 1.5.1 什么是 Fork/Join 框架 35 1.5.2 工作窃取算法 . 36 1.5.3 Fork/Join 框架的介绍 38 1.5.4 使用 ForkJoin 框架 . 38 1.5.5 Fork/Join 框架的异常处理 40 3 1.5.6 Fork/Join 框架的实现原理 40 1.6 本章小结 . 42 第十三章 Java 并发机制的底层实现原理 . 43 1.7 Volatile 的实现原理 . 43 1.7.1 术语定义 43 1.7.2 Volatile 的官方定义 44 1.7.3

5、为什么要使用 Volatile . 44 1.7.4 Volatile 的实现原理 44 1.7.5 Volatile 的使用优化 45 1.8 Synchronized 的实现原理 . 47 1.8.1 同步的基础 . 47 1.8.2 同步的原理 . 47 1.8.3 Java 对象头 48 1.8.4 锁的升级 . 49 1.8.5 偏向锁 . 49 1.8.6 轻量级锁 . 52 1.8.7 锁的优缺点对比 . 53 1.8.8 6 参考源码 53 1.9 原子操作的实现原理 . 53 1.9.1 术语定义 . 53 1.9.2 处理器如何实现原子操作 . 54 1.9.3 处理器自动

6、保证基本内存操作的原子性 . 54 1.9.4 使用总线锁保证原子性 . 55 1.9.5 使用缓存锁保证原子性 . 56 1.9.6 Java 如何实现原子操作 56 1.9.7 使用循环 CAS 实现原子操作 56 1.9.8 使用锁机制实现原子操作 . 60 1.10 本章小节 . 60 4 推荐序 欣闻腾飞兄弟的 聊聊并发 系列文章将要集结成 InfoQ 迷你书进行发布, 我感到非常的振奋。 这一系列文章从最开始的发布到现在已经经历了两年多的时间,这两年间,Java 世界发生了翻天 覆地的变化。Java 7 已经发布,而且 Java 8 也将在下个月姗姗来迟。围绕着 JVM 已经形成了

7、一个 庞大且繁荣的生态圈, Groovy、 Scala、 Clojure、 Ceylon 等众多 JVM 语言在蓬勃发展着, 如今的 Java 已经不是几年前的 Java 了,众多运行在 JVM 上的编程语言为我们带来了更多的选择,提供了更好 的机会。 纵观这几年的技术发展趋势,唱衰 Java 的论调一直都萦绕在我们耳边。不可否认,Java 的发 展确实有些缓慢,而且有些臃肿;但放眼望去,有如此之多的核心与关键系统依旧在使用 Java 进 行开发并运行在 JVM 之上,这不仅得益于 Java 语言本身,强大的 JVM 及繁荣的 Java 生态圈在这 其中更是发挥着重要的作用。在 Java 的世

8、界中,我们想要完成一件事情有太多可用的选择了。 虽然如此,对于国内的一些开发人员来说,但凡提到 Java,想到的都是所谓的 SSH(Struts、 Spring 及 Hibernate 等相关框架) 。不可否认,这些框架对于我们又快又好地完成任务起到了至关重 要的推进作用,然而 Java 并不是 SSH,SSH 也不是 Java 的代名词。 由于之前的系列文章都是本人审校的, 因此我也非常幸运地成为了这些文章的第一个读者, 在 阅读之际不禁感叹腾飞的技术造诣及对技术执着的追求。腾飞兄弟的聊聊并发系列文章从发布 以来一直高居 InfoQ 中文站浏览量的前列,每篇文章之后都有大量的读者评论,或是提

9、问,或是补 充相关知识,腾飞兄弟也都非常耐心地对读者的问题进行解答。并发是一个学科,Java 中也有自 己的一套处理并发的框架与体系;不过遗憾的是,很多读者对这一领域知之甚少,这也直接造成了 很多人并不了解有关并发的理论与实践知识。幸运的是,腾飞的聊聊并发系列文章非常完美地 填补了这一空白,文章从 synchronized 关键字、volatile 实现原理到 ConcurrentHashMap、 ConcurrentLinkedQueue 源码分析,再到阻塞队列和 Fork/Join 框架,为读者献上了一道丰盛的 Java 并发大餐。 相信腾飞以在淘宝的实际工作经验凝结而成的这部 InfoQ

10、 迷你书会为广大读者打开通往 Java 并发之路的大门。这里我要小声做一个提示,也许文章中很多内容看一次未必就能完全消化吸收, 这时请不要放弃,多看几次,多动手做实验,相信你会很快掌握 Java 并发的精髓的。 5 另外,值得一提的是,腾飞兄弟现在在维护着一个关于 Java 并发资源的站点并发编程网 (http:/ ,上面有大量高质量的原创与翻译文章,都是关于并发领域相关内容的,感兴 趣的读者不妨移步一观。 最后,祝大家阅读愉快,能够轻松驾驭 Java 并发。 是为序。 InfoQ 中文站中文站 Java 主编:张龙主编:张龙 6 第九章 Java 并发容器和框架 Java 程序员进行并发编程

11、时,相比于其他语言的程序员而言要倍感幸福,因为并发编程大师 Doug Lea 不遗余力的为 Java 开发者提供了非常多的并发容器和框架。本章让我们一起来见识下大 师操刀编写的这些容器和框架, 并通过每节的原理分析一起来学习下, 如何设计出精妙的并发程序。 1.1 ConcurrentHashMap 的分析和使用 ConcurrentHashMap 是线程安全并且高效的 HashMap。 本节让我们一起研究下该容器是如何在 保证线程安全的同时又能保证高效的操作。 1.1.1 为什么要使用 ConcurrentHashMap 线程不安全的线程不安全的 HashMap。因为多线程环境下,使用 Ha

12、shMap 进行 put 操作会引起死循环, 导致 CPU 利用率接近 100%,所以在并发情况下不能使用 HashMap,如执行以下代码: final HashMap map = new HashMap(2); Thread t = new Thread(new Runnable() Override public void run() for (int i = 0; i MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS; / Find power-of-two sizes best matching arguments int sshift = 0

13、; int ssize = 1; while (ssize MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; int c = initialCapacity / ssize; if (c * ssize (cap, loadFactor); 上面代码中的变量 cap 就是 segment 里 HashEntry 数组的长度, 它等于 initialCapacity 除以 ssize 的倍数 c, 如果 c 大于 1, 就会取大于等于 c 的 2 的 N 次方值, 所以 cap 不是 1, 就是 2 的 N 次方。 segment 的容量

14、 threshold(int)cap*loadFactor,默认情况下 initialCapacity 等于 16,loadfactor 等于 0.75,通过运算 cap 等于 1,threshold 等于零。 11 1.1.5 定位 Segment 既然 ConcurrentHashMap 使用分段锁 Segment 来保护不同段的数据, 那么在插入和获取元素的 时候, 必须先通过哈希算法定位到 Segment。 可以看到 ConcurrentHashMap 会首先使用 Wang/Jenkins hash 的变种算法对元素的 hashCode 进行一次再哈希。 private static

15、int hash(int h) h += (h 10); h += (h 6); h += (h 16); 之所以进行再哈希,其目的是为了减少哈希冲突,使元素能够均匀的分布在不同的 Segment 上,从而提高容器的存取效率。假如哈希的质量差到极点,那么所有的元素都在一个 Segment 中, 不仅存取元素缓慢,分段锁也会失去意义。我做了一个测试,不通过再哈希而直接执行哈希计算。 System.out.println(Integer.parseInt(“0001111“, 2) System.out.println(Integer.parseInt(“0011111“, 2) System.o

16、ut.println(Integer.parseInt(“0111111“, 2) System.out.println(Integer.parseInt(“1111111“, 2) 计算后输出的哈希值全是 15,通过这个例子可以发现如果不进行再哈希,哈希冲突会非常严 重,因为只要低位一样,无论高位是什么数,其哈希值总是一样。我们再把上面的二进制数据进行 再哈希后结果如下,为了方便阅读,不足 32 位的高位补了 0,每隔四位用竖线分割下。 01000111011001111101101001001110 11110111010000110000000110111000 011101110110

17、10010100011000111110 10000011000000001100100000011010 可以发现每一位的数据都散列开了, 通过这种再哈希能让数字的每一位都能参加到哈希运算当 中,从而减少哈希冲突。ConcurrentHashMap 通过以下哈希算法定位 segment。 final Segment segmentFor(int hash) return segments(hash segmentShift) 默认情况下 segmentShift 为 28,segmentMask 为 15,再哈希后的数最大是 32 位二进制数据, 向右无符号移动28位, 意思是让高4位参与到

18、hash运算中, (hash segmentShift) return segmentFor(hash).get(key, hash); get 操作的高效之处在于整个 get 过程不需要加锁,除非读到的值是空的才会加锁重读,我们 知道 HashTable 容器的 get 方法是需要加锁的,那么 ConcurrentHashMap 的 get 操作是如何做到不 加锁的呢?原因是它的 get 方法里将要使用的共享变量都定义成 volatile, 如用于统计当前 Segement 大小的 count 字段和用于存储值的 HashEntry 的 value。定义成 volatile 的变量,能够在线

19、程之间保 持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况 可以被多线程写,就是写入的值不依赖于原值) ,在 get 操作里只需要读不需要写共享变量 count 和 value,所以可以不用加锁。只所以不会读到过期的值,是根据 java 内存模型的 happen before 原 则,对 volatile 字段的写入操作先于读操作,即使两个线程同时修改和获取 volatile 变量,get 操作 也能拿到最新的值,这是用 volatile 替换锁的经典应用场景。 transient volatile int count; volatile V value;

20、 在定位元素的代码里我们可以发现定位 HashEntry 和定位 Segment 的哈希算法虽然一样, 都与 数组的长度减去一相与,但是相与的值不一样,定位 Segment 使用的是元素的 hashcode 通过再哈 希后得到的值的高位,而定位 HashEntry 直接使用的是再哈希后的值。其目的是避免两次哈希后的 值一样,导致元素虽然在 Segment 里散列开了,但是却没有在 HashEntry 里散列开。 hash segmentShift) / 定位 HashEntry 所使用的 hash 算法 1.1.7 ConcurrentHashMap 的 put 操作 由于 put 方法里需要

21、对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须得 加锁。Put 方法首先定位到 Segment,然后在 Segment 里进行插入操作。插入操作需要经历两个步 13 骤,第一步判断是否需要对 Segment 里的 HashEntry 数组进行扩容,第二步定位添加元素的位置然 后放在 HashEntry 数组里。 是否需要扩容是否需要扩容。 在插入元素前会先判断 Segment 里的 HashEntry 数组是否超过容量 (threshold) , 如果超过阀值,数组进行扩容。值得一提的是,Segment 的扩容判断比 HashMap 更恰当,因为 HashMap 是在插入元素后

22、判断元素是否已经到达容量的,如果到达了就进行扩容,但是很有可能 扩容之后没有新元素插入,这时 HashMap 就进行了一次无效的扩容。 如何扩容。如何扩容。扩容的时候首先会创建一个两倍于原容量的数组,然后将原数组里的元素进行再 hash 后插入到新的数组里。为了高效 ConcurrentHashMap 不会对整个容器进行扩容,而只对某个 segment 进行扩容。 1.1.8 ConcurrentHashMap 的 size 操作 如果我们要统计整个 ConcurrentHashMap 里元素的大小, 就必须统计所有 Segment 里元素的大 小后求和。Segment 里的全局变量 coun

23、t 是一个 volatile 变量,那么在多线程场景下,我们是不是直 接把所有 Segment 的 count 相加就可以得到整个 ConcurrentHashMap 大小了呢?不是的,虽然相加 时可以获取每个 Segment 的 count 的最新值,但是拿到之后可能累加前使用的 count 发生了变化, 那么统计结果就不准了。所以最安全的做法,是在统计 size 的时候把所有 Segment 的 put,remove 和 clean 方法全部锁住,但是这种做法显然非常低效。 因为在累加 count 操作过程中,之前累加过的 count 发生变化的几率非常小,所以 ConcurrentHas

24、hMap 的做法是先尝试 2 次通过不锁住 Segment 的方式来统计各个 Segment 大小,如 果统计的过程中,容器的 count 发生了变化,则再采用加锁的方式来统计所有 Segment 的大小。 那么 ConcurrentHashMap 是如何判断在统计的时候容器是否发生了变化呢?使用 modCount 变 量,在 put , remove 和 clean 方法里操作元素前都会将变量 modCount 进行加 1,那么在统计 size 前 后比较 modCount 是否发生变化,从而得知容器的大小是否发生变化。 1.2 ConcurrentLinkedQueue 的分析和使用 在并

25、发编程中我们有时候需要使用线程安全的队列。 如果我们要实现一个线程安全的队列有两 种实现方式一种是使用阻塞算法, 另一种是使用非阻塞算法。 使用阻塞算法的队列可以用一个锁 (入 14 队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则 可以使用循环 CAS 的方式来实现, 本节让我们一起来研究下 Doug Lea 是如何使用非阻塞的方式来 实现线程安全队列 ConcurrentLinkedQueue 的,相信从大师身上我们能学到不少并发编程的技巧。 1.2.1 ConcurrentLinkedQueue 的介绍 ConcurrentLinkedQueue 是

26、一个基于链接节点的无界线程安全队列,它采用先进先出的规则对 节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它 会返回队列头部的元素。它采用了“waitfree”算法(即 CAS 算法)来实现,该算法在 Michael 15 1.2.3 入队列 入队列就是将入队节点添加到队列的尾部入队列就是将入队节点添加到队列的尾部。为了方便理解入队时队列的变化,以及 head 节点和 tair 节点的变化,每添加一个节点我就做了一个队列的快照图,如图 8-2 所示。 图1-4 步骤步骤1 添加元素 1。队列更新 head 节点的 next 节点为元素 1 节点。又因为 t

27、ail 节点 默 认情况下等于 head 节点,所以它们的 next 节点都指向元素 1 节点。 步骤步骤2 添加元素 2。队列首先设置元素 1 节点的 next 节点为元素 2 节点,然后更新 tail 节点 指向元素 2 节点。 步骤步骤3 添加元素 3,设置 tail 节点的 next 节点为元素 3 节点。 步骤步骤4 添加元素 4, 设置元素 3 的 next 节点为元素 4 节点, 然后将 tail 节点指向元素 4 节点。 通过 debug 入队过程并观察 head 节点和 tail 节点的变化,发现入队主要做两件事情,第一是 将入队节点设置成当前队列尾节点的下一个节点。第二是更

28、新 tail 节点,如果 tail 节点的 next 节点 不为空,则将入队节点设置成 tail 节点,如果 tail 节点的 next 节点为空,则将入队节点设置成 tail 的 next 节点,所以 tail 节点不总是尾节点,理解这一点对于我们研究源码会非常有帮助。 上面的分析让我们从单线程入队的角度来理解入队过程, 但是多个线程同时进行入队情况就变 得更加复杂,因为可能会出现其他线程插队的情况。如果有一个线程正在入队,那么它必须先获取 16 尾节点,然后设置尾节点的下一个节点为入队节点,但这时可能有另外一个线程插队了,那么队列 的尾节点就会发生变化,这时当前线程要暂停入队操作,然后重新

29、获取尾节点。让我们再通过源码 来详细分析下它是如何使用 CAS 算法来入队的。 public boolean offer(E e) if (e = null) throw new NullPointerException(); /入队前,创建一个入队节点 Node n = new Node(e); retry: /死循环,入队不成功反复入队。 for (;) /创建一个指向 tail 节点的引用 Node t = tail; /p 用来表示队列的尾节点,默认情况下等于 tail 节点。 Node p = t; for (int hops = 0; ; hops+) /获得 p 节点的下一个节点

30、。 Node next = succ(p); /next 节点不为空,说明 p 不是尾节点,需要更新 p 后在将它指向 next 节点 if (next != null) /循环了两次及其以上,并且当前节点还是不等于尾节点 if (hops HOPS p = next; /如果 p 是尾节点,则设置 p 节点的 next 节点为入队节点。 else if (p.casNext(null, n) /如果 tail 节点有大于等于 1 个 next 节点, 则将入队节点设置成 tair 节点, 更新失败了也没关系, 因为失败了表示有其他线程成功更新了 tair 节点。 if (hops = HOP

31、S) casTail(t, n); / 更新 tail 节点,允许失败 return true; / p 有 next 节点,表示 p 的 next 节点是尾节点,则重新设置 p 节点 else p = succ(p); 从源代码角度来看整个入队过程主要做二件事情。第一是定位出尾节点,第二是使用 CAS 算 法能将入队节点设置成尾节点的 next 节点,如不成功则重试。 17 定位尾节点。定位尾节点。tail 节点并不总是尾节点,所以每次入队都必须先通过 tail 节点来找到尾节点, 尾节点可能就是 tail 节点, 也可能是 tail 节点的 next 节点。 代码中循环体中的第一个 if

32、就是判断 tail 是否有 next 节点,有则表示 next 节点可能是尾节点。获取 tail 节点的 next 节点需要注意的是 p 节 点等于 p 的 next 节点的情况,只有一种可能就是 p 节点和 p 的 next 节点都等于空,表示这个队列 刚初始化,正准备添加第一次节点,所以需要返回 head 节点。获取 p 节点的 next 节点代码如下 final Node succ(Node p) Node next = p.getNext(); return (p = next) ? head : next; 设置入队节点为尾节点设置入队节点为尾节点。p.casNext(null, n

33、)方法用于将入队节点设置为当前队列尾节点的 next 节点, p 如果是 null 表示 p 是当前队列的尾节点, 如果不为 null 表示有其他线程更新了尾节点, 则需要重新获取当前队列的尾节点。 hops 的设计意图的设计意图。 上面分析过对于先进先出的队列入队所要做的事情就是将入队节点设置成 尾节点,doug lea 写的代码和逻辑还是稍微有点复杂。那么我用以下方式来实现行不行? public boolean offer(E e) if (e = null) throw new NullPointerException(); Node n = new Node(e); for (;) N

34、ode t = tail; if (t.casNext(null, n) 让 tail 节点永远作为队列的尾节点,这样实现代码量非常少,而且逻辑非常清楚和易懂。但是 这么做有个缺点就是每次都需要使用循环 CAS 更新 tail 节点。如果能减少 CAS 更新 tail 节点的次 数,就能提高入队的效率,所以 doug lea 使用 hops 变量来控制并减少 tail 节点的更新频率,并不 是每次节点入队后都将 tail 节点更新成尾节点,而是当 tail 节点和尾节点的距离大于等于常量 HOPS 的值(默认等于 1)时才更新 tail 节点,tail 和尾节点的距离越长使用 CAS 更新 t

35、ail 节点的 次数就会越少, 但是距离越长带来的负面效果就是每次入队时定位尾节点的时间就越长, 因为循环 体需要多循环一次来定位出尾节点, 但是这样仍然能提高入队的效率, 因为从本质上来看它通过增 加对 volatile 变量的读操作来减少了对 volatile 变量的写操作,而对 volatile 变量的写操作开销要远 18 远大于读操作,所以入队效率会有所提升。 private static final int HOPS = 1; 注意:入队方法永远返回 true,所以不要通过返回值判断入队是否成功。 1.2.4 出队列 出队列的就是从队列里返回一个节点元素从队列里返回一个节点元素, 并

36、清空该节点对元素的引用。 让我们通过每个节 点出队的快照来观察下 head 节点的变化,如图 8-3 所示。 图1-5 从上图可知,并不是每次出队时都更新 head 节点,当 head 节点里有元素时,直接弹出 head 节点里的元素,而不会更新 head 节点。只有当 head 节点里没有元素时,出队操作才会更新 head 节点。这种做法也是通过 hops 变量来减少使用 CAS 更新 head 节点的消耗,从而提高出队效率。 让我们再通过源码来深入分析下出队过程。 public E poll() Node h = head; / p 表示头节点,需要出队的节点 Node p = h; fo

37、r (int hops = 0; hops+) 19 / 获取 p 节点的元素 E item = p.getItem(); / 如果 p 节点的元素不为空,使用 CAS 设置 p 节点引用的元素为 null,如果成功则 返回 p 节点的元素。 if (item != null updateHead(h, (q != null) ? q : p); return item; / 如果头节点的元素为空或头节点发生了变化, 这说明头节点已经被另外一个线程修 改了。那么获取 p 节点的下一个节点 Node next = succ(p); / 如果 p 的下一个节点也为空,说明这个队列已经空了 if (

38、next = null) / 更新头节点。 updateHead(h, p); break; / 如果下一个元素不为空,则将头节点的下一个节点设置成头节点 p = next; return null; 首先获取头节点的元素,然后判断头节点元素是否为空,如果为空,表示另外一个线程已经进 行了一次出队操作将该节点的元素取走,如果不为空,则使用 CAS 的方式将头节点的引用设置成 null,如果 CAS 成功,则直接返回头节点的元素,如果不成功,表示另外一个线程已经进行了一次 出队操作更新了 head 节点,导致元素发生了变化,需要重新获取头节点。 1.3 Java 线程池的分析和使用 1.3.1

39、为什么需要使用线程池 合理利用线程池能够带来三个好处。第一:降低资源消耗。通过重复利用已创建的线程降低线 程创建和销毁造成的消耗。第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建 就能立即执行。第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系 20 统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。但是要做到合理 的利用线程池,必须对其原理了如指掌。 1.3.2 线程池的创建 我们可以通过 ThreadPoolExecutor 来创建一个线程池。 new ThreadPoolExecutor(corePoolSize, maximum

40、PoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler); 创建一个线程池需要输入几个参数: q corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执 行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于 线程池基本大小时就不再创建。如果调用了线程池的 prestartAllCoreThreads 方法,线程池会提 前创建并启动所有基本线程。 q runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻

41、塞队列。 1) ArrayBlockingQueue: 是一个基于数组结构的有界阻塞队列, 此队列按 FIFO (先进先出) 原则对元素进行排序。 2) LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按 FIFO (先进先出) 排 序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法 Executors.newFixedThreadPool()使用了这个队列。 3) SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用 移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlocki

42、ngQueue, 静态工厂方法 Executors.newCachedThreadPool 使用了这个队列。 4) PriorityBlockingQueue:一个具有优先级得无限阻塞队列。 5) maximumPoolSize(线程池最大大小) :线程池允许创建的最大线程数。如果队列满了, 并且已创建的线程数小于最大线程数, 则线程池会再创建新的线程执行任务。 值得注意的 是如果使用了无界的任务队列这个参数就没什么效果。 6) ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置 更有意义的名字。 21 7) RejectedExecutionHand

43、ler(饱和策略) :当队列和线程池都满了,说明线程池处于饱和状 态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是 AbortPolicy,表 示无法处理新任务时抛出异常。 1.3.3 线程池的饱和策略 在 JDK1.5 中 Java 线程池框架提供了以下四种策略: q AbortPolicy:直接抛出异常。 q CallerRunsPolicy:只用调用者所在线程来运行任务。 q DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。 q DiscardPolicy:不处理,丢弃掉。 当然也可以根据应用场景需要来实现 RejectedExecutio

44、nHandler 接口自定义策略。如记录日志 或持久化不能处理的任务。 q keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果 任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。 q TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟 (MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒 (NANOSECONDS, 千分之一微秒)。 1.3.4 向线程池提交任务 我们可以使用 execute 提交的任务,但是 execu

45、te 方法没有返回值,所以无法判断任务知否被 线程池执行成功。通过以下代码可知 execute 方法输入的任务是一个 Runnable 类的实例。 threadsPool.execute(new Runnable() Override public void run() / TODO Auto-generated method stub ); 我们也可以使用 submit 方法来提交任务, 它会返回一个 future,通过这个 future 来判断任 务是否执行成功,通过 future 的 get 方法来获取返回值,get 方法会阻塞住直到任务完成,而使用 22 get(long timeou

46、t, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。 Future future = executor.submit(harReturnValuetask); try Object s = future.get(); catch (InterruptedException e) / 处理中断异常 catch (ExecutionException e) / 处理无法执行任务异常 finally / 关闭线程池 executor.shutdown(); 1.3.5 线程池的关闭 我们可以通过调用线程池的 shutdown 或 shutdownNow 方法来

47、关闭线程池,它们的原理是遍历 线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可 能永远无法终止。但是它们存在一定的的区别,shutdownNow 首先将线程池的状态设置成 STOP, 然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而 shutdown 只是 将线程池的状态设置成 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程。 只要调用了这两个关闭方法的其中一个,isShutdown 方法就会返回 true。当所有的任务都已关 闭后,才表示线程池关闭成功,这时调用 isTerminaed 方法会返回 tru

48、e。至于我们应该调用哪一种方 法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用 shutdown 来关闭线程池,如果 任务不一定要执行完,则可以调用 shutdownNow。 1.3.6 线程池的分析 线程池的主要工作流程如图 1-6: 23 图1-6 从上图我们可以看出,当提交一个新任务到线程池时,线程池的处理流程如下: 1) 首先线程池判断基本线程池基本线程池是否已满?没满,创建一个工作线程来执行任务。满了,则 进入下个流程。 2) 其次线程池判断工作队列工作队列是否已满?没满, 则将新提交的任务存储在工作队列里。 满了, 则进入下个流程。 3) 最后线程池判断整个线程池整个线程

49、池是否已满?没满,则创建一个新的工作线程来执行任务,满 了,则交给饱和策略来处理这个任务。 源码分析:源码分析:上面的流程分析让我们很直观的了解的线程池的工作原理,让我们再通过源代码 来看看是如何实现的,线程池执行任务的方法如下: public void execute(Runnable command) if (command = null) throw new NullPointerException(); /如果线程数小于基本线程数,则创建线程并执行当前任务 if (poolSize = corePoolSize | !addIfUnderCorePoolSize(command) /如线程数大于等于基本线程数或线程创建失败,则将当

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 建筑/环境 > 装饰装潢


经营许可证编号:宁ICP备18001539号-1