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)。我想公平地按排队顺序获得锁,而不是随机抢(公平锁)。
我想同时多个线程读,只有写时才互斥(读写锁)。
于是有了 ReentrantLock、ReadWriteLock、StampedLock 等。
四、举例说明不同锁解决的不同问题
场景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;这种乐观读在无竞争时完全无锁,性能极高。
五、总结:多线程是“病因”,锁是“药方”
一句话:多线程带来了性能提升的可能,但也带来了数据错乱的风险;锁就是用来在“安全”和“性能”之间做权衡的工具。不同的锁对应不同的使用场景——没有万能的锁,只有合适的锁。