杨斌
发布于 2026-04-15 / 9 阅读
0
0

Java多线程中的锁

Java 中之所以有这么多锁,根本原因就是多线程
当多个线程同时访问共享数据时,会产生三个经典问题——原子性、可见性、有序性。锁就是为了解决这些问题而诞生的工具。

一、没有锁会怎样?—— 多线程捣乱现场

下面用生活中的例子 + 代码来分别说明为什么需要锁,以及不同的锁解决什么问题。

假设你和其他9个朋友们共同有一个银行账户,余额 100 元。
你们俩同时去 ATM 取钱,每人取 10 元。正常逻辑:取完后余额应为 0 元。
但由于多线程并发,可能出现下面情况:

package com.thinkdifferent.aipicturebackend;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class NoLockDemo {
    static int balance = 100;

    public static void withdraw(int amount) {
        if (balance >= amount) {
            // 模拟取钱过程中的耗时操作
            try { Thread.sleep(10); } catch (InterruptedException e) {}
            balance = balance - amount;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                5,                               // corePoolSize
                10,                               // maximumPoolSize
                60L,                             // keepAliveTime
                TimeUnit.SECONDS,                // 时间单位
                new ArrayBlockingQueue<>(10),    // 有界队列,避免内存爆炸
                Executors.defaultThreadFactory(), // 线程工厂
                new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );
        for (int i = 1; i <= 10; i++) {
            int taskid=i;
            executor.execute(() -> {
                withdraw(10);
                System.out.println(Thread.currentThread().getName() + " 进行取钱,余额为: "+balance+"任务id: "+taskid);
            });
        }
        executor.shutdown();
        Thread.sleep(1000);
        System.out.println("最终余额:" + balance); 
    }
}

结果:最终余额可能不是 0。
原因:线程同时读到 balance=100,都判断 >=10 成立,然后各自减 10,后执行的线程覆盖了前一个的结果,导致只减了一次。

这就是原子性被破坏:balance - amount 不是一步完成的,它分“读-改-写”三步,线程在中间被打断。

二、锁的作用:把“三步”变成“一步”

1. synchronized —— 简单粗暴的锁

给取钱方法加上 synchronized,保证同一时刻只有一个线程能执行该方法。

java

public static synchronized void withdraw(int amount) {
    if (balance >= amount) {
        balance = balance - amount;
    }
}

加上后,余额永远正确为 0。
作用:保证了原子性、可见性、有序性。是最基础的锁。


三、为什么还要其他锁?—— 因为需求不同

synchronized 虽然好用,但不够灵活。比如:

  • 我想尝试加锁,如果锁被别人占着,我不等,直接干别的事(tryLock)。

  • 我想中断正在等锁的线程(lockInterruptibly)。

  • 我想公平地按排队顺序获得锁,而不是随机抢(公平锁)。

  • 我想同时多个线程读,只有写时才互斥(读写锁)。

于是有了 ReentrantLockReadWriteLockStampedLock 等。


四、举例说明不同锁解决的不同问题

场景1:读多写少 —— ReadWriteLock

一个在线文档,很多人同时阅读(读操作),但偶尔有人修改(写操作)。
读操作之间不互斥,写操作与任何读写互斥。

java

public class Document {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private String content = "Hello";

    public String read() {
        rwLock.readLock().lock();
        try { return content; }
        finally { rwLock.readLock().unlock(); }
    }

    public void write(String newContent) {
        rwLock.writeLock().lock();
        try { content = newContent; }
        finally { rwLock.writeLock().unlock(); }
    }
}

如果用 synchronized,所有读操作也得排队,性能很差。读写锁提升了并发度。


场景2:需要尝试锁 —— ReentrantLock

你的程序要发送一条重要消息,如果网络锁被占用,不要阻塞,而是记录日志并放弃。

java

ReentrantLock lock = new ReentrantLock();
if (lock.tryLock()) {
    try { sendMessage(); }
    finally { lock.unlock(); }
} else {
    log("网络正忙,稍后重试");
}

synchronized 做不到这种“尝试”行为。


场景3:极致的读性能 —— StampedLock 乐观读

一个股票行情系统,价格变化非常频繁,但读操作极多(成千上万次/秒)。
我们希望读的时候几乎不加锁,只在检测到有写操作时才重读。

java

StampedLock sl = new StampedLock();
long stamp = sl.tryOptimisticRead();
int price = this.price;
if (!sl.validate(stamp)) { // 写操作发生过
    stamp = sl.readLock();
    try { price = this.price; }
    finally { sl.unlockRead(stamp); }
}
return price;

这种乐观读在无竞争时完全无锁,性能极高。


五、总结:多线程是“病因”,锁是“药方”

并发问题

没有锁的后果

锁的解决方案

原子性

i++ 丢失更新

synchronized, ReentrantLock, Atomic*

可见性

线程看不到最新值

synchronized, volatile, Lock

有序性

指令重排导致奇怪结果

synchronized, volatile, Lock

读多写少性能差

所有操作串行

ReadWriteLock, StampedLock

需要灵活加锁

无法尝试、中断

ReentrantLock

一句话:多线程带来了性能提升的可能,但也带来了数据错乱的风险;锁就是用来在“安全”和“性能”之间做权衡的工具。不同的锁对应不同的使用场景——没有万能的锁,只有合适的锁。


评论