从 65 节到 82 节,我们用了 18 篇文章讨论并发,本节进行简要总结。
多线程开发有两个核心问题,一个是竞争,另一个是协作。竞争会出现线程安全问题,所以,本节首先总结线程安全的机制,然后是协作的机制。管理竞争和协作是复杂的,所以 Java 提供了更高层次的服务,比如并发容器类和异步任务执行服务,我们也会进行总结。本节纲要如下:
线程安全的机制
线程表示一条单独的执行流,每个线程有自己的执行计数器,有自己的栈,但可以共享内存,共享内存是实现线程协作的基础,但共享内存有两个问题,竞态条件和内存可见性,之前章节探讨了解决这些问题的多种思路:
synchronized
synchronized 简单易用,它只是一个关键字,大部分情况下,放到类的方法声明上就可以了,既可以解决竞态条件问题,也可以解决内存可见性问题。
需要理解的是,它保护的是对象,而不是代码,只有对同一个对象的 synchronized 方法调用,synchronized 才能保证它们被顺序调用。对于实例方法,这个对象是 this,对于静态方法,这个对象是类对象,对于代码块,需要指定哪个对象。
另外,需要注意,它不能尝试获取锁,也不响应中断,还可能会死锁。不过,相比显式锁,synchronized 简单易用,JVM 也可以不断优化它的实现,应该被优先使用。
显式锁
显式锁是相对于 synchronized 隐式锁而言的,它可以实现 synchronzied 同样的功能,但需要程序员自己创建锁,调用锁相关的接口,主要接口是 Lock,主要实现类是 ReentrantLock。
相比 synchronized,显式锁支持以非阻塞方式获取锁、可以响应中断、可以限时、可以指定公平性、可以解决死锁问题,这使得它灵活的多。
在读多写少、读操作可以完全并行的场景中,可以使用读写锁以提高并发度,读写锁的接口是 ReadWriteLock,实现类是 ReentrantReadWriteLock。
volatile
synchronized 和显式锁都是锁,使用锁可以实现安全,但使用锁是有成本的,获取不到锁的线程还需要等待,会有线程的上下文切换开销等。保证安全不一定需要锁。如果共享的对象只有一个,操作也只是进行最简单的 get/set 操作,set 也不依赖于之前的值,那就不存在竞态条件问题,而只有内存可见性问题,这时,在变量的声明上加上 volatile 就可以了。
原子变量和 CAS
使用 volatile,set 的新值不能依赖于旧值,但很多时候,set 的新值与原来的值有关,这时,也不一定需要锁,如果需要同步的代码比较简单,可以考虑原子变量,它们包含了一些以原子方式实现组合操作的方法,对于并发环境中的计数、产生序列号等需求,考虑使用原子变量而非锁。
原子变量的基础是 CAS,比较并设置,一般的计算机系统都在硬件层次上直接支持 CAS 指令。通过循环 CAS 的方式实现原子更新是一种重要的思维,相比 synchronized,它是乐观的,而 synchronized 是悲观的,它是非阻塞式的,而 synchronized 是阻塞式的。CAS 是 Java 并发包的基础,基于它可以实现高效的、乐观、非阻塞式数据结构和算法,它也是并发包中锁、同步工具和各种容器的基础。
写时复制
之所以会有线程安全的问题,是因为多个线程并发读写同一个对象,如果每个线程读写的对象都是不同的,或者,如果共享访问的对象是只读的,不能修改,那也就不存在线程安全问题了。
我们在介绍容器类 CopyOnWriteArrayList 和 CopyOnWriteArraySet 时介绍了写时复制技术,写时复制就是将共享访问的对象变为只读的,写的时候,再使用锁,保证只有一个线程写,写的线程不是直接修改原对象,而是新创建一个对象,对该对象修改完毕后,再原子性地修改共享访问的变量,让它指向新的对象。
ThreadLocal
ThreadLocal 就是让每个线程,对同一个变量,都有自己的独有拷贝,每个线程实际访问的对象都是自己的,自然也就不存在线程安全问题了。
线程的协作机制
多线程之间的核心问题,除了竞争,就是协作。我们在 67 节和 68 节介绍了多种协作场景,比如生产者 / 消费者协作模式、主从协作模式、同时开始、集合点等。之前章节探讨了协作的多种机制:
wait/notify
wait/notify 与 synchronized 配合一起使用,是线程的基本协作机制,每个对象都有一把锁和两个等待队列,一个是锁等待队列,放的是等待获取锁的线程,另一个是条件等待队列,放的是等待条件的线程,wait 将自己加入条件等待队列,notify 从条件等待队列上移除一个线程并唤醒,notifyAll 移除所有线程并唤醒。
需要注意的是,wait/notify 方法只能在 synchronized 代码块内被调用,调用 wait 时,线程会释放对象锁,被 notify/notifyAll 唤醒后,要重新竞争对象锁,获取到锁后才会从 wait 调用中返回,返回后,不代表其等待的条件就一定成立了,需要重新检查其等待的条件。
wait/notify 方法看上去很简单,但往往难以理解 wait 等的到底是什么,而 notify 通知的又是什么,只能有一个条件等待队列,这也是 wait/notify 机制的局限性,这使得对于等待条件的分析变得复杂,67 节和 68 节通过多个例子演示了其用法,这里就不赘述了。
显式条件
显式条件与显式锁配合使用,与 wait/notify 相比,可以支持多个条件队列,代码更为易读,效率更高,使用时注意不要将 signal/signalAll 误写为 notify/notifyAll。
中断
Java 中取消 / 关闭一个线程的方式是中断,中断并不是强迫终止一个线程,它是一种协作机制,是给线程传递一个取消信号,但是由线程来决定如何以及何时退出,线程在不同状态和 IO 操作时对中断有不同的反应,作为线程的实现者,应该提供明确的取消 / 关闭方法,并用文档清楚描述其行为,作为线程的调用者,应该使用其取消 / 关闭方法,而不是贸然调用 interrupt。
协作工具类
除了基本的显式锁和条件,针对常见的协作场景,Java 并发包提供了多个用于协作的工具类。
信号量类 Semaphore 用于限制对资源的并发访问数。
倒计时门栓 CountDownLatch 主要用于不同角色线程间的同步,比如在 "裁判"-"运动员" 模式中,"裁判" 线程让多个 "运动员" 线程同时开始,也可以用于协调主从线程,让主线程等待多个从线程的结果。
循环栅栏 CyclicBarrier 用于同一角色线程间的协调一致,所有线程在到达栅栏后都需要等待其他线程,等所有线程都到达后再一起通过,它是循环的,可以用作重复的同步。
阻塞队列
对于最常见的生产者 / 消费者协作模式,可以使用阻塞队列,阻塞队列封装了锁和条件,生产者线程和消费者线程只需要调用队列的入队 / 出队方法就可以了,不需要考虑同步和协作问题。
阻塞队列有普通的先进先出队列,包括基于数组的 ArrayBlockingQueue 和基于链表的 LinkedBlockingQueue/LinkedBlockingDeque,也有基于堆的优先级阻塞队列 PriorityBlockingQueue,还有可用于定时任务的延时阻塞队列 DelayQueue,以及用于特殊场景的阻塞队列 SynchronousQueue 和 LinkedTransferQueue。
Future/FutureTask
在常见的主从协作模式中,主线程往往是让子线程异步执行一项任务,获取其结果,手工创建子线程的写法往往比较麻烦,常见的模式是使用异步任务执行服务,不再手工创建线程,而只是提交任务,提交后马上得到一个结果,但这个结果不是最终结果,而是一个 Future,Future 是一个接口,主要实现类是 FutureTask。
Future 封装了主线程和执行线程关于执行状态和结果的同步,对于主线程而言,它只需要通过 Future 就可以查询异步任务的状态、获取最终结果、取消任务等,不需要再考虑同步和协作问题。
容器类
线程安全的容器有两类,一类是同步容器,另一类是并发容器。在理解 synchronized 一节,我们介绍了同步容器。关于并发容器,我们介绍了:
同步容器
Collections 类中有一些静态方法,可以基于普通容器返回线程安全的同步容器,比如:
- public static Collection synchronizedCollection(Collection c)
- public static List synchronizedList(List list)
- public static Map synchronizedMap(Map m)
它们是给所有容器方法都加上 synchronized 来实现安全的。同步容器的性能比较低,另外,还需要注意一些问题,比如复合操作和迭代,需要调用方手工使用 synchronized 同步,并注意不要同步错对象。
而并发容器是专为并发而设计的,线程安全、并发度更高、性能更高、迭代不会抛出 ConcurrentModificationException、很多容器以原子方式支持一些复合操作。
写时拷贝的 List 和 Set
CopyOnWriteArrayList 基于数组实现了 List 接口,CopyOnWriteArraySet 基于 CopyOnWriteArrayList 实现了 Set 接口,它们采用了写时拷贝,适用于读远多于写,集合不太大的场合。不适用于数组很大,且修改频繁的场景。它们是以优化读操作为目标的,读不需要同步,性能很高,但在优化读的同时就牺牲了写的性能。
ConcurrentHashMap
HashMap 不是线程安全的,在并发更新的情况下,HashMap 的链表结构可能形成环,出现死循环,占满 CPU。ConcurrentHashMap 是并发版的 HashMap,通过分段锁和其他技术实现了高并发,读操作完全并行,写操作支持一定程度的并行,以原子方式支持一些复合操作,迭代不用加锁,不会抛出 ConcurrentModificationException。
基于 SkipList 的 Map 和 Set
ConcurrentHashMap 不能排序,容器类中可以排序的 Map 和 Set 是 TreeMap 和 TreeSet,但它们不是线程安全的。Java 并发包中与 TreeMap/TreeSet 对应的并发版本是 ConcurrentSkipListMap 和 ConcurrentSkipListSet。ConcurrentSkipListMap 是基于 SkipList 实现的,SkipList 称为跳跃表或跳表,是一种数据结构,主要操作复杂度为 O(log(N)),并发版本采用跳表而不是树,是因为跳表更易于实现高效并发算法。
ConcurrentSkipListMap 没有使用锁,所有操作都是无阻塞的,所有操作都可以并行,包括写。与 ConcurrentHashMap 类似,迭代器不会抛出 ConcurrentModificationException,是弱一致的,也直接支持一些原子复合操作。
各种队列
各种阻塞队列主要用于协作,非阻塞队列适用于多个线程并发使用一个队列的场合,有两个非阻塞队列,ConcurrentLinkedQueue 和 ConcurrentLinkedDeque,ConcurrentLinkedQueue 实现了 Queue 接口,表示一个先进先出的队列,ConcurrentLinkedDeque 实现了 Deque 接口,表示一个双端队列。它们都是基于链表实现的,都没有限制大小,是无界的,这两个类最基础的实现原理是循环 CAS,没有使用锁。
任务执行服务
关于任务执行服务,我们介绍了:
基本概念
任务执行服务大大简化了执行异步任务所需的开发,它引入了一个 "执行服务" 的概念,将 "任务的提交" 和 "任务的执行" 相分离,"执行服务" 封装了任务执行的细节,对于任务提交者而言,它可以关注于任务本身,如提交任务、获取结果、取消任务,而不需要关注任务执行的细节,如线程创建、任务调度、线程关闭等。
任务执行服务主要涉及以下接口:
使用者只需要通过 ExecutorService 提交任务,通过 Future 操作任务和结果即可,不需要关注线程创建和协调的细节。
线程池
任务执行服务的主要实现机制是线程池,实现类是 ThreadPoolExecutor,线程池主要由两个概念组成,一个是任务队列,另一个是工作者线程。任务队列是一个阻塞队列,保存待执行的任务。工作者线程主体就是一个循环,循环从队列中接受任务并执行。ThreadPoolExecutor 有一些重要的参数,理解这些参数对于合理使用线程池非常重要,78 节对这些参数进行了详细介绍,这里就不赘述了。
ThreadPoolExecutor 实现了生产者 / 消费者模式,工作者线程就是消费者,任务提交者就是生产者,线程池自己维护任务队列。当我们碰到类似生产者 / 消费者问题时,应该优先考虑直接使用线程池,而非重新发明轮子,自己管理和维护消费者线程及任务队列。
CompletionService
在异步任务程序中,一种场景是,主线程提交多个异步任务,然后希望有任务完成就处理结果,并且按任务完成顺序逐个处理,对于这种场景,Java 并发包提供了一个方便的方法,使用 CompletionService,这是一个接口,它的实现类是 ExecutorCompletionService,它通过一个额外的结果队列,方便了对于多个异步任务结果的处理。
定时任务
异步任务中,常见的任务是定时任务。在 Java 中,有两种方式实现定时任务:
Timer 有一些需要特别注意的事项:
ScheduledExecutorService 的主要实现类是 ScheduledThreadPoolExecutor,它没有 Timer 的问题:
所以,实践中建议使用 ScheduledExecutorService。
小结
针对多线程开发的两个核心问题,竞争和协作,本节总结了线程安全和协作的多种机制,针对高层服务,本节总结了并发容器和任务执行服务,它们让我们在更高的层次上访问共享的数据结构,执行任务,而避免陷入线程管理的细节。到此为止,关于并发我们就告一段落了。
与之前章节一样,我们的探讨都是基于 Java 7 的,不过 Java 7 引入了一个 Fork/Join 框架,我们没有讨论。Java 8 在并发方面也有一些更新,比如:
关于这些内容,我们在探讨 Java 8 的时候再继续讨论。
从下一节开始,我们来探讨 Java 中的一些动态特性,比如反射、注解、动态代理等,它们到底是什么呢?
---------------------
并发相关原创文章
(65) 线程的基本概念
(66) 理解 synchronized
(67) 线程的基本协作机制 (上)
(68) 线程的基本协作机制 (下)
(69) 线程的中断
(70) 原子变量和 CAS
(71) 显式锁
(72) 显式条件
(73) 并发容器 - 写时拷贝的 List 和 Set
(74) 并发容器 - ConcurrentHashMap
(75) 并发容器 - 基于 SkipList 的 Map 和 Set
(76) 并发容器 - 各种队列
(77) 异步任务执行服务
(78) 线程池
(79) 方便的 CompletionService
(80) 定时任务的那些坑
(81) 并发同步协作工具
(82) 理解 ThreadLocal
----------------
未完待续,查看最新文章,敬请关注微信公众号 "老马说编程"(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索 Java 编程及计算机技术的本质。用心原创,保留所有版权。
来源: http://www.cnblogs.com/swiftma/p/6765099.html