找回密码
 立即注册
首页 业界区 业界 深入理解Java线程安全与锁优化

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

准挝 12 小时前
一、概述:从现实世界到计算机世界

在软件开发的早期,程序员采用面向过程的编程思想,将数据和操作分离。而面向对象编程则更符合现实世界的思维方式,把数据和行为都封装在对象中。然而,现实世界与计算机世界之间存在一个重要差异:在计算机世界中,对象的工作可能会被频繁中断和切换,属性可能在中断期间被修改,这导致了线程安全问题的产生。
  1. // 一个简单的计数器类
  2. public class Counter {
  3.     private int count = 0;
  4.    
  5.     public void increment() {
  6.         count++; // 非原子操作,存在线程安全问题
  7.     }
  8.    
  9.     public int getCount() {
  10.         return count;
  11.     }
  12. }
复制代码
当我们开始讨论"高效并发"时,首先需要确保并发的正确性,然后才考虑如何实现高效。这正是本章要探讨的核心内容。
二、线程安全的定义与分类

2.1 什么是线程安全?

Brian Goetz在《Java并发编程实战》中给出了一个精准的定义:
"当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。"
这个定义要求线程安全的代码必须封装所有必要的正确性保障手段,使调用者无需关心多线程问题。
2.2 Java语言中的线程安全等级

我们可以按照线程安全的"安全程度"将Java中的共享数据操作分为五类:
1. 不可变(Immutable)

不可变对象一定是线程安全的,因为它们的可见状态永远不会改变。
  1. // 使用final关键字创建不可变对象
  2. public final class ImmutableValue {
  3.     private final int value;
  4.    
  5.     public ImmutableValue(int value) {
  6.         this.value = value;
  7.     }
  8.    
  9.     public int getValue() {
  10.         return value;
  11.     }
  12.    
  13.     // 返回新对象而不是修改现有对象
  14.     public ImmutableValue add(int delta) {
  15.         return new ImmutableValue(this.value + delta);
  16.     }
  17. }
复制代码
Java中的String、Integer、Long等包装类都是不可变的。
2. 绝对线程安全

绝对线程安全完全满足Brian Goetz的定义,但实践中很难实现。即使Java中标注为线程安全的类,如Vector,也并非绝对线程安全。
  1. // Vector的线程安全局限性示例
  2. public class VectorTest {
  3.     private static Vector<Integer> vector = new Vector<>();
  4.    
  5.     public static void main(String[] args) {
  6.         while (true) {
  7.             for (int i = 0; i < 10; i++) {
  8.                 vector.add(i);
  9.             }
  10.             
  11.             Thread removeThread = new Thread(() -> {
  12.                 for (int i = 0; i < vector.size(); i++) {
  13.                     vector.remove(i);
  14.                 }
  15.             });
  16.             
  17.             Thread printThread = new Thread(() -> {
  18.                 for (int i = 0; i < vector.size(); i++) {
  19.                     System.out.println(vector.get(i));
  20.                 }
  21.             });
  22.             
  23.             removeThread.start();
  24.             printThread.start();
  25.             
  26.             // 不要同时产生过多线程,防止操作系统假死
  27.             while (Thread.activeCount() > 20) ;
  28.         }
  29.     }
  30. }
复制代码
上述代码可能抛出ArrayIndexOutOfBoundsException,因为虽然Vector的每个方法都是同步的,但复合操作(先检查再执行)仍需外部同步。
3. 相对线程安全

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

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

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

3.1 互斥同步

互斥同步是最常见的并发保障手段,synchronized是最基本的互斥同步手段。
synchronized的实现原理
  1. public class SynchronizedExample {
  2.     // 同步实例方法
  3.     public synchronized void instanceMethod() {
  4.         // 同步代码
  5.     }
  6.    
  7.     // 同步静态方法
  8.     public static synchronized void staticMethod() {
  9.         // 同步代码
  10.     }
  11.    
  12.     public void method() {
  13.         // 同步块
  14.         synchronized(this) {
  15.             // 同步代码
  16.         }
  17.     }
  18. }
复制代码
synchronized编译后会在同步块前后生成monitorenter和monitorexit字节码指令。执行monitorenter时:

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

  • 可重入:同一线程可重复获取同一把锁
  • 阻塞性:未获取锁的线程会无条件阻塞
  • 重量级:线程阻塞和唤醒需要操作系统介入,成本高
ReentrantLock:更灵活的互斥同步
  1. public class ReentrantLockExample {
  2.     private final ReentrantLock lock = new ReentrantLock();
  3.    
  4.     public void method() {
  5.         lock.lock();  // 获取锁
  6.         try {
  7.             // 同步代码
  8.         } finally {
  9.             lock.unlock();  // 确保锁被释放
  10.         }
  11.     }
  12. }
复制代码
ReentrantLock相比synchronized的高级特性:

  • 等待可中断:避免长期等待
  1. public boolean tryLockWithTimeout() throws InterruptedException {
  2.     return lock.tryLock(5, TimeUnit.SECONDS);  // 最多等待5秒
  3. }
复制代码

  • 公平锁:按申请顺序获取锁
  1. private final ReentrantLock fairLock = new ReentrantLock(true);  // 公平锁
复制代码

  • 绑定多个条件
  1. public class ConditionExample {
  2.     private final ReentrantLock lock = new ReentrantLock();
  3.     private final Condition condition = lock.newCondition();
  4.    
  5.     public void await() throws InterruptedException {
  6.         lock.lock();
  7.         try {
  8.             condition.await();  // 释放锁并等待
  9.         } finally {
  10.             lock.unlock();
  11.         }
  12.     }
  13.    
  14.     public void signal() {
  15.         lock.lock();
  16.         try {
  17.             condition.signal();  // 唤醒等待线程
  18.         } finally {
  19.             lock.unlock();
  20.         }
  21.     }
  22. }
复制代码
synchronized vs ReentrantLock


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

非阻塞同步基于冲突检测的乐观并发策略,先操作后检测冲突。
CAS(Compare-and-Swap)原理

CAS操作需要三个参数:内存位置V、旧预期值A和新值B。当且仅当V的值等于A时,才用B更新V的值。
  1. public class CASExample {
  2.     private AtomicInteger atomicValue = new AtomicInteger(0);
  3.    
  4.     public void increment() {
  5.         int oldValue;
  6.         int newValue;
  7.         do {
  8.             oldValue = atomicValue.get();  // 获取当前值
  9.             newValue = oldValue + 1;       // 计算新值
  10.         } while (!atomicValue.compareAndSet(oldValue, newValue));  // CAS操作
  11.     }
  12. }
复制代码
Java中的原子类(如AtomicInteger)使用CAS实现无锁线程安全:
  1. public class AtomicExample {
  2.     public static AtomicInteger race = new AtomicInteger(0);
  3.    
  4.     public static void increase() {
  5.         race.incrementAndGet();  // 原子自增
  6.     }
  7.    
  8.     public static void main(String[] args) throws InterruptedException {
  9.         Thread[] threads = new Thread[20];
  10.         
  11.         for (int i = 0; i < threads.length; i++) {
  12.             threads[i] = new Thread(() -> {
  13.                 for (int j = 0; j < 10000; j++) {
  14.                     increase();
  15.                 }
  16.             });
  17.             threads[i].start();
  18.         }
  19.         
  20.         for (Thread thread : threads) {
  21.             thread.join();
  22.         }
  23.         
  24.         System.out.println(race.get());  // 总是输出200000
  25.     }
  26. }
复制代码
ABA问题

CAS操作存在ABA问题:如果一个值从A变成B,又变回A,CAS操作会误以为它没变化。
解决方案:使用AtomicStampedReference或AtomicMarkableReference维护版本号。
  1. public class ABAExample {
  2.     public static void main(String[] args) {
  3.         AtomicStampedReference<Integer> atomicRef =
  4.             new AtomicStampedReference<>(100, 0);
  5.         
  6.         int stamp = atomicRef.getStamp();
  7.         Integer reference = atomicRef.getReference();
  8.         
  9.         // 更新值并增加版本号
  10.         atomicRef.compareAndSet(reference, 101, stamp, stamp + 1);
  11.     }
  12. }
复制代码
3.3 无同步方案

可重入代码(纯代码)

可重入代码不依赖共享数据,所有状态都由参数传入,不会调用非可重入方法。
  1. // 可重入代码示例
  2. public class MathUtils {
  3.     // 纯函数:输出只依赖于输入,没有副作用
  4.     public static int add(int a, int b) {
  5.         return a + b;
  6.     }
  7.    
  8.     // 非纯函数:依赖外部状态
  9.     private int base = 0;
  10.     public int addToBase(int value) {
  11.         return base + value;  // 非可重入,依赖共享状态
  12.     }
  13. }
复制代码
线程本地存储(ThreadLocal)

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

ThreadLocal允许你将状态与线程关联起来,每个线程都有自己独立初始化的变量副本。这些变量通常用于保持线程的上下文信息,如用户会话、事务ID等。
ThreadLocal的基本使用
  1. public class ThreadLocalExample {
  2.     // 创建ThreadLocal变量,并提供初始值
  3.     private static ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 0);
  4.     private static ThreadLocal<String> threadLocalUser = new ThreadLocal<>();
  5.    
  6.     public static void increment() {
  7.         threadLocalCounter.set(threadLocalCounter.get() + 1);
  8.     }
  9.    
  10.     public static int getCounter() {
  11.         return threadLocalCounter.get();
  12.     }
  13.    
  14.     public static void setUser(String user) {
  15.         threadLocalUser.set(user);
  16.     }
  17.    
  18.     public static String getUser() {
  19.         return threadLocalUser.get();
  20.     }
  21.    
  22.     public static void clear() {
  23.         // 清理ThreadLocal变量,防止内存泄漏
  24.         threadLocalCounter.remove();
  25.         threadLocalUser.remove();
  26.     }
  27.    
  28.     public static void main(String[] args) throws InterruptedException {
  29.         Runnable task = () -> {
  30.             // 设置线程用户
  31.             setUser(Thread.currentThread().getName());
  32.             
  33.             // 每个线程独立计数
  34.             for (int i = 0; i < 5; i++) {
  35.                 increment();
  36.             }
  37.             
  38.             System.out.println(Thread.currentThread().getName() +
  39.                 ": Counter=" + getCounter() +
  40.                 ", User=" + getUser());
  41.                
  42.             // 清理ThreadLocal变量
  43.             clear();
  44.         };
  45.         
  46.         // 创建多个线程
  47.         Thread[] threads = new Thread[3];
  48.         for (int i = 0; i < threads.length; i++) {
  49.             threads[i] = new Thread(task, "Thread-" + (i + 1));
  50.             threads[i].start();
  51.         }
  52.         
  53.         // 等待所有线程完成
  54.         for (Thread thread : threads) {
  55.             thread.join();
  56.         }
  57.     }
  58. }
复制代码
ThreadLocal的实现原理

ThreadLocal的实现依赖于每个Thread对象内部的ThreadLocalMap数据结构。下面是ThreadLocal的核心实现机制:
  1. // ThreadLocal的核心方法源码简析
  2. public class ThreadLocal<T> {
  3.     // 获取当前线程的变量值
  4.     public T get() {
  5.         Thread t = Thread.currentThread();
  6.         ThreadLocalMap map = getMap(t); // 获取线程的ThreadLocalMap
  7.         if (map != null) {
  8.             ThreadLocalMap.Entry e = map.getEntry(this);
  9.             if (e != null) {
  10.                 @SuppressWarnings("unchecked")
  11.                 T result = (T)e.value;
  12.                 return result;
  13.             }
  14.         }
  15.         return setInitialValue(); // 设置初始值
  16.     }
  17.    
  18.     // 设置当前线程的变量值
  19.     public void set(T value) {
  20.         Thread t = Thread.currentThread();
  21.         ThreadLocalMap map = getMap(t);
  22.         if (map != null) {
  23.             map.set(this, value);
  24.         } else {
  25.             createMap(t, value); // 创建ThreadLocalMap
  26.         }
  27.     }
  28.    
  29.     // 获取与线程关联的ThreadLocalMap
  30.     ThreadLocalMap getMap(Thread t) {
  31.         return t.threadLocals;
  32.     }
  33.    
  34.     // 创建ThreadLocalMap
  35.     void createMap(Thread t, T firstValue) {
  36.         t.threadLocals = new ThreadLocalMap(this, firstValue);
  37.     }
  38. }
复制代码
Thread、ThreadLocal与ThreadLocalMap的关系

ThreadLocal的实现依赖于Thread类中的两个重要字段:
  1. public class Thread implements Runnable {
  2.     // 线程本地变量Map
  3.     ThreadLocal.ThreadLocalMap threadLocals = null;
  4.    
  5.     // 继承自父线程的线程本地变量Map
  6.     ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  7.    
  8.     // 其他字段和方法...
  9. }
复制代码
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),而值是强引用:
[code]static class Entry extends WeakReference

相关推荐

您需要登录后才可以回帖 登录 | 立即注册