5. 取消与关闭
- 可取消(Cancellable): 如果外部代码能在某个操作正常完成之前将其置入"完成状态", 这个操作可称为可取消.
- Java中没有一种安全的抢占式方法来停止线程, 也就没有安全的抢占式方法来停止任务.
- 中断: 线程中断是一种协作机制, 线程可以通过这种机制来通知另一个线程, 告诉它在合适的或者可能的情况下停止当前工作, 并转而执行其他的工作.
- 中断操作: 并不会中断一个正在运行的线程, 而只是发出一个中断请求, 然后由线程在下一个合适的时刻中断自己.
- "毒丸"对象: 是指一个放在队列上的对象, 其含义是: "当得到这个对象时, 立即停止".
- 幂等: f(f(x)) = f(x).
- 线程可分为两种: 普通线程和守护线程. 默认情况下, 主线程创建的所有线程是普通线程. 当一个线程退出时, JVM会检查其他正在运行的线程, 如果这些线程是守护线程, JVM会正常退出操作. 当JVM停止 时, 所有仍然存在的守护线程都将被抛弃-即不会执行finally代码块, 也不会执行回卷栈, 而JVM只是直接退出.
6. 线程池
- 只有当任务是同类型的并且相互独立时, 线程池的性能才能达到最佳.
- 死锁: 在线程池中, 如果任务依赖于其他任务, 那么可能产生死锁.
- 线程饥饿死锁: 线程池的任务需要无限地等待一些必须由线程池中其他任务才能提供的资源或条件.
- 对于计算密集型任务, 在拥有N(cpu)个处理器的系统上, 当线程池的大小为N(cpu)+1时, 通常能现实最优的利用率.
- 基本的任务排队方法有3中: 无界队列, 有界队列和同步移交(Synchronous Handoff). 在使用有界队列时, 队列的大小与线程池的大小必须一起调节.
- 对于并发执行的任务, Executor框架是一种强大且灵活的框架. 它提供了大量可调节的选项, 例如创建线程和关闭线程的策略, 处理队列任务的策略, 处理过多任务的策略, 并且提供了钩子方法来扩展它的行为.
7. 活跃性危险
- 过度的使用加锁, 则可能导致顺序死锁(Lock-Ordering DeadLock).
- 如果在持有锁时调用某个外部方法, 那么将出现活跃性问题. 在这个外部方法中可能会获取其他锁(这可能会产生死锁), 或阻塞时间过长, 导致其他线程无法及时获得当前被持有的锁.
- JVM同通过线程转存(Thread Dump) 来帮助识别死锁的发生. 线程转存包括各个运行中的线程栈追踪信息.
- 当线程由于无法访问它所需要的资源而不能继续执行时, 就发生了"饥饿(Starvation)". 引发饥饿的最常见资源就是CPU时钟周期.
- 活锁: 不会阻塞线程, 但也不能继续执行, 因为线程将不断重复执行相同的操作, 而且总会失败. 要解决活锁问题, 需要在重试机制中引入随机性.
8. 性能与可伸缩性
- 当操作性能由于某种特定的资源而受到限制时, 通常将该操作称为资源密集型的操作.
- 多个线程总会引入一些额外的性能开销: 线程之间的协调(加锁, 触发信号,内存同步..)、增加上下文切换、线程的创建与销毁、线程的调度...
- 可伸缩性指的是: 当增加计算资源时(例如CPU、内存、存储容量、I/O带宽...), 程序的吞吐量或者处理能力能相应的增加.
- 衡量优化的指标有: 平均计算时间, 最差时间, 可预知性.
- 很多性能优化措施通常都是牺牲可读性和可维护性为代价-代码越"聪明"或越"晦涩", 就越难以理解和维护. 通常越快的算法越复杂.
- 锁粗粒度化(Lock Coarsening)操作, 即将临近的同步代码块用同一个锁合并起来.
- 降低锁粒度技术: 锁分解(将一个锁分解为两个锁. 在锁上存在适中而不是激烈的竞争时, 通过将一个说分解为两个锁, 能最大限度地提升性能)和锁分段(把一个锁分解为多个锁(ConcurrentHashMap的实现中使用了一个16个锁的数组, 每个锁保护所有散列桶的1/16, 其中第N个散列桶由第(N/16)个锁来保护)).
- JVM实现阻塞行为时, 可以采用自旋等待(Spin-Waiting, 指通过循环不断的尝试获取锁, 直到成功[会导致CPU时钟周期浪费])或者通过操作系统挂起被阻塞的线程[休眠会导致低响应性]. 如果等待时间较短, 则适合采用自旋等待方式, 时间较长, 则适合采用线程挂起方式.
- 3种降低锁竞争程度的方式:
- 减少锁的持有时间[降低锁粒度和锁分段].
- 降低锁的请求频率.
- 使用带协调机制的独占锁, 这些机制允许更高的并发性. - 任务在运行和阻塞这两个状态之间切换时, 就相当于一次上下文切换.
- 提升可伸缩性的方式: 减小锁持有时间、降低锁粒度、以及采用非独占的锁或非阻塞锁来代替独占锁.
9. 显示锁
- Lock: 提供了一种无条件的、可轮询的、定时的以及可以中断的锁获取操作, 所以加锁和解锁的方法都是显示的.
- ReentrantLock实现了Lock接口, 并提供了与synchronized相同的互斥性和内出可见性, 还提供了可重入的加锁语意.
- ReentrandLock不能完全替代synchronized的原因: 当程序的执行控制离开被保护的代码时, 不会自动清除锁(必须在finally中释放Lock).
- 在公平锁上, 线程将按照它们发出请求的顺序来获取锁. 非公平锁上, 则允许"插队": 当一个线程请求非公平锁时, 如过在发出请求的同时该锁的状态变为可用, 那么这个线程将跳过队列中所有的等待线程并获得这个锁. 大多数情况下, 非公平锁的性能要高于公平锁的性能: 在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟. 当持有锁的时间相对较长, 或者请求的平均时间间隔较长, 那么应该选择使用公平锁.
- 仅当内置锁不能满足需求时, 才可以考虑使用ReentrantLock.
- 读写锁: 一个资源可以被多个读锁操作访问, 或者被一个写锁访问, 但是两者不能同时访问(允许多个读操作同时进行, 但每次只允许一个写操作). 在特定场景下能实现更高的并发性.
10. 自定义同步工具
- Object.wait 会自动释放锁, 并请求操作系统挂起当前线程, 从而使其他线程能够获得这个锁并修改对象的状态. 当被挂起的线程醒来时, 它将在返回之前重新获取锁.
- 条件谓词: 使某个操作成为状态依赖操作的前提条件['值存在时'修改].
- "入口协议与出口协议":用来描述wait和notify方法的正确使用. 入口协议就是该操作的条件谓词, 出口协议则包括, 检查该操作修改的所有状态变量, 并确认他们是否使某个其他的条件谓词变为真, 如果是, 则通知相关的条件队列.
- AbstractQueueSynchronizer(AQS)这个类是许多同步类的基类, AQS是一个用于构建锁和同步器的框架, 许多同步器都可以通过AQS很容易并且高效的构造出来. 不仅ReentrantLock和Semaphore是基于AQS构建的, 还包括CountDownLatch、ReentrantReadWriteLock、SynchronousQueue和FutureTask. 基于AQS来构建同步器能带来很多好处: 它不仅能极大的减少时间工作, 而且也不必处理在多个位置上的竞争问题. 在基于AQS构建的同步器中, 知恩那个在一个时刻发生阻塞, 从而降低上下文切换的开销, 并提高吞吐量. AQS内部维护一个等待线程队列, 其中记录了某个线程请求的是独占访问还是共享访问.
- ReadWriteLock接口表示存在两个锁: 一个读取锁和写入锁, 但在基于AQS实现的ReentrantReadWriteLock中, 单个AQS子类将同时管理读取锁加锁和写入加锁. ReentrantReadWriteLock使用一个16位的状态来表示写入锁的计数, 并且使用另外一个16位的状态来标识读取锁的计数. 在读取锁上的操作将使用共享的获取方法和释放方法, 在写入锁上的操作将使用独占的获取方法和写入方法.
11. 原子变量与非阻塞同步机制
- 非阻塞算法: 使用底层的原子机器指令(例如必将并交换指令(Compare And Swap))代替锁来确保数据在并发中的一致性. 非阻塞算法被广泛地用于操作系统和JVM中实现线程/进程调度机制、垃圾回收机制以及锁和其他并发数据结构. 非阻塞算法可以使多个线程在竞争相同的数据时不会发生阻塞, 因此它能在粒度更细的层次上进行协调, 并且极大地减少调度开销. 在非阻塞算法中不存在死锁和其他活跃性问题.
- 原子变量: 提供了与volatile类型变量相同的内存语义, 此外还支持原子的更新操作, 从而使它们更加适用于实现计数器、序列发生器和统计数据收集等, 同时还能比基于锁的方法提供更高的可伸缩性. 从Java5.0开始, 可以使用原子变量类(AtomicInteger、AtomicReferece...)来构建高效的非阻塞算法.
- 独占锁是一项悲观技术-它假设最坏的情况(如果你不锁门, 那么捣蛋鬼就会闯入搞破坏), 并且只有在确保其他线程不会造成干扰(通过获取正确的锁)的情况下才能执行下去.
- Compare And Swap(CAS)是一项乐观锁的技术, 它希望能成功执行更新操作, 并且如果有另外一个线程在最近一次检查后更新了该变量, 那么CAS能够检测到这个错误. CAS包含三个操作数-需要读写的内存位置V、进行比较的值A和拟写入的新值B. 当且仅当V的值等于A时, CAS才会通过原子方式用新值B来更新V值, 否则不会执行任何操作. 由于CAS能够检测到来自其他线程的干扰, 因此即使不使用锁也能够实现原子的读-改-写操作的序列.
- CAS的主要缺点: 它将使调用者处理竞争问题(通过重试、回退、放弃), 而在锁中能自动处理竞争问题(线程在获得锁之前将一直阻塞). CAS最大的缺陷在于难以围绕CAS正确地构造外部算法.
- 原子变量类比锁的粒度更细, 量级更轻, 并且对于在多个处理器系统上实现高性能的并发代码来说是非常关键的. 在使用基于原子变量而非锁的算法中, 线程在执行时更不容易出现延迟, 而且如果遇到竞争, 也更容易恢复过来. 共有12个原子变量类, 可分为4组: 标量类(Scalar)、更新器类、数组类以及复合变量类. 最常用的原子变量类就是标量类:AtomicInteger、AtomicLong、AtomicBoolean以及AtomicRefence. 基本类型的包装类是不可修改的, 而原子变量类是可修改的.
- 在实际情况中, 原子变量在可伸缩行上要高于锁, 因为在应对常见的竞争线程时, 原子变量的效率会更高. 在中低程度的竞争下, 原子变量能提供更高的可伸缩性, 而在高强度的竞争下, 锁能够更有效地避免竞争.
- 非阻塞算法: 如果在某种算法中, 一个线程的失败或挂起不会导致其他线程也失败或挂起, 那个称这个算法为非阻塞算法. 在非阻塞算法中通常不会出现死锁和优先级反转问题(但可能会出现饥饿和活锁问题,因为算法中会反复地重试).
- 无锁算法(Lock-Free): 如果在算法中的每个步骤都存在某个线程能够执行下去, 那个称这个算法为无锁算法.
- 如果在算法中仅将CAS用与协调线程之间的操作, 并且能够正确的实现, 那么它既是一种无阻塞算法, 又是一种无锁算法.
- 在实现相同功能的前提下, 非阻塞算法通常比基于锁的算法更复杂. 构建非阻塞算法的技巧在于: 将执行原子修改的范围缩小到单个变量上.
- ABA问题: 如果V的值首先由A变成B, 再由B变成A, 那么仍然被认为是发生了变化, 并需要重新执行算法中的某些不走. 相对简单的解决方案: 不是更新某个引用值, 而是更新两个值, 包括一个引用和版本号. 即使这个值由A变成B, 然后又变成A, 版本号也将是不同的.
- 线程饥饿死锁(Thread-Starvation DeadLock).
- 随机数生成器(RNG Random Number Generator).
- 检测CPU利用率工具: Unix: vmstat和mpstat. Windows: perform.
- 静态代码分析工具: FindBugs. 能够检测出多种常见的编码错误, 其中许多错误都很容易在测试与代码审查中遗漏.
多线程推荐资料
1.《Java并发编程实战》博客内容整理自此书.
2.《Java并发编程的艺术》- 阿里系技术书籍.
3. 《java并发思考-简书文集》