第1章:线程与锁(Threads and Locks)
一、导读
1.1 本章学习目标
第1章介绍最经典的并发模型——线程与锁。这是 Java、C++、Python 等语言中最常见的并发方式,也是最容易出错的。
本章的核心学习目标包括:
- 理解线程的基本概念:创建、同步、通信
- 掌握锁的使用:互斥锁、读写锁、条件变量
- 理解死锁的产生条件和预防方法
- 认识线程与锁模型的固有缺陷
1.2 为什么本章如此重要
线程与锁是并发编程的"原始模型"。理解它的优势和局限,才能理解为什么后续章节要介绍其他并发模型。大多数并发 bug——数据竞争、死锁、活锁、饥饿——都源于对锁的不当使用。
1.3 核心问题引导
为什么线程与锁模型如此难以正确使用?
死锁的四个必要条件是什么?如何预防?
锁的粒度如何影响性能和正确性?
为什么有人说"线程与锁是并发的汇编语言"?
二、核心概念详解
2.1 线程基础
java
// Java 线程
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
// 创建线程
Thread t = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t.start();
t.join();2.2 锁与同步
java
// synchronized 关键字
public synchronized void transfer(Account from, Account to, int amount) {
from.withdraw(amount);
to.deposit(amount);
}
// ReentrantLock
private final Lock lock = new ReentrantLock();
public void criticalSection() {
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}
}
// 条件变量
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();2.3 死锁
java
// 经典死锁场景
class Account {
private int balance;
private final Lock lock = new ReentrantLock();
// 危险!不同顺序获取锁导致死锁
public void transfer(Account other, int amount) {
lock.lock();
try {
other.lock.lock(); // 可能死锁!
try {
this.balance -= amount;
other.balance += amount;
} finally {
other.lock.unlock();
}
} finally {
lock.unlock();
}
}
}
// 解决方案:锁排序
public void transfer(Account other, int amount) {
Account first = this.id < other.id ? this : other;
Account second = this.id < other.id ? other : this;
first.lock.lock();
try {
second.lock.lock();
try {
this.balance -= amount;
other.balance += amount;
} finally {
second.lock.unlock();
}
} finally {
first.lock.unlock();
}
}2.4 线程安全集合
java
// ConcurrentHashMap
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
map.computeIfAbsent("key", k -> 0);
// BlockingQueue
BlockingQueue<Task> queue = new LinkedBlockingQueue<>();
// 生产者
queue.put(new Task("task1"));
// 消费者
Task task = queue.take(); // 阻塞等待三、实战案例
3.1 案例一:生产者-消费者模式
java
class ProducerConsumer {
private final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
class Producer implements Runnable {
public void run() {
try {
for (int i = 0; i < 100; i++) {
queue.put(i);
Thread.sleep(10);
}
queue.put(-1); // 结束信号
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
class Consumer implements Runnable {
public void run() {
try {
while (true) {
int item = queue.take();
if (item == -1) break;
System.out.println("Consumed: " + item);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}3.2 案例二:读写锁优化
java
class ReadWriteCache {
private final Map<String, String> cache = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public String get(String key) {
rwLock.readLock().lock();
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void put(String key, String value) {
rwLock.writeLock().lock();
try {
cache.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}四、本章小结
线程与锁是最基础的并发模型,但容易出错。
死锁的四个必要条件:互斥、持有并等待、不可抢占、循环等待。
锁的粒度影响性能和正确性——太粗影响并发度,太细增加开销。
线程与锁模型被称为"并发的汇编语言"——强大但难以直接使用。
五、思考与练习
为什么线程与锁模型如此难以正确使用?根本原因是什么?
如何检测和预防死锁?有哪些工具和方法?
读写锁在什么场景下比互斥锁更合适?