本文摘自 Java并发编程实战:03 | 互斥锁(上):解决原子性问题
本质
解决原子性问题的本质是:保证同一时刻只有一个线程执行,并行的问题串行化。
如果我们能够保证对共享变量的修改是互斥的,那么,无论是单核 CPU 还是多核 CPU,就都能保证原子性了。
模型
简易模型
我们把一段需要互斥执行的代码称为 临界区。线程在进入临界区之前,首先尝试加锁 lock(),如果成功,则进入临界区,此时我们称这个线程持有锁;否则呢就等待,直到持有锁的线程解锁;持有锁的线程执行完临界区的代码后,执行解锁 unlock()。
上面的模型容易让我们忽视两个非常非常重要的点:我们锁的是什么?我们保护的又是什么?
改进后的锁模型
我们知道在现实世界里,锁和锁要保护的资源是有对应关系的,比如你用你家的锁保护你家的东西,我用我家的锁保护我家的东西。在并发编程世界里,锁和资源也应该有这个关系,但这个关系在我们上面的模型中是没有体现的,所以我们需要完善一下我们的模型。
首先,我们要把临界区要保护的资源标注出来,如图中临界区里增加了一个元素:受保护的资源 R;其次,我们要保护资源 R 就得为它 创建一把锁 LR;最后,针对这把锁 LR,我们还需在进出临界区时添上 加锁 操作和 解锁 操作。另外,在锁 LR 和受保护资源之间,特地用一条线做了关联,这个关联关系非常重要。
Java 中提供的锁技术:synchronized
锁是一种通用的技术方案,Java 语言提供的 synchronized 关键字,就是锁的一种实现。synchronized 关键字可以用来修饰方法,也可以用来修饰代码块,它的使用示例基本上都是下面这个样子:
class X {
// 修饰非静态方法
synchronized void foo() {
// 临界区
}
// 修饰静态方法
synchronized static void bar() {
// 临界区
}
// 修饰代码块
Object obj = new Object();
void baz() {
synchronized(obj) {
// 临界区
}
}
}
看完之后你可能会觉得有点奇怪,这个和我们上面提到的模型有点对不上号啊,加锁 lock() 和解锁 unlock() 在哪里呢?加锁和解锁操作是 JVM 隐式控制的。如下面的代码:
public class SynchronizedTest {
private static Object object = new Object();
public static void main(String[] args) {
synchronized (object){
System.out.println("get lock");
}
}
}
使用 javap -c 查看其字节码文件
可以看到 JVM 分别加入了 monitorenter
和 monitorexit
指令。
更详细的底层原理参考该 链接
受保护 资源 和 锁 之间的关联关系是 N:1 的关系
Synchronized 四种状态
- 无锁
很好理解,就是不存在竞争,线程没有获取synchronized锁的状态。
- 偏向锁
即偏向第一个拿到锁的线程,锁会在对象头的Mark Word通过CAS(Compare And Swap)记录获得锁的线程id,同时将Mark Word里的锁状态置为偏向锁,是否为偏向锁的位也置为1,当下一次还是这个线程获取锁时就不需要通过CAS。 如果其他的线程尝试通过CAS获取锁(即想将对象头的Mark Word中的线程ID改成自己的)会获取失败,此时锁由偏向锁升级为轻量级锁。
- 轻量级锁
JVM会给线程的栈帧中创建一个锁记录(Lock Record)的空间,将对象头的Mark Word拷贝到Lock Record中,并尝试通过CAS把原对象头的Mark Word中指向锁记录的指针指向当前线程中的锁记录,如果成功,表示线程拿到了锁。如果失败,则进行自旋(自旋锁),自旋超过一定次数时升级为重量级锁,这时该线程会被内核挂起。
- 自旋锁
轻量级锁升级为重量级锁之前,线程执行monitorenter指令进入Monitor对象的EntryList队列,此时会通过自旋尝试获得锁,如果自旋次数超过了一定阈值(默认10),才会升级为重量级锁,等待线程被唤起。 线程等待唤起的过程涉及到Linux系统用户态和内核态的切换,这个过程是很消耗资源的,自选锁的引入正是为了解决这个问题,先不让线程立马进入阻塞状态,而是先给个机会自旋等待一下。
- 重量级锁
就是通常说的synchronized重量级锁
锁升级
锁升级的顺序为:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁,且锁升级的顺序是不可逆的
线程第一次获取锁获时锁的状态为偏向锁
如果下次还是这个线程获取锁,则锁的状态不变;否则会升级为CAS轻量级锁;
如果还有线程竞争获取锁,如果线程获取到了轻量级锁没啥事了;如果没获取到会自旋;
自旋期间获取到了锁没啥事,超过了10次还没获取到锁,锁就升级为重量级的锁,此时如果其他线程没获取到重量级锁,就会被阻塞等待唤起,此时效率就低了。