本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》(马俊昌著),由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买:京东自营连接 html
![]()
从65节到82节,咱们用了18篇文章讨论并发,本节进行简要总结。java
多线程开发有两个核心问题,一个是竞争,另外一个是协做。竞争会出现线程安全问题,因此,本节首先总结线程安全的机制,而后是协做的机制。管理竞争和协做是复杂的,因此Java提供了更高层次的服务,好比并发容器类和异步任务执行服务,咱们也会进行总结。本节纲要以下:程序员
线程表示一条单独的执行流,每一个线程有本身的执行计数器,有本身的栈,但能够共享内存,共享内存是实现线程协做的基础,但共享内存有两个问题,竞态条件和内存可见性,以前章节探讨了解决这些问题的多种思路:算法
synchronized简单易用,它只是一个关键字,大部分状况下,放到类的方法声明上就能够了,既能够解决竞态条件问题,也能够解决内存可见性问题。编程
须要理解的是,它保护的是对象,而不是代码,只有对同一个对象的synchronized方法调用,synchronized才能保证它们被顺序调用。对于实例方法,这个对象是this,对于静态方法,这个对象是类对象,对于代码块,须要指定哪一个对象。数组
另外,须要注意,它不能尝试获取锁,也不响应中断,还可能会死锁。不过,相比显式锁,synchronized简单易用,JVM也能够不断优化它的实现,应该被优先使用。安全
显式锁是相对于synchronized隐式锁而言的,它能够实现synchronzied一样的功能,但须要程序员本身建立锁,调用锁相关的接口,主要接口是Lock,主要实现类是ReentrantLock。微信
相比synchronized,显式锁支持以非阻塞方式获取锁、能够响应中断、能够限时、能够指定公平性、能够解决死锁问题,这使得它灵活的多。数据结构
在读多写少、读操做能够彻底并行的场景中,可使用读写锁以提升并发度,读写锁的接口是ReadWriteLock,实现类是ReentrantReadWriteLock。多线程
synchronized和显式锁都是锁,使用锁能够实现安全,但使用锁是有成本的,获取不到锁的线程还须要等待,会有线程的上下文切换开销等。保证安全不必定须要锁。若是共享的对象只有一个,操做也只是进行最简单的get/set操做,set也不依赖于以前的值,那就不存在竞态条件问题,而只有内存可见性问题,这时,在变量的声明上加上volatile就能够了。
使用volatile,set的新值不能依赖于旧值,但不少时候,set的新值与原来的值有关,这时,也不必定须要锁,若是须要同步的代码比较简单,能够考虑原子变量,它们包含了一些以原子方式实现组合操做的方法,对于并发环境中的计数、产生序列号等需求,考虑使用原子变量而非锁。
原子变量的基础是CAS,比较并设置,通常的计算机系统都在硬件层次上直接支持CAS指令。经过循环CAS的方式实现原子更新是一种重要的思惟,相比synchronized,它是乐观的,而synchronized是悲观的,它是非阻塞式的,而synchronized是阻塞式的。CAS是Java并发包的基础,基于它能够实现高效的、乐观、非阻塞式数据结构和算法,它也是并发包中锁、同步工具和各类容器的基础。
之因此会有线程安全的问题,是由于多个线程并发读写同一个对象,若是每一个线程读写的对象都是不一样的,或者,若是共享访问的对象是只读的,不能修改,那也就不存在线程安全问题了。
咱们在介绍容器类CopyOnWriteArrayList和CopyOnWriteArraySet时介绍了写时复制技术,写时复制就是将共享访问的对象变为只读的,写的时候,再使用锁,保证只有一个线程写,写的线程不是直接修改原对象,而是新建立一个对象,对该对象修改完毕后,再原子性地修改共享访问的变量,让它指向新的对象。
ThreadLocal就是让每一个线程,对同一个变量,都有本身的独有拷贝,每一个线程实际访问的对象都是本身的,天然也就不存在线程安全问题了。
多线程之间的核心问题,除了竞争,就是协做。咱们在67节和68节介绍了多种协做场景,好比生产者/消费者协做模式、主从协做模式、同时开始、集合点等。以前章节探讨了协做的多种机制:
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,Future是一个接口,主要实现类是FutureTask。
Future封装了主线程和执行线程关于执行状态和结果的同步,对于主线程而言,它只须要经过Future就能够查询异步任务的状态、获取最终结果、取消任务等,不须要再考虑同步和协做问题。
线程安全的容器有两类,一类是同步容器,另外一类是并发容器。在理解synchronized一节,咱们介绍了同步容器。关于并发容器,咱们介绍了:
Collections类中有一些静态方法,能够基于普通容器返回线程安全的同步容器,好比:
public static <T> Collection<T> synchronizedCollection(Collection<T> c) public static <T> List<T> synchronizedList(List<T> list) public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) 复制代码
它们是给全部容器方法都加上synchronized来实现安全的。同步容器的性能比较低,另外,还须要注意一些问题,好比复合操做和迭代,须要调用方手工使用synchronized同步,并注意不要同步错对象。
而并发容器是专为并发而设计的,线程安全、并发度更高、性能更高、迭代不会抛出ConcurrentModificationException、不少容器以原子方式支持一些复合操做。
CopyOnWriteArrayList基于数组实现了List接口,CopyOnWriteArraySet基于CopyOnWriteArrayList实现了Set接口,它们采用了写时拷贝,适用于读远多于写,集合不太大的场合。不适用于数组很大,且修改频繁的场景。它们是以优化读操做为目标的,读不须要同步,性能很高,但在优化读的同时就牺牲了写的性能。
HashMap不是线程安全的,在并发更新的状况下,HashMap的链表结构可能造成环,出现死循环,占满CPU。ConcurrentHashMap是并发版的HashMap,经过分段锁和其余技术实现了高并发,读操做彻底并行,写操做支持必定程度的并行,以原子方式支持一些复合操做,迭代不用加锁,不会抛出ConcurrentModificationException。
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实现了生产者/消费者模式,工做者线程就是消费者,任务提交者就是生产者,线程池本身维护任务队列。当咱们碰到相似生产者/消费者问题时,应该优先考虑直接使用线程池,而非从新发明轮子,本身管理和维护消费者线程及任务队列。
在异步任务程序中,一种场景是,主线程提交多个异步任务,而后但愿有任务完成就处理结果,而且按任务完成顺序逐个处理,对于这种场景,Java并发包提供了一个方便的方法,使用CompletionService,这是一个接口,它的实现类是ExecutorCompletionService,它经过一个额外的结果队列,方便了对于多个异步任务结果的处理。
异步任务中,常见的任务是定时任务。在Java中,有两种方式实现定时任务:
Timer有一些须要特别注意的事项:
ScheduledExecutorService的主要实现类是ScheduledThreadPoolExecutor,它没有Timer的问题:
因此,实践中建议使用ScheduledExecutorService。
针对多线程开发的两个核心问题,竞争和协做,本节总结了线程安全和协做的多种机制,针对高层服务,本节总结了并发容器和任务执行服务,它们让咱们在更高的层次上访问共享的数据结构,执行任务,而避免陷入线程管理的细节。到此为止,关于并发咱们就告一段落了。
与以前章节同样,咱们的探讨都是基于Java 7的,不过Java 7引入了一个Fork/Join框架,咱们没有讨论。Java 8在并发方面也有一些更新,好比:
关于这些内容,咱们在探讨Java 8的时候再继续讨论。
从下一节开始,咱们来探讨Java中的一些动态特性,好比反射、注解、动态代理等,它们究竟是什么呢?
未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深刻浅出,老马和你一块儿探索Java编程及计算机技术的本质。用心原创,保留全部版权。