对锁的理解

关于锁的基本概念

锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。

互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。

可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。

互斥

  • 使用 synchronized 来确保线程的互斥,也就是确保线程对共享数据的原子性操作.

可见性

  • 使用 volatile 来确保可见性,多个线程总能读到共享变量的最新值

  • 要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

    1. 对变量的写操作不依赖于当前值(也就是不能进行 读取->修改->写入 的操作, 只能进行 修改->写入)。

    2. 该变量没有包含在具有其他变量的不变式中。

  • 对于第二个条件,具体的例子如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@NotThreadSafe 
public class NumberRange {
private int lower, upper;

public int getLower() { return lower; }
public int getUpper() { return upper; }

public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}

public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}
  • 对于上面的例子,如果初始状态是 (0, 5),同一时间内,线程 A 调用 setLower(4) 并且线程 B 调用 setUpper(3),显然这两个操作交叉存入的值是不符合条件的,那么两个线程都会通过用于保护不变式的检查,使得最后的范围值是 (4, 3) —— 一个无效值。至于针对范围的其他操作,我们需要使 setLower() 和 setUpper() 操作原子化 —— 而将字段定义为 volatile 类型是无法实现这一目的的。

synchronized 和 volatile 组合使用

  • 结合使用的一个例子, 其中 synchronized一次只允许一个线程访问值, volatile 允许多个线程执行读操作, 这样组合使用后既可以保证操作的原子性,也可以确保所有的线程在上能够取得很好的性能
1
2
3
4
5
6
7
8
9
10
11
12
@ThreadSafe
public class CheesyCounter {
// Employs the cheap read-write lock trick
// All mutative operations MUST be done with the 'this' lock held
@GuardedBy("this") private volatile int value;

public int getValue() { return value; }

public synchronized int increment() {
return value++;
}
}

参考

Buy me a cup of coffee