Tavio's blog Tavio's blog
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Tavio Zhang

努力学习的小码喽
首页
  • JVM底层原理
  • 邪恶多线程
  • MyBatis底层原理
  • Spring底层原理
  • MySQL的优化之路
  • ClickHouse的高性能
  • Redis的快速查询
  • RabbitMQ的生产
  • Kafka的高吞吐量
  • ES的入门到入坑
  • MySQL自增ID主键空洞
  • 前端实现长整型排序
  • MySQL无感换表
  • Redis延时双删
  • 高并发秒杀优惠卷
  • AOP无侵入式告警
  • 长短链接跳转
  • 订单超时取消
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java多线程
  • Java线程锁
    • 一、线程锁的核心概念
      • 1.1 什么是线程锁?
      • 1.2 为什么需要线程锁?
    • 二、线程锁的分类
      • 2.1 按核心策略:悲观锁 vs 乐观锁
      • 2.2 按互斥粒度:独占锁 vs 共享锁
      • 2.3 按可重入性:可重入锁 vs 不可重入锁
      • 2.4 按公平性:公平锁 vs 非公平锁
    • 三、Java 内置锁:Synchronized
      • 3.1 用法:三种同步场景
      • 3.2 底层实现:字节码视角
      • 3.2.1 同步代码块:monitorenter与monitorexit
      • 3.2.2 同步方法:ACC_SYNCHRONIZED标志
      • 3.3 锁的底层结构:对象头与Monitor
      • 3.3.1 对象的内存结构
      • 3.3.2 Mark Word的动态变化
      • 3.3.3 Monitor的作用
      • 3.4 锁的升级机制(JDK 1.6+)
      • 升级流程详解:
      • 3.5 核心特性
    • 四、轻量级同步:Volatile
      • 4.1 核心作用
      • 4.2 为什么不保证原子性?
      • 4.3 适用场景
    • 五、总结:线程锁的选择与实践
    • 扩展:常见问题
  • Java JMM
  • Java ThreadLocal
  • Java 线程池
  • Java CompletableFuture
  • 《多线程》笔记
Tavio
2022-03-27
目录

Java线程锁

在多线程编程中,当多个线程共享资源时,"并发操作导致数据不一致"是最常见的问题。例如两个线程同时执行count++,最终结果可能比预期少1——这就是竞态条件(Race Condition)。线程锁作为解决该问题的核心机制,通过控制共享资源的访问权限,保证多线程操作的安全性。

# 一、线程锁的核心概念

# 1.1 什么是线程锁?

线程锁是一种同步机制,用于保证多个线程对共享资源的互斥访问。它通过限制"同一时间只有一个线程能操作共享资源",避免并发操作导致的数据错乱。

# 1.2 为什么需要线程锁?

线程共享进程的内存空间,当多个线程同时操作共享变量、对象等资源时,单条指令(如count++)可能被拆分为多个步骤(读取→修改→写入)。若步骤被其他线程打断,会导致结果异常。

示例:竞态条件导致的错误

public class RaceConditionDemo {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        // 两个线程同时执行1000次count++
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) count++;
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) count++;
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终结果:" + count); // 预期2000,实际可能小于2000
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

原因:线程1读取count=500后,线程2也读取count=500,两者分别加1后写入,最终count=501(而非502)。

# 二、线程锁的分类

根据设计思想、粒度和特性,线程锁可分为以下类型:

# 2.1 按核心策略:悲观锁 vs 乐观锁

  • 悲观锁
    假设线程一定会发生并发冲突,因此在访问资源前必须先获取锁,全程独占资源。

    • 典型例子:Java的Synchronized、ReentrantLock,数据库的行锁/表锁。
    • 优点:安全性高;缺点:频繁加锁释放锁会增加性能开销。
  • 乐观锁
    假设线程不会发生冲突,因此不主动加锁,仅在更新资源时检查是否被其他线程修改。

    • 实现方式:版本号机制(如数据库乐观锁)、CAS(Compare-And-Swap,比较并交换)。
    • 优点:无锁竞争,性能高;缺点:冲突频繁时会不断重试,反而降低效率。

# 2.2 按互斥粒度:独占锁 vs 共享锁

  • 独占锁(排他锁)
    同一时间只允许一个线程获取锁,其他线程必须等待。

    • 典型例子:Synchronized、ReentrantLock(默认模式)。
    • 适用场景:写操作(修改共享资源)。
  • 共享锁
    允许多个线程同时获取锁,但仅支持读操作;写操作仍需独占锁。

    • 典型例子:ReentrantReadWriteLock.ReadLock(读锁共享,写锁独占)。
    • 优点:提高读操作的并发效率(多线程可同时读)。

# 2.3 按可重入性:可重入锁 vs 不可重入锁

  • 可重入锁
    同一线程可多次获取同一把锁,锁会记录"持有次数",释放次数需与获取次数一致(避免自己锁死自己)。

    • 典型例子:Synchronized、ReentrantLock(名称中"Reentrant"即表示可重入)。
    • 示例:递归调用同步方法时,不会因重复获取锁而死锁。
  • 不可重入锁
    同一线程多次获取同一把锁会导致死锁(线程已持有锁,再次获取时会阻塞自己)。

    • 典型例子:早期的SimpleLock(自定义简易锁,未实现重入逻辑)。

# 2.4 按公平性:公平锁 vs 非公平锁

  • 公平锁
    按线程请求锁的先后顺序分配锁(先到先得),避免线程饥饿(长期得不到锁)。

    • 典型例子:ReentrantLock(true)(构造参数传true)。
    • 优点:公平性高;缺点:需维护等待队列,性能较低。
  • 非公平锁
    线程获取锁时不按顺序,允许"插队"(刚释放锁的线程可能立即再次获取锁)。

    • 典型例子:Synchronized(默认非公平)、ReentrantLock(false)(默认)。
    • 优点:性能高(减少线程切换开销);缺点:可能导致部分线程长期等待。

# 三、Java 内置锁:Synchronized

synchronized是Java最常用的内置锁机制,可保证代码块/方法的原子性、可见性和有序性,无需手动释放锁(JVM自动管理)。

# 3.1 用法:三种同步场景

synchronized的锁对象由修饰范围决定,具体用法如下:

用法 锁对象 作用范围 示例代码
修饰静态方法 当前类的Class对象 整个静态方法 public static synchronized void staticMethod() { ... }
修饰实例方法 当前实例对象(this) 整个实例方法 public synchronized void instanceMethod() { ... }
修饰代码块 自定义对象(如lock) 代码块内部 synchronized (lock) { ... } (lock可为任意Object对象)

注意:

  • 静态方法锁是类级别的,所有实例共享同一把锁;
  • 实例方法锁是对象级别的,不同实例的锁相互独立(不会互斥)。

# 3.2 底层实现:字节码视角

synchronized的底层依赖JVM的监视器(Monitor) 机制,通过字节码指令或标志位实现锁的获取与释放。

# 3.2.1 同步代码块:monitorenter与monitorexit

编译后,同步代码块会生成monitorenter(获取锁)和monitorexit(释放锁)指令:

public class SyncBlockDemo {
    private final Object lock = new Object();
    public void test() {
        synchronized (lock) { // 同步代码块
            System.out.println("临界区代码");
        }
    }
}
1
2
3
4
5
6
7
8

反编译字节码(javap -v SyncBlockDemo.class)核心片段:

Code:
     0: aload_0         // 加载锁对象lock
     1: getfield      #2 // Field lock:Ljava/lang/Object;
     4: dup
     5: astore_1
     6: monitorenter   // 尝试获取锁(进入Monitor)
     7: getstatic     #3 // 执行临界区代码(打印逻辑)
    37: aload_1
    38: monitorexit    // 正常释放锁(退出Monitor)
    39: goto          47
    43: astore_2
    44: aload_1
    45: monitorexit    // 异常时也释放锁(避免锁泄露)
    46: aload_2
    47: return
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • monitorenter:线程尝试获取锁,成功则进入临界区,失败则阻塞(进入Monitor的等待队列)。
  • monitorexit:无论代码正常执行还是抛出异常,都会释放锁(保证锁一定会被释放)。

# 3.2.2 同步方法:ACC_SYNCHRONIZED标志

同步方法通过方法表中的ACC_SYNCHRONIZED标志实现,无需显式指令:

public class SyncMethodDemo {
    public synchronized void test() { // 同步实例方法
        System.out.println("同步方法代码");
    }
}
1
2
3
4
5

反编译字节码核心片段:

public synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED  // 同步方法标志
Code:
    0: getstatic     #2 // 执行方法逻辑
   30: return
1
2
3
4
5
6

JVM执行时会检查ACC_SYNCHRONIZED标志:若存在,先获取锁(锁对象为this或Class),执行完方法后自动释放锁。

# 3.3 锁的底层结构:对象头与Monitor

synchronized的本质是对象锁——所有Java对象都可作为锁,因为每个对象内置了一个Monitor(监视器),且对象头存储了锁的状态信息。

# 3.3.1 对象的内存结构

Java对象在堆内存中分为三部分:

对象头(Header) | 实例数据(Instance Data) | 对齐填充(Padding)
1

其中对象头是实现锁的核心,包含:

  • Mark Word:存储对象的锁状态、哈希码(HashCode)、GC年龄、线程ID等(动态变化)。
  • Klass Pointer:指向对象所属类的元数据(如类的方法、字段信息)。

# 3.3.2 Mark Word的动态变化

Mark Word的内容会随锁状态改变,以32位JVM为例:

锁状态 Mark Word存储内容(32位) 锁标志位 偏向锁标志
无锁 哈希码(25位) + GC年龄(4位) + 0 + 01(未使用) 01 0
偏向锁 线程ID(23位) + 偏向时间戳(2位) + GC年龄(4位) + 1 + 01 01 1
轻量级锁 指向栈中锁记录(Lock Record)的指针(30位) + 00 00 -
重量级锁 指向Monitor对象的指针(30位) + 10 10 -
GC标记 空 + 11 11 -

# 3.3.3 Monitor的作用

当锁升级为重量级锁时,Mark Word会指向一个Monitor对象(C++结构体),其核心结构包括:

  • _owner:持有锁的线程(初始为null)。
  • _WaitSet:等待锁的线程队列(调用wait()后进入)。
  • _EntryList:竞争锁失败的线程队列(阻塞状态)。

线程获取锁时,_owner设为当前线程;释放锁时,_owner设为null,并唤醒_EntryList中的线程竞争锁。

# 3.4 锁的升级机制(JDK 1.6+)

为减少锁的性能开销,JDK 1.6引入锁升级机制:锁状态从"无锁"逐步升级为"偏向锁"→"轻量级锁"→"重量级锁",避免直接使用重量级锁(性能低)。

# 升级流程详解:

  1. 无锁状态
    对象刚创建时,Mark Word存储哈希码和GC年龄,无锁标志(01),偏向锁标志(0)。

  2. 偏向锁(单线程优化)

    • 触发条件:只有一个线程竞争锁。
    • 原理:线程第一次获取锁时,通过CAS将Mark Word的"线程ID"设为自己的ID。后续该线程进入/退出锁时,只需检查线程ID是否为自己,无需CAS操作(几乎无开销)。
    • 撤销:若其他线程尝试获取锁,偏向锁会撤销为无锁或轻量级锁(有额外开销,因此适合单线程长期持有锁的场景)。
  3. 轻量级锁(多线程交替执行)

    • 触发条件:多个线程交替竞争锁(非同时竞争)。
    • 原理:线程获取锁时,在栈中创建Lock Record(存储Mark Word副本),通过CAS将Mark Word改为指向Lock Record的指针。成功则获取锁;失败则通过自旋(循环重试)获取锁(避免线程阻塞,适合短任务)。
  4. 重量级锁(多线程同时竞争)

    • 触发条件:自旋次数超过阈值(默认10次),或有线程阻塞。
    • 原理:锁膨胀为重量级锁,Mark Word指向Monitor对象。竞争失败的线程进入Monitor的_EntryList阻塞(不消耗CPU,但线程切换开销大)。

# 3.5 核心特性

  1. 原子性:通过Monitor保证同一时间只有一个线程进入临界区,确保代码块/方法的操作不可分割。
  2. 可见性:依赖内存屏障(Memory Barrier):
    • 释放锁时,线程工作内存的变量会刷新到主内存;
    • 获取锁时,线程工作内存会清空,从主内存重新读取变量。
  3. 有序性:禁止指令重排序(临界区内代码执行顺序稳定)。
  4. 可重入性:同一线程多次获取同一锁时,Mark Word记录线程ID,无需重新竞争(避免死锁)。

# 四、轻量级同步:Volatile

volatile是Java的轻量级同步关键字,用于修饰共享变量,解决可见性和有序性问题,但不保证原子性。

# 4.1 核心作用

  • 可见性:一个线程修改volatile变量后,其他线程能立即看到最新值(通过内存屏障强制刷新主内存)。
  • 有序性:禁止指令重排序(通过内存屏障限制编译器和CPU的优化)。

# 4.2 为什么不保证原子性?

volatile仅能保证单个变量的读写可见,但无法保证复合操作(如i++)的原子性。例如:

public class VolatileAtomicDemo {
    private static volatile int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> { for (int j = 0; j < 1000; j++) i++; });
        Thread t2 = new Thread(() -> { for (int j = 0; j < 1000; j++) i++; });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i); // 结果可能小于2000(原子性不保证)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 4.3 适用场景

  • 状态标记位(如boolean isRunning,控制线程启动/停止);
  • 单线程写、多线程读的变量(无需原子性)。

# 五、总结:线程锁的选择与实践

  • synchronized:适合大多数场景,无需手动释放锁,JDK优化后性能接近ReentrantLock。
  • ReentrantLock:适合需要灵活控制(如公平锁、尝试获取锁、中断等待)的场景。
  • volatile:适合轻量级同步(可见性/有序性),不涉及复合操作的场景。

核心原则:根据并发强度(竞争频率)、功能需求(公平性、可中断性)选择合适的锁,优先使用JDK内置机制(减少手动管理成本)。

# 扩展:常见问题

  1. 死锁如何产生?
    多个线程相互持有对方需要的锁,且不释放(如线程1持有锁A等待锁B,线程2持有锁B等待锁A)。
  2. 如何避免死锁?
    • 按固定顺序获取锁;
    • 设定锁的获取超时时间(如ReentrantLock.tryLock(timeout));
    • 使用LockSupport中断线程等待。
编辑 (opens new window)
#Java线程锁#syncironized
上次更新: 2026/01/21, 19:29:14
Java多线程
Java JMM

← Java多线程 Java JMM→

最近更新
01
订单超时取消
01-21
02
双 Token 登录
01-21
03
长短链接跳转
01-21
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式