Walt You - 行是知之始

Java 并发编程实战-学习日志(一)1:线程安全


进入第一部分的第一章节:并发基础之线程安全。


什么是线程安全

书中定义:

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类就能表现出正确的行为,那么就称这个类是线程安全的。

自己理解:

一个类或者方法在多线程情况下,不管怎么使用它,它的结果都符合预期,这就是线程安全。

无状态的对象,天生是线程安全,因为无状态对象不包含任何域,也不包含任何对其他域的引用。

原子性

关于原子性的一个例子就是多线程下,对一个变量进行加一操作:

count ++

虽然看似是一行代码,但是其实包含了三个动作:读出来,加一,写回去。所以在多线程情况下,可能会出现两个线程都读了原始值,比如是1,然后各自加一,再写回去。这时候我们预期结果是3,但是实际结果却为 2。

这种由于执行时序而出现不正确的情况,学名叫做:竞态条件。

1. 竞态条件

当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。

最常见的竞态条件类型就是“先检查后执行(Check-then-Act)”,即通过一个可能失效的观测结果来决定下一步的动作。

一个常见的例子就是单例模式中单例的初始化。

@NotThreadSafe
public class LazyInitRace{
	private ExpensiveObject instance = null;
	
	public ExpensiveObject getInstance(){
		if(instance == null){
			instance = new ExpensiveObject();
		}
		return instance;
	}
}

以上代码,就存在一个竞态条件,因为有可能会有两个线程同时进行 instance 是否为 null 判断的情况,那么这两个线程都可能创建一个新的 instance 的。

2. 复合操作

所谓复合操作就是指包含了一组以原子方式执行的操作。

加锁机制

要保持状态的一致性,就需要在单个原子操作中,更新所有相关的状态变量。

1. 内置锁

关键字: synchronized

同步代码块,包含两个部分:1,作为锁的对象引用,2,作为这个锁保护的代码块。

以 synchronized 来修饰的方法,它持有的锁就是方法调用所在的对象;静态的 synchronized 方法的锁,是 Class 对象。

synchronized 其实是一个互斥锁,意思就是同一时间最多只能有一个线程持有这个锁,其余的线程就处于等待或阻塞状态。

2. 重入

因为内置锁是可重入的,所以如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。

重入意味着获取锁的操作粒度是“线程”,而不是“调用”。

实现方式:为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁被认为没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数值减1。

重入提升了加锁行为的封装性,因此简化了面向对象并发代码的执行。

如果不能使用重入,子类重写父类的同步方法后,再调用父类方法,就会产生死锁:

public class Widget {
    public synchronized void doSomething() {
    ...
    }
}
public class LoggingWidget extends Widget {
    public synchronized void doSomething() {
        System.out.println(toString() + ": calling doSomething");
        super.doSomething();
    }
}

3. 用锁来保护状态

锁能够使其被保护的对象以串行方式来执行。

对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。

对象的内置锁与其状态之间没有内在的联系,虽然大多数类都将内置锁用做一种有效的加锁机制,但对象的域并不一定要通过内置锁来保护

一种常见的加锁约定:将所有可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得该对象上不会发生并发访问。

对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。

4. 活跃性与性能

将同步代码块分解得过细并不好,因为获取与释放锁操作需要开销。

通常,在简单性和性能之间存在着相互制约因素。当实现某个同步策略时,一定不要盲目地为了性能而放弃简单性(这可能会破坏安全性)。

当执行时间较长的计算或者可能无法快速完成的操作时(如I/O),一定不要持有锁。


Content