Java不秃,面试不慌!
欢迎来到这片 Java修炼场!这里没有枯燥的教科书,只有每日一更的 硬核知识+幽默吐槽,让你在欢笑中掌握 Java基础、算法、面试套路,摆脱“写代码如写诗、看代码如看天书”的困境。
锁(Lock)就像是一把 “门锁”,控制多个线程(或者多个任务)访问 同一个资源,防止它们互相踩踏,导致数据混乱。
想象一下,你和朋友们一起去 共享单车停车点,但是 只有一辆单车:
在计算机里,多个线程可能同时想修改 同一个变量、同一个文件、同一个数据库,如果没有锁,它们可能会 互相干扰,导致数据错误,比如:、
class BankAccount {
private int balance = 100;
public void withdraw(int amount) {
if (balance >= amount) { // 检查余额
balance -= amount; // 扣钱
}
}
}
假设 两个线程 同时调用 withdraw(50)
:
为了解决这个问题,我们可以 加锁,确保 同一时刻只能有一个线程操作账户:
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(); // 解锁
}
}
}
synchronized
或 ReentrantLock
。ReentrantReadWriteLock
。CAS(Compare And Swap)
机制。锁(Lock)并不是凭空出现的,它的实现依赖 硬件支持 和 软件机制 结合,主要通过 CPU 指令、操作系统的调度 以及 JVM 内部机制 来保证线程的安全访问。
我们可以从硬件层、操作系统层和 Java 层来看看锁是怎么实现的。
在现代计算机中,多个线程是由 CPU 的多个核心或多个线程并发执行的,但 CPU 需要保证某些关键操作的 原子性(要么全部执行,要么完全不执行)。
CAS 是一种 乐观锁 机制,它的原理是:
CPU 提供了一些原子操作指令(如 cmpxchg
),用于实现 CAS:
AtomicInteger count = new AtomicInteger(0);
count.compareAndSet(0, 1); // 只有当 count 是 0 时,才会修改成 1
锁底层就是调用 CPU 的 CAS 指令,确保修改是原子的,避免加锁带来的性能开销。
操作系统提供了一些 底层同步机制,让线程能够安全地访问共享资源:
class SpinLock {
private AtomicBoolean lock = new AtomicBoolean(false);
public void lock() {
while (!lock.compareAndSet(false, true)) {
// 自旋等待
}
}
public void unlock() {
lock.set(false);
}
}
缺点:如果锁竞争激烈,自旋会导致 CPU 资源浪费,影响性能。
如果线程长时间无法获取锁,操作系统会让线程进入等待状态,这样就不会占用 CPU 资源。
常见的操作系统锁机制包括:
futex
(Fast User-space Mutex):Linux 下的快速用户态互斥锁,减少内核态切换。当 Java 代码使用 synchronized
或 ReentrantLock
时,底层可能会使用这些 系统级锁,尤其是当线程需要被挂起时。
Java 提供了多种锁机制,底层主要由 synchronized
和 Lock
(ReentrantLock) 来实现。
synchronized
是 Java 提供的最基本的锁,它的实现依赖于 JVM 内部的对象头,可以有 不同的锁优化级别:
synchronized 的底层实现 synchronized
依赖于 对象头的 MarkWord,当 JVM 发现竞争加剧时,会升级锁的状态:
synchronized (obj) {
// 临界区代码
}
JVM 在编译后,会转换成类似的底层指令:
monitorenter // 进入同步块,加锁
monitorexit // 退出同步块,解锁
当 monitorenter
执行时:
Java 提供了 ReentrantLock
作为 synchronized
的替代,它的底层实现是 AQS(AbstractQueuedSynchronizer)。
AQS 的核心
state
变量 来表示锁的状态。class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void doWork() {
lock.lock(); // 获取锁
try {
System.out.println("线程 " + Thread.currentThread().getName() + " 执行任务");
} finally {
lock.unlock(); // 释放锁
}
}
}
特性 | synchronized |
ReentrantLock |
---|---|---|
底层实现 | JVM 内置(基于对象头) | AQS(基于 CAS 和队列) |
可重入 | ✅ 是 | ✅ 是 |
公平锁支持 | ❌ 不能设置 | ✅ 可以设为公平锁 |
尝试获取锁 | ❌ 不能 | ✅ tryLock() 非阻塞获取 |
超时获取锁 | ❌ 不能 | ✅ tryLock(time) 支持超时 |
中断支持 | ❌ 不能被中断 | ✅ 可被 lockInterruptibly() 中断 |
锁的底层实现涉及多个层次:
synchronized
通过 对象头的 MarkWord 进行锁优化(偏向锁、轻量级锁、重量级锁)。ReentrantLock
依赖 AQS(AbstractQueuedSynchronizer),使用 CAS + 队列机制。所以,锁并不是“凭空”存在的,而是 CPU、操作系统、JVM 三者配合的结果! 、
一般来说,只要有多个线程同时访问共享资源,并且可能导致数据错误,就需要加锁。常见的场景有:
多个线程同时修改一个 共享变量,如果没有加锁,可能会发生数据不一致问题:
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++
操作是 原子的,不会被多个线程同时修改。
假设有两个线程:
如果没有加锁,可能会导致 数据不一致,甚至钱凭空消失或凭空增加!
✅ 正确做法:
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();
}
}
}
在多线程队列(如消息队列、任务队列)中,多个线程可能同时向队列添加任务或从队列取任务,如果没有加锁,可能会导致:
✅ 正确做法:
class TaskQueue {
private Queue 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();
}
}
}
这样可以确保:
在多线程环境下,单例模式 可能会被多个线程同时创建多个实例。比如:
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;
}
}
当一个线程获取了锁,其他线程如果没有获得锁,会进入以下几种状态:
CAS
机制),线程会不断尝试获取锁,而不进入休眠状态。class SpinLock {
private AtomicBoolean lock = new AtomicBoolean(false);
public void lock() {
while (!lock.compareAndSet(false, true)) {
// 自旋等待
}
}
public void unlock() {
lock.set(false);
}
}
如果是普通锁(如 ReentrantLock
),没有获得锁的线程 不会一直占用 CPU,而是进入阻塞状态:
private final ReentrantLock lock = new ReentrantLock();
public void doWork() {
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
}
没有获得锁的线程会被挂起,等锁释放后再恢复执行。
wait()
/ await()
队列如果线程在 synchronized
代码块中调用 wait()
,它会进入 等待队列,等待 notify()
重新唤醒:
synchronized (lock) {
while (!conditionMet) {
lock.wait(); // 进入等待状态
}
}
场景 | 是否需要加锁? | 加锁后其他线程的状态 |
---|---|---|
共享变量修改 | ✅ 需要 | 其他线程 等待锁释放 |
银行转账 | ✅ 需要 | 其他线程 进入阻塞状态 |
生产者-消费者 | ✅ 需要 | 生产者/消费者 等待队列 |
单例模式 | ✅ 需要 | 只有第一次创建实例时需要加锁 |
计算密集型操作 | ❌ 不需要 | 加锁会影响性能 |
看到这里,你已经掌握了 锁的概念、底层实现、应用场景以及线程等待机制,恭喜你迈向了并发编程的大门!
如果你觉得这篇文章对你有所帮助,别忘了:
✅ 点赞 ❤️ 让我知道你喜欢这类内容!
✅ 关注 ⭐ 解锁更多并发编程、Java高并发、JVM底层原理等硬核知识!
✅ 分享 让更多人受益,一起提升技术!
感谢阅读,期待下次见!