第2章 线程安全
同步机制
当多个线程访问某个状态变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同对变量的访问。
Java中主要的同步机制
关键字synchronized
同步这个术语还包括
- volatile类型的变量
- 显示锁
- 原子变量
正确性
某个类的行为与其规范完全一致。在任何操作中不违背不变性条件和后验条件。
线程安全性
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调度代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
原子性相关知识点
竞态条件
- 定义
由于不恰当的执行时序而出现不正确的结果的情况。 - 本质
基于一种可能失效的观察结果来做出判断或者执行某个结果 - 举例
-
“先检查后执行”
首先观察到某个条件为真(例如文件X不存在),然后根据这个观察结果采用相应的动作(创建文件X),但事实上,在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效(另一个线程在这期间创建了文件X),从而导致各种问题(未预期的异常、数据被覆盖、文件被破坏等)。
-
计算机网络课程设计时的滑动窗口问题
没有将发送报文这一复合操作变成原子的,导致在发送报文的过程中发送窗口可以发生变化,最终导致竞态条件的发生(发送了超出发送范围的报文)。
-
复合操作
将“先检查后执行”以及”读取-修改-写入“等操作统称为复合操作:包含一组必须以原子方式执行的操作以确保线程安全性。
原子性
- 假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么B完全执行完,要么完全不执行B,那么A和B对彼此来说是原子的。
原子操作
对于访问同一状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作。
原子变量类
- 是一种线程安全类,能确保对自身实例的访问操作都为原子的。
- java.util.concurrent.atomic中包含了一些原子变量。
加锁机制
是Java用来确保原子性的内置机制,可将复合操作变成原子操作
同步代码块
-
是什么
- 是Java一种用来支持原子性的内置的锁机制
-
组成
- 作为锁的对象引用
- 由这个锁保护的代码块
-
表现形式
- 基本形式
synchronized(lock){
//访问或修改由锁保护的共享状态
}
lock为作为锁的对象
内部为由这个锁保护的代码块
- 基本形式
-
用关键字synchronized修饰的方法
会影响并发性,造成性能问题。 -
两类方法所用到的锁
- 实例方法
以该方法调用所在的对象作为锁 - 静态方法
以Class对象作为锁
- 实例方法
-
内置锁(Intrinsic Lock)或称监视器锁(Monitor Lock)
- 定义
每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁 - 锁的获取与释放
线程在进入同步代码块之前会自动获得锁,并且在退出同步代码时自动释放锁,而无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出 - 互斥性
Java的内置锁相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁。
- 定义
-
保证了原子性
任何一个执行同步代码块的线程,都不可能看到有其他线程正在执行由同一个锁保护的同步代码块
重入
- 定义
如果某个线程试图获得一个已经由自己持有的锁,那么这个请求就会成功,形成重入。 - 重入与释放机制
如果同一个线程再次获取这个锁,计数值递增,而当线程退出同步代码块时,计数器会相应地递减。当计数器为0时,这个锁将被释放。 - 重入在一定情况避免了死锁的发生
具体看书21页,程序清单2-7
用锁来保护状态
串行访问
- 意味着多个线程依次以独占的方式访问对象,而不是并发的访问
- 锁能使其保护的代码以串行形式来访问
其他注意的点
-
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
-
多个原子操作组成的复合操作并不是原子操作,还需要有额外的加锁机制
活跃性与性能
不良并发引用程序
- 可同时调用的数量,不仅受可用处理资源的限制,还受应用程序本身结构的限制
要怎么做
- 应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出来,从而在这些操作的执行过程中,其他线程可以访问共享状态
三个要素
同步代码块的大小需要从安全性(必须保证)、性能和简单性(加锁和释放锁都将有一定的消耗)三者考虑
例子(书p25 程序2-8)
- 之前直接在整个方法上加上synchronized,导致后面的线程要等前面占有锁的线程执行完毕后才能执行
- 之后通过将同步代码块分层两部分,这样在一个线程计算结果的同时,其他线程可以进行判断