Skip to content

synchronized关键字

synchronized保证原子性,synchronized保证只有一个线程拿到锁,能够进入同步代码块。

如果要完整理解锁对象、Monitor、字节码、可重入、内存可见性、锁升级、JDK 7/8 和新版本差异、线上阻塞排查,建议继续看:synchronized全过程原理

通过javap对字节码进行反编译synchronized可以查看到monitorenter和monitorexit指令

monitorenter

官方描述 monitorenter Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows: • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor. • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count. • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

翻译过来: 每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获 取该monitor。 当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应 的monitor的所有权。其过程如下:

  1. 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为 monitor的owner(所有者)
  2. 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
  3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直 到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

monitorexit

官方文档 monitorexit

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref. The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

翻译过来:

  1. 能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
  2. 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出 monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个 monitor的所有权。

monitorexit释放锁。 monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。 面试题synchroznied出现异常会释放锁吗? 会释放锁

同步方法

文档

同步方法在反编译后,会增加ACC_SYNCHRONIZED 修饰。会隐式调用monitorenter和monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit。

synchronized与Lock的区别

  1. synchronized是关键字,而Lock是一个接口。
  2. synchronized会自动释放锁,而Lock必须手动释放锁。
  3. synchronized是不可中断的,Lock可以中断也可以不中断。
  4. 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
  5. synchronized能锁住方法和代码块,而Lock只能锁住代码块。
  6. Lock可以使用读锁提高多线程读效率。
  7. synchronized是非公平锁,ReentrantLock可以控制是否是公平锁

synchronized锁升级过程 高效并发是从JDK 5到JDK 6的一个重要改进,HotSpot虛拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,包括偏向锁( Biased Locking )、轻量级锁( Lightweight Locking )和如适应性自旋(Adaptive Spinning)、锁消除( Lock Elimination)、锁粗化( Lock Coarsening )等,这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。 无锁-->偏向锁-->轻量级锁–->重量级锁

锁升级为什么存在

如果每次 synchronized 都直接让线程阻塞、唤醒、进入操作系统互斥量,成本会很高。但很多锁在真实业务里并没有激烈竞争:可能一直只有一个线程进入,或者只是短时间两个线程交替进入。

所以 HotSpot 对 synchronized 做了优化,让锁根据竞争程度逐步升级:

mermaid
flowchart TD
    A["无锁"] --> B["偏向锁<br/>偏向第一个线程"]
    B --> C{"是否出现其他线程竞争"}
    C -- "否" --> B
    C -- "是" --> D["轻量级锁<br/>CAS 尝试获取"]
    D --> E{"竞争是否激烈"}
    E -- "否" --> D
    E -- "是" --> F["重量级锁<br/>线程阻塞等待 monitor"]
锁状态适合场景代价
偏向锁基本只有一个线程反复进入竞争出现时需要撤销
轻量级锁多线程交替进入,竞争不激烈CAS 自旋消耗 CPU
重量级锁竞争激烈,长时间拿不到锁涉及阻塞和唤醒,成本较高

注意:偏向锁在较新的 JDK 中已经逐步弱化或废弃,面试时更重要的是理解“锁会按竞争程度优化”,不要死背某个版本细节。

synchronized 为什么能保证可见性

线程释放锁时,会把同步代码块内对共享变量的修改刷新出去;另一个线程获取同一把锁后,能看到之前线程释放锁前的修改。也就是:

text
unlock 同一把锁 happens-before 后续 lock 同一把锁

这解释了为什么 synchronized 不只保证互斥,也保证可见性。

java
class SafeFlag {
    private boolean ready;

    public synchronized void markReady() {
        ready = true;
    }

    public synchronized boolean isReady() {
        return ready;
    }
}

只要读写使用同一把锁,读线程就能看到写线程释放锁前的结果。

工作原理图:monitor 获取和释放

mermaid
flowchart TD
    A["线程进入 synchronized"] --> B{"monitor 是否空闲"}
    B -- "空闲" --> C["获取 monitor 所有权"]
    C --> D["执行同步代码块"]
    D --> E["monitorexit 释放 monitor"]
    E --> F["其他线程继续竞争"]
    B -- "已被其他线程持有" --> G["进入阻塞等待"]
    G --> F
    B -- "当前线程已持有" --> H["重入计数 +1"]
    H --> D

synchronized 的核心不是“锁住代码”,而是线程要先拿到对象关联的 monitor。拿不到就等待;同一个线程再次进入同一把锁时可以重入。

代码 Demo:保护共享变量

java
public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

如果去掉 synchronized,多个线程同时执行 count++ 时可能丢失更新,因为 count++ 不是一个原子操作,而是读取、加一、写回三个步骤。

查看字节码:

bash
javac Counter.java
javap -c -v Counter

同步方法会出现 ACC_SYNCHRONIZED 标记,同步代码块会出现 monitorentermonitorexit