【面试真题拆解】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(监视器)对象。

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

相关推荐
努力往上爬de蜗牛2 小时前
extends
java·开发语言
2401_853576502 小时前
代码自动生成框架
开发语言·c++·算法
牢七2 小时前
PHP Debug配置记录
开发语言·php
逆境不可逃2 小时前
【从零入门23种设计模式23】行为型之模板模式
java·开发语言·算法·设计模式·职场和发展·模板模式
IronMurphy2 小时前
【算法二十五】105. 从前序与中序遍历序列构造二叉树 236. 二叉树的最近公共祖先
java·数据结构·算法
2401_853576502 小时前
C++中的组合模式变体
开发语言·c++·算法
snakeshe10102 小时前
从 MySQL 到 Elasticsearch:构建高性能新闻爬虫的数据存储与搜索体系
java
技术小白菜2 小时前
海康平台通过代理播放视频流
java·java ee
学习3人组2 小时前
Workerman实现 WSS 基于客户端 ID 的精准推送
android·java·开发语言