一、线程安全性概述1.1 什么是线程安全性一个类或者方法在被多个线程同时访问时无论这些线程在运行时如何交替执行也无需调用方进行任何额外的同步或协同都能表现出正确的行为那么就称这个类是线程安全的。线程安全的核心是管理对共享可变状态的访问。如果多个线程同时访问一个可变的状态变量并且至少有一个线程执行写操作那么必须通过同步来协调对变量的访问。1.2 并发编程的三大特性要理解线程安全性必须先明白并发编程中导致问题的三个根源原子性一个或多个操作在CPU执行过程中不被中断的特性。比如i看似一行代码实际上包含“读取-修改-写入”三步在多线程下会丢失更新。可见性当一个线程修改了共享变量的值其他线程能否立即看到这个修改。由于现代CPU有多级缓存每个线程可能将变量拷贝到自己的本地内存导致修改对其他线程不可见。有序性程序执行的顺序按照代码的先后顺序执行。编译器和处理器为了优化性能可能会对指令进行重排序在多线程环境下可能导致意想不到的结果。1.3 线程安全性的级别根据线程安全的程度可以将类划分为几个等级从弱到强不可变声明为final的不可变对象如String、Integer天生线程安全。绝对线程安全不管运行时环境如何调用者都不需要额外的同步措施。Java中标注为线程安全的类大多属于这一类但达到“绝对”很难。相对线程安全通常意义上的线程安全保证对对象的单独操作是线程安全的但连续调用复合操作可能需要额外加锁。如Vector、Hashtable。线程兼容对象本身不是线程安全的但可以通过调用方正确使用同步手段来安全使用如ArrayList。线程对立无论调用方如何同步都无法保证线程安全这种情况极少见。二、Java内存模型JMM与happens-before2.1 JMM的抽象结构Java内存模型规定所有变量都存储在主内存中如堆内存每个线程拥有自己的工作内存可以理解为CPU缓存和寄存器的抽象线程对变量的操作必须在工作内存中进行不能直接读写主内存。线程之间的变量传递需要通过主内存完成。这种模型导致了可见性问题线程A修改了变量只有当A把工作内存的值刷新回主内存且线程B从主内存重新加载后B才能看到A的修改。2.2 happens-before原则JMM通过happens-before规则来保证两个操作之间的顺序性和可见性。如果操作A happens-before 操作B那么A的结果对B可见且A的执行顺序在B之前。主要的happens-before规则包括程序次序规则在一个线程内书写在前的代码先行发生于书写在后的代码控制流顺序。volatile变量规则对一个volatile变量的写操作先行发生于后面对这个变量的读操作。锁规则对一个锁的解锁unlock先行发生于后面对同一个锁的加锁lock。传递性如果A先行发生于BB先行发生于C则A先行发生于C。线程启动规则Thread对象的start()方法先行发生于该线程的任何动作。线程终止规则线程中的所有操作先行发生于对该线程的终止检测如Thread.join()返回。线程中断规则对线程interrupt()的调用先行发生于被中断线程检测到中断事件如Thread.interrupted()。对象终结规则一个对象的构造函数结束先行发生于finalize()方法的开始。2.3 volatile的内存语义volatile是轻量级的同步机制它保证了两点可见性对一个volatile变量的写会立即刷新到主内存对一个volatile变量的读会从主内存重新加载。禁止指令重排序在volatile变量读写前后插入内存屏障防止重排序。注意volatile不保证原子性比如volatile int count; count依然是线程不安全的。三、实现线程安全的基本方法3.1 互斥同步阻塞同步互斥同步是最常见的保证并发正确性的方式通过锁来实现临界区的串行访问。synchronized关键字JVM内置锁通过monitor机制实现使用简单自动释放锁。java.util.concurrent.locks.Lock接口提供了比synchronized更灵活的锁操作如可中断、可超时、公平锁等。典型实现ReentrantLock。3.2 非阻塞同步乐观锁非阻塞同步基于冲突检测和重试机制不需要线程挂起通常效率更高。CASCompare and Swap硬件层面的原子指令Java通过Unsafe类暴露。CAS有三个操作数内存位置V旧的预期值A新的值B。只有当V的值等于A时才用B更新V否则重试或失败。原子类AtomicInteger等底层使用CAS实现线程安全的计数器。3.3 无同步方案如果一个方法不涉及共享数据或者数据本身就是不可变的那么它就天然线程安全。不可变对象对象创建后状态不能改变如String、枚举类型、用final修饰的基本类型。不可变对象一定是线程安全的。线程局部存储使用ThreadLocal为每个线程保存独立的变量副本线程之间互不影响。四、深入synchronized4.1 synchronized的三种使用方式修饰实例方法锁是当前实例对象this。修饰静态方法锁是当前类的Class对象。修饰代码块可以指定任意对象作为锁。4.2 底层原理在JVM中synchronized是基于进入和退出Monitor对象实现的。每个对象都有一个monitor与之关联当monitor被持有时就处于锁定状态。同步代码块通过字节码指令monitorenter和monitorexit实现。同步方法通过方法常量池中的ACC_SYNCHRONIZED标志来隐式调用。4.3 Java对象头与锁的状态Java对象头Mark Word中存储了对象的哈希码、GC分代年龄、锁状态标志等信息。锁有四种状态随着竞争加剧而升级不可降级无锁没有线程占用。偏向锁一段同步代码一直被同一个线程访问该线程自动获取锁降低获取锁的代价。通过CAS在对象头中记录线程ID。轻量级锁当有第二个线程竞争偏向锁时偏向锁升级为轻量级锁。其他线程通过自旋尝试获取锁不会阻塞。重量级锁当自旋超过一定次数或者有一个线程在自旋时又有新线程竞争轻量级锁膨胀为重量级锁此时未获取锁的线程会进入阻塞状态依赖于操作系统的mutex。锁升级是JVM为了减少锁开销的优化。五、显式锁Lock接口5.1 ReentrantLock的特性ReentrantLock实现了Lock接口提供了比synchronized更丰富的功能可重入同一个线程可以多次获取同一把锁通过计数器实现。可中断lockInterruptibly()允许等待锁的线程响应中断。公平锁new ReentrantLock(true)创建公平锁按照线程等待时间分配锁但会降低吞吐量。绑定多个条件通过newCondition()可以创建多个Condition对象实现更精细的等待/通知机制。5.2 与synchronized的比较用法synchronized自动释放锁Lock必须手动在finally中释放锁容易忘记。灵活性Lock支持非阻塞获取锁tryLock()、超时获取锁、可中断synchronized不支持。性能在JDK 1.6之后两者性能接近synchronized有锁升级优化。底层synchronized是JVM层面Lock是API层面使用AQS实现。5.3 AQSAbstractQueuedSynchronizerAQS是Java并发包的基础框架ReentrantLock、CountDownLatch等同步器都基于AQS实现。它维护了一个volatile int state表示同步状态和一个CLH等待队列。通过CAS对state进行操作实现锁的获取与释放。六、原子操作类java.util.concurrent.atomic6.1 原子类概览基本类型AtomicInteger, AtomicLong, AtomicBoolean。数组类型AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray。引用类型AtomicReference, AtomicStampedReference解决ABA问题AtomicMarkableReference。字段更新器AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater。累加器JDK 1.8LongAdder, DoubleAdder在高并发下性能优于AtomicLong采用分段思想。6.2 CAS与ABA问题CAS操作可能遇到ABA问题线程1读取变量值为A线程2将A改成B再改回A线程1进行CAS时发现值还是A于是操作成功但实际上变量被修改过。如果业务逻辑关心这个过程就会出错。解决ABA问题可以使用带版本号的原子引用AtomicStampedReference通过pairint, reference存储版本号和引用。七、并发集合Java提供了线程安全的集合类主要分为两类阻塞队列BlockingQueue和并发容器ConcurrentHashMap等。7.1 ConcurrentHashMapJDK 1.7采用分段锁Segment每个Segment是一把锁继承自ReentrantLock。默认16个Segment最多支持16个线程并发写。JDK 1.8放弃分段锁改用CAS synchronized。数据结构与HashMap类似数组链表红黑树。锁粒度细化到每个数组元素Node。put操作先CAS插入头结点若失败则对头结点使用synchronized加锁。支持并发扩容。7.2 CopyOnWriteArrayList适用于读多写少的场景。每次修改add, set等都会复制一份新的数组在新数组上进行修改然后将引用指向新数组。读操作不加锁因此读性能极高。缺点内存占用大数据一致性弱最终一致性。7.3 BlockingQueue主要用于生产者-消费者模式提供了阻塞的put和take方法。常用实现ArrayBlockingQueue有界基于数组。LinkedBlockingQueue可有界也可无界基于链表。SynchronousQueue不存储元素每个put必须等待一个take。PriorityBlockingQueue支持优先级无界。DelayQueue支持延迟获取元素。7.4 其他并发容器ConcurrentLinkedQueue非阻塞无界队列基于CAS。ConcurrentSkipListMap跳表实现支持排序并发版本TreeMap。八、线程池与线程安全8.1 线程池的作用线程池通过复用线程、管理线程生命周期降低了资源开销并提高了响应速度。同时线程池也提供了一种任务提交与执行分离的机制有利于线程安全的控制。8.2 线程池的核心参数corePoolSize核心线程数。maximumPoolSize最大线程数。keepAliveTime非核心线程空闲存活时间。workQueue任务阻塞队列。threadFactory线程工厂。handler拒绝策略AbortPolicy, CallerRunsPolicy, DiscardPolicy, DiscardOldestPolicy。8.3 提交任务时的线程安全任务本身应该是线程安全的或者任务之间不共享可变状态。如果多个任务共享数据必须通过同步机制锁、原子类等保护共享数据。使用线程池提交Callable/Runnable时要注意任务内部对共享资源的访问。8.4 线程池的关闭shutdown()不再接受新任务等待已提交任务执行完毕。shutdownNow()尝试停止正在执行的任务返回等待执行的任务列表。停止通过Thread.interrupt()实现所以任务需要响应中断。九、线程安全的设计策略9.1 优先使用不可变对象不可变对象天生线程安全如String、Integer、LocalDate等。如果状态需要改变可以考虑创建一个新对象。9.2 封装可变状态将可变状态封装在类内部并提供同步的方法访问。例如javapublic class Counter { private int count; public synchronized void increment() { count; } public synchronized int get() { return count; } }9.3 遵循“先同步后操作”原则在访问共享可变状态前必须获得适当的锁。并且在整个操作期间保持锁直到操作完成。9.4 缩小同步范围只对必要的代码块进行同步减少锁持有的时间提高并发性能。但要注意不要过于细化导致原子性被破坏。9.5 使用现有的并发工具优先使用java.util.concurrent包提供的工具如BlockingQueue、CountDownLatch等而非自己实现复杂的同步逻辑。十、并发工具类10.1 CountDownLatch允许一个或多个线程等待直到其他线程完成一组操作。计数器不可重置。典型用法主线程等待所有子任务完成。10.2 CyclicBarrier让一组线程互相等待直到所有线程都到达某个公共屏障点然后继续执行。计数器可以重置。10.3 Semaphore控制同时访问特定资源的线程数量。通过acquire()获取许可release()释放许可。可以用于限流。10.4 Exchanger用于两个线程之间交换数据。每个线程在exchange()方法上等待直到另一个线程也到达然后交换数据。10.5 PhaserJDK 1.7更灵活的屏障可以动态注册参与者支持分阶段执行。十一、线程安全的单例模式单例模式在多线程环境下必须考虑线程安全。以下是几种常见的线程安全实现11.1 双重检查锁DCLjavapublic class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance null) { synchronized (Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; } }关键点instance必须用volatile修饰防止new Singleton()的指令重排序导致其他线程拿到未初始化完成的对象。11.2 静态内部类javapublic class Singleton { private Singleton() {} private static class Holder { private static final Singleton INSTANCE new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } }利用类加载机制保证线程安全且懒加载。11.3 枚举javapublic enum Singleton { INSTANCE; public void doSomething() { ... } }最简单、最安全的方式防止反射和序列化破坏。十二、并发编程最佳实践12.1 避免共享可变状态尽量将变量限定在单个线程内使用ThreadLocal或者设计为不可变对象。12.2 使用并发容器而非同步容器同步容器如Vector、Hashtable、Collections.synchronizedList()虽然线程安全但性能较差对整个集合加锁。优先使用ConcurrentHashMap、CopyOnWriteArrayList等。12.3 缩小锁粒度将一个锁分解为多个锁例如ConcurrentHashMap的分段思想或者使用读写锁ReadWriteLock。12.4 避免死锁锁顺序所有线程按照相同的顺序获取锁。锁超时使用tryLock(timeout)主动获取锁失败时回退。死锁检测通过工具jstack定位死锁。12.5 线程池使用注意合理设置线程池参数避免任务堆积或创建过多线程。在任务中要正确处理异常避免吞没异常导致线程池误判。12.6 正确使用ThreadLocal使用完后及时调用remove()防止内存泄漏特别是线程池中的线程重用。不要使用ThreadLocal传递全局变量会破坏封装性。12.7 警惕逸出在构造器内部启动线程或注册监听器可能导致this引用逸出被其他线程访问到未完全构造的对象。不要在构造器中将this暴露出去。十三、结语Java并发编程博大精深保证线程安全性不仅仅是记住几个关键字和类库更重要的是理解并发问题的本质原子性、可见性、有序性以及JMM提供的内存屏障和happens-before规则。在实际开发中我们应该根据场景选择合适的同步策略优先考虑不可变对象和线程局部变量其次是非阻塞同步原子类最后才是互斥同步锁。同时充分利用JDK提供的并发工具避免重复造轮子。