Java中的synchronized详解

synchronized是一个java语言的关键字,可以用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍可以访问该object中的非加锁代码块。

由此可知,synchronized关键字有三种场景:对象、方法、代码块。平时我们用的时候都信手拈来,但是我们真的了解synchronized 的原理吗?

从字节码层面来看:

synchronized被编译成 class 文件, 翻译成字节码指令有两个重要的指令 : monitorenter monitorexit , 可以发现有两个 monitorexit, 一个是正常退出, 另一个是异常退出, 所以synchronized 不会造成死锁。

我们可以通过idea的插件,查看字节码指令

从JVM层面来看:

synchronized是如何判断对象被“锁住“了呢?原来synchronized 使用的锁是存在对象的对象头之中。JVM 规范有这样一句话 : JVM 基于进入和退出 monitor 实现代码同步, 任何对象都有一个 monitor 与之关联, 当 monitor 被持有后, 它将处于锁定状态。

从操作系统层面来看:

在 JDK1.6 之前, synchronized 是重量级锁, Java 进程是工作在用户态空间上的, 如果需要实现同步, 就必须使用内核的互斥锁, 那就需要操作系统从用户态切换到内核态 (使用 0x80 指令切换到内核态)。

什么是用户态和内核态呢?

如今的 HotSpot 采用的是解释器和编译器并存的架构, 可以通过 JIT 即时编译期生成的汇编指令来查看 synchronized 底层是如何实现的。我们编写如下一段代码:

public class Test {
private static synchronized void test1(){

}
public static void main(String[] args) {
for (int i=0;i<100000;i++) {
test1();
}
}
}

接下来在命令行使用如下指令生成汇编代码:

java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly Test

我们在生成的汇编代码中即可找到 test1 方法, 并且可以看到 lock cmpxchg 指令。

升级锁

JDK 1.6 开始, HotSpot 开发团队实现了锁优化 : 偏向锁, 轻量级 (自旋锁), 大大提高了并发效率。下面我们简单介绍一下:

偏向锁

当一个线程第一次进入到同步块时, JVM使用 CAS 将对象的 Mark Word 的偏向状态, 将 0 改为 1 同时将 Mark Wrod 的线程 id 指向该线程 id, 如果这个操作成功, 持有偏向锁的线程每次进入这个锁相关的同步块时, JVM 都不会进行任何同步操作, 一旦出现另一个线程去尝试获取这个锁的情况, 偏向模式立刻结束。

偏向锁用于有同步但无竞争的程, 但是其效率平不一定比轻量级锁高, 在有竞争的情况下, 偏向锁一定会被撤销, 这个过程也是消耗资源的。

JVM 启动的时候, 存在明显的线程竞争 (加载 class 文件), 所以默认启动时不会立刻打开偏向锁, 过一段时间才会打, 可以通过 JVM 参数设置。

-XX:BiasedLockingStartupDelay=0
轻量级锁

当代码即将进入同步块的时候, JVM 首先会在本线程的栈帧中创建一个锁记录空间 (Lock Record) , 用于存储 Mark Word 的一个拷贝 (官方命名为 Displaced Mark Word), 接着利用 CAS 将对象的 Mark Word 修改为指向锁记录的指针, 如果这个操作成功了, 则说明这个线程获取到了这个对象的锁, 同时对象的 Mark Word 的锁标志位会被修改为 ” 00 “, 表示这个对象处于轻量级锁状态。

轻量级锁是是通过 CAS 操作避免了传统的重量级锁的使用而减少系统互斥量产生的性能消耗, 如果明显存在锁的竞争, 不仅会产生互斥量, 同时也会进行CAS操作, 相对而言比传统的重量级锁开销更大。

重量级锁

如果发现对象处于轻量级锁的状态, 并且锁已被持有, 那么线程需要自适应自旋去获得锁, 当自旋次数超过一定次数时, 或者 自旋线程 > CPU 核数一半, 轻量级锁不在有效, 锁膨胀升级为重量级锁, 对象的锁标志位变为 ” 10 “, 此时 Mark Word 存储的就是指向重量级锁 (互斥量) 的指针, 后面等待锁的线程也必须立刻阻塞。

当持有偏向锁的线程调用 wait() 过长, 锁也会直接升级为重量级锁。

当锁为重量级锁, 如果此时有多个线程来竞争锁, OS 会把这些线程放入ObjectMonitor 的 waitSet 队列中, 供OS调度。

为什么有了轻量级锁还要用重量级锁?

当线程数一多, 自旋的线程也会随之变多, 我们知道自旋是消耗CPU的, 所以当线程超过一定限制, 就会严重影响 CPU 性能, 此时必须使用重量级锁。

为什么不直接上重量级锁?

如果直接上重量级锁, 需要向 OS 申请互斥锁, 会使 OS 在用户态到内核态之中切换, 在涉及到线程上下文切换的时候非常耗费资源。

为什么线程的上下文切换会非常耗费资源?

上下文切换是指 CPU 的控制权由运行状态的线程转换到就绪状态的线程所发生的事件;该操作会保存当前线程的执行现场, 同时载入接下来要执行的线程的执行现场, 这个过程免不了一些寄存器, 缓存之间数据的拷贝, 这个过程并不是轻量级的操作。

发表评论

邮箱地址不会被公开。 必填项已用*标注