深入理解Java线程安全与锁优化

一、概述:从现实世界到计算机世界

在软件开发的早期,程序员采用面向过程的编程思想,将数据和操作分离。而面向对象编程则更符合现实世界的思维方式,把数据和行为都封装在对象中。然而,现实世界与计算机世界之间存在一个重要差异:在计算机世界中,对象的工作可能会被频繁中断和切换,属性可能在中断期间被修改,这导致了线程安全问题的产生。

// 一个简单的计数器类
public class Counter {
    private int count = 0;
    
    public void increment() {
        count++; // 非原子操作,存在线程安全问题
    }
    
    public int getCount() {
        return count;
    }
}

当我们开始讨论"高效并发"时,首先需要确保并发的正确性,然后才考虑如何实现高效。这正是本章要探讨的核心内容。

二、线程安全的定义与分类

2.1 什么是线程安全?

Brian Goetz在《Java并发编程实战》中给出了一个精准的定义:

"当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。"

这个定义要求线程安全的代码必须封装所有必要的正确性保障手段,使调用者无需关心多线程问题。

2.2 Java语言中的线程安全等级

我们可以按照线程安全的"安全程度"将Java中的共享数据操作分为五类:

1. 不可变(Immutable)

不可变对象一定是线程安全的,因为它们的可见状态永远不会改变。

// 使用final关键字创建不可变对象
public final class ImmutableValue {
    private final int value;
    
    public ImmutableValue(int value) {
        this.value = value;
    }
    
    public int getValue() {
        return value;
    }
    
    // 返回新对象而不是修改现有对象
    public ImmutableValue add(int delta) {
        return new ImmutableValue(this.value + delta);
    }
}

Java中的String、Integer、Long等包装类都是不可变的。

2. 绝对线程安全

绝对线程安全完全满足Brian Goetz的定义,但实践中很难实现。即使Java中标注为线程安全的类,如Vector,也并非绝对线程安全。

// Vector的线程安全局限性示例
public class VectorTest {
    private static Vector<Integer> vector = new Vector<>();
    
    public static void main(String[] args) {
        while (true) {
            for (int i = 0; i < 10; i++) {
                vector.add(i);
            }
            
            Thread removeThread = new Thread(() -> {
                for (int i = 0; i < vector.size(); i++) {
                    vector.remove(i);
                }
            });
            
            Thread printThread = new Thread(() -> {
                for (int i = 0; i < vector.size(); i++) {
                    System.out.println(vector.get(i));
                }
            });
            
            removeThread.start();
            printThread.start();
            
            // 不要同时产生过多线程,防止操作系统假死
            while (Thread.activeCount() > 20) ;
        }
    }
}

上述代码可能抛出ArrayIndexOutOfBoundsException,因为虽然Vector的每个方法都是同步的,但复合操作(先检查再执行)仍需外部同步。

3. 相对线程安全

相对线程安全保证单次操作是线程安全的,但特定顺序的连续调用可能需要外部同步。Java中大部分声称线程安全的类属于此类,如Vector、HashTable等。

4. 线程兼容

线程兼容指对象本身不是线程安全的,但可以通过正确使用同步手段保证安全。如ArrayList、HashMap等。

5. 线程对立

线程对立指无论是否采取同步措施,都无法在多线程环境中安全使用。如Thread类的suspend()和resume()方法。

三、线程安全的实现方法

3.1 互斥同步

互斥同步是最常见的并发保障手段,synchronized是最基本的互斥同步手段。

synchronized的实现原理

public class SynchronizedExample {
    // 同步实例方法
    public synchronized void instanceMethod() {
        // 同步代码
    }
    
    // 同步静态方法
    public static synchronized void staticMethod() {
        // 同步代码
    }
    
    public void method() {
        // 同步块
        synchronized(this) {
            // 同步代码
        }
    }
}

synchronized编译后会在同步块前后生成monitorenter和monitorexit字节码指令。执行monitorenter时:

  1. 如果对象未被锁定,或当前线程已持有锁,则锁计数器+1
  2. 如果获取锁失败,当前线程阻塞直到锁被释放

synchronized的特性:

  • 可重入:同一线程可重复获取同一把锁
  • 阻塞性:未获取锁的线程会无条件阻塞
  • 重量级:线程阻塞和唤醒需要操作系统介入,成本高

ReentrantLock:更灵活的互斥同步

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    
    public void method() {
        lock.lock();  // 获取锁
        try {
            // 同步代码
        } finally {
            lock.unlock();  // 确保锁被释放
        }
    }
}

ReentrantLock相比synchronized的高级特性:

  1. 等待可中断:避免长期等待
public boolean tryLockWithTimeout() throws InterruptedException {
    return lock.tryLock(5, TimeUnit.SECONDS);  // 最多等待5秒
}
  1. 公平锁:按申请顺序获取锁
private final ReentrantLock fairLock = new ReentrantLock(true);  // 公平锁
  1. 绑定多个条件
public class ConditionExample {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    
    public void await() throws InterruptedException {
        lock.lock();
        try {
            condition.await();  // 释放锁并等待
        } finally {
            lock.unlock();
        }
    }
    
    public void signal() {
        lock.lock();
        try {
            condition.signal();  // 唤醒等待线程
        } finally {
            lock.unlock();
        }
    }
}

synchronized vs ReentrantLock

  • 简单性:synchronized更简单清晰
  • 性能:JDK6后两者性能相近
  • 功能:ReentrantLock更灵活
  • 推荐:优先使用synchronized,需要高级功能时使用ReentrantLock

3.2 非阻塞同步

非阻塞同步基于冲突检测的乐观并发策略,先操作后检测冲突。

CAS(Compare-and-Swap)原理

CAS操作需要三个参数:内存位置V、旧预期值A和新值B。当且仅当V的值等于A时,才用B更新V的值。

public class CASExample {
    private AtomicInteger atomicValue = new AtomicInteger(0);
    
    public void increment() {
        int oldValue;
        int newValue;
        do {
            oldValue = atomicValue.get();  // 获取当前值
            newValue = oldValue + 1;       // 计算新值
        } while (!atomicValue.compareAndSet(oldValue, newValue));  // CAS操作
    }
}

Java中的原子类(如AtomicInteger)使用CAS实现无锁线程安全:

public class AtomicExample {
    public static AtomicInteger race = new AtomicInteger(0);
    
    public static void increase() {
        race.incrementAndGet();  // 原子自增
    }
    
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[20];
        
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    increase();
                }
            });
            threads[i].start();
        }
        
        for (Thread thread : threads) {
            thread.join();
        }
        
        System.out.println(race.get());  // 总是输出200000
    }
}

ABA问题

CAS操作存在ABA问题:如果一个值从A变成B,又变回A,CAS操作会误以为它没变化。

解决方案:使用AtomicStampedReference或AtomicMarkableReference维护版本号。

public class ABAExample {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> atomicRef = 
            new AtomicStampedReference<>(100, 0);
        
        int stamp = atomicRef.getStamp();
        Integer reference = atomicRef.getReference();
        
        // 更新值并增加版本号
        atomicRef.compareAndSet(reference, 101, stamp, stamp + 1);
    }
}

3.3 无同步方案

可重入代码(纯代码)

可重入代码不依赖共享数据,所有状态都由参数传入,不会调用非可重入方法。

// 可重入代码示例
public class MathUtils {
    // 纯函数:输出只依赖于输入,没有副作用
    public static int add(int a, int b) {
        return a + b;
    }
    
    // 非纯函数:依赖外部状态
    private int base = 0;
    public int addToBase(int value) {
        return base + value;  // 非可重入,依赖共享状态
    }
}

线程本地存储(ThreadLocal)

ThreadLocal是Java中实现线程本地存储的核心类,它为每个线程提供独立的变量副本,避免了多线程环境下的竞争条件。

ThreadLocal的核心概念

ThreadLocal允许你将状态与线程关联起来,每个线程都有自己独立初始化的变量副本。这些变量通常用于保持线程的上下文信息,如用户会话、事务ID等。

ThreadLocal的基本使用
public class ThreadLocalExample {
    // 创建ThreadLocal变量,并提供初始值
    private static ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 0);
    private static ThreadLocal<String> threadLocalUser = new ThreadLocal<>();
    
    public static void increment() {
        threadLocalCounter.set(threadLocalCounter.get() + 1);
    }
    
    public static int getCounter() {
        return threadLocalCounter.get();
    }
    
    public static void setUser(String user) {
        threadLocalUser.set(user);
    }
    
    public static String getUser() {
        return threadLocalUser.get();
    }
    
    public static void clear() {
        // 清理ThreadLocal变量,防止内存泄漏
        threadLocalCounter.remove();
        threadLocalUser.remove();
    }
    
    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            // 设置线程用户
            setUser(Thread.currentThread().getName());
            
            // 每个线程独立计数
            for (int i = 0; i < 5; i++) {
                increment();
            }
            
            System.out.println(Thread.currentThread().getName() + 
                ": Counter=" + getCounter() + 
                ", User=" + getUser());
                
            // 清理ThreadLocal变量
            clear();
        };
        
        // 创建多个线程
        Thread[] threads = new Thread[3];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(task, "Thread-" + (i + 1));
            threads[i].start();
        }
        
        // 等待所有线程完成
        for (Thread thread : threads) {
            thread.join();
        }
    }
}
ThreadLocal的实现原理

ThreadLocal的实现依赖于每个Thread对象内部的ThreadLocalMap数据结构。下面是ThreadLocal的核心实现机制:

// ThreadLocal的核心方法源码简析
public class ThreadLocal<T> {
    // 获取当前线程的变量值
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t); // 获取线程的ThreadLocalMap
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue(); // 设置初始值
    }
    
    // 设置当前线程的变量值
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value); // 创建ThreadLocalMap
        }
    }
    
    // 获取与线程关联的ThreadLocalMap
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    // 创建ThreadLocalMap
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
}
Thread、ThreadLocal与ThreadLocalMap的关系

ThreadLocal的实现依赖于Thread类中的两个重要字段:

public class Thread implements Runnable {
    // 线程本地变量Map
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    // 继承自父线程的线程本地变量Map
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    
    // 其他字段和方法...
}

ThreadLocalMap是ThreadLocal的静态内部类,它使用弱引用(WeakReference)作为键来存储线程本地变量,这是为了避免内存泄漏。

graph TB Thread1[Thread 1] --> ThreadLocalMap1[ThreadLocalMap] Thread2[Thread 2] --> ThreadLocalMap2[ThreadLocalMap] ThreadLocalMap1 --> Entry1_1[Entry: key=ThreadLocalA, value=value1] ThreadLocalMap1 --> Entry1_2[Entry: key=ThreadLocalB, value=value2] ThreadLocalMap2 --> Entry2_1[Entry: key=ThreadLocalA, value=value3] ThreadLocalMap2 --> Entry2_2[Entry: key=ThreadLocalB, value=value4] ThreadLocalA[ThreadLocalA] --> Entry1_1 ThreadLocalA --> Entry2_1 ThreadLocalB[ThreadLocalB] --> Entry1_2 ThreadLocalB --> Entry2_2 style Thread1 fill:#e6f3ff style Thread2 fill:#e6f3ff style ThreadLocalMap1 fill:#fff2e6 style ThreadLocalMap2 fill:#fff2e6 style ThreadLocalA fill:#f9e6ff style ThreadLocalB fill:#f9e6ff

从上图可以看出:

  • 每个Thread对象都有一个ThreadLocalMap实例
  • ThreadLocalMap中存储了多个Entry,每个Entry的键是ThreadLocal对象,值是线程本地变量
  • 不同的ThreadLocal对象可以在不同的线程中存储不同的值
ThreadLocal的内存泄漏问题

ThreadLocal可能引起内存泄漏,原因在于ThreadLocalMap中的Entry键是弱引用(WeakReference),而值是强引用:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    
    Entry(ThreadLocal<?> k, Object v) {
        super(k);  // 键是弱引用
        value = v; // 值是强引用
    }
}

当ThreadLocal对象没有外部强引用时,GC会回收键(ThreadLocal对象),但值仍然被Entry强引用,导致值无法被回收,造成内存泄漏。

解决方案

  1. 使用完ThreadLocal后,及时调用remove()方法清理
  2. 将ThreadLocal变量声明为static final,避免重复创建
InheritableThreadLocal:可继承的线程本地变量

InheritableThreadLocal是ThreadLocal的子类,它允许子线程继承父线程的线程本地变量:

public class InheritableThreadLocalExample {
    private static InheritableThreadLocal<String> inheritableThreadLocal = 
        new InheritableThreadLocal<>();
    
    public static void main(String[] args) {
        inheritableThreadLocal.set("Parent Value");
        
        Thread childThread = new Thread(() -> {
            System.out.println("Child thread value: " + inheritableThreadLocal.get());
            inheritableThreadLocal.set("Child Value");
            System.out.println("Child thread value after set: " + inheritableThreadLocal.get());
        });
        
        childThread.start();
        
        try {
            childThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("Parent thread value after child modification: " + 
            inheritableThreadLocal.get());
    }
}
ThreadLocal的使用场景
  1. 数据库连接管理:每个线程使用独立的数据库连接
  2. 会话管理:在Web应用中存储用户会话信息
  3. 全局参数传递:避免在方法参数中传递上下文信息
  4. 日期格式化:SimpleDateFormat不是线程安全的,可以使用ThreadLocal为每个线程提供独立的实例
public class DateFormatterUtils {
    private static final ThreadLocal<SimpleDateFormat> DATE_FORMATTER = 
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    
    public static String formatDate(Date date) {
        return DATE_FORMATTER.get().format(date);
    }
    
    public static Date parseDate(String dateString) throws ParseException {
        return DATE_FORMATTER.get().parse(dateString);
    }
}

四、锁优化技术

HotSpot虚拟机实现了多种锁优化技术,提高并发性能。

4.1 自旋锁与自适应自旋

当线程请求锁时,如果锁被占用,线程不立即阻塞,而是执行忙循环(自旋)等待锁释放。

// 自旋锁伪代码
public class SpinLock {
    private AtomicReference<Thread> owner = new AtomicReference<>();
    
    public void lock() {
        Thread currentThread = Thread.currentThread();
        // 自旋等待
        while (!owner.compareAndSet(null, currentThread)) {
            // 空循环,等待锁释放
        }
    }
    
    public void unlock() {
        Thread currentThread = Thread.currentThread();
        owner.compareAndSet(currentThread, null);
    }
}

自适应自旋:根据前一次的自旋时间和锁拥有者的状态动态调整自旋时间。

4.2 锁消除

JVM通过逃逸分析检测不可能存在共享数据竞争的锁,并消除这些锁。

public String concatString(String s1, String s2, String s3) {
    return s1 + s2 + s3;
}

上述代码编译后相当于:

public String concatString(String s1, String s2, String s3) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);  // 同步方法
    sb.append(s2);  // 同步方法
    sb.append(s3);  // 同步方法
    return sb.toString();
}

JVM通过逃逸分析发现sb不会逃逸出方法,自动消除锁操作。

4.3 锁粗化

将连续的对同一对象加锁解锁操作合并为一次范围更大的加锁操作。

// 多次加锁解锁
public void method() {
    synchronized(lock) {
        // 操作1
    }
    // 一些其他代码...
    synchronized(lock) {
        // 操作2
    }
}

// 锁粗化后
public void method() {
    synchronized(lock) {
        // 操作1
        // 一些其他代码...
        // 操作2
    }
}

4.4 轻量级锁

轻量级锁减少传统重量级锁使用操作系统互斥量产生的性能消耗。

轻量级锁工作流程:

sequenceDiagram participant T as 线程 participant O as 对象头 participant S as 线程栈帧 T->>O: 检查锁标志位(01) T->>S: 创建Lock Record空间 T->>S: 复制对象头Mark Word(Displaced Mark Word) T->>O: CAS尝试将Mark Word指向Lock Record alt CAS成功 O->>O: 将锁标志位改为00(轻量级锁) T->>T: 获取锁成功 else CAS失败 alt 检查是否当前线程已持有锁 T->>T: 获取锁成功(重入) else 其他线程竞争 O->>O: 膨胀为重量级锁(10) T->>T: 线程阻塞 end end

4.5 偏向锁

偏向锁消除无竞争情况下的同步原语,偏向于第一个获取它的线程。

graph LR A[对象未锁定<br>标志位01] -->|第一个线程访问| B[偏向模式<br>标志位01+偏向模式1] B -->|同一线程再次访问| C[直接访问<br>不需要同步] B -->|其他线程访问| D[检查偏向线程是否活跃] D -->|已不活跃| E[撤销偏向模式<br>恢复到未锁定01或轻量级锁00] D -->|仍然活跃| F[膨胀为轻量级锁00] E -->|竞争| G[轻量级锁竞争] G -->|多线程竞争| H[膨胀为重量级锁10]

偏向锁的撤销:

  1. 当对象计算过哈希码后,无法进入偏向状态
  2. 当偏向锁收到计算一致性哈希码请求时,撤销偏向状态,膨胀为重量级锁

五、实践建议

  1. 优先使用synchronized:在简单场景下,synchronized更简洁且性能足够好
  2. 需要高级功能时使用ReentrantLock:如定时锁等待、可中断锁等待、公平锁等
  3. 使用读多写少的并发容器:如ConcurrentHashMap、CopyOnWriteArrayList等
  4. 使用原子类替代同步:在简单原子操作场景下,使用AtomicInteger等原子类
  5. 谨慎使用线程本地存储:避免内存泄漏,及时调用remove()方法清理
  6. 根据场景选择合适锁优化:在竞争激烈场景下,考虑禁用偏向锁(-XX:-UseBiasedLocking)

六、总结

线程安全与锁优化是Java并发编程的核心内容。理解线程安全的不同级别、掌握各种同步机制的原理和适用场景,能够帮助我们编写出更高效、更安全的并发程序。

从基本的互斥同步到非阻塞同步,从锁消除到偏向锁,Java虚拟机提供了丰富的线程安全保障和优化手段。作为开发者,我们应该根据具体场景选择最合适的同步方式,在保证正确性的前提下追求更高的性能。

ThreadLocal作为实现线程安全的重要工具,通过为每个线程提供独立的变量副本,避免了共享数据的竞争条件。然而,使用ThreadLocal时需要注意内存泄漏问题,及时清理不再需要的变量。

记住,并发编程是一门艺术,而了解底层实现原理是掌握这门艺术的基础。只有深入理解线程安全与锁优化的机制,才能写出真正高效、可靠的并发程序。

posted @ 2025-11-04 08:51  佛祖让我来巡山  阅读(166)  评论(0)    收藏  举报

佛祖让我来巡山博客站 - 创建于 2018-08-15

开发工程师个人站,内容主要是网站开发方面的技术文章,大部分来自学习或工作,部分来源于网络,希望对大家有所帮助。

Bootstrap中文网