线程安全笔记
1. 基本概念
-
线程安全:在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。
-
线程不安全:在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。
2. 线程不安全示例
银行账户转账问题
public class UnsafeBankAccount {
private int balance = 1000; // 初始余额1000元
// 线程不安全的转账方法
public void transfer(int amount) {
// 步骤1: 读取当前余额
int currentBalance = this.balance;
// 模拟一些处理时间(比如网络延迟、计算等)
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 步骤2: 计算新余额
int newBalance = currentBalance - amount;
// 步骤3: 写入新余额
this.balance = newBalance;
System.out.println(Thread.currentThread().getName() +
" 转账 " + amount + "元,余额: " + this.balance);
}
public int getBalance() {
return balance;
}
}
测试代码
public class ThreadSafetyDemo {
public static void main(String[] args) throws InterruptedException {
UnsafeBankAccount account = new UnsafeBankAccount();
// 创建多个线程同时转账
Thread thread1 = new Thread(() -> account.transfer(100), "线程1");
Thread thread2 = new Thread(() -> account.transfer(200), "线程2");
Thread thread3 = new Thread(() -> account.transfer(150), "线程3");
// 同时启动三个线程
thread1.start();
thread2.start();
thread3.start();
// 等待所有线程完成
thread1.join();
thread2.join();
thread3.join();
System.out.println("最终余额: " + account.getBalance());
System.out.println("预期余额: " + (1000 - 100 - 200 - 150) + " = 550");
}
}
问题分析
可能的错误输出:
线程1 转账 100元,余额: 900
线程2 转账 200元,余额: 800
线程3 转账 150元,余额: 850 // 错误!应该是650
最终余额: 850
预期余额: 550
原因:
- 线程1读取余额1000,准备减去100
- 线程2也读取余额1000(还没被线程1更新),准备减去200
- 线程3也读取余额1000,准备减去150
- 三个线程基于相同的初始值1000进行计算,导致数据丢失
3. 线程安全解决方案
方案1:synchronized 关键字
public class SafeBankAccount {
private int balance = 1000;
// 线程安全的转账方法
public synchronized void transfer(int amount) {
int currentBalance = this.balance;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
int newBalance = currentBalance - amount;
this.balance = newBalance;
System.out.println(Thread.currentThread().getName() +
" 转账 " + amount + "元,余额: " + this.balance);
}
public synchronized int getBalance() {
return balance;
}
}
特点:
- 简单易用
- 自动获取和释放锁
- 性能相对较低
方案2:ReentrantLock
import java.util.concurrent.locks.ReentrantLock;
public class LockBankAccount {
private int balance = 1000;
private final ReentrantLock lock = new ReentrantLock();
public void transfer(int amount) {
lock.lock(); // 获取锁
try {
int currentBalance = this.balance;
Thread.sleep(1);
int newBalance = currentBalance - amount;
this.balance = newBalance;
System.out.println(Thread.currentThread().getName() +
" 转账 " + amount + "元,余额: " + this.balance);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock(); // 释放锁
}
}
public int getBalance() {
lock.lock();
try {
return balance;
} finally {
lock.unlock();
}
}
}
特点:
- 更灵活的锁控制
- 支持尝试获取锁、超时获取锁
- 支持公平锁和非公平锁
- 需要手动释放锁
方案3:AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicBankAccount {
private AtomicInteger balance = new AtomicInteger(1000);
public void transfer(int amount) {
// 原子操作:读取-修改-写入
int newBalance = balance.addAndGet(-amount);
System.out.println(Thread.currentThread().getName() +
" 转账 " + amount + "元,余额: " + newBalance);
}
public int getBalance() {
return balance.get();
}
}
特点:
- 无锁编程,性能最好
- 基于 CAS(Compare-And-Swap)操作
- 适用于简单的数值操作
4. 其他常见线程安全问题
计数器问题
// 线程不安全
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 这不是原子操作!实际是:读取->加1->写入
}
}
// 线程安全
public class SafeCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子操作
}
}
集合类的线程安全问题
// 线程不安全
List<String> unsafeList = new ArrayList<>();
// 线程安全的替代方案
List<String> safeList1 = Collections.synchronizedList(new ArrayList<>());
List<String> safeList2 = new CopyOnWriteArrayList<>();
List<String> safeList3 = new Vector<>(); // 虽然安全但性能较差
5. 线程安全问题的根本原因
- 多个线程同时访问共享数据
- 操作不是原子性的(读取-修改-写入被分割)
- 缺乏同步机制
6. 解决方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| synchronized | 简单易用,自动管理 | 性能较低,不够灵活 | 简单的同步需求 |
| ReentrantLock | 灵活,功能丰富 | 需要手动管理,容易出错 | 复杂的同步需求 |
| AtomicInteger | 性能最好,无锁 | 只适用于简单操作 | 简单的数值操作 |
| volatile | 轻量级,保证可见性 | 不保证原子性 | 简单的状态标记 |
7. 最佳实践
- 优先使用线程安全的类:如
ConcurrentHashMap、AtomicInteger等 - 最小化同步范围:只对必要的代码块进行同步
- 避免嵌套锁:防止死锁
- 使用不可变对象:天然线程安全
- 合理选择同步机制:根据具体场景选择最适合的方案
8. 注意事项
synchronized方法会锁定整个对象- 使用
ReentrantLock时必须在finally块中释放锁 volatile只保证可见性,不保证原子性- 过度同步会影响性能,要在安全性和性能之间找到平衡