
1. 为什么我们需要关心线程安全我第一次在生产环境遇到线程安全问题是在一个电商促销系统里。凌晨三点被报警电话吵醒发现库存出现了超卖——明明设置了100件限量商品却卖出了120多件。查看日志发现多个线程同时通过了库存检查导致最终扣减数量出错。那次事故让我深刻理解了线程安全的重要性。线程安全问题的本质在于当多个线程同时访问和修改共享数据时如果没有适当的同步控制程序的行为将变得不可预测。这种不可预测性会导致数据损坏、逻辑错误甚至系统崩溃。在Java中几乎所有业务系统都会涉及多线程场景比如Web服务器处理并发请求每个请求都在独立线程中定时任务与主程序并行执行异步处理消息队列批量数据处理时的多线程加速提示即使你觉得自己在写单线程程序很多框架如Spring底层也是多线程环境。忽略线程安全就像在悬崖边开车不系安全带——可能暂时没事但一旦出事就是灾难性的。2. 线程不安全的具体表现与原理2.1 典型线程安全问题案例让我们通过一个简单的计数器例子来观察线程不安全的表现public class UnsafeCounter { private int count 0; public void increment() { count; } public int getCount() { return count; } }当多个线程同时调用increment()时最终结果往往会小于预期。这是因为count这个看似简单的操作实际上包含三个步骤从内存读取count当前值到寄存器将寄存器中的值加1将新值写回内存如果两个线程同时执行这些步骤可能会出现以下交错执行的情况时间线程1操作线程2操作count值t1读取count0-0t2-读取count00t3计算011-0t4-计算0110t5写入count1-1t6-写入count11最终两个线程都执行了加1操作但count只增加了1——这就是典型的竞态条件(Race Condition)。2.2 Java内存模型(JMM)的影响Java内存模型定义了线程如何与内存交互其中几个关键概念直接影响线程安全可见性问题一个线程对共享变量的修改另一个线程可能无法立即看到。这是因为现代CPU有多级缓存写操作可能暂时停留在CPU缓存中编译器会进行指令重排序优化原子性问题像count这样的操作不是原子性的可能被线程切换打断。有序性问题代码的编写顺序不一定是实际执行顺序编译器和处理器会进行指令重排。以下是一个可见性问题的例子public class VisibilityIssue { private boolean flag true; public void writer() { flag false; // 线程1执行 } public void reader() { while (flag) { // 线程2执行 // 可能永远循环即使线程1已经将flag设为false } } }3. synchronized关键字详解3.1 基本用法与原理synchronized是Java中最基本的同步机制它可以用于实例方法锁住当前对象实例public synchronized void method() { // 同步代码 }静态方法锁住当前类的Class对象public static synchronized void staticMethod() { // 同步代码 }代码块指定锁对象public void block() { synchronized(this) { // 也可以是其他对象 // 同步代码 } }synchronized的实现原理涉及Java对象头中的Mark Word和监视器锁(Monitor)。当线程进入同步块时尝试通过CAS操作获取锁成功则记录锁信息到对象头失败则进入阻塞队列等待3.2 锁升级优化过程现代JVM对synchronized进行了大量优化实现了锁升级机制无锁状态初始状态偏向锁第一个线程获取锁时记录线程ID到对象头轻量级锁当有竞争时转换为轻量级锁CAS自旋重量级锁自旋超过阈值后升级为操作系统层面的互斥锁这种设计使得在低竞争情况下同步开销非常小。3.3 使用注意事项锁对象选择错误做法使用字符串常量或基本类型包装类可能被JVM缓存推荐做法使用专门创建的Object实例锁粒度控制过粗降低并发性能过细增加死锁风险经验法则锁住最小必要共享数据但相关操作应放在同一个锁中避免嵌套锁容易导致死锁// 危险代码 synchronized(lockA) { synchronized(lockB) { // ... } }4. volatile关键字的适用场景4.1 volatile的作用volatile解决了可见性和有序性问题但不保证原子性。它有两个主要作用禁止指令重排序确保写操作前的所有操作都已完成强制内存可见性写操作立即刷新到主内存读操作直接从主内存读取典型使用场景是作为状态标志public class TaskRunner { private volatile boolean running true; public void stop() { running false; } public void run() { while (running) { // 执行任务 } } }4.2 volatile vs synchronized特性volatilesynchronized原子性不保证保证可见性保证保证有序性保证保证阻塞不阻塞可能阻塞适用场景单一变量的状态标志复杂操作或需要原子性注意volatile不能替代synchronized。常见的错误是认为volatile可以保证count的原子性——实际上它仍然需要同步。5. 线程安全的实践策略5.1 不可变对象最简单的线程安全策略是使用不可变对象public final class ImmutableValue { private final int value; public ImmutableValue(int value) { this.value value; } public int getValue() { return value; } }不可变对象的特点所有字段final类本身final没有setter方法如果包含可变对象引用需要防御性拷贝5.2 线程封闭将对象限制在单个线程中使用栈封闭局部变量每个线程有自己的栈ThreadLocal为每个线程保存独立副本public class ThreadLocalExample { private static ThreadLocalSimpleDateFormat dateFormat ThreadLocal.withInitial(() - new SimpleDateFormat(yyyy-MM-dd)); public String formatDate(Date date) { return dateFormat.get().format(date); } }5.3 并发集合Java提供了多种线程安全的集合类非线程安全线程安全替代特点ArrayListCopyOnWriteArrayList写时复制适合读多写少HashMapConcurrentHashMap分段锁高并发性能好HashSetConcurrentHashSet基于ConcurrentHashMapLinkedListConcurrentLinkedQueue无锁算法使用示例MapString, String map new ConcurrentHashMap(); map.put(key, value); // 线程安全5.4 原子变量类java.util.concurrent.atomic包提供了一系列原子操作类AtomicInteger counter new AtomicInteger(0); // 线程安全的递增 counter.incrementAndGet(); // CAS操作 boolean updated counter.compareAndSet(expect, update);这些类底层使用CAS(Compare-And-Swap)实现比锁有更好的性能。6. 常见陷阱与最佳实践6.1 隐藏的线程安全问题看似原子的操作if (map.containsKey(key)) { map.put(key, value); }不是原子的应该使用map.putIfAbsent()等原子方法逸出发布public class ThisEscape { public ThisEscape(EventSource source) { source.registerListener( new EventListener() { public void onEvent(Event e) { doSomething(e); // 此时ThisEscape可能还未构造完成 } }); } }双重检查锁定(DCL)问题// 错误的DCL实现 public class Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance null) { // 第一次检查 synchronized (Singleton.class) { if (instance null) { // 第二次检查 instance new Singleton(); // 问题出在这里 } } } return instance; } }正确的实现应该使用volatile或静态内部类方式。6.2 性能考量减少锁竞争缩小同步块范围使用读写锁(ReentrantReadWriteLock)分离读/写考虑无锁算法避免过早优化先保证正确性再优化性能使用性能分析工具定位真正瓶颈上下文切换成本创建过多线程反而会降低性能合理使用线程池6.3 调试技巧使用线程转储(Thread Dump)jstack pid分析死锁和线程状态并发测试工具JUnit ConcurrentUnitStress测试日志记录添加线程信息%t或Thread.currentThread().getName()注意日志工具本身的线程安全性我在实际项目中总结出一个检查清单用于验证线程安全所有共享变量是否有适当的同步是否避免了跨锁的多重操作是否考虑到了内存可见性问题同步范围是否足够小是否使用了线程安全的工具类是否有文档说明类的线程安全特性线程安全就像程序中的暗礁——平时看不见但一旦撞上就会造成严重事故。理解这些原理和最佳实践才能编写出既正确又高效的多线程程序。