最近恰好有点时间看《Java并发编程实践》,以前没有写过博客,顺手写一下笔记和自己的感悟。
目录
简介
并发简史
在操作系统出现之前,早期的计算机只能从头到尾执行一个独占所有计算机资源的程序,不仅很难编写和运行程序,而且每次只能运行一个程序,
这对昂贵和稀有的计算机资源是一种浪费。
操作系统的出现实现了多个程序同时执行,不同的程序在单独进程中运行,操作系统出现的原因:
- 提高系统资源的利用率:在一个进程阻塞等在外部操作完成时,另外一个进程同时运行将提高资源利用率
- 公平性:不同的程序对计算机上的资源有同等的使用权,通过粗粒度的时间片轮询可以使程序较公平的共享计算机资源
- 便利性:在执行多个任务时编写多个程序,程序之间在必要时相互通信,比只编写一个程序执行所有任务更容易实现。
促使进程出现的因素同样也促使了线程的出现,线程是同一个进程中同时存在的多个程序控制流。线程会共享进程范围内的资源,但每个线程也有各自的程序计数器、栈以及局部变量等,线程还提供了一种直观的分解模式来充分利用多处理器系统的硬件并行性,同一个程序中的多个线程可以被同时调度到多个CPU上并行执行。
线程的优势
- 发挥多核处理器的优势
- 程序建模更简单
- 处理异步事件更简单
通过线程可以将复杂的异步任务流进一步分解为一组简单的同步工作流,每个工作流在单独线程中执行,并在特定的同步位置进行交互
线程的风险
- 安全性问题:安全性意味着”永远不发生超乎程序设计意愿的情况”,多线程并发操作未正确同步的共享可变状态变量可能会出现无法预料的数据变化。
- 活跃性问题:活跃性意味着:”某件正确的事情最终会发生”,线程死锁、饥饿等属于活跃性问题。
- 性能问题:线程执行时会带来运行时的开销,如上下文切换,如果线程过多,程序执行时cpu就会频繁的出现上下文切换操作,将带来极大的开销:保存和恢复上下文,丢失局部性,并且cpu更多的时间花在线程调度上而不是线程运行。
线程安全性
- 编写线程安全的代码的核心在于管理状态访问操作,特别是共享的可变状态变量的访问。
- 对象的状态是指存储在对象的状态变量(例如实例或静态域)中的数据,对象的状态可能还包括其他依赖对象的域。
- 在对象的状态中包含了任何可能影响其外部可见行为的数据。
- 一个对象是否需要是线程安全的取决于它是否被多个线程访问。这指的是程序中访问对象的方式,而不是对象要实现的功能。要使对象是线程安全的,需要采用同步机制来协同这些线程对对象的访问。
如果多个线程访问同一个可变的状态变量却没有使用正确的同步,程序会出现问题,解决方式如下:
1.不在线程间共享变量
2.将可变状态变量修改为不可变对象
3.使用同步访问机制
什么是线程安全性
线程安全性是对象(类和实例)的属性
在线程安全性的定义中,最核心的就是正确性,和正确性相关的几个概念:
先验条件(precondition):针对方法(method)规定调用该方法前必须真条件
后验条件(precondition):针对方法(method)规定调用该方法后必须真条件
不变性条件(Invariant):约束对象的状态
在设计类的时候,每个类都为实现功能,而功能有一定约束,这个约束被称为类的规范。比如一个类的功能是判断一个整数是否在一个特定的范围内,那么这个类中肯定有一个最大值和最小值,还有设置最大值和最小值的方法,这个类的规范有:最大值必须大于等于最小值(不变性条件),
在设置最大值的时候最大值不能小于最小值且设置最小值时最小值不能大于最大值(后验条件),如果违反了类的规范,这个类的功能就是不正确的,和设计者的意图相悖的。
类的正确性:类的行为和其规范(类设计者的意图)完全一致
当多个线程访问某个类时,不管运行时环境如何调度或者这些线程如何交替执行,并且在主调代码中不需要任何同步和协同,这个类都能表现出正确的行为(不变性条件和后验条件),那么称这个类是线程安全的。
在线程安全类中封装了必要的同步机制,在客户端代码中不需要进一步采取同步机制
无状态的对象一定是线程安全的
原子性
竞态(Race Condition):当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竞态。
原子操作:不可被中断的一个或一系列操作
最常见的竞态就是“先检查后执行”操作,即通过一个可能失效的观测结果决定下一步的动作。
复合操作:包含一组必须以原子方式执行的操作(以确保线程的安全性)
原子操作可以避免竞态
在实际情况中,应尽可能地使用现有的线程安全对象(例如JDK自带的线程安全组件)来管理类的状态。与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更容易,从而也更容易维护和验证线程的安全性
加锁机制
当在不变性条件中涉及多个状态变量时,并且各个变量之间不是彼此独立的,而是某个变量的值会约束其他变量的值,那么在更新某一个变量时需要在同一个原子操作中更新其他变量。
内置锁
每个Java对象都可以作为实现同步的锁,被称为内置锁(Intrinsic Lock)或者监视器锁(Monitor Lock),线程在进入同步代码时会自动获得锁,并在推出同步代码块时释放锁,无论是通过正常的控制路径退出,还是在代码中抛出异常退出。获取内置锁的唯一方式就是进入有这个锁保护的代码。
java提供了一种内置的锁机制来支持原子性:同步代码块
java
synchronized (lock){
//访问或修改由锁保护的共享状态
}
包括两个部分:作为锁对象的引用,由锁保护的代码
synchronized可以修饰方法:
- 修饰非静态方法时同步方法的锁是执行这个方法的实例
- 修饰静态方法是同步的锁是Class对象
Java内置锁是一种互斥锁,即最多只能有一个线程能持有这把锁。
在一个线程获取内置锁后,其余线程如果要执行锁保护的代码,必须先等待锁被释放后获得,但是未同步代码的执行不受影响,依然可以被多线程同时访问。
重入
内置锁是可重入,即获得锁的线程如果试图再次获得自己持有的锁会成功。
同步代码里可以调用另一个同步方法
重入意味着获取锁的操作的粒度是“线程”,而不是“调用”
重入进一步提升了加锁行为的封装性,简化了并发代码的开发。
用锁来保护状态
对于可能被多个线程同时访问的共享可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
对象的内置锁与其状态之间没有内在的关联。虽然大多数类都通过内置锁机制,但对象的域并不一定要通过内置锁来保护。当获取与对象关联的锁时,并不能阻止其他线程访问该对象,某个线程在获得线程锁后,只能阻止其他线程获取同一个锁。
每个共享的可变状态变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。
对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由一个锁来保护
活跃性和性能
应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去,降低锁粒度,使得在这些操作执行过程中其他线程也可以访问共享状态.
通常,在加锁的简单性和性能之间存在相互制约,当实现某个同步策略时,一定不要盲目的为了性能而牺牲简单性(这可能会破坏安全性)
当执行时间较长的计算或者可能无法快速完成的操作时(例如:网络I/O或控制台I/O),一定不要持有锁