原文是发表在并发编程网上翻译后的 《Java 7 并发编程指南》,这里对其中的目录做个更加详细的描述,并且写出了重点说明,方便日后快速查阅。建议仔细查看每节的代码实现,非常具有参考价值。可以直接点击标题,查看原文。
原文目录地址:http://ifeve.com/java-7-concurrency-cookbook/
代码实现:https://github.com/Wang-Jun-Chao/java-concurrency
目录
前言
第一章: 线程管理
-
介绍
本节主要介绍线程基本的操作。
-
线程的创建和运行
在Java中,我们有2个方式创建线程:
- 通过直接继承thread类,然后覆盖run()方法。
- 构建一个实现Runnable接口的类, 然后创建一个thread类对象并传递Runnable对象作为构造参数
之后调用这个线程对象的start()对象即可运行。
-
获取和设置线程信息
Thread类的对象中保存了一些属性信息能够帮助我们来辨别每一个线程,知道它的状态,调整控制其优先级。 这些属性是:
- ID: 每个线程的独特标识。
- Name: 线程的名称。
- Priority: 线程对象的优先级。优先级别在1-10之间,1是最低级,10是最高级。不建议改变它们的优先级,但是你想的话也是可以的。
- Status: 线程的状态。在Java中,线程只能有这6种中的一种状态: new, runnable, blocked, waiting, time waiting, 或 terminated.
-
线程的中断
Java提供中断机制来通知线程表明我们想要结束它。中断机制的特性是线程需要检查是否被中断,而且还可以决定是否响应结束的请求。所以,线程可以忽略中断请求并且继续运行。如果需要中断线程,只需要调用线程对象的interrupt()方法即可,线程通过isInterrupted()方法来检测自己是否被中断。比如,你可以像例子中一样,轮询检测自己是否被中断,如果被中断了就直接return。
-
操作线程的中断机制
我们可以选择使用抛出InterruptedException异常的方式来停止线程。通常它可以由并发API来抛出比如sleep()方法
-
线程的睡眠和恢复
当你调用sleep()方法, Thread 离开CPU并在一段时间内停止运行。在这段时间内,它是不消耗CPU时间的,使得CPU可以执行其他任务。
当 Thread 是睡眠并且处于中断状态(比如调用了线程的interrupt()方法)的时候,sleep方法会立刻抛出InterruptedException异常并不会一直等到睡眠时间过去。
Java 并发 API 有另一种方法能让线程对象离开 CPU。它是 yield() 方法, 它向JVM表示线程对象可以让CPU执行其他任务。JVM 不保证它会遵守请求。通常,它只是用来试调的。
-
等待线程的终结
当线程调用某个其他线程的join()方法时,它会暂停当前线程,直到被调用join()方法的其他线程执行完成。
-
守护线程的创建和运行
调用线程对象的setDaemon(true);来使其成为一个守护线程。
-
处理线程的不受控制异常
Java里有2种异常:
- 检查异常(Checked exceptions): 这些异常必须强制捕获它们或在一个方法里的throws子句中。 例如, IOException 或者ClassNotFoundException。
- 未检查异常(Unchecked exceptions): 这些异常不用强制捕获它们。例如, NumberFormatException。当一个非检查异常被抛出,默认的行为是在控制台写下stack trace并退出程序。
我们可以使用线程对象的setUncaughtExceptionHandler方法来设置未检测异常的回调函数。这个回调函数以Thread 对象和 Exception 作为参数。
此外调用Thread.setDefaultUncaughtExceptionHandler()可以为应用里的所有线程对象建立异常 handler 。
当一个未捕捉的异常在线程里被抛出,JVM会寻找此异常的3种可能潜在的处理者(handler)。首先, 它寻找这个未捕捉的线程对象的异常handle,如我们在在这个指南中学习的。如果这个handle 不存在,那么JVM会在线程对象的ThreadGroup里寻找非捕捉异常的handler,如在处理线程组内的不受控制异常里介绍的那样(下面的第12节)。如果此方法也不存在,那么 JVM 会寻找默认非捕捉异常handle,就是调用Thread.setDefaultUncaughtExceptionHandler()设置的那个handle。如果没有一个handler存在, 那么 JVM会把异常的 stack trace 写入操控台并结束任务。
-
使用本地线程变量
使用ThreadLocal来存储线程局部变量。它的使用类似于这种:
/**
* 线程局部变量,其中的内容不能共享,线程被初始化时会创建其包含的变量
*/
private static ThreadLocal<Date> startDate = new ThreadLocal<Date>() {
@Override
protected Date initialValue() {
return new Date();
}
};
-
线程组
Java 提供 ThreadGroup 类来组织线程。 ThreadGroup 对象可以由 Thread 对象组成和由另外的 ThreadGroup 对象组成,生成线程树结构。
ThreadGroup 类储存线程对象和其他有关联的 ThreadGroup 对象,所以它可以访问他们的所有信息 (例如,状态) 和全部成员的操作表现 (例如,中断)。
-
处理线程组内的不受控制异常
重写ThreadGroup的uncaughtException方法来为线程组设置未捕获异常的回调方法
-
用线程工厂创建线程
实现ThreadFactory接口来使用线程工厂创建线程,你需要知道使用线程工厂创建线程有什么优势,一般来说它具有下面这些优势:
- 更简单的改变了类的对象创建或者说创建这些对象的方式。
- 更简单的为了限制的资源限制了对象的创建。 例如, 我们只new一个此类型的对象。
- 更简单的生成创建对象的统计数据。
第二章 : 基本线程同步
-
介绍
在并发应用程序中,多个线程读或写相同的数据或访问同一文件或数据库连接这是正常的。这些共享资源会引发错误或数据不一致的情况,我们必须通过一些机制来避免这些错误。
-
同步方法
synchronized关键字基本使用
-
在同步的类里安排独立属性
当你使用synchronized关键字来保护代码块时,你必须通过一个对象的引用作为参数。通常,你将会使用this关键字来引用执行该方法的对象,但是你也可以使用其他对象引用。通常情况下,这些对象被创建只有这个目的。比如,你在一个类中有被多个线程共享的两个独立属性。你必须同步访问每个变量,如果有一个线程访问一个属性和另一个线程在同一时刻访问另一个属性,这是没有问题的。
-
在同步代码中使用条件
涉及wait(),notify(),和notifyAll() 方法。
一个线程可以在synchronized代码块中调用wait()方法。如果在synchronized代码块外部调用wait()方法,JVM会抛出IllegalMonitorStateException异常。当线程调用wait()方法,JVM让这个线程睡眠,并且释放控制 synchronized代码块的对象,这样,虽然它正在执行但允许其他线程执行由该对象保护的其他synchronized代码块。为了唤醒线程,你必 须在由相同对象保护的synchronized代码块中调用notify()或notifyAll()方法。
通常我们会将wait()写在一个while()循环中,以在唤醒之后再次检测是否满足条件,满足则继续执行,否则再次进入wait(),因为wait()有的时候会被错误的唤醒。。
-
使用Lock来同步代码块
Java提供另外的机制用来同步代码块。它比synchronized关键字更加强大、灵活。它是基于Lock接口和实现它的类(如ReentrantLock)。这种机制有如下优势:
- 它允许以一种更灵活的方式来构建synchronized块。使用synchronized关键字,你必须以结构化方式得到释放synchronized代码块的控制权。Lock接口允许你获得更复杂的结构来实现你的临界区。
- Lock 接口比synchronized关键字提供更多额外的功能。新功能之一是实现的tryLock()方法。这种方法试图获取锁的控制权并且如果它不能获取该锁是因为其他线程在使用这个锁,它将返回这个锁。使用synchronized关键字的时候,当线程A试图执行synchronized代码块,如果线程B正在执行它,那么线程A将阻塞直到线程B执行完synchronized代码块。而使用锁,你可以执行tryLock()方法,这个方法返回一个 Boolean值表示是否有其他线程正在运行这个锁所保护的代码。
- 当有多个读者和一个写者时,Lock接口允许读写操作分离。
- Lock接口比synchronized关键字提供更好的性能。
-
使用读/写锁来同步数据访问
锁所提供的最重要的改进之一就是ReadWriteLock接口和唯一 一个实现它的ReentrantReadWriteLock类。这个类提供两把锁,一把用于读操作和一把用于写操作。同时可以有多个线程执行读操作。当一个线程正在执行一个写操作,不可能有任何线程执行读操作。当一个线程正在执行一个读操作,也不可能有任何线程执行写操作。总结来说就是:读读不互斥,读写,写写都是互斥的。
-
修改Lock的公平性
ReentrantLock类和 ReentrantReadWriteLock类的构造器中,允许一个名为fair的boolean类型参数,它允许你来控制这些类的行为。默认值为 false,这将启用非公平模式。在这个模式中,当有多个线程正在等待一把锁(ReentrantLock或者 ReentrantReadWriteLock),这个锁必须选择它们中间的一个来获得进入临界区,选择任意一个是没有任何标准的。true值将开启公平 模式。在这个模式中,当有多个线程正在等待一把锁(ReentrantLock或者ReentrantReadWriteLock),这个锁必须选择它们 中间的一个来获得进入临界区,它将选择等待时间最长的线程。
-
在Lock中使用多条件
涉及await()、signal()和signallAll();
这有点像前面的第4节在同步代码中使用条件。但是await()、signal()和signallAll()是配合Lock来进行使用的。而wait(),notify(),和notifyAll() 是配合synchronized来使用的。并且前者可以实现锁上的多个条件的等待。可以查看该章节示例代码中的Buffer类来查看如何使用。
第三章: 线程同步工具
-
介绍
在第二章,基本的线程同步中,我们学会了以下2个同步机制:
- 关键词同步(synchronized)
- Lock接口和它的实现类们:ReentrantLock, ReentrantReadWriteLock.ReadLock, 和 ReentrantReadWriteLock.WriteLock
在此章节,我们将学习怎样使用高等级的机制来达到多线程的同步。这些高等级机制有:
- Semaphores: 控制访问多个共享资源的计数器。此机制是并发编程的最基本的工具之一,而且大部分编程语言都会提供此机制。
- CountDownLatch: CountDownLatch 类是Java语言提供的一个机制,它允许线程等待多个操作的完结。
- CyclicBarrier: CyclicBarrier 类是又一个java语言提供的机制,它允许多个线程在同一个点同步。
- Phaser: Phaser类是又一个java语言提供的机制,它控制并发任务分成段落来执行。全部的线程在继续执行下一个段之前必须等到之前的段执行结束。这是Java 7 API的一个新特性。
- Exchanger: Exchanger类也是java语言提供的又一个机制,它提供2个线程间的数据交换点。
-
控制并发访问一个资源
使用Semaphore(信号量)类来实现一种比较特殊的semaphores种类,称为binary semaphores。
-
控制并发访问多个资源
使用semaphores来保护多个资源的副本,也就是说当你有一个代码片段可以同时被多个线程执行。关于这一点可以查看该章节的示例代码,它很好的说明了一个使用它的场景。信号量也可以用来控制并发线程数量。
-
等待多个并发事件完成
CountDownLatch 类的基本使用方法。有一个线程调用CountDownLatch对象的await()方法等待,等待其他线程完成必须在await()方法之后的之前完成的任务。
-
在一个相同点同步任务
CyclicBarrier 类有一个内部计数器控制到达同步点的线程数量。每次线程到达同步点,它调用 await() 方法告知 CyclicBarrier 对象到达同步点了。CyclicBarrier 把线程放入睡眠状态直到全部的线程都到达他们的同步点。
当全部的线程都到达他们的同步点,CyclicBarrier 对象叫醒全部正在 await() 方法中等待的线程们,然后,选择性的,为CyclicBarrier的构造函数 传递的 Runnable 对象(例子里,是 Grouper 对象)创建新的线程执行外加任务。
-
运行并发阶段性任务
Phaser类的使用,可以参考这里来加深对Phaser的使用。
-
控制并发阶段性任务的改变
同上节,在参考的文章中有说明。
-
在并发任务间交换数据
Exchange类的使用,2个线程间定义同步点,当2个线程到达这个点,他们相互交换数据类型,使用第一个线程的数据类型变成第二个的,然后第二个线程的数据类型变成第一个的。
第四章: 线程执行者
-
介绍
提出Executor framework概念,这有点像我们之前提到的线程工厂,实际上这些线程执行者框架就是使用线程工厂来创建线程的。这里给出Executor框架类图
-
创建一个线程执行者
ThreadPoolExecutor的基本使用,execute提交任务,shutdown停止线程执行者。 -
创建一个大小固定的线程执行者
上一节我们使用newCachedThreadPool()来创建ThreadPoolExecutor对象,它的线程池为无限大。但是在这里我们需要使用newFixedThreadPool来创建线程池大小固定的ThreadPoolExecutor对象,定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。 -
执行者执行返回结果的任务
Executor framework还可以并发执行返回结果的任务,这个时候我们使用的是submit函数提交任务,而不是execute。submit()函数将会返回一个Future对象,使用这个对象的get方法,你将会获得执行结果,不止如此Future还提供了其他获得任务执行信息和控制任务的方法,它使你不至于将任务交给执行器之后对任务状态一无所知。 -
运行多个任务并处理第一个结果
如果你不需要像前面一样,需要所有提交的任务的结果,而是只需要最先完成的任务的结果,你可以使用执行器的invokeAny()方法。该方法接收一个任务列表作为参数,返回最先执行完成的任务的结果。 -
运行多个任务并处理所有的结果
如果你觉得像第4小节讲到的那样一个一个submit()任务,然后获得Future比较繁杂,你可以直接将任务列表使用invokeAll()方法直接全部给执行器,它会返回一个对应的Future列表,对应的Future在列表中的位置和提交的任务一致,invokeAll将所有的任务提交后,等待全部任务完成才返回; -
在延迟后执行者运行任务
创建ScheduledThreadPoolExecutor和利用这个执行器安排任务在指定的时间后执行。它是使用这个执行器对象的schedule()函数来完成这个操作的,如:executor.schedule(task, delayTime, TimeUnit.SECONDS); -
执行者定期的执行任务
使用ScheduledThreadPoolExecutor类还可以执行定期任务,这一点是调用执行器对象的scheduleAtFixedRate()方法来实现的。当执行器对象调用shutdown()方法后,将不会执行周期性任务。 -
执行者取消任务
通过调用执行器返回的Future的cancel方法,可以取消任务的执行。 -
执行者控制一个结束任务
重写FutureTask类的done()方法来实现在执行者执行任务完成后执行额外的一些代码。 -
执行者分离运行任务和处理结果
使用CompletionService类来实现一边执行任务线程,一边处理执行完任务线程的结果。原文在这里的解释不是特别清除,可以参考这里 -
执行者控制被拒绝的任务
实现RejectedExecutionHandler,来处理线程池shutdown()后提交的任务。
第五章: Fork/Join 框架
-
介绍
Fork/Join框架被设计用来解决可以使用分而治之技术将任务分解成更小的问题。具体可以查看该节内容。
-
创建 Fork/Join 池
创建一个ForkJoinPool对象来执行任务。创建一个ForkJoinPool执行的ForkJoinTask类。
-
加入任务的结果
在上一节的基础上,添加说明如果处理任务的子线程有返回结果如何处理。主要就是在Override的compute()函数中返回结果,然后可以使用处理任务的子线程的get方法获得这个返回结果。并且从例子中可以看出,Fork/Join框架可以提交不一样的子任务,具体表现如下;
所以Fork/Join框架更加强调的是,父线程等待invokeAll出来的子线程完成后,从invokeAll退出继续完成接下来的逻辑程序。父线程从逻辑上来说像是从invokeAll处暂停了(等待子线程完成后退出),但是实际上父线程的线程资源还可以去执行其他没有被执行的任务,但是执行的内容不是父线程原来的内容,这样可以减少创建的线程的数量,这使用了work-stealing算法,即分配一个新的任务给正在执行睡眠任务的工作线程。 -
异步运行任务
使用ForkJoinTask的异步方法(比如fork()),来向线程池提交任务。之前我们使用的是同步方法(比如invokeAll()),这将阻塞父线程。而异步方法将继续父线程的内容,我们使用异步方法也就意味着不能使用work-stealing算法,除非在父线程中使用调用join()或get()方法来等待任务的完成时。
-
任务中抛出异常
处理任务中抛出的异常,检查异常不能从compute()中抛出,你必须手动解决(使用try-catch)。未检查异常将不会输入任何信息到控制台。就像没有发生一样,但是抛出非检查异常的线程和它的所有父辈异常不会正常执行。我们自然不会希望,任务没有正常执行但是又没有给任何提示,所以最后我们可以使用task.isCompletedAbnormally()来检查execute()给线程池的任务到底有没有抛出非检查异常。
非检查异常也可以使用try-catch来捕获,但是一般我们是不会这样做的,因为非检查异常的抛出一般是意味着我们的代码有错误,应该去改正它。 -
取消任务
ForkJoinTask类提供cancel()方法用于这个目的。当你想要取消一个任务时,有一些点你必须考虑一下,这些点如下:
- ForkJoinPool类并没有提供任何方法来取消正在池中运行或等待的所有任务。
- 当你取消一个任务时,你不能取消一个已经执行的任务。
但是你依旧可以调用运行的任务的cancel()方法,但是具体执不执行就要看到底这个任务有没有在运行了。可以参考示例代码中,创建一个TaskManager类来管理任务。
第六章: 并发集合
-
介绍
这里需要了解到并发集合分为两种:- 阻塞集合:这种集合包括添加和删除数据的操作。如果操作不能立即进行,是因为集合已满或者为空,该程序将被阻塞,直到操作可以进行。
- 非阻塞集合:这种集合也包括添加和删除数据的操作。如果操作不能立即进行,这个操作将返回null值或抛出异常,但该线程将不会阻塞。
-
使用非阻塞线程安全列表
非阻塞并发列表ConcurrentLinkedDeque类。 -
使用阻塞线程安全列表
阻塞列表LinkedBlockingDeque类。 -
用优先级对使用阻塞线程安全列表排序
阻塞优先队列PriorityBlockingQueue类 -
使用线程安全与带有延迟元素的列表
阻塞队列DelayedQueue类,可以控制元素延迟出现 -
使用线程安全的可遍历映射
ConcurrentSkipListMap提供了多线程并发存取<Key, Value>数据并且希望保证数据有序时的数据结构,底层使用跳表实现。关于ConcurrentSkipListMap更加详细的信息可以参考这里 -
生成并行随机数
使用ThreadLocalRandom类来生成随机数 -
使用原子变量
使用原子变量,原子变量不使用任何锁或者其他同步机制来保护它们的值的访问。他们的全部操作都是基于CAS操作。关于CAS操作更具体的内容网上有很多,务必理解。特别是要理解底层是如何实现的。我建议查看这里。特别是其一直深入到底层的分析,非常有意思。 -
使用原子阵列
使用原子数组,这和原子变量差不多。
第七章: 定制并发类
-
介绍
-
定制ThreadPoolExecutor 类
复写ThreadPoolExecutor 类的一些方法。这里需要去理解ThreadPoolExecutor类构造函数的一些参数。
-
实现一个优先级制的执行者类
实现一个按优先级执行的ThreadPoolExecutor 类,实际上就是TASK(任务)实现Comparable接口,而后将PriorityBlockingQueue作为ThreadPoolExecutor的提交任务时所使用的队列。
-
实现ThreadFactory接口来生成自定义线程
使用线程工厂来创造线程
-
在执行者对象中使用我们的 ThreadFactory
以自己的线程工厂为参数构造执行器对象,使执行器对象使用我们的线程工厂创造线程。
-
在计划好的线程池中定制运行任务
使用ScheduledThreadPoolExecutor 来运行计划任务。
-
实现ThreadFactory接口来生成自定义线程给Fork/Join框架
实现ThreadFactory接口创建自己的线程工厂提供给Fork/Join框架使用,而不是使用其默认的线程工厂。需要明确的是,不要将任务(callable或runnable)和线程(thread)混为一谈。一个线程可以在不同的时期执行多个任务。线程也可以有自己的内存空间,保存自己的数据。如果能将任务和线程这两个概念区分开来,代码还是比较好理解的。
-
在Fork/Join框架中定制运行任务
上一节,我们为Fork/Join框架创建的线程工厂,这一节我们将定制我们自己的任务类,加入到Fork/Join框架的任务类一般都继承ForkJoinTask这个抽象类,一般我们使用:
- RecursiveAction: 如果你的任务没有返回结果
- RecursiveTask: 如果你的任务返回结果
但是,这里我们将自己实现一个类继承ForkJoinTask这个抽象类,从而使任务运行更加更加符合我们的想法。
当我们想去实现和任务具体的内容没有关系的功能的时候,比如统计,控制线程的时候,我们可以尝试定制自己的并发类。 -
实现一个自定义锁类
实现Lock接口,从而实现自定义锁类。
-
实现一个基于优先级传输Queue
本节实现一个有优先级的传输队列,用来解决生产者/消费者问题。
-
实现你自己的原子对象
继承原子变量,实现自己的类似原子变量的一些操作,比如并发安全的加1或者减1。
第八章: 测试并发应用程序
-
介绍
本节主要讲述如何测试你的并发应用程序是否正确。2-5节都是说明如何从并发类中获取相应的信息,也就是通过获取状态变量来对其相关对象进行监控。下面每节的内容很直白的体现在小节题目中了。
-
监控锁接口
-
监控Phaser类
-
监控执行者框架
-
监控Fork/Join池
-
编写有效的日志
-
FindBugs分析并发代码
-
配置Eclipse来调试并发代码
-
配置NetBeans来调试并发代码
-
MultithreadedTC测试并发代码