一、线程安全问题的本质

在多线程环境下,当多个线程同时访问共享资源时,如果没有正确的同步机制,就可能导致数据不一致或程序行为异常。举个生活中的例子:就像多个收银员同时操作同一个收银台,如果不加控制,很容易算错账。

示例1:经典的银行转账问题(Java技术栈)

public class UnsafeBank {
    private static int balance = 1000; // 共享账户余额

    public static void withdraw(int amount) {
        if (balance >= amount) {
            try {
                Thread.sleep(10); // 模拟处理延迟
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance -= amount;
            System.out.println(Thread.currentThread().getName() + "取款" + amount + ",余额:" + balance);
        }
    }

    public static void main(String[] args) {
        // 两个线程同时取款
        new Thread(() -> withdraw(800)).start();
        new Thread(() -> withdraw(800)).start();
    }
}

输出可能结果:

Thread-0取款800,余额:200  
Thread-1取款800,余额:-600  // 余额异常!

问题分析:
由于balance的检查和修改不是原子操作,两个线程可能同时通过if判断,导致超额取款。


二、快速解决方案

1. 同步代码块(synchronized)

最直接的解决方案,通过锁机制保证同一时间只有一个线程执行关键代码。

示例2:使用synchronized修复银行问题

public class SafeBank {
    private static int balance = 1000;
    private static final Object lock = new Object(); // 专用锁对象

    public static void withdraw(int amount) {
        synchronized (lock) { // 加锁
            if (balance >= amount) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                balance -= amount;
                System.out.println(Thread.currentThread().getName() + "取款" + amount + ",余额:" + balance);
            }
        }
    }
}

关键点:

  • 锁对象建议使用专用Object而非this,避免与外部锁冲突
  • 锁粒度不宜过大,否则会降低并发性能

2. 原子类(AtomicInteger等)

适合简单变量的原子操作,底层基于CAS(Compare-And-Swap)实现。

示例3:使用AtomicInteger优化计数器

import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // 原子操作
    }

    public int getCount() {
        return count.get();
    }
}

优势:

  • 无锁竞争,性能高于synchronized
  • 适合高频读写的简单场景

三、进阶解决方案

1. ReentrantLock显式锁

synchronized更灵活,支持超时、公平锁等特性。

示例4:ReentrantLock实现限时等待

import java.util.concurrent.locks.ReentrantLock;

public class TimeoutLockDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void performTask() {
        try {
            if (lock.tryLock(1, TimeUnit.SECONDS)) { // 尝试获取锁,最多等待1秒
                try {
                    System.out.println("执行关键操作");
                    Thread.sleep(2000); // 模拟耗时操作
                } finally {
                    lock.unlock();
                }
            } else {
                System.out.println("获取锁超时,放弃操作");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

适用场景:

  • 需要尝试获取锁或中断响应的场景
  • 需要实现公平锁(按申请顺序获取锁)

2. ThreadLocal线程封闭

彻底避免共享,每个线程维护独立副本。

示例5:SimpleDateFormat的线程安全方案

public class DateUtil {
    private static final ThreadLocal<SimpleDateFormat> threadLocal = 
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

    public static String format(Date date) {
        return threadLocal.get().format(date); // 每个线程独立实例
    }
}

注意事项:

  • 使用后需调用threadLocal.remove()防止内存泄漏(尤其在线程池中)

四、实战经验与陷阱规避

  1. 死锁预防:按固定顺序获取多个锁,或使用tryLock超时机制
  2. 性能权衡
    • 读多写少场景用ReadWriteLock
    • 低竞争场景用原子类,高竞争场景用ConcurrentHashMap
  3. JUC工具类:优先使用ConcurrentHashMapCopyOnWriteArrayList等线程安全容器

终极建议:

  • 优先考虑无锁设计(如不可变对象)
  • 使用final字段避免意外修改
  • 通过Collections.unmodifiableXXX返回防御性副本