Java 的锁机制:原理与源码详解

        本文将从底层原理和源代码层面详细解释Java的锁机制,尽量用通俗易懂的语言让初学者也能理解。本文会从概念开始,逐步深入到实现细节,涵盖Java锁的种类、底层原理、源码分析,并提供完整的步骤和推导。


一、什么是锁?为什么需要锁?

1.1 锁的通俗概念

        锁(Lock)就像现实生活中房间的门锁:当多个人(线程)同时想进入同一个房间(访问共享资源,比如一个变量或对象)时,锁确保只有一个人能进去,其他人得在门外等着。这样可以避免混乱,比如防止两个人同时修改一个银行账户余额导致数据错误。

        在Java中,锁是用来解决多线程并发访问共享资源时可能出现的数据不一致问题。简单说,锁是多线程编程中保证线程安全的工具。

1.2 为什么需要锁?

        假设有两个线程同时操作一个共享变量 counter = 0,每个线程都想执行 counter++。表面上看,counter++ 是一条指令,但实际上它包含三个步骤:

  1. 读取 counter 的值(比如 0)。
  2. 将值加 1(0 + 1 = 1)。
  3. 将新值写回 counter(写回 1)。

如果没有锁,两个线程可能交错执行这些步骤:

  • 线程1:读取 counter = 0
  • 线程2:读取 counter = 0
  • 线程1:计算 0 + 1 = 1,写回 counter = 1
  • 线程2:计算 0 + 1 = 1,写回 counter = 1

        结果是,执行了两次 counter++,但 counter 还是 1,而不是期望的 2。这就是线程不安全的表现。锁通过限制同一时间只有一个线程能执行这些步骤,解决了这个问题。


二、Java中的锁种类

Java提供了多种锁机制,主要分为以下几类:

  1. 内置锁(synchronized关键字):Java语言层面的锁,基于对象监视器(Monitor)。
  2. 显式锁(Lock接口):Java并发包(java.util.concurrent.locks)提供的锁,比如 ReentrantLock。
  3. 其他并发工具:如读写锁(ReentrantReadWriteLock)、信号量(Semaphore)等,基于锁的更高层次抽象。

我们重点讲解synchronized 和 ReentrantLock,因为它们是Java锁的核心实现。


三、synchronized锁的原理和源码分析

3.1 synchronized的基本用法

  synchronized 是Java内置的锁机制,可以用来修饰方法或代码块。以下是两种常见用法:

修饰方法
public synchronized void increment() {
    counter++;
}

这表示整个方法是线程安全的,同一时间只有一个线程能执行这个方法。

修饰代码块
public void increment() {
    synchronized(this) {
        counter++;
    }
}

这表示只有被 synchronized 包裹的代码块是线程安全的,锁的对象是 this。

3.2 synchronized的底层原理

  synchronized 的实现依赖于Java对象中的监视器(Monitor)。每个Java对象都可以作为一个锁,因为对象头中包含一个Monitor结构。

3.2.1 对象头和Monitor

在JVM(Java虚拟机)中,每个对象都有一个对象头,包含以下信息:

  • Mark Word:存储锁状态、哈希码、GC标记等。
  • Class Metadata Address:指向对象所属类的元数据。
  • Array Length(如果对象是数组)。

如果需要详细了解对象头的内容见我这篇文章:Java的对象头:原理与源码详解

        当一个线程尝试获取 synchronized 锁时,JVM会检查对象的 Mark Word,看它是否已经关联了一个 Monitor。Monitor 是一个操作系统级别的互斥锁(Mutex),用来实现线程的互斥访问。

3.2.2 Monitor的工作机制

Monitor 有三个核心状态:

  1. 进入(Entry):线程尝试获取锁。如果锁空闲,线程进入并持有锁;如果锁被占用,线程进入等待队列(Entry Set)
  2. 拥有(Owner):当前持有锁的线程。
  3. 等待(Wait):线程调用 wait() 方法后,释放锁并进入等待集合(Wait Set),等待被 notify() 或 notifyAll() 唤醒。

用生活中的例子解释:

  • 假设一个厕所(共享资源)只有一个坑位,门上有个锁(Monitor)。
  • 有人(线程)想进去,先检查锁:
    • 如果没人,锁上并进去(获取锁,进入Owner状态)。
    • 如果有人,排队等着(进入Entry Set)。
  • 如果里面的人按了“暂停”按钮(调用 wait()),他会出来等在旁边(Wait Set),让别人用。
    等有人喊“可以了”(notify()),他再回去继续用。

如果需要详细了解Monitor 的机制见我这篇文章::Java 的 Monitor 机制:原理与源码详解

3.2.3 synchronized的执行流程

以下是 synchronized 锁的详细步骤:

  1. 线程尝试获取锁
    • JVM检查对象头的 Mark Word,看是否已经有 Monitor。
    • 如果没有,创建一个 Monitor,并将 Mark Word 指向它,线程成为 Owner。
    • 如果已有 Monitor,且被其他线程持有,当前线程进入 Entry Set 等待。
  2. 执行同步代码:持有锁的线程执行 synchronized 块中的代码。
  3. 释放锁
    • 代码执行完,JVM更新 Monitor 状态,释放锁。
    • 如果有等待线程(在 Entry Set 或 Wait Set),JVM唤醒一个线程,让它尝试获取锁。

3.3 synchronized的源码分析

        虽然 synchronized 是JVM内置的,但它的核心实现在JVM的C++代码中,位于 HotSpot JVM 的源码中。我们可以看看关键部分(简化版,非完整源码)。

JVM中的Monitor实现

        在HotSpot JVM中,ObjectMonitor 类负责实现Monitor的功能,位于 src/hotspot/share/runtime/objectMonitor.cpp。以下是核心逻辑的伪代码解释:

class ObjectMonitor {
private:
    Thread* _owner; // 当前持有锁的线程
    int _recursions; // 重入次数(支持可重入锁)
    ObjectWaiter* _entry_list; // 等待锁的线程队列
    ObjectWaiter* _wait_set; // 等待notify的线程集合

public:
    void enter(Thread* thread) {
        // 尝试获取锁
        if (_owner == nullptr) {
            _owner = thread; // 无人持有,当前线程获取锁
        } else if (_owner == thread) {
            _recursions++; // 已持有,支持重入
        } else {
            // 锁被占用,线程加入_entry_list等待
            thread->park(); // 线程阻塞
        }
    }

    void exit(Thread* thread) {
        // 释放锁
        if (_recursions > 0) {
            _recursions--; // 重入次数减1
        } else {
            _owner = nullptr; // 释放锁
            notify_waiters(); // 唤醒_entry_list中的线程
        }
    }

    void wait(Thread* thread) {
        // 线程调用wait,进入_wait_set
        _wait_set->add(thread);
        exit(thread); // 释放锁
        thread->park(); // 线程阻塞
    }

    void notify() {
        // 唤醒_wait_set中的一个线程
        if (_wait_set != nullptr) {
            Thread* t = _wait_set->remove_one();
            t->unpark(); // 唤醒线程
        }
    }
};

关键点解释
  1. enter():线程尝试获取锁。如果锁空闲,持有锁;如果是自己已持有,增加重入计数;否则阻塞等待。
  2. exit():释放锁,唤醒等待队列中的线程。
  3. wait()/notify():实现线程的等待和唤醒机制,涉及 Wait Set。
  4. 可重入性:synchronized 支持可重入锁,即同一个线程可以多次获取同一把锁,只需记录重入次数(_recursions)。
字节码层面

在Java代码中,synchronized 会被编译成字节码指令 monitorenter 和 monitorexit。例如:

synchronized(obj) {
    counter++;
}

编译后的字节码(简化):

monitorenter // 获取锁
iload counter
iadd 1
istore counter
monitorexit // 释放锁
  • monitorenter:调用JVM的锁获取逻辑。
  • monitorexit:调用JVM的锁释放逻辑。

3.4 synchronized的优化

JVM对 synchronized 做了大量优化,提高性能:

  1. 偏向锁:如果锁通常被同一线程获取,JVM直接“偏向”这个线程,减少锁获取的开销。
  2. 轻量级锁:如果竞争不激烈,使用CAS(Compare-And-Swap)操作,避免调用操作系统Mutex。
  3. 重量级锁:高竞争时,退化为操作系统级别的Mutex,性能开销较大。

这些优化通过对象头的 Mark Word 动态切换锁状态(无锁 → 偏向锁 → 轻量级锁 → 重量级锁)。


四、ReentrantLock的原理和源码分析

4.1 ReentrantLock的基本用法

  ReentrantLock 是 java.util.concurrent.locks 包中的显式锁,提供比 synchronized 更灵活的功能,比如公平锁、超时锁等。

示例:

import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int counter = 0;

    public void increment() {
        lock.lock(); // 获取锁
        try {
            counter++;
        } finally {
            lock.unlock(); // 释放锁
        }
    }
}

4.2 ReentrantLock的底层原理

  ReentrantLock 基于 AQS(Abstract Queued Synchronizer) 框架实现。AQS 是Java并发包的核心,提供了基于队列的同步器,用于管理线程的竞争和等待

4.2.1 AQS的核心思想

AQS维护以下核心组件:

  1. 状态变量(state):一个 int 变量,表示锁的状态(0表示空闲,1表示被占用)。
  2. 线程队列(CLH队列):一个双向链表,存储等待锁的线程。
  3. CAS操作:通过原子操作(Compare-And-Swap)更新 state,确保线程安全。

用生活中的例子解释:

  • 想象一个售票窗口(共享资源),只有一个售票员(锁)。
  • 顾客(线程)排队买票,窗口有个计数器(state):
    • state = 0:窗口空闲,第一个顾客直接买票,state 设为 1。
    • state = 1:窗口忙,其他顾客排队(加入CLH队列)。
  • 售票员忙完后(释放锁),通知队列里的下一个顾客(唤醒线程)。
4.2.2 ReentrantLock的工作流程
  1. 获取锁(lock())
    • 检查 state 是否为 0。
    • 如果是 0,用CAS将 state 设为 1,当前线程持有锁。
    • 如果 state 非 0,检查是否是自己已持有(支持重入),若是则 state++;否则加入等待队列。
  2. 释放锁(unlock())
    • 将 state 减 1。
    • 如果 state 变为 0,释放锁,唤醒队列中的下一个线程。

4.3 ReentrantLock的源码分析

以下是 ReentrantLock 的核心源码(简化版,基于JDK 8):

ReentrantLock类
public class ReentrantLock implements Lock {
    private final Sync sync;

    public ReentrantLock() {
        sync = new NonfairSync(); // 默认非公平锁
    }

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

    public void lock() {
        sync.lock();
    }

    public void unlock() {
        sync.release(1);
    }

    abstract static class Sync extends AbstractQueuedSynchronizer {
        abstract void lock();
    }

    static final class NonfairSync extends Sync {
        void lock() {
            if (compareAndSetState(0, 1)) // CAS尝试获取锁
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1); // 失败则进入AQS队列
        }
    }

    static final class FairSync extends Sync {
        void lock() {
            acquire(1); // 公平锁直接进入AQS逻辑
        }
    }
}

AQS核心方法

ReentrantLock 的锁逻辑依赖 AQS 的 acquire() 和 release() 方法:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {
    private volatile int state; // 锁状态
    private transient volatile Node head; // 等待队列头
    private transient volatile Node tail; // 等待队列尾

    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h); // 唤醒下一个线程
            return true;
        }
        return false;
    }
}
非公平锁 vs 公平锁
  • 非公平锁(NonfairSync)
    • 新线程会先尝试用CAS抢锁,可能插队,效率高但可能导致某些线程“饿死”。
    • 源码中的 compareAndSetState(0, 1) 体现了这一点。
  • 公平锁(FairSync)
    • 严格按照队列顺序获取锁,避免插队,保证公平性,但效率稍低。
    • 源码中的 tryAcquire 会检查队列是否有等待线程。
关键点解释
  1. state:表示锁的占用状态。state = 0 表示空闲,state > 0 表示被占用,支持重入(state 累加)。
  2. CLH队列:基于节点(Node)的双向链表,每个节点代表一个等待线程。
  3. CAS:通过 Unsafe 类的 compareAndSetState 实现原子操作。
  4. park/unpark:基于 LockSupport 的方法,阻塞或唤醒线程。

4.4 ReentrantLock的优点

相比 synchronized,ReentrantLock 提供了更多功能:

  1. 公平性:支持公平锁和非公平锁。
  2. 可中断:lockInterruptibly() 允许线程在等待锁时被中断。
  3. 超时:tryLock(long timeout, TimeUnit unit) 支持超时获取锁。
  4. 条件变量:通过 Condition 支持更灵活的线程等待/通知机制。

五、synchronized vs ReentrantLock

特性synchronizedReentrantLock
实现方式JVM内置,基于MonitorJava类库,基于AQS
性能JDK 6后优化,性能接近ReentrantLock灵活,可优化为公平/非公平锁
灵活性简单,只能锁方法或代码块支持中断、超时、条件变量等
可重入性支持支持
公平性非公平可选公平/非公平
使用场景简单场景,代码简洁复杂场景,需要高级功能

六、底层操作系统支持

        无论是 synchronized 还是 ReentrantLock,最终都依赖操作系统的互斥锁(Mutex)条件变量。JVM通过以下方式与操作系统交互:

  1. Monitor(synchronized):调用操作系统的 pthread_mutex(Linux)或类似机制。
  2. AQS(ReentrantLock):通过 LockSupport.park/unpark,底层调用操作系统的线程调度原语(如 futex 在Linux上)。

七、总结

7.1 完整流程

  1. 问题背景:多线程并发访问共享资源可能导致数据不一致,需要锁来保证线程安全。
  2. 锁的实现
    • synchronized :基于JVM的Monitor,对象头管理锁状态,字节码用 monitorenter/monitorexit。
    • ReentrantLock:基于AQS,state 和 CLH队列管理锁状态,CAS和 park/unpark 实现高效同步。
  3. 底层支持:JVM调用操作系统的互斥锁和线程调度机制。
  4. 优化:偏向锁、轻量级锁(synchronized),公平/非公平锁(ReentrantLock)。

7.2 通俗比喻

  • 锁就像一个“排队规则”,确保多线程有序访问共享资源。
  • synchronized 是简单粗暴的门锁,适合简单场景。
  • ReentrantLock 是智能门锁,功能丰富,适合复杂场景。
  • 底层靠JVM和操作系统协作,核心是管理“谁能进,谁得等”。

八、扩展阅读

  1. 源码推荐
    • HotSpot JVM:objectMonitor.cpp(synchronized实现)。
    • JDK源码:ReentrantLock.java 和 AbstractQueuedSynchronizer.java。
  2. 工具:用 jstack 查看线程状态,分析锁竞争。
  3. 书籍:《Java并发编程实战》(深入学习并发机制)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值