《Java并发编程实战》部分笔记
重点:线程安全类,concurrent包的并发构建基础模块,Java内存模型,Java线程的实现,线程池。并发控制。同步机制。
——《Java编程思想》中并发
——第5章 基础构建模块:线程安全的容器类,协调线程控制流的同步工具类。
——第16章 内存模型。
《Java并发编程实战》
从 并发性和线程安全性的基本概念出发,介绍了如何使用类库提供的基本并发构建块来避免并发危险。如何有效地利用并发性。如何在Java并发应用程序中正确且 高效的使用底层提供的基本的并发功能。Java虚拟机中能支持一些高性能的并且具有高可伸缩性的并发类,此外还支持一组新的并发基础构建模块。工作原理、 使用方式、研究背景与设计模式。
并发的理论基础,实际的开发技术。构建可靠的,可伸缩的,可维护的并发应用程序。并发API及机制,设计原则,设计模式,思维模式。
本书内容:
并发性与线程安全性的基本概念
构建,组合各种线程安全类的技术。
使用【java.util.concurrent】包中的各种并发构建基础模块。
性能优化中的注意事项。
高级主题:原子变量,无阻塞算法,Java内存模型。
第一部分:Java并发编程的基础理论
线程安全性、状态对象的基础知识,如何构造线程安全的类,并将多个小型的线程安全类构建成更大的线程安全类。Java平台库中的一些基础并发模块。
第二部分:并发应用程序的构造理论
应用程序中并行语义的分解及其与逻辑任务的映射,任务的取消与关闭等行为的实现。Java【线程池】中的一些高级功能。
第三部分:并发编程的性能调优
测试并发代码性能。
第四部分:并发编程中的一些高级主题
显式锁,原子变量,非阻塞算法,开发自定义的同步工具类。
前提:了解Java基本的并发机制。
本书提供各种实用的设计规则,用于帮助开发人员创建安全的和高性能的并发类。
本书诞生于JSR166为Java 5.0开发java.util.concurrent包的过程。
第1章 简介
使用线程,发挥多处理器的计算能力。如何高效使用并发(异步性)很重要。
线程共享进程的内存地址空间。需要比进程间共享数据粒度更细的数据共享机制。
线程的优势:
发挥多处理器的强大能力。
建模的简单性:程序中只包含一种类型的任务。
异步事件的简化处理:每个请求都有自己的处理线程,那么在处理某个请求时发生的阻塞将不会影响其他请求的处理。
Java提供了各种【同步机制】来协同对共享变量的访问操作。
根据本书第二章和第三章提出的指导原则,就可以绕开底层JVM优化的细节问题。
问题:
安全性问题
活跃性问题:死锁,困难因为这依赖于不同线程的事件发生的时序。因此在开发或者测试中并不总是能够重视。
性能问题:上下文切换操作。
框架通过在框架线程中调用应用程序代码将并发性引入到程序中。在代码中将不可避免的访问应用程序状态,因为所有访问这些状态的代码路径都必须是线程安全的。
Servlet需要是线程安全的。
RMI对象应该做好被多个线程同时调用的准备,并且必须确保它们自身的线程安全性。
第一部分:基础知识
第2章 线程安全性
线程和锁在并发编程中很重要。
核心在于:对状态访问操作进行管理,特别是对共享的和可变的状态的访问。
要使得对象是线程安全的,需要采用同步机制,来协同对对象可变状态的访问。
Java中的主要同步机制是关键字synchronized,提供独占的加锁方式。“同步”还包括volatile类型的变量,显式锁Explicit Lock和原子变量。
如果当多个线程访问同一个可变的状态变量时,没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题。
- 不在线程之间共享该状态变量。
- 将状态变量修改为不可变的变量。
- 在访问状态变量时使用同步。
程序状态的封装性越好,就越容易实现程序的线程安全性。线程安全性主要针对:封装自己状态的整个代码。
原则:首先使代码正确运行,然后再提高代码的速度。即便如此,最好也只是当性能测试结果表示必须提高性能的时候才进行优化。
2.1 什么是线程安全性
当 多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确 的行为,那么称这个类是线程安全的。在线程安全类中封装了必要的同步机制,因此客户端无须进一步采取同步措施。通常,线程安全性的需求并非来源于对线程的 直接使用,而是使用Servlet这样的框架。
无状态对象:即不包含任何域,也不包含任何其他类中域的引用。无状态对象一定是线程安全的,因为计算过程中的临时状态仅存在于线程栈上的局部变量中,并且只能正在执行的线程访问。两个线程没有共享状态,就好像它们都在访问不同的实例。
大多数Servlet都是无状态的,从而极大降低了实现Servlet线程安全性时候的复杂性,只有当Servlet在处理请求时需要保存一些信息,线程安全性才会成为一个问题。
2.2 原子性
在并发编程中,由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况。叫做竞争条件。大多数竞争条件的本质:基于一种可能失效的观察结构来做出判断或者执行某个计算。这种类型的竞争条件称为”先检查后执行“。(读取,修改,写入)
2.2.2 延迟初始化中的竞争条件
单例模式。
2.2.3 复合操作
加锁机制:用于确保原子性的内置机制。
使用AtomicLong类型的变量来统计已处理请求的数量:
publicclass CountingFactorizer implements Servlet{
private final AtomicLong count = new AtomicLong (0);
public long getCount(){return count.get();}
public void service(ServiceRequest req,ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(resp,factors);
}
}
在 java.util.concurrent.atomic包中包含了一些原子变量类。在实际情况中,应该尽可能使用现有的线程安全对象例如 AcomicLong来管理类的状态。与非线程安全的对象相对,判断线程安全对象的可能状态及其状态转换情况要更为容易。从而也更容易维护和验证线程安全 性。
2.3 加锁机制
当更新同一个变量时,需要在同一个原子操作中对其他变量同时进行更新。要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
2.3.1 内置锁
内置的锁机制来支持原子性:同步代码块(Synchronized Block)。此方法较为极端,因为多个客户端无法同时访问,服务的响应性非常低。【并发性非常糟糕】
public class SynchronizedFactorizer implements Servlet{ @GuardedBy("this") private BigInteger lastNumber; @GuardedBy("this") private BigInteger lastFactors; //都由Servlet对象的内置锁来保护
public synchronized void service(ServletRequest req,ServletResponse resp){ BigInteger i = extractFromRequest(req); if(i.equals(lastNumber)) encodeIntoResponse(resp,lastFactors); else{ BigInteger[] factors = factor[i]; lastNumber = i; lastFactors = factors; encodeIntoResponse(resp,factors); } } } |
2.3.2 重入
由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求可以成功。重入意味着获取锁的操作的粒度是“线程”,而不是“调用”。
可重入避免死锁:
public class Widget{ public synchronized void doSomething(){ ... } } public class LoggingWidget extends Widget{ public synchronized void doSomething(){ System.out.println(toString() + ":calling doSomething"); super.doSomething(); } } |
2.4 用锁来保护状态
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
一种常见的加锁约定是:将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步。使得在该对象上不会发生并发访问。
只有被多个线程同时访问的可变数据才需要通过锁来保护。
虽然synchronized方法可以确保单个操作的原子性,但如果把多个操作合并为一个复合操作,还是需要额外的加锁机制。
将每个方法都作为同步方法还可能导致活跃性问题或者性能问题。
2.5 活跃性和性能
尽量将不影响共享状态,且执行时间较长的操作从同步代码中分离出去。
从而在这些操作执行过程中,其他线程可以访问共享状态。
——简单性:对整个方法进行同步。
——并发性:对尽可能短的代码路径进行同步。
通常,在简单性与性能之间存在着相互制约的因素。当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性(这可能会破坏安全性)
重要:当执行时间较长的计算或者可能无法快速完成的操作时,例如网络I/O或控制台 I/O,一定不要持有锁。
第3章 对象的共享
关键:在访问共享的可变状态时需要进行正确的管理。
本章介绍如何共享和发布对象,从而使它们能够安全地由多个线程同时访问。
第2,3章合在一起就形成了构建线程安全类以及通过java.util.concurrent类库来构建并发应用程序的重要基础。同步还有一个内存可见性Memory Visibility,还希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。
3.1 可见性
无法确保执行读操作的线程能适时地看到其他线程写入的值。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
错误实例:多个线程,在没有同步的情况下,共享变量。
在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要相对内存操作的执行顺序进行判断,几乎无法得到正确的结论。
3.1.1 失效数据
一个线程可能获得某个变量的最新值,而获得另一个变量的失效值。可能的故障:意料之外的异常、被破坏的数据结构、不精确的计算、无限循环等。
线程安全的可变整数类:
public class SynchronizedInteger{ @GuardedBy("this") private int value;
public synchronized int get(){return value;} public synchronized void set(int value){this.value = value;} } |
3.1.2 非原子的64位操作