浅谈Synchronized:偏向锁,轻量级锁和重量级锁及底层原理
SryYa Three

锁的概述

线程安全问题:

在并发环境下,由于线程各自私有的内存不共享,当多个线程修改同一份公有的内存数据时就可能会出现数据不一致的问题。

Java中的锁:

  • 按实现方式分类: Java中一般有两个实现的锁,分别是内置锁Synchronized(JVM实现)和在JUC包下的各种锁(如ReentrantLock),API 层面实现。
  • 按公平性分类: 公平锁和非公平锁。公平锁严格按照先进先出的顺序获取锁;非公平锁可以插队,线程之间竞争抢锁。
  • 按特性分类: 有读写锁、自旋锁、可重入锁、可中断锁等。

Synchronized锁的三种状态:

  • 偏向锁

  • 轻量级锁

  • 重量级锁


Synchronized的用法

Synchronized修饰实例方法:

1
2
3
4
5
6
7
public class Counter {
private int count = 0;
//在方法上加synchronized
public synchronized void increment() {
count++;
}
}
  • 当Synchronized修饰实例方法时,线程必须获取当前实例(this)的锁才能执行被修饰的方法。

  • 不同的实例对象拥有独立的锁,如果线程1调用Counter1.increment()线程2调用Counter2.increment(),相当于两个线程并行,互不干扰,不会互相阻塞。

  • 同一时间只有一个线程执行方法,不会出现中间状态被其他线程插入,保证了原子性


Synchronized修饰静态方法:

1
2
3
4
5
6
7
public class Counter {
private int count = 0;
//在静态方法上加synchronized
public static synchronized void increment() {
count++;
}
}
  • 当Synchronized修饰静态方法时,锁住的是整个类Class对象,如上面的代码里锁住的就是Counter.Class而非实例Counter1Counter2

  • JVM里一个类Class对象是唯一的。所以当前类的所有实例对象共享一把锁,如果线程1调用Counter1线程2调用Counter2,那么这两个线程尝试获取的锁都是Counter.Class锁,如果线程1先拿到锁,线程2就会阻塞


Synchronized修饰代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Counter {
private int count = 0;
private Object lock = new Object();//设置锁,这里是每个实例都有一个自己的锁

public void increment() {
//要想执行大括号里面的代码,需要先获取实例的锁
synchronized (lock) {
count++;
}
}
}
-------------------------------------------
public class Counter {
private int count = 0;
private static Object lock = new Object();//设置锁,这里加了static是所有实例共有一个锁

public void increment() {
//要想执行大括号里面的代码,需要先获取共有的那一个锁
synchronized (lock) {
count++;
}
}
}
全局锁的另一种写法:
private Object lock = new Object();
public void increment() {
//获取lock的Class对象,上文讲述过JVM里只有一个类的Class对象。
synchronized (lock.getClass()) {
count++;
}
}

Synchronized的底层实现:

对象头

  • 每个Java对象在JVM内存中都包含一个对象头(Object Header),它是实现synchronized锁机制的基石。

  • Mark Word(核心动态结构):

    • 动态记录:根据对象状态(无锁/偏向/轻量/重量)复用存储空间。
    • 关键信息:存储了HashCode、GC分代年龄、锁状态标志、持有线程的ID等。
  • Klass Pointer(类型指针):

    • 指向对象所属类的元数据。
    • JVM通过该指针确定对象的具体类型。

图示:

image


监视器(Monitor)

核心定义:同步机制的载体

Monitor是一种同步工具,通常被描述为一个对象。当synchronized锁住一个对象时,本质是获得了该对象Monitor的所有权。

内部解析:

  • Owner(拥有者): 当前持有Monitor锁的线程,同一时刻只有一个线程拥有所有权。
  • Entry Set(等待区): 存放所有等待获取锁的线程集合,里面的线程处于阻塞状态(BLOCKED)。
  • Wait Set(等待集合): 存放使用了wait()方法后释放锁并等待唤醒的线程,里面的线程处于等待状态(WAITING)。

图示:

image


锁的升级

偏向锁

设计目的:消除无竞争开销

针对锁总是由同一线程多次获取的场景,通过**“偏向”**该线程来避免每次获取锁都进行同步操作,从而提高性能。

核心原理:Mark Word标记

首次获取时,JVM通过CAS操作将对象头Mark Word设置为偏向锁状态并记录线程ID。后续进入只需简单检查ID是否跟记录的相同。

使用场景:单线程重复进入

适合无竞争的单线程同步场景。一旦有其他线程竞争, 偏向锁会被撤销并升级为轻量级锁。

image


轻量级锁

核心目的

针对多线程交替执行同步块的场景,避免重量级锁阻塞带来的高昂性能开销。

实现原理:CAS + 自旋

  • 创建记录: JVM在线程栈帧中创建Lock Record,并复制对象头Mark Word。

  • CAS尝试: 通过CAS操作将对象头更新为指向Lock Record的指针。若成功则获得锁。

  • 自旋等待: 若CAS失败(存在竞争),线程进入自旋(忙等)而非立即阻塞。

使用场景:

多线程交替执行同步块,且锁持时时间短。

图示:

image


重量级锁

核心目的:避免CPU空耗

当自旋等待无法获取锁或竞争激烈时,升级为重量级锁,防止CPU资源被无效占用。

实现原理:Monitor机制

自旋达到阈值或竞争加剧时,轻量级锁膨胀。未获锁的线程被阻塞并进入Monitor的Entry Set等待队列中。

使用场景:高并发竞争

适用于多线程激烈竞争同步块的场景,通过牺牲线程切换开销换取CPU效率。

图示:

image


为什么要有锁升级?

答: 系统并不是每时每刻都是高并发状态,很多时候就只有一两个线程在工作。如果一直使用重量级锁,操作系统一直在用户态和内核态切换,性能开销会非常大。

synchronized是怎么释放锁的?

答: synchronized会自动释放锁,在字节码层面,synchronized会先后执行monitorenter(加锁)monitorexit(释放锁)


总结

synchronized是Java内置的、最基础的同步锁,通过对象头(Mark Word)Monitor机制实现。它支持锁的自动升级,从偏向锁到轻量级锁,再到重量级锁,以适应不同的竞争场景,在保证正确性的同时尽可能地提高性能。

由 Hexo 驱动 & 主题 Keep
总字数 28.1k 访客数 访问量