Java 中的同步机制语义
- synchronize
- volatile
- 显式锁
- 原子变量
程序状态的封装性越好,就越容易实现程序的线程安全性,并且代码的维护人员也越容易保持这种方式。
当设计线程安全的类时,良好的面向对象技术、不可修改性,以及明晰的不变性规范都能起到一定的帮助作用。
什么是线程安全
- 在线程安全的定义中,最核心的概念就是正确性。
- 正确性的含义是,某个类的行为与其规范完全一致。在良好的规范中通常会定义各种不变性条件(Invariant)来约束对象的状态,以及定义各种后验条件(Postcondition)来描述对象操作的结果。
- 线程安全性:当多个线程访问某各类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
- 在线程安全类中封装了必要的同步机制,因此客户端无须进一步采取同步措施。
无状态对象一定是线程安全的。-
原子性
在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它有一个正式的名字:竞态条件(Race Condition)。
竞态条件
- 当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。
- 最长阿金的竞态条件就是“先检查后执行(Check-Then-Act)”操作,即通过一个可能失效的观测结果来决定下一步的动作。
复合操作
- 原子操作:操作不可分割,要么全部执行完,要么完全不执行。
- 为了确保线程安全性,“先检查后执行”(例如延迟初始化)和“读取-修改-写入”(例如递增运算)等操作必须是原子的。我们将这类操作统称为复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性。
- 可以使用一个现有的线程安全类 AtomicLong 来实现复合操作的线程安全。AtomicLong 封装了 AtomicLong.incrementAndGet() 的原子操作。
- 在实际情况中,应尽可能使用现有的线程安全对象(如:AtomicLong)来管理类的状态。
加锁机制
- 要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
内置锁
- Java 提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。(后续将介绍加锁机制以及其他同步机制的另一个重要方面:可见性)。同步代码块包括两部分:一个作为锁的对象的引用,一个作为这个锁保护的代码块。
- 以关键字 synchronized 来修饰的方法就是一个横跨整个方法体的同步代码块。其中该同步代码块的锁就是方法调用所在的对象,静态的 synchronized 方法以Class对象作为锁。
|
|
- 每个Java对象都可以用作一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或者监视器锁(Monitor Lock)。线程在进入同步代码块之前会自动获得锁,并在推出同步代码块时自动释放锁。
- 获得内置锁的唯一途径就是进入这个锁保护的块或方法。
重入
- “重入”一位置获得锁的操作的粒度是“线程”,而不是“调用”。
- 重入进一步提升加锁行为的封装性,简化了面相对象并发代码的开发。在面向对象编程中,子类重写了父类的 synchronized 方法,然后调用父类中的方法,如果内置锁不是可重入的,那么将产生死锁。
用锁来保护状态
- 对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
- 之所以每个对象都有一个内置锁,只是为了免去显式地创建锁对象。
- 对于包含多个变量的不变性条件,其中涉及的所有的变量都需要由同一个锁来保护。
- 原子操作虽然是线程安全的,但多个原子操作合并为一个复合操作,还是需要额外的加锁机制。
活跃性与性能
- 对整个类中的方法进行同步,虽然这种简单而且粗粒度的方法能保证线程安全性,但付出的代价却很高。我们称之为不良并发(Poor Concurrency)应用程序:可同时调用的数量,不仅受到了可用处理资源的限制,还受到应用程序本身结构的限制。幸运的是,通过缩小同步代码块的作用范围,可以即确保并发性同时又维护线程安全。
- 当执行时间比较长的计算或者可能无法快速完成的操作时(例如网络I/O或者控制台I/O),一定不要持有锁。