线程安全 vs 线程不安全

141 阅读4分钟

线程安全笔记

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. 线程1读取余额1000,准备减去100
  2. 线程2也读取余额1000(还没被线程1更新),准备减去200
  3. 线程3也读取余额1000,准备减去150
  4. 三个线程基于相同的初始值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. 线程安全问题的根本原因

  1. 多个线程同时访问共享数据
  2. 操作不是原子性的(读取-修改-写入被分割)
  3. 缺乏同步机制

6. 解决方案对比

方案优点缺点适用场景
synchronized简单易用,自动管理性能较低,不够灵活简单的同步需求
ReentrantLock灵活,功能丰富需要手动管理,容易出错复杂的同步需求
AtomicInteger性能最好,无锁只适用于简单操作简单的数值操作
volatile轻量级,保证可见性不保证原子性简单的状态标记

7. 最佳实践

  1. 优先使用线程安全的类:如 ConcurrentHashMapAtomicInteger
  2. 最小化同步范围:只对必要的代码块进行同步
  3. 避免嵌套锁:防止死锁
  4. 使用不可变对象:天然线程安全
  5. 合理选择同步机制:根据具体场景选择最适合的方案

8. 注意事项

  • synchronized 方法会锁定整个对象
  • 使用 ReentrantLock 时必须在 finally 块中释放锁
  • volatile 只保证可见性,不保证原子性
  • 过度同步会影响性能,要在安全性和性能之间找到平衡