
1. 线程安全问题的本质与表现当多个线程同时访问共享资源时如果没有正确的同步机制就会出现线程安全问题。最常见的问题表现为数据竞争Data Race和内存可见性问题。数据竞争发生在多个线程同时读写同一个变量时。比如下面这个经典案例public class Counter { private int count 0; public void increment() { count; // 这不是原子操作 } public int getCount() { return count; } }count看似是一个操作实际上包含三个步骤读取count值、增加1、写回count值。当多个线程同时执行这个方法时就可能出现更新丢失的情况。内存可见性问题则更为隐蔽。由于现代CPU的多级缓存架构线程对变量的修改可能不会立即对其他线程可见。例如public class VisibilityDemo { private boolean flag true; public void writer() { flag false; // 线程A修改 } public void reader() { while (flag) { // 线程B可能永远看不到修改 // 空循环 } } }注意在单核CPU上可能无法复现这些问题但在多核环境下几乎必然会出现。这也是为什么线程安全问题有时在开发环境难以发现却在生产环境频繁出现。2. synchronized的深度解析2.1 基本用法与原理synchronized是Java中最基础的同步机制它通过对象的内置锁Monitor来实现同步。主要有三种使用方式实例方法同步public synchronized void method() { // 同步代码 }静态方法同步public static synchronized void staticMethod() { // 同步代码 }同步代码块public void method() { synchronized(this) { // 也可以是其他对象 // 同步代码 } }这三种方式的锁对象各不相同实例方法锁的是当前实例对象this静态方法锁的是类的Class对象同步代码块锁的是指定的对象2.2 锁的升级过程JDK1.6之后synchronized实现了锁升级机制大大提高了性能无锁状态新建对象初始状态偏向锁第一个线程访问时会记录线程ID适用于单线程重复访问场景轻量级锁当有第二个线程尝试获取锁时升级为CAS自旋锁重量级锁自旋超过一定次数默认10次后升级为操作系统级别的互斥锁锁升级是不可逆的过程但可以重置回无锁状态。通过JVM参数-XX:UseBiasedLocking可以开启/关闭偏向锁。2.3 使用注意事项锁对象选择不要使用String常量或基本类型包装类作为锁对象建议使用专门创建的Object作为私有锁锁粒度控制过粗的锁会导致性能下降过细的锁会增加死锁风险避免嵌套锁synchronized(lockA) { synchronized(lockB) { // 容易导致死锁 // ... } }3. volatile关键字的适用场景3.1 内存可见性保证volatile解决了内存可见性问题确保写操作会立即刷新到主内存读操作会从主内存读取最新值典型使用场景是状态标志位public class TaskRunner { private volatile boolean running true; public void stop() { running false; } public void run() { while (running) { // 执行任务 } } }3.2 禁止指令重排序volatile通过内存屏障Memory Barrier禁止指令重排序。这在单例模式的双重检查锁定中尤为重要public class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { if (instance null) { // 第一次检查 synchronized(Singleton.class) { if (instance null) { // 第二次检查 instance new Singleton(); } } } return instance; } }如果没有volatileinstance new Singleton()可能会被重排序导致其他线程获取到未初始化完成的对象。3.3 volatile的局限性volatile不能保证复合操作的原子性。例如private volatile int count 0; public void increment() { count; // 仍然不是原子操作 }这种情况下仍然需要synchronized或其他同步机制。4. 线程安全的实践策略4.1 无状态设计最简单的线程安全策略就是避免共享状态public class StatelessUtil { // 没有成员变量只有方法参数和局部变量 public int add(int a, int b) { return a b; } }4.2 不可变对象通过final关键字创建不可变对象public final class ImmutablePoint { private final int x; private final int y; public ImmutablePoint(int x, int y) { this.x x; this.y y; } // 只有getter方法没有setter }4.3 线程封闭将对象限制在单个线程中使用栈封闭使用局部变量ThreadLocal线程私有变量public class UserContext { private static final ThreadLocalUser currentUser new ThreadLocal(); public static void setUser(User user) { currentUser.set(user); } public static User getUser() { return currentUser.get(); } }4.4 并发容器优先使用java.util.concurrent包中的线程安全容器ConcurrentHashMapCopyOnWriteArrayListBlockingQueue等5. 常见问题排查与性能优化5.1 死锁检测与预防死锁的四个必要条件互斥条件占有且等待不可抢占循环等待预防策略按固定顺序获取锁使用tryLock设置超时时间避免嵌套锁5.2 锁竞争优化减小锁粒度将大锁拆分为多个小锁锁分段如ConcurrentHashMap的实现读写分离使用ReadWriteLock无锁算法使用Atomic类或CAS操作5.3 性能监控工具jstack查看线程堆栈和锁状态VisualVM图形化监控线程状态JProfiler分析锁竞争情况Arthas在线诊断工具6. Java内存模型JMM深入理解6.1 happens-before原则Java内存模型定义了以下happens-before规则程序顺序规则监视器锁规则volatile变量规则线程启动规则线程终止规则中断规则终结器规则传递性6.2 内存屏障类型LoadLoad屏障StoreStore屏障LoadStore屏障StoreLoad屏障开销最大synchronized和volatile的实现都依赖于这些内存屏障。6.3 final字段的特殊处理正确构造的不可变对象所有字段都是final可以安全发布无需额外同步public class SafePublication { public final int x; public final int y; public SafePublication(int x, int y) { this.x x; this.y y; // 构造函数完成前保证可见性 } }7. 高级并发工具类7.1 Atomic类java.util.concurrent.atomic包提供了一系列原子操作类AtomicInteger count new AtomicInteger(0); // 原子递增 count.incrementAndGet(); // CAS操作 boolean updated count.compareAndSet(expect, update);7.2 Lock接口比synchronized更灵活的锁机制Lock lock new ReentrantLock(); try { lock.lock(); // 临界区代码 } finally { lock.unlock(); // 必须在finally中释放 }优势可中断的获取锁超时获取锁公平性选择7.3 CountDownLatch等同步器常用同步工具CountDownLatch等待多个任务完成CyclicBarrier线程到达屏障点等待Semaphore控制资源访问数量Phaser更灵活的屏障8. 实际案例分析8.1 线程安全的单例模式最佳实现方式枚举实现public enum Singleton { INSTANCE; public void doSomething() { // ... } }8.2 高效缓存实现使用ConcurrentHashMap和FutureTaskpublic class ConcurrentCacheK, V { private final ConcurrentMapK, FutureV cache new ConcurrentHashMap(); public V get(final K key) throws Exception { FutureV future cache.get(key); if (future null) { FutureTaskV task new FutureTask(() - computeExpensiveValue(key)); future cache.putIfAbsent(key, task); if (future null) { future task; task.run(); } } return future.get(); } private V computeExpensiveValue(K key) { // 耗时计算 } }8.3 生产者-消费者模式使用BlockingQueue实现public class ProducerConsumer { private final BlockingQueueItem queue new LinkedBlockingQueue(100); class Producer implements Runnable { public void run() { while (true) { Item item produceItem(); queue.put(item); // 队列满时阻塞 } } } class Consumer implements Runnable { public void run() { while (true) { Item item queue.take(); // 队列空时阻塞 consumeItem(item); } } } }9. 并发编程的最佳实践优先使用高层抽象如Executor框架、并发集合避免过早优化先保证正确性再考虑性能文档化线程安全策略明确说明类的线程安全级别测试多线程代码使用压力测试和静态分析工具理解平台内存模型不同JVM实现可能有差异10. 常见误区与陷阱认为volatile能替代synchronized它只能保证可见性不能保证原子性过度同步不必要的同步会降低性能依赖线程优先级不同平台表现不一致忽视异常处理线程中的异常可能导致程序静默失败误用ThreadLocal忘记清理可能导致内存泄漏在实际项目中我经常遇到开发者在简单的场景过度使用同步机制而在复杂的场景又忽视了必要的同步。理解线程安全的本质和Java提供的各种同步工具的特性才能写出既正确又高效的多线程代码。对于性能关键的系统建议结合JMH进行基准测试用数据而不是直觉来指导优化决策。