共享和可变
线程安全的代码,核心在于对状态访问操作的管理,特别是对共享的和可变的状态访问。
- 共享(Shared):意味着可以由多个线程同时访问。
- 可变(Mutable):意味着变量的值在其生命周期内可以发生变化。
在非正式的意义上来说,对象的状态是指储存在状态变量(例如实例或者静态域)中的数据,对象本身的状态可能包含其他依赖对象的状态。
当多个线程访问某个状态变量并且其中一个线程在执行写入操作时,必须采用同步机制来协同这些线程对对象的访问。在java中,同步包含:
- synchronized(主要)
- volatile
- 显式锁
- 原子变量
如果多个线程同时访问同一个可变状态时没有使用合适的同步,那么代码就会出现问题。有三种解决方式:
- 不在线程之间共享该状态变量。
- 将状态变量修改为不可变的变量。
- 在访问变量时使用同步。
线程安全性
-
线程安全性的含义:
- 某个类的行为与其规范完全一致。在良好的规范中通常会定义各种不变性条件(Invariant)来约束对象的状态,以及定义各种后验条件(Postcondition)来描述对象操作的结果。
-
定义线程安全性:
- 当多个线程访问某个类时,不管运行时采用何种调度方式或者线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类始终都能表现出正确的行为,那么称这个类是线程安全的。
- 无状态对象一定是线程安全的。
原子性
在并发编程中,由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它有一个正式的名字:竞态条件(Race Condition)。
竞态条件
常见的竞态条件类型:
-
先检查后执行(Check-Then-Act) :基于一种已经失效的观察结果来作出判断或者执行某个条件:
- 观察到某个条件为真:(例如文件X不存在)
- 根据观察结果执行相应的动作:(创建文件X)
- 事实上,在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效:(另一个线程在这期间创建好了文件X)
- 发生各种问题:(文件被覆盖,数据无效,报错,文件被破坏等)
复合操作
要避免竞态条件问题,就必须在某个就线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或者之后读取和修改状态,而不是在修改状态的过程中。
我们把“先检查后执行”,“递增运算的读取-修改-写入”等操作,统称为复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性。
加锁机制
要保持状态的一致性,就必须要在当个原子操作中更新所有相关的状态变量。
内置锁
java提供了一种内置的锁机制来支持原子性:
-
同步代码块(Synchronized Block):同步代码块包含了两部分:
- 锁的对象引用
- 由这个锁保护的代码块
synchronized(lock){
// 访问或修改由锁保护的共享状态
}
- 每个java对象都可以用作一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。线程在进入同步代码块之前会自动获得锁,并且在推出同步代码块时自动释放锁,而无论通过正常的控制路径退出,还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
- java的内置锁相当于一个互斥锁,意味着最多只有一个线程能持有这个锁。当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或阻塞,直到线程B释放这个锁。如果B不释放这个锁,A将永远的等待下去。
重入
- 当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。但是由于内置锁是可重入的,因此如果某个线程试图获取一个已经由它自己持有的锁,那么这个请求就会成功。
- 重入的功能,意味着锁操作的粒度是线程,而不是调用。
重入的实现方式
-
为每个锁关联一个获取计数值和一个所有者线程。
- 当计数值为0时,这个锁被认为是没有任何线程持有。
- 当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将计数器+1
- 如果线程再一次请求这个锁,计数值递增。
- 当线程退出同步代码块时,计数器会相应的递减。
- 当计数值为0时,这个锁将释放。
用锁来保护状态
- 由于锁能使其保护的代码路径以串行形式来访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。
- 对于可能被多个线程同时访问的可变状态变量,在访问它的时候都需要持有同一把锁,在这种情况下,我们称状态变量是由这个锁保护的。
- 每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。
- 并非所有数据都需要锁的保护,只有被多个线程同时访问的可变数据才需要通过锁来保护。
- 对于包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。
活跃性与性能
- 通常而言,简单性与性能之间存在相互制约因素。当实现某个同步策略时,一定不要盲目的为了性能而牺牲简单性。(不能破坏安全性,要保证业务的正确)
- 当执行时间比较长,或者无法快速完成的操作,一定不要持有锁。(例如网络IO和控制台IO)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。