并发三:互斥锁

同一个时刻只有一个线程执行,我们称之为互斥,如果我们能保证对共享变量的修改都是互斥的,那么无论单核CPU还是多核CPU,都能保证原子性了。

简单锁模型

我们把一段需要互斥执行的代码称为临界区,线程在进入临界区之前,首先尝试加锁lock(),如果成功,则进入临界区,此时我们称这个线程持有锁,否则就等待,直到持有锁的线程解锁,持有锁的线程执行完临界区的代码后,执行解锁unlock(). 两个很重要的点:我们锁的是什么,保护的又是什么?

改进后的锁模型

锁和锁要保护的资源是有对应关系的。但是上面的模型是没有体现的,所以需要完善一下:

首先,我们要把临界区要保护的资源标注出来,如图中临界区里增加了一个元素:受保护的资源 R;其次,我们要保护资源 R 就得为它创建一把锁 LR;最后,针对这把锁 LR,我们 还需在进出临界区时添上加锁操作和解锁操作。另外,在锁 LR 和受保护资源之间,我特地 用一条线做了关联,这个关联关系非常重要。很多并发 Bug 的出现都是因为把它忽略了, 然后就出现了类似锁自家门来保护他家资产的事情,这样的 Bug 非常不好诊断,因为潜意 识里我们认为已经正确加锁了。

Synchronized

锁是一种通用的技术方案,Java中提供的synchronized关键字,就是锁的一种体现,synchronized 既可以用来修饰方法,也可以用来修饰代码块。,Java 编译器会在 synchronized 修饰的方法或代码块前后自动加上加锁 lock() 和解 锁 unlock(),这样做的好处就是加锁 lock() 和解锁 unlock() 一定是成对出现的。

那 synchronized 里的加锁 lock() 和解锁 unlock() 锁定的对象在哪里呢?上面的代码我们 看到只有修饰代码块的时候,锁定了一个 obj 对象,那修饰方法的时候锁定的是什么呢? 这个也是 Java 的一条隐式规则:

当修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就 是 Class X; 当修饰非静态方法的时候,锁定的是当前实例对象 this。

1
2
3
4
5
class X{
synchronized(X.class) static void bar(){
// 临界区
}
} // 修饰静态方法,X.class可以省略
1
2
3
4
5
class X{
synchronized(this) void foo(){
// 临界区
}
}// 修饰非静态方法

锁和受保护资源的关系

受保护资源和锁之间的关系是N:1的关系,也就说可以用一把锁来保护多个资源,但是不能用多把锁来保护一个资源。

如何用一把锁来保护多个资源。其实也就是用Class类对象来进行上锁。