Java 的 synchronized
synchronized 是 java 内置的关键字,用于多线程的同步操作。
当 synchronized
修饰在方法或代码块上时,会对特定的对象或类加锁,从而确保同一时刻只有一个线程能执行加锁的代码块。
- synchronized 修饰方法:会在方法的访问标志中增加一个
ACC_SYNCHRONIZED
标志。每当一个线程访问该方法时,JVM 会检查方法的访问标志。如果包含ACC_SYNCHRONIZED
标志,线程必须先获得该方法对应的对象的监视器锁(即对象锁),然后才能执行该方法,从而保证方法的同步性。 - synchronized 修饰代码块:会在代码块的前后插入
monitorenter
和monitorexit
字节码指令。可以把monitorenter
理解为加锁,monitorexit
理解为解锁。
synchronized 无法完全禁止指令重排序,但能通过内存屏障保证多线程环境下的有序性。对于需要严格禁止重排序的场景,应优先选择 volatile。
实现原理
通过 JVM 的 Monitor(监视器锁) 和 对象头(Object Header)。
对象头(Object Header)
- mark word:记录对象的哈希码、分代年龄、锁标志位等。
- klass pointer:指向对象类型的元信息
64 位时 MarkWord 在不同状态下的内存布局
Mark Word 是实现 synchronized 的关键,因为它会根据锁的状态保存不同的信息,具体包括:
- 未锁定状态:Mark Word 存储对象的哈希码和 GC 分代信息。
- 偏向锁状态:Mark Word 保存获取该锁的线程 ID 和一些偏向锁标志位。
- 轻量级锁状态:Mark Word 存储的是指向栈中锁记录的指针。
- 重量级锁状态:Mark Word 存储的是指向 Monitor 对象的指针。
对象结构图
锁升级过程
synchronized 的锁升级过:无锁–>偏向锁–>轻量级锁–>重量级锁。
偏性锁
- 加锁过程:当有一个线程去获取锁时,jvm 会将该线程的 id 记录在对象头 mark word 中,等线程再次获锁时,jvm 无须在进行加锁操作。
- 撤销偏向锁:如果在偏向锁持有期间,另一个线程请求同一把锁,jvm 会撤销偏向锁并升级为轻量级锁。
轻量级锁
- 加锁过程:当线程获取锁时,jvm 会在当前线程的栈帧中创建一个锁记录,并将对象头中的 mark word 复制到锁记录中,然后利用 CAS 将对象头的 mark word 替换为指向锁记录的指针, 如果成功,则该线程获得轻量级锁,如果失败,表示其他线程已经持有该锁,此时会升级为重量级锁。
- 解锁过程:线程退出同步块时,JVM 会将对象头中的 Mark Word 恢复为原始值。
轻量级锁 CAS 失败了之后,不会有自旋操作,会直接进入重量级锁膨胀过程。在已经升级成重量级锁之后,线程如果没有争抢到锁,会进行一段自旋等待锁的释放。
重量级锁
当锁竞争激烈时,JVM 会升级为重量级锁,重量级锁使用操作系统的互斥量(Mutex) 机制来实现线程的阻塞与唤醒。
- 加锁过程:如果线程无法通过轻量级锁获取锁时,则会升级为重量级锁。
- 解锁过程:当线程释放重量级锁时,jvm 会唤醒所有阻塞的线程,允许他们再次获取锁。
当重量级锁释放了之后,锁对象是无锁的。有新的线程来竞争的话又会从无锁再到轻量级锁开始后续的升级流程。
锁升级过程总结
偏性锁:当线程第一次获取锁时,jvm 会将当前线程的 id 记录在对象头中,后续该线程再次获取锁时,几乎没有开销。
轻量级锁:当一个线程去获取已经被偏性的锁时,此时锁会升级为轻量级锁。
重量级锁:如果 CAS 失败无法获取锁时,此时锁会升级为重量级锁,线程会被挂起,直到锁被释放。
Synchronized 的可重入性
synchronized
是可重入的,每获取一次锁,计数器加一,释放锁时,计数器减一,直到计数器为 0,锁才会真正释放。
锁消除和锁粗化
- 锁消除:JVM 会通过逃逸分析判断对象是否只在当前线程使用,如果是,那么会消除不必要的加锁操作。
- 锁粗化:当多个锁操作频繁出现时,JVM 会将这些锁操作合并,减少锁获取和释放的开销。
修饰静态方法和修饰普通方法的区别
- 修饰静态方法:锁的是这个类的
Class
对象。也就是说,无论创建了多少个该类的实例,所有的实例共享同一个锁,因为这个锁属于类本身而不是某个对象实例。 - 修饰实例方法:锁的是当前实例(调用该方法的对象),也就是这个对象的内在锁。这也就是说每个对象实例都有自己独立的锁。