【面试真题拆解】Java锁机制:synchronized、ReentrantLock、锁升级、可重入锁

一句话速通:

Java 锁机制核心是 synchronized 和 ReentrantLock,两者都是可重入锁;

JDK 1.6+ 后 synchronized 引入锁升级(无锁➡️偏向锁➡️轻量级锁➡️重量级锁),性能大幅提升;

简单场景优先用 synchronized,需要高级功能(比如可中断、可超时、公平锁)时用 ReentrantLock。

为什么需要锁?

在并发场景下,多个线程同时修改同一个共享变量,会出现线程安全问题。

举个例子说明一下:

java 复制代码
public class UnsafeCounter {
    private int count = 0;

    public void increment() {
        count++; // 这行代码不是原子操作!
    }

    public int getCount() {
        return count;
    }
}

如果1000个线程同时调用 increment(),最终 count 的值很可能小于1000。

因为 count++ 不是原子操作,它分为读取countcount+1写回count三步,多线程同时执行会互相覆盖。

锁的作用就是把这段代码加锁,让同一时间只有一个线程能执行,从而保证操作的原子性。

synchronized

synchronized 是JVM内置的关键字,不需要手动释放锁。

用法 锁的对象 示例
锁实例方法 锁当前对象 this public synchronized void increment()
锁静态方法 锁当前类的 Class 对象 public static synchronized void increment()
锁代码块 锁指定的对象 synchronized (lock) { ... }

可以用synchronized给之前的例子上锁:

java 复制代码
public class SafeCounter {
    private int count = 0;

    // 锁实例方法:同一时间只有一个线程能执行
    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

ReentrantLock

ReentrantLockjava.util.concurrent(JUC)包下的锁,是基于代码实现的锁,比 synchronized 更灵活,但需要手动释放锁。

也可以用ReentrantLock给之前的例子上锁:

java 复制代码
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockCounter {
    private int count = 0;
    // 创建 ReentrantLock 实例
    private ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock(); // 加锁
        try {
            count++;
        } finally {
            lock.unlock(); // 必须在 finally 里释放锁,防止死锁
        }
    }

    public int getCount() {
        return count;
    }
}

ReentrantLock 有3个 synchronized 没有的功能:

功能 说明 示例
可中断 线程在等待锁的过程中,可以被中断,停止等待 lock.lockInterruptibly()
可超时 尝试获取锁,等待一段时间后如果还没拿到,就放弃 lock.tryLock(1, TimeUnit.SECONDS)
公平锁 按线程请求锁的顺序分配锁(先来先得),默认是非公平锁 new ReentrantLock(true)

可重入锁

synchronizedReentrantLock 都是可重入锁。

那什么是可重入呢?

可重入的意思就是同一个线程,可以多次获取同一把锁,不会被自己阻塞。

那又为什么需要可重入呢?

举个递归调用的例子:

java 复制代码
public class ReentrantExample {
    //  synchronized 是可重入的
    public synchronized void methodA() {
        System.out.println("执行 methodA");
        methodB(); // 调用 methodB,methodB 也需要同一把锁
    }

    public synchronized void methodB() {
        System.out.println("执行 methodB");
    }

    public static void main(String[] args) {
        ReentrantExample example = new ReentrantExample();
        example.methodA();
    }
}

如果锁是不可重入的,那么线程在执行 methodA() 时已经拿到了锁,调用 methodB() 时会因为拿不到锁而被自己阻塞,导致死锁。

synchronized 的锁升级

在 JDK 1.6 之前,synchronized 是重量级锁,性能很差;

JDK 1.6 之后,引入了锁升级机制,提升了 synchronized 的性能。

锁升级的意思是:

随着多线程竞争的加剧,锁会从【无锁】➡️【偏向锁】➡️【轻量级锁】➡️【重量级锁】逐步升级,而且升级是单向的,只能升不能降。

Java对象在内存中分为3部分:

对象头、实例数据、对齐填充,其中对象头里的 Mark Word 会记录锁的状态。

synchronized 的锁是存放在【对象头】里的。

Mark Word 的结构(32 位 JVM):

锁状态 25位 4位 1位(是否偏向锁) 2位(锁标志位)
无锁 对象的哈希码 分代年龄 0 01
偏向锁 偏向线程ID 分代年龄 1 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向重量级锁(monitor)的指针 10

锁升级的四个阶段

1. 无锁

没有线程竞争锁,对象处于无锁状态。

2. 偏向锁

只有一个线程多次获取同一把锁,没有其他线程竞争。

第一个线程获取锁时,会在对象头的 Mark Word 里记录【偏向线程ID】,这个线程以后再次获取锁时,只需要检查偏向线程ID是不是自己,如果是,直接获取锁。

当有第二个线程来竞争这把锁时,偏向锁会撤销,升级为轻量级锁。

3. 轻量级锁

多个线程交替获取锁,但是竞争不太激烈(比如线程A获取锁,执行完释放了,线程B再获取)。

线程在自己的栈帧里创建一个锁记录(Lock Record),用 CAS(Compare And Swap,比较并交换)操作,尝试把对象头的 Mark Word 替换成指向自己栈中锁记录的指针。

如果 CAS 成功,就获取到了轻量级锁。

当有多个线程同时竞争锁,CAS 失败多次,就会升级为重量级锁。

4. 重量级锁

多个线程同时竞争锁,竞争激烈。

锁升级为重量级锁后,对象头的 Mark Word 会指向一个monitor(监视器)对象。

没有获取到锁的线程会进入阻塞队列,被操作系统挂起,等待持有锁的线程释放锁后唤醒。

相关推荐
小羊没烦恼!1 天前
微服务化的基石——持续集成
java·大数据·word·powerpoint·.net
俊昭喜喜里1 天前
java中的继承和多态的区别
java
小羊没烦恼!1 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
譕痕1 天前
JSONObject与JSONArray封装数据格式区别
java·json
胡写代码1 天前
别再前后端各写一套表单校验了
java·后端
小鱼能吃糖1 天前
缺陷修复总览 · mall电商项目:5类缺陷,1个病根,4个业务域
java·电商
此时不提桶,更待何时1 天前
01-06-A-JVM排查实战详解
java·jvm
伞伞悦读1 天前
【第38期】Python 模块与包详解:import、from、模块搜索路径、包结构和 __init__
开发语言·python
晨米酱1 天前
AGENTS.md:Agent 的上下文策略层
面试·架构·agent
vipxieliang1 天前
ValidX 在 DDD 领域驱动设计中的实践
java·spring boot