Java并发与面试-每日必看(13)

前言

Java不秃,面试不慌!
欢迎来到这片 Java修炼场!这里没有枯燥的教科书,只有每日一更的 硬核知识+幽默吐槽,让你在欢笑中掌握 Java基础、算法、面试套路,摆脱“写代码如写诗、看代码如看天书”的困境。


什么是锁?用通俗易懂的方式解释

锁(Lock)就像是一把 “门锁”,控制多个线程(或者多个任务)访问 同一个资源,防止它们互相踩踏,导致数据混乱。

想象一下,你和朋友们一起去 共享单车停车点,但是 只有一辆单车

  • 如果没有锁,大家可能会 同时抢车,结果要么 谁也骑不了(车被抬翻),要么 两个人一起骑(灾难现场)。
  • 但如果有锁,一个人上车之前先上锁,骑完之后再解锁,其他人才有机会骑。

在计算机里,多个线程可能同时想修改 同一个变量、同一个文件、同一个数据库,如果没有锁,它们可能会 互相干扰,导致数据错误,比如:、

class BankAccount {
    private int balance = 100;

    public void withdraw(int amount) {
        if (balance >= amount) {  // 检查余额
            balance -= amount;     // 扣钱
        }
    }
}

假设 两个线程 同时调用 withdraw(50)

  1. 线程 A 检查余额:发现余额是 100(可以取款)。
  2. 线程 B 也检查余额:发现余额也是 100(可以取款)。
  3. 线程 A 扣款后,余额变成 50
  4. 线程 B 也扣款后,余额变成 50(本应该是 0,结果数据错误了!)。

为了解决这个问题,我们可以 加锁,确保 同一时刻只能有一个线程操作账户

class BankAccount {
    private int balance = 100;
    private final ReentrantLock lock = new ReentrantLock();

    public void withdraw(int amount) {
        lock.lock();  // 🔒加锁
        try {
            if (balance >= amount) {
                balance -= amount;
            }
        } finally {
            lock.unlock();  // 🔓解锁
        }
    }
}

常见的锁类型

  1. 互斥锁(Mutex):一把锁 同时只能被一个线程持有,别人只能等它释放,比如 synchronizedReentrantLock
  2. 读写锁(ReadWriteLock):允许多个线程同时 读取,但只允许 一个线程写入,比如 ReentrantReadWriteLock
  3. 乐观锁:不加锁,假设不会冲突,更新数据时 检查是否被改动,失败则重试,比如 CAS(Compare And Swap) 机制。
  4. 悲观锁:假设会有冲突,每次操作前都先加锁,确保安全。

锁的底层实现:它是怎么工作的?

锁(Lock)并不是凭空出现的,它的实现依赖 硬件支持软件机制 结合,主要通过 CPU 指令操作系统的调度 以及 JVM 内部机制 来保证线程的安全访问。

我们可以从硬件层、操作系统层和 Java 层来看看锁是怎么实现的。

1. 硬件层:CPU 指令级别的保证

在现代计算机中,多个线程是由 CPU 的多个核心或多个线程并发执行的,但 CPU 需要保证某些关键操作的 原子性(要么全部执行,要么完全不执行)。

🔹 CAS(Compare-And-Swap,比较并交换)

CAS 是一种 乐观锁 机制,它的原理是:

  • 读取一个变量的 当前值
  • 判断这个值是否仍然是预期值(没有被别的线程改动)。
  • 如果是,就修改它;如果不是,说明被别人动过了,操作失败,可能需要重试。

💡 CPU 提供了一些原子操作指令(如 cmpxchg),用于实现 CAS:

AtomicInteger count = new AtomicInteger(0);
count.compareAndSet(0, 1); // 只有当 count 是 0 时,才会修改成 1

锁底层就是调用 CPU 的 CAS 指令,确保修改是原子的,避免加锁带来的性能开销。


2. 操作系统层:锁的调度和同步

操作系统提供了一些 底层同步机制,让线程能够安全地访问共享资源:

🔹 自旋锁(SpinLock)

  • 自旋锁是一种 轻量级锁,当一个线程发现锁被占用时,它 不会马上阻塞,而是 短时间内不断循环尝试获取锁,等锁释放后立刻执行。
  • 适用于 锁竞争不严重锁持有时间短 的情况,避免了线程切换的开销。
class SpinLock {
    private AtomicBoolean lock = new AtomicBoolean(false);

    public void lock() {
        while (!lock.compareAndSet(false, true)) {
            // 自旋等待
        }
    }

    public void unlock() {
        lock.set(false);
    }
}

💡 缺点:如果锁竞争激烈,自旋会导致 CPU 资源浪费,影响性能。

🔹 互斥锁(Mutex,基于内核的锁)

如果线程长时间无法获取锁,操作系统会让线程进入等待状态,这样就不会占用 CPU 资源。
常见的操作系统锁机制包括:

  • futex(Fast User-space Mutex):Linux 下的快速用户态互斥锁,减少内核态切换。
  • 信号量(Semaphore):控制多个线程的访问。
  • 条件变量(Condition Variable):用于线程间的等待和唤醒。

当 Java 代码使用 synchronizedReentrantLock 时,底层可能会使用这些 系统级锁,尤其是当线程需要被挂起时。


3. Java 层:JVM 里的锁

Java 提供了多种锁机制,底层主要由 synchronizedLock(ReentrantLock) 来实现。

🔹 synchronized:JVM 内置锁

synchronized 是 Java 提供的最基本的锁,它的实现依赖于 JVM 内部的对象头,可以有 不同的锁优化级别

  1. 无锁状态:对象刚创建,没有竞争。
  2. 偏向锁:如果一个线程多次获取同一把锁,JVM 偏向它,让它以后获取锁时 不再需要 CAS 操作(提升性能)。
  3. 轻量级锁:多个线程尝试获取锁,但没有强烈竞争时,使用 CAS 让一个线程先获得锁。
  4. 重量级锁:如果锁竞争严重,JVM 会升级为 重量级锁,使用 操作系统的 Mutex 机制,让线程 进入等待状态,避免 CPU 资源浪费。

🔹 synchronized 的底层实现 synchronized 依赖于 对象头的 MarkWord,当 JVM 发现竞争加剧时,会升级锁的状态:

synchronized (obj) {
    // 临界区代码
}

JVM 在编译后,会转换成类似的底层指令:

monitorenter  // 进入同步块,加锁
monitorexit   // 退出同步块,解锁

monitorenter 执行时:

  • JVM 先检查 当前对象的锁状态(MarkWord)。
  • 如果锁是 偏向锁,直接获取。
  • 如果锁是 轻量级锁,CAS 竞争。
  • 如果锁竞争严重,升级为 重量级锁,进入操作系统 Mutex 机制。

🔹 ReentrantLock:显式锁

Java 提供了 ReentrantLock 作为 synchronized 的替代,它的底层实现是 AQS(AbstractQueuedSynchronizer)

🔹 AQS 的核心

  • AQS 维护了一个 state 变量 来表示锁的状态。
  • 线程尝试用 CAS 修改 state,如果成功,就获得锁。
  • 失败的线程会被放入 FIFO 队列,等待唤醒。
class ReentrantLockDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void doWork() {
        lock.lock();  // 获取锁
        try {
            System.out.println("线程 " + Thread.currentThread().getName() + " 执行任务");
        } finally {
            lock.unlock();  // 释放锁
        }
    }
}

ReentrantLock vs. synchronized

特性synchronizedReentrantLock
底层实现JVM 内置(基于对象头)AQS(基于 CAS 和队列)
可重入✅ 是✅ 是
公平锁支持❌ 不能设置✅ 可以设为公平锁
尝试获取锁❌ 不能tryLock() 非阻塞获取
超时获取锁❌ 不能tryLock(time) 支持超时
中断支持❌ 不能被中断✅ 可被 lockInterruptibly() 中断

总结

锁的底层实现涉及多个层次:

  1. CPU 层
    • 通过 CAS 进行原子操作,确保多线程安全。
  2. 操作系统层
    • 依靠 自旋锁(轻量级锁)、互斥锁(Mutex,重量级锁)来调度线程。
  3. JVM 层
    • synchronized 通过 对象头的 MarkWord 进行锁优化(偏向锁、轻量级锁、重量级锁)。
    • ReentrantLock 依赖 AQS(AbstractQueuedSynchronizer),使用 CAS + 队列机制。

所以,锁并不是“凭空”存在的,而是 CPU、操作系统、JVM 三者配合的结果! 🚀、


📌 一、哪些场景需要加锁?

一般来说,只要有多个线程同时访问共享资源,并且可能导致数据错误,就需要加锁。常见的场景有:

1️⃣ 共享变量的并发修改

多个线程同时修改一个 共享变量,如果没有加锁,可能会发生数据不一致问题:

class Counter {
    private int count = 0;

    public void increment() {
        count++;  // 这里没有加锁,可能导致并发问题
    }
}

多个线程执行 increment() 时,可能出现 count < 期望值 的情况。

 ✅ 正确做法

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

    public void increment() {
        lock.lock();
        try {
            count++; // 线程安全
        } finally {
            lock.unlock();
        }
    }
}

这样可以保证 count++ 操作是 原子的,不会被多个线程同时修改。

2️⃣ 银行转账等事务操作

假设有两个线程:

  • 线程 A:从账户 1 转 100 元到账户 2
  • 线程 B:从账户 2 转 200 元到账户 1

如果没有加锁,可能会导致 数据不一致,甚至钱凭空消失凭空增加

正确做法

class BankAccount {
    private int balance;
    private final ReentrantLock lock = new ReentrantLock();

    public void transfer(BankAccount target, int amount) {
        lock.lock();
        try {
            if (balance >= amount) {
                balance -= amount;
                target.deposit(amount);
            }
        } finally {
            lock.unlock();
        }
    }

    public void deposit(int amount) {
        lock.lock();
        try {
            balance += amount;
        } finally {
            lock.unlock();
        }
    }
}

3️⃣ 生产者-消费者模型(多线程队列)

多线程队列(如消息队列、任务队列)中,多个线程可能同时向队列添加任务或从队列取任务,如果没有加锁,可能会导致:

  • 任务丢失
  • 任务重复消费
  • 队列数据损坏

正确做法

class TaskQueue {
    private Queue<String> queue = new LinkedList<>();
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();

    public void addTask(String task) {
        lock.lock();
        try {
            queue.offer(task);
            notEmpty.signal(); // 通知等待的消费者
        } finally {
            lock.unlock();
        }
    }

    public String getTask() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await(); // 没任务就等待
            }
            return queue.poll();
        } finally {
            lock.unlock();
        }
    }
}

这样可以确保:

  • 生产者添加任务时不会被多个线程同时修改队列
  • 消费者在任务队列为空时等待,而不是空轮询浪费 CPU

4️⃣ 单例模式

在多线程环境下,单例模式 可能会被多个线程同时创建多个实例。比如:

class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) { // 线程不安全
            instance = new Singleton();
        }
        return instance;
    }
}

如果两个线程同时执行 getInstance(),可能会创建 两个不同的实例,导致 单例模式失效

正确做法(双重检查 + volatile

class Singleton {
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

📌 二、加锁后,其他线程去哪了?

当一个线程获取了锁,其他线程如果没有获得锁,会进入以下几种状态

1️⃣ 自旋等待(自旋锁)

  • 如果使用的是 自旋锁(如 CAS 机制),线程会不断尝试获取锁,而不进入休眠状态。
  • 适用于 锁持有时间短 的情况,否则会浪费 CPU 资源。
class SpinLock {
    private AtomicBoolean lock = new AtomicBoolean(false);

    public void lock() {
        while (!lock.compareAndSet(false, true)) {
            // 自旋等待
        }
    }

    public void unlock() {
        lock.set(false);
    }
}

2️⃣ 阻塞等待(线程进入等待队列)

如果是普通锁(如 ReentrantLock,没有获得锁的线程 不会一直占用 CPU,而是进入阻塞状态

private final ReentrantLock lock = new ReentrantLock();

public void doWork() {
    lock.lock();
    try {
        // 临界区代码
    } finally {
        lock.unlock();
    }
}

没有获得锁的线程会被挂起,等锁释放后再恢复执行

3️⃣ 进入 wait() / await() 队列

如果线程在 synchronized 代码块中调用 wait(),它会进入 等待队列,等待 notify() 重新唤醒:

synchronized (lock) {
    while (!conditionMet) {
        lock.wait();  // 进入等待状态
    }
}

三、总结

场景是否需要加锁?加锁后其他线程的状态
共享变量修改✅ 需要其他线程 等待锁释放
银行转账✅ 需要其他线程 进入阻塞状态
生产者-消费者✅ 需要生产者/消费者 等待队列
单例模式✅ 需要只有第一次创建实例时需要加锁
计算密集型操作❌ 不需要加锁会影响性能

🎉 最后的话:锁住知识,解锁成长! 🚀

看到这里,你已经掌握了 锁的概念、底层实现、应用场景以及线程等待机制,恭喜你迈向了并发编程的大门!💪

如果你觉得这篇文章对你有所帮助,别忘了:

点赞 ❤️ 让我知道你喜欢这类内容!
关注 ⭐ 解锁更多并发编程、Java高并发、JVM底层原理等硬核知识!
分享 🔄 让更多人受益,一起提升技术!

感谢阅读,期待下次见!🚀🚀🚀

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Starry-Walker

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值