1.简介
1.1并发简史
产生原因:资源利用率、公平性、便利性
1.2线程的优势
1.2.1发挥多处理器的强大功能
1.2.2建模的简单性
1.2.3异步事件的简化处理
1.2.4响应更灵敏的用户界面
1.3线程带来的风行
1.3.1安全性问题
永远不发生糟糕的事情
1.3.2活跃性问题
某件正确的事情最终会发生
1.3.3性能问题
希望正确的事情尽快发生
1.4线程无处不在
所使用的框架可能会创建线程
Timer类,用于延迟运行任务一次或多次,确保访问对象是线程安全的
Servlet和JSP,需要保证多个Servlet共享对象的线程安全
远程方法调用
Swing和AWT
2.线程安全性
2.1什么是线程安全性
多个线程访问某个类时,这个类始终都能表现出正确的行为,这个类是线程安全的
无状态对象一定是线程安全的
2.2原子性
2.2.1竞态条件
当计算的正确性取决于线程的执行时序时,就会发生竟态条件
基于失效的观察结果做出判断的竟态条件称为“先检查后执行”
2.2.2示例:延迟初始化中的竞态条件
2.2.3复合操作
将“先检查后执行”和“读取-修改-写入”等操作统称为复合操作
2.3加锁机制
2.3.1内置锁
2.3.2重入
2.4用锁来保护状态
2.5活跃性与性能
当执行时间较长的计算或者可能无法快速完成的操作时,一定不要持有锁
3.对象的共享
3.1可见性
3.1.1失效数据
获取到一个未更新的值
3.1.2非原子的64位操作
获取的失效数据是之前的值而非随机值是最低安全性,但不适用于非volaties类型的64位数值变量,因为JVM会把64位的读操作和写操作分解为2个32位的操作
在多线程中使用共享且可变的long和double等类型的变量是不安全的,除非用volatie声明或锁保护
3.1.3加锁与可见性
3.1.4Volatile变量
Volatile声明的变Volatile变量量是共享的,读取Volatile变量总能获取最新的值
但是Volatile变量比较脆弱,仅当Volatile变量能简化代码的失效以及对同步策略的验证时才应该使用它们
加锁机制可以确保可见性和原子性,Volatile变量只能确保可见性
3.2发布与逸出
发布:使对象能够在当前作用域之外的代码中使用
逸出:不应该被发布的对象被发布时称为逸出
安全的对象构造过程
不要在构造过程使用this逸出
使用工厂来放在this引用在构造过程中逸出
3.3线程封闭
3.3.1Ad-hoc线程封闭
维护线程封闭性的职责完全由程序实现,但是具有脆弱性,尽量少用
3.3.2栈封闭
3.3.3ThreadLocal类
使线程中的值与保存值的对象关联起来,总能获取到最新值
ThreadLocal变量类似于全局变量,降低了代码的可重用性,在类之间引入了隐含的耦合性
3.4不变性
3.4.1Final域
3.4.2示例:使用Volatile类型来发布不可变对象
3.5安全发布
3.5.1不正确的发布:正确的对象被破坏
3.5.2不口不对象与初始化安全性
3.5.3安全发布的常用模式
在静态初始化函数中初始化一个对象引用
将对象的引用保存到volatile类型的域或者AtomicReferance对象中
将对象的引用保存到某个正确构造对象的final类型域中
将对象的引用保存到一个由锁保护的域中
3.5.4事实不可变对象
对象在技术上看是可变,但是其状态在发布后不会再改变的对象称为“事实不可变对象”
3.5.5可变对象
不可变对象可以任意发布
事实不可变对象通过安全方式发布
可变对象通过安全方式发布,并且必须是线程安全的或者锁保护起来
3.5.6安全的共享对象
策略:线程封闭、只读共享、线程安全共享、保护对象
4.对象的组合
4.1设计线程安全的类
4.1.1收集同步需求
4.1.2依赖状态的操作
4.1.3状态的所有权
4.2实例封闭
4.2.1Java监视器模式
4.2.2实例:车辆追踪
4.3线程安全性的委托
4.3.1示例:基于委托的车辆追踪器
4.3.2独立的状态变量
4.3.3当委托失效时
4.3.4发布底层的状态变量
4.3.5示例:发布状态的车辆追踪器
4.4在现有的线程安全类中添加功能
4.4.1客户端加锁机制
对使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户端代码
若没有则添加的正确的辅助类
public class ListHelper<E> { public List<E> list = Collections.synchronizedList(new ArrayList<>()); public boolean putIfAbsent(E x) { synchronized (list) {//注意加锁的对象 boolean absent = !list.contains(x); if (absent) { list.add(x); } return absent; } } }
会破坏同步策略的封装性
4.4.2组合
public class ImprovedList<T> implements List<T> { private final List<T> list; public ImprovedList(List<T> list) { this.list = list; } public synchronized boolean putIfAbsent(T x) { boolean contains = list.contains(x); if (contains) { list.add(x); } return !contains; } }
4.5将同步策略文档化
解释含糊的文档
5.基础构建模块
5.1同步容器类
5.1.1同步容器类的问题
5.1.2迭代器与Concurrent-ModificationException
5.1.3隐藏迭代器
5.2并发容器
5.2.1ConcurrentHashMap
5.2.2额外的原子Map操作
5.2.3CopyOnWriteArrayList
当迭代操作远远多于修改操作时才应该使用“写入时复制”容器
5.3阻塞队列和生产者-消费模式
5.3.1示例:桌面搜索
5.3.2串行线程封闭
5.3.3双端队列与工作密取
5.4阻塞方法与中断方法
恢复中断
5.5同步工具类
5.5.1闭锁
延迟线程的进度直到其到达终止状态,用于确保某些活动直到其他活动都完成才执行
public class TestHarness { public long timeTasks(int nThrads, final Runnable task) throws InterruptedException { final CountDownLatch startGate = new CountDownLatch(1);//起始门 final CountDownLatch endGate = new CountDownLatch(nThrads);//结束门 for (int i = 0; i < nThrads; i++) { Thread t = new Thread() { public void run() { try { startGate.await();//等待所有线程准备就绪 try { task.run(); } finally { endGate.countDown();//完成一件事情就减1 } } catch (InterruptedException ignored) { } } }; t.start(); } long start = System.nanoTime(); startGate.countDown(); endGate.await(); long end = System.nanoTime(); return end - start; } }
5.5.2FutureTask
用于闭锁
5.5.3信号量
计数信号量用于控制同时访问特定资源的操作数量
5.5.4栅栏
所有线程都必须同时到达栅栏才能继续执行,栅栏可以重置复用
5.6构建高效且可伸缩的结果缓存
6.任务执行
6.1在线程中执行任务
6.1.1串行地执行任务
6.1.2显示地为任务创建线程
6.1.3无限制创建线程的不足
线程生命周期的开销非常高
资源消耗大
稳定性差
6.2Executor框架
6.2.1示例:基于Executor的Web服务器
public class Test { private static final int NTHREADS = 100; private static final Executor exec = Executors.newFixedThreadPool(NTHREADS); public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(8080); while (true) { final Socket connection = socket.accept(); Runnable task = new Runnable() { public void run() { handleRequest(connection); } }; exec.execute(task); } } }
6.2.2执行策略
6.2.3线程池
newFixedThreadPool()创建固定长度的线程池
newCachedThreadPool()创建可缓存的线程池,当前规模超过处理需求时,回收空闲线程,需求增加时,添加新线程,规模大小无限制
newScheduledThreadPool()以延迟或定时方式执行任务的固定线程池
6.2.4Executor的生命周期
6.2.5延迟任务与周期任务
Timer类精准性有问题,而且抛出异常就会被取消
newScheduledThreadPool是基于相对时间的,用于间隔时间的执行的任务,例如1分钟检查一次活动
6.3找出可利用的并行性
6.3.1示例:串行的页面渲染器
6.3.2携带结果的任务Callable与Future
6.3.3示例使用Future实现页面渲染器
6.3.4在异构任务并行化中存在的局限
6.3.5CompletionService:Executor与BlockingQueue
6.3.6示例:使用CompletionService实现页面渲染器
6.3.7为任务设置时限
6.3.8示例:旅行预定门户网站
invokeAll()可以将按照任务集合中迭代器的顺序将所有Future添加到返回的集合中,当所有任务执行完毕或中断或超时,invokeAll将返回
7.取消与关闭
7.1任务取消
7.1.1中断
用户请求取消
有时间限制的操作,搜索任务超时,取消正在搜索的任务
应用程序事件,解决方法找到时,取消其他搜索解决方案的任务
错误
关闭,程序或服务器关闭
发出中断请求,线程在合适的时刻中断自己
7.1.2中断策略
7.1.3响应中断
7.1.4示例:计时运行
7.1.5通过Future来实现取消
7.1.6处理不可中断的阻塞
Java.io包中的同步Socket I/O
Java.io包中的同步I/O
Selector的异步I/O
获取某个锁
7.1.7才有newTaskFor来封装非标准的取消
7.2停止基于线程的服务
7.2.1示例:日志服务
7.2.2关闭ExecutorService
7.2.3“毒丸”对象
当队列得到这个“毒丸”对象,立即停止
7.2.4示例:只执行一次的服务
boolean checkMail(Set<String> hosts, long timeout, TimeUnit unit) throws InterruptedException { ExecutorService exec = Executors.newCachedThreadPool(); final AtomicBoolean hasNewMail = new AtomicBoolean((false)); try { for (final String host : hosts) { exec.execute(new Runnable() { public void run() { if (checkMail(host)) { hasNewMail.set(true); } } }); } } finally { exec.shutdown();//安全的关闭 exec.awaitTermination(timeout, unit); } return hasNewMail.get(); }
7.2.5shuadownNow的局限性
7.3处理非正常的线程终止
未捕获一次的处理
7.4JVM关闭
7.4.1关闭钩子
通过Runtime.addShutdownHook注册的但尚未开始的线程
用于实现服务或应用程序的清理工作
7.4.2守护线程
主线程创建的所有线程都是普通线程
主线程创建之外的所有线程都是守护线程
7.4.3终结器
8.线程池的使用
8.1在任务与执行处理之间的耦合
8.1.1线程饥饿死锁
一个线程无期限的等待其他线程的资源或条件会发送线程饥饿死锁
8.1.2运行时间较长的任务
8.2设置线程池的大小
8.3配置ThreadPoolExecutor
8.3.1线程的创建与销毁
8.3.2管理队列任务
8.3.3饱和策略
“中止”策略,抛弃下一个将要被执行的任务
“调用者运行”策略,将任务回退给调用者
8.3.4线程工厂
8.3.5在调用构造函数后再定制ThreadPoolExecutor
8.4扩展ThreadPoolExecutor
示例:给线程池添加统计信息
8.5递归算法的并行化
示例:谜题框架
9.图形用户界面应用程序
9.1为什么GUI是多线程的
9.1.1串行事件处理
9.1.2Swing中的线程封闭机制
9.2短时间的GUI任务
9.3长时间的GUI任务
9.3.1取消
9.3.2进度标识和完成标识
9.3.3SwingWorker
9.4共享数据模型
9.4.1线程安全的数据模型
9.4.2分解数据模型
9.5其他形式的单线程子系统
10.避免活跃性危险
10.1死锁
10.1.1锁顺序死锁
10.1.2动态的锁顺序死锁
10.1.3在协作对象之间发送的死锁
10.1.4开放调用
调用方法时不持有锁,这种调用称为开放调用
10.1.5资源死锁
10.2死锁的避免与诊断
10.2.1支持定时的锁
10.2.2通过线程转储信息来分析死锁
10.3其他活跃性危险
10.3.1饥饿
要避免使用线程优先级,会增加平台依赖性
10.3.2糟糕的响应性
10.3.3活锁
多个相互协作的线程对彼此进行响应从而修改各自的状态,使得任何一个线程都无法继续执行时,就发生了活锁
通过等待随机长度的时间和回退可以有效的避免活锁的发生
11.性能与可伸缩性
11.1对性能的思考
11.1.1性能与可伸缩性
运行速度指标:服务器时间、等待时间
处理能力指标:生产量、吞吐量
可伸缩性:增加计算资源时,程序的吞吐量或者处理能力相应地增加
11.1.2评估各种性能权衡因素
11.2Amdahl定律
11.2.1示例:在各种框架中隐藏的串行部分
11.2.2Amdahl定律的应用
准确估计出执行过程中串行部分所占的比例
11.3线程引入的开销
11.3.1上下文切换
可运行的线程大于CPU数量时,保存当前运行线程的执行上下文,新调度进来的线程执行上下文设置为当前上下文
11.3.2内存同步
11.3.3阻塞
11.4减少锁的竞争
11.4.1缩小锁的范围("快进快出")
11.4.2减小锁的粒度
11.4.3锁分段
11.4.4避免热点域
11.4.5一些替代独占锁的方法
11.4.6监测CPU的利用率
负载不充足
I/O密集
外部限制
锁竞争
11.4.7向对象池说“不”
11.5示例:比较Map的性能
11.6减少上下文切换的开销
12.并发程序的测试
12.1正确性测试
12.1.1基本的单元测试
12.1.2对阻塞操作的测试
12.1.3安全性测试
12.1.4资源管理的测试
12.1.5使用回调
12.1.6产生更多的交替操作
12.2性能测试
12.2.1在PutTakeTest增加计时功能
12.2.2多种算法的比较
12.2.3响应性衡量
12.3避免性能测试的陷阱
12.3.1垃圾回收
12.3.2动态编译
12.3.3对代码路径的不真实采样
12.3.4不真实的竞争程度
12.3.5无用代码的清除
12.4其他的测试方法
12.4.1代码审查
12.4.2静态分析工具
12.4.3面向方面的测试技术
12.4.4分析与监测工具
13.显示锁
13.1Lock与ReentrantLock
13.1.1轮询锁与定时锁
13.1.2可中断的锁获取操作
13.1.3非块结构的加锁
13.2性能考虑因素
13.3公平性
13.4在synchronized和ReentrantLock之间进行宣传
13.5读-写锁
14.构建自定义的同步工具
14.1状态依赖性的管理
14.1.1示例:将前提条件的失败传递给调用者
14.1.2示例:通过轮询与休眠来实现简单的阻塞
14.1.3条件队列
14.2使用条件队列
14.2.1条件谓词
14.2.2过早唤醒
14.2.3丢失的信号
14.2.4通知
14.2.5示例:阀门类
14.2.6子类的安全问题
14.2.7封装条件队列
14.2.8入口协议与出口协议
14.3显示的Condition对象
14.4Synchronized剖析
14.5AbstractQueuedSynchronizer
14.6java.util.concurrent同步器类的AQS
14.6.1ReentrantLock
14.6.2Semaphore与CountDownLatch
14.6.3FutureTask
14.6.4ReentrantReadWriteLock
15.原子变量与非阻塞同步机制
15.1锁的劣势
15.2硬件对并发的支持
15.2.1比较并交换
15.2.2非阻塞的计数器
15.2.3JVM对CAS的支持
15.3原子变量类
15.3.1原子变量是一种更好的volatile
15.3.2性能比较:锁与原子变量
15.4非阻塞算法
15.4.1非阻塞的栈
15.4.2非阻塞的链表
15.4.3原子的域更新器
15.4.4ABA问题
16.Java内存模型
16.1什么是内存模型,为什么需要它
16.1.1平台的内存模型
16.1.2重排序
16.1.3Java内存模型简介
16.1.4借助同步
16.2发布
16.2.1不安全的发布
16.2.2安全的发布
16.2.3安全初始化模式
16.2.4双重检查加锁
16.3初始化过程中的安全性