当前位置: 首页 > news >正文

从 Volatile 到 ThreadLocal:Java 线程安全机制备忘

目录

一、volatile(轻量级同步机制)

作用:

1. 保证可见性

2. 禁止指令重排序

2.1指令重排的场景

3. 不保证原子性(简单赋值的话安全)

适用场景:

1、开销较低的读写锁策略

2、 状态标志位

二、无锁/CAS(Compare-And-Swap)

作用:

1、保证原子性

2、实现无锁并发,无阻塞

特点:

1、乐观锁机制

2、高性能但CPU 友好度差

3、依赖硬件指令

4、 存在 ABA 问题

应用场景:

1、原子类操作

2、自旋锁

3、并发集合

三、互斥同步(加锁)

1、synchronized关键字

特点:

自动管理:

可重入:

不可中断:

性能进化:

单等待队列:

锁升级机制

简略:

详细:

应用场景

1、修饰实例方法

2、修饰静态方法

3、修饰代码块

核心原理:

注意:

2、ReentrantLock

常用方法:

特点:

手动管理:

尝试获取:

两种方法重载:

可中断:

示例:

公平性:

耗时对比示例:

多条件队列:

对比synchronized的单队列等待机制

使用ReentrantLock的多队列模型

3、ReentrantReadWriteLock

特点:

读写锁:

适用于读远多于写:

锁降级

执行示例:

四、ThreadLocal

特点:

绝对的线程隔离性

数据跟随线程生命周期

父子线程默认隔离

应用场景:

可继承的InheritableThreadLocal

原理:

触发时机

数据的浅拷贝

不支持线程池

简单示例:

TransmittableThreadLocal(支持线程池)

五、并发工具类

1、 CountDownLatch


一、volatile(轻量级同步机制)

作用:

1. 保证可见性


当一个线程修改了 volatile 变量的值,新值会立即刷新到主内存中
其他线程读取该变量时,会从主内存重新读取最新值,而不是使用工作内存中的缓存值
确保了多线程间变量的可见性


2. 禁止指令重排序


volatile 通过内存屏障机制,防止编译器和处理器对指令进行重排序优化
保证了代码执行的有序性

2.1指令重排的场景

new Singleton()不是原子操作,分为三步1. 分配内存空间2. 调用构造函数初始化对象3. 将 instance 指向分配的内存地址,如果发生指令重排导致线程A先执行13后执行2,

public class Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance == null) { // 第1次检查(无锁) synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }

new Singleton()不是原子操作,分为三步1. 分配内存空间2. 调用构造函数初始化对象3. 将 instance 指向分配的内存地址,如果发生指令重排导致线程A先执行13后执行2,此时instance!=null但还没有初始化,线程B检测到instance!=null就直接返回了没初始化的instance就会发生错误。

要解决的话需要在instance前加volatile修饰,会在 instance = new Singleton() 前后插入内存屏障让其按顺序执行。

3. 不保证原子性(简单赋值的话安全)


volatile如果写操作是简单赋值(不依赖当前值)的话也是安全的, 但不能保证复合操作(如 i++)的原子性。
如果需要原子性,需要使用 synchronized、Lock 或 AtomicInteger 等

适用场景:

1、开销较低的读写锁策略

结合synchronized修饰更新

public class Value1 { private volatile int count = 0; public int getCount() { return count; // 频繁读取,无锁 } public synchronized void increment() { count++; // 偶尔写入,使用同步 } }

2、 状态标志位

最常见的用法,用于控制线程的执行状态

加入volatile修饰后在其他线程(例如B)更改这个变量,正在run()的线程A也能立即响应,不加的话线程 A 可能一直使用工作内存中的旧值 true会出现死循环。

public class TaskRunner { private volatile boolean running = true; public void run() { while (running) { // 执行任务 } } public void stop() { running = false; // 其他线程调用此方法可以停止任务 } }

二、无锁/CAS(Compare-And-Swap)

作用:

CAS 的经典应用场景主要集中在无锁并发编程中。简单来说,就是多个线程同时修改一个变量时,不加锁,而是通过"比较并交换"来保证原子性。

1、保证原子性

2、实现无锁并发,无阻塞

避免了线程上下文切换的开销,避免了死锁风险。

特点:

1、乐观锁机制

2、高性能但CPU 友好度差

在低竞争环境下,性能远高于上述两种锁,因为没有线程上下文切换的开销。
如果竞争极其激烈,大量的自旋会消耗大量 CPU 资源。

3、依赖硬件指令

CAS 不是 Java 独有的,它底层依赖 CPU 提供的原子指令。Java 通过 Unsafe 类调用这些底层指令,所以 CAS 的执行速度非常快(纳秒级)。

4、 存在 ABA 问题

具体来说:

线程1 读到 owner 是 null (A)。
线程2 把 owner 改成 Thread-2 (B),然后又改回 null (A)。
线程1 再次检查,发现还是 null (A),于是 CAS 成功。

应用场景:

1、原子类操作

////Java 的 java.util.concurrent.atomic 包下的所有类都基于 CAS import java.util.concurrent.atomic.AtomicInteger; public class Counter { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); // 底层用 CAS 实现 i++ } public int getCount() { return count.get(); } }

作用:多线程下 i++ 不安全,加 synchronized 太重,CAS 性能更好。

2、自旋锁

import java.util.concurrent.atomic.AtomicReference; public class SpinLock { private AtomicReference<Thread> owner = new AtomicReference<>(); private int holdCount = 0; // 记录重入次数 public void lock() { Thread current = Thread.currentThread(); // 如果当前线程已经持有锁,直接增加计数 if (owner.get() == current) { holdCount++; return; } // 如果 owner 是 null,就改成当前线程;否则一直循环重试 int cnts = 0; while (!owner.compareAndSet(null, current)) { cnts++; if(cnts>10){ Thread.yield(); // 尝试多次后,让出 CPU } } } public void unlock() { Thread current = Thread.currentThread(); if (owner.get() != current) { throw new IllegalMonitorStateException("当前线程不持有锁"); } holdCount--; if (holdCount == 0) { owner.set(null); // 完全释放 } } }

作用:避免线程挂起和唤醒的开销,适合锁持有时间短的场景。上方代码实现了可重复加锁,while循环就是自旋的实现。

3、并发集合

例如并发队列ConcurrentLinkedQueue

import java.util.concurrent.ConcurrentLinkedQueue; ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>(); // 多个线程同时添加,内部用 CAS 保证线程安全 queue.add("item1"); queue.add("item2"); String item = queue.poll(); 第一步:包装节点 把你传进来的元素 e 变成一个 Node 对象。此时这个节点的 next 是 null。 第二步:寻找“真正的”尾节点 这是最关键的一步。因为 tail 指针可能滞后(HOPS 优化),所以不能直接把新节点挂在 tail 后面。 线程会从 tail 开始,顺着 next 往后找。 直到找到一个节点 p,它的 next 是 null。这个 p 才是当前队列里最后一名。 第三步:CAS 抢位置 (p.casNext(null, newNode)) 线程会尝试执行一个原子操作: “如果 p 的 next 还是 null,就把我的新节点挂上去。” 情况 1:成功,说明没人跟你抢,你的节点正式进入队列了! 情况 2:失败,说明在你寻找尾部和执行 CAS 的这一瞬间,Thread B 已经手速更快,把一个节点挂在 p 后面了。 此时,p.next 不再是 null。 你的 CAS 返回 false。 动作:回到 for (;;) 循环开头,重新找最新的尾节点,再次尝试。这就是自旋。 第四步:更新 tail 指针 (casTail) 当你的节点成功挂上去后,你会尝试把全局的 tail 指针移到你的新节点上。 注意:这一步不是必须成功的。 如果此时又有别人插队,导致你的 casTail 失败了,没关系。你的数据已经在链表里了,这就够了。tail 指针可以暂时不动,等下次有机会再移。

作用:传统队列加锁性能差,CAS 可以实现高并发的无锁队列。

三、互斥同步(加锁)

1、synchronized关键字

这是 Java 最原生、最简单的锁。

特点:
自动管理:

不需要手动加锁/解锁,JVM 自动处理,不会发生“忘了释放锁”的情况。

可重入:

同一个线程可以多次获取同一把锁。

不可中断:

一旦进入阻塞状态,只能等待锁释放,不能通过 interrupt() 强行打断。

性能进化:

在 JDK 1.6 之后引入了偏向锁、轻量级锁、重量级锁的升级机制,性能已经非常接近 ReentrantLock。

单等待队列:

synchronized 维护了两个主要的队列:

EntryList(入口队列):那些想进房间但还没拿到锁的线程,在这里排队。

WaitSet(等待队列):那些拿到了锁但因为条件不满足调用 wait() 而释放锁的线程,在这里睡觉。

同一对象不同被synchronized修饰的方法中调用wait()的线程都会添加到同一个WaitSet。被notifyAll()的时候都会被唤醒,可能导致额外资源占用。

锁升级机制

简略:

没有线程竞争时,就使用低开销的“偏向锁”,此时没有额外的 CAS 操作;轻度竞争时,使用“轻量级锁”,采用 CAS 自旋,避免线程阻塞;只有在重度竞争时,才使用“重量级锁”,由 Monitor 机制实现,需要线程阻塞。

详细:
锁升级的四个阶段 第一阶段:无锁 状态:对象没有被任何线程锁定。 表现:Mark Word 存储的是对象的 HashCode 和分代年龄。 触发:刚创建的对象。 第二阶段:偏向锁 核心思想:“偏心”。大多数情况下,锁不仅不存在竞争,而且总是由同一个线程多次获得。 过程: 当第一个线程(Thread A)访问同步块时,JVM 会在对象头的 Mark Word 中记录 Thread A 的 ID。 以后 Thread A 再来加锁,JVM 发现 Mark Word 里的 ID 就是自己,直接执行,无需任何 CAS 或系统调用。 优点:消除所有同步原语,性能最高。 缺点:一旦出现第二个线程(Thread B)来抢锁,偏向锁就失效了。 第三阶段:轻量级锁 核心思想:“自旋/CAS”。当偏向锁失效(有另一个线程来尝试获取),但竞争还不算太激烈时。 过程: 偏向锁撤销,升级为轻量级锁。 线程会在自己的栈帧中创建一个 Lock Record(锁记录)。 线程尝试通过 CAS 将对象头的 Mark Word 替换为指向自己 Lock Record 的指针。 如果成功:获取锁,执行代码。 如果失败:说明有竞争。当前线程会通过自旋(循环重试)尝试获取锁。 优点:避免了线程挂起和唤醒的系统调用开销。 缺点:如果自旋很久还拿不到锁,会白白消耗 CPU。 第四阶段:重量级锁 核心思想:“阻塞/排队”。当竞争非常激烈,或者自旋次数超过阈值时。 过程: 轻量级锁膨胀为重量级锁。 对象头的 Mark Word 指向堆中的 Monitor Object(监视器锁)。 没抢到锁的线程会被挂起(Blocked),进入操作系统的等待队列。 持有锁的线程释放后,操作系统负责唤醒等待队列中的线程。 优点:线程不占用 CPU,适合长时间等待。 缺点:涉及用户态和内核态切换,性能开销最大。

应用场景

1、修饰实例方法

同一时间只有一个线程能调用该对象的同步方法

public class Counter { private int count = 0; public synchronized void increment() { count++; } public synchronized void decrement() { count--; } }
2、修饰静态方法

所有实例共享一把锁,锁定整个类

public class Counter { private static int count = 0; public static synchronized void increment() { count++; } }
3、修饰代码块
核心原理:

线程想要执行 synchronized 代码块中的内容,必须先获取括号内对象的监视器锁。

注意:

只要锁定的是同一个对象引用,内容是否一样都互斥。

1. this(当前实例对象) public void method() { synchronized (this) { // 锁定当前实例 } } 2. 类对象(Class对象) public void method() { synchronized (MyClass.class) { // 锁定整个类,所有实例共享 } }//等价于 synchronized 修饰静态方法。 3. 任意对象引用 public class Counter { private final Object lock = new Object(); private final String lockStr = "lock"; public void method1() { synchronized (lock) { // 推荐:专用锁对象 // 同步代码 } } public void method2() { synchronized (lockStr) { // 也可以,但不推荐 // 同步代码 } } } 4. 成员变量对象 public class Bank { private final List<String> accounts = new ArrayList<>(); public void addAccount(String account) { synchronized (accounts) { /* 锁定accounts对象,只有当多个线程操作的是同一个 accounts 对象时,才会产生锁竞争*/ accounts.add(account); } } }

2、ReentrantLock

这是 Java 5 引入的基于 AQS实现的显式锁。提供比 synchronized 更灵活的锁控制。

常用方法:

lock():获取锁。如果锁被占用,则一直等待。

tryLock():尝试获取锁。如果锁可用则返回 true,否则立即返回 false(不等待)。

tryLock(long time, TimeUnit unit):限时等待获取锁。在指定时间内尝试获取,超时则返回 false。

lockInterruptibly():可中断地获取锁。如果在等待过程中线程被中断,会抛出异常并停止等待。示例见特点内的公平性

unlock():释放锁。

isLocked():判断锁是否被任何线程持有。

isHeldByCurrentThread():判断锁是否被当前线程持有(常用于断言或递归逻辑检查)。

getHoldCount():获取当前线程持有该锁的次数(因为是可重入锁,可能多次加锁)。

hasQueuedThreads():判断是否有线程在等待获取这把锁。

getQueueLength():获取正在等待这把锁的线程数量。

newCondition():创建一个与该锁绑定的 Condition 对象。可以创建多个,实现精准通知,示例见特点中的多条件队列

特点:
手动管理:

必须手动调用 lock() 和 unlock()(通常在 finally 块中)。


尝试获取:

支持 tryLock(),如果拿不到锁可以立即返回或等待指定时间,避免死等。

两种方法重载:

1、tryLock()

不等待,立即尝试获取锁,获取成功返回 true,失败返回 false,不会阻塞线程

2、tryLock(long time, TimeUnit unit) - 等待指定时间

返回true:在超时前成功获取锁,false:超时仍未获取到锁

可能抛出的异常:
InterruptedException:等待过程中被中断


可中断:

支持 lockInterruptibly(),等待锁的过程中可以响应中断。

示例:
// 线程2:尝试获取锁,但可以被中断 Thread thread2 = new Thread(() -> { try { System.out.println("[线程2] 尝试获取锁..."); lock.lockInterruptibly(); // 可中断地获取锁 try { System.out.println("[线程2] 成功获取锁!"); } finally { lock.unlock(); } } catch (InterruptedException e) { System.out.println("[线程2]被中断!不再等待锁,执行其他逻辑"); } }, "Thread-2"); // 中断线程2 thread2.interrupt(); //等待线程结束 thread2.join();

公平性:

可以选择创建公平锁(先到先得)或非公平锁(默认,允许插队,性能更高)。

使用公平锁的场景:需要严格的顺序。公平锁能确保执行顺序。

注意:线程持有锁时间较长的时候和使用非公平锁相差时间不大,此时公平锁耗时的劣势不明显,但线程持有锁时间较短的时候耗时会多很多。

使用非公平锁的场景:大多数通用场景(默认选择),追求高吞吐量,线程持有锁的时间很短。

原因:非公平锁性能更好,例如线程C发来请求的时候刚好线程A释放,线程C直接获取锁避免了如果先唤醒B再唤醒C,C会多一次唤醒操作的开销。

耗时对比示例:
import java.util.concurrent.*; import java.util.concurrent.locks.ReentrantLock; public class main { public static void main(String[] args) throws InterruptedException { /** * 演示公平锁 */ // 创建公平锁:构造函数传入 true ReentrantLock fairLock = new ReentrantLock(true); // 创建多个线程竞争锁 Runnable task = () -> { for (int i = 0; i < 100; i++) { try { fairLock.lock(); } catch (Exception e) { e.printStackTrace(); } finally { fairLock.unlock(); } } }; int threadCount=20; Thread[] threads = new Thread[threadCount]; for (int i = 0; i < threadCount; i++) threads[i] = new Thread(task, "线程" + i); long fairStartTime = System.currentTimeMillis(); for (Thread t:threads){ t.start(); } for (Thread t:threads){ t.join(); } long fairEndTime = System.currentTimeMillis(); long fairCost = fairEndTime - fairStartTime; /** * 演示非公平锁 */ // 创建非公平锁:构造函数传入 false(默认) ReentrantLock nonFairLock = new ReentrantLock(false); // 创建多个线程竞争锁 Runnable task2 = () -> { for (int i = 0; i < 100; i++) { try { nonFairLock.lock(); } catch (Exception e) { e.printStackTrace(); } finally { nonFairLock.unlock(); } } }; Thread[] threads2=new Thread[threadCount]; for (int i = 0; i < threadCount; i++) threads2[i] = new Thread(task2, "线程" + i); long nonFairStartTime = System.currentTimeMillis(); for (Thread t:threads2) t.start(); for (Thread t:threads2) t.join(); long nonFairEndTime = System.currentTimeMillis(); long nonFairCost = nonFairEndTime - nonFairStartTime; System.out.println("公平锁耗时: " + fairCost + " ms"); System.out.println("非公平锁耗时: " + nonFairCost + " ms"); } } //运行结果 公平锁耗时: 23 ms 非公平锁耗时: 3 ms

多条件队列:

可以绑定多个 Condition,实现精确唤醒。

对比synchronized的单队列等待机制

当使用synchronized的时候,JVM会为每个对象维护一个等待队列,wait()进入的队列是属于对象的,而不是属于方法的,意思就是同一对象的消费者和生产者方法中调用的wait()和notifyall()都会直接应用于对象的等待队列。

例如:生产者消费者模型单队列等待会浪费更多资源

public class BlockingQueue { private Queue<Integer> queue = new LinkedList<>(); private final int MAX_SIZE = 10; public synchronized void produce(int item) throws InterruptedException { while (queue.size() == MAX_SIZE) { wait(); // 队列满,生产者进入 WaitSet 等待 } queue.add(item); notifyAll(); // 唤醒 WaitSet 中的所有线程 } public synchronized int consume() throws InterruptedException { while (queue.isEmpty()) { wait(); // 队列空,消费者也进入同一个WaitSet 等待 } int item = queue.poll(); notifyAll(); // 唤醒 WaitSet 中的所有线程 } }

当队列满了的时候,又有10个生产者在等待,如果消费者取出了一个元素,唤醒了所有线程,此时一个生产者线程抢到了锁添加了元素后又会唤醒其他所有生产者线程,这一步是无用浪费了CPU资源的。

使用ReentrantLock的多队列模型
public class ReentrantLockTest { private final Queue<Integer> queue = new LinkedList<>(); private final int capacity;// 队列容量 private final ReentrantLock lock = new ReentrantLock(); // 1. 创建两个条件队列 private final Condition notFull = lock.newCondition(); // 队列不满的条件(生产者) private final Condition notEmpty = lock.newCondition(); // 队列不空的条件(消费者) public ReentrantLockTest(int capacity) { this.capacity = capacity; } /** * 生产者:放入元素 */ public void produce(int item) throws InterruptedException { lock.lock(); try { // 如果队列满了,在 notFull 队列里等待 while (queue.size() == capacity) { notFull.await(); // 释放锁,进入 notFull 等待队列 } queue.add(item); // 只唤醒一个正在等待的消费者 notEmpty.signal(); } finally { lock.unlock(); } } /** * 消费者:取出元素 */ public int consume() throws InterruptedException { lock.lock(); try { // 如果队列空了,在 notEmpty 队列里等待 while (queue.isEmpty()) { notEmpty.await(); // 释放锁,进入 notEmpty 等待队列 } int item = queue.poll(); // 只唤醒一个正在等待的生产者 notFull.signal(); return item; } finally { lock.unlock(); } } }

这里的Condition.signal()每次都只会从指定等待队列中唤醒一个线程,如果队列为空也不会报错,程序会继续运行。

3、ReentrantReadWriteLock

特点:

与ReentrantLock像可重入、独占互斥、公平与非公平模式以及基于AQS底层实现这些核心机制,本质上都是完全通用的。

读写锁:

ReentrantReadWriteLock 内部维护了一对相关的锁:一个用于只读操作(读锁),另一个用于写入操作(写锁)。

读锁是共享的:只要没有线程持有写锁,多个线程可以同时获取读锁。这极大地提高了读多写少场景下的并发吞吐量。

写锁是独占的:当有线程持有写锁时,其他任何线程(无论是读还是写)都必须等待。

适用于读远多于写:

在读写比例接近(如 5:5)甚至写操作更多的场景下,ReentrantReadWriteLock 由于内部需要维护读、写两种复杂的状态,其性能反而会低于 简单直接的 ReentrantLock。 只有当读操作远远多于写操作(例如 80%~90% 以上都是读)时,ReentrantReadWriteLock 的性能优势才会爆发出来。

锁降级

允许一个线程在持有写锁的情况下,再去获取读锁,然后释放写锁。这样做的好处是,线程在更新完数据后,可以立刻以读锁的身份继续持有数据,保证其他线程无法在释放写锁的瞬间插入修改,从而保证了数据的可见性和一致性。

执行示例:
核心步骤: 获取写锁:线程首先获取独占的写锁,准备修改数据。 获取读锁:在持有写锁的同时,再去获取共享的读锁。 释放写锁:释放独占的写锁,但此时线程依然持有读锁。 执行读操作:线程带着读锁去读取刚刚更新的数据。 释放读锁:读取完毕后,释放读锁,完成整个降级过程。 private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); // 1. 获取写锁(独占,开始修改数据) rwLock.writeLock().lock(); private Object data; try { // 执行业务写操作,比如更新缓存数据 data = "最新的数据"; // 2. 在释放写锁之前,先获取读锁 rwLock.readLock().lock(); } finally { // 3. 释放写锁(此时其他写线程依然无法获取锁,因为当前线程还拿着读锁) rwLock.writeLock().unlock(); } try { // 4. 执行读操作(此时可以安全地读取刚刚写入的数据,不用担心被其他线程修改) System.out.println("读取到的数据:" + data); } finally { // 5. 释放读锁 rwLock.readLock().unlock(); }

四、ThreadLocal

特点

绝对的线程隔离性

为每个使用该变量的线程提供独立的变量副本,线程之间互不干扰。

数据跟随线程生命周期

ThreadLocal 的变量副本是绑定在当前线程上的。只要线程存活,副本就一直存在;当线程结束销毁后,对应的变量副本也会被垃圾回收。

父子线程默认隔离

普通的 ThreadLocal 在父子线程之间是完全隔离的。子线程无法直接访问父线程的变量副本(如果需要传递,需要使用它的子类 InheritableThreadLocal)。

应用场景:

在过滤器中将信息存入ThreadLocal。

/* 封装一个工具类来管理 */ public class UserContextHolder { // 使用 private static 修饰,全局唯一 private static final ThreadLocal<Long> USER_ID_HOLDER = new ThreadLocal<>(); // 封装 set 方法 public static void setUserId(Long userId) { USER_ID_HOLDER.set(userId); } // 封装 get 方法 public static Long getUserId() { return USER_ID_HOLDER.get(); } // 封装 remove 方法 public static void clear() { USER_ID_HOLDER.remove(); } } /* 在拦截器或过滤器中 */ public void doFilter(...) { try { //请求进来时,把用户信息存入 ThreadLocal UserContextHolder.setUserId(i); //后续任何地方都能通过 UserContextHolder.getUserId() 拿到当前用户ID chain.doFilter(request, response); } finally { //不清楚在线程池环境下会导致数据串用和内存泄漏 UserContextHolder.clear(); } }

也可以封装一个上下文对象代替这里的userId存到一个ThreadLocal中。

可继承的InheritableThreadLocal

原理:

在 Thread 类的定义中,每个线程都有两个 ThreadLocalMap:

public class Thread { /* 普通 ThreadLocal 变量存储的地方 */ ThreadLocal.ThreadLocalMap threadLocals = null; /* InheritableThreadLocal 变量存储的地方 */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; }

threadLocals
用于存储普通的 ThreadLocal 变量
子线程不能继承父线程的 ThreadLocal 变量


inheritableThreadLocals
用于存储 InheritableThreadLocal 变量
子线程可以继承父线程的 InheritableThreadLocal 变量
在创建子线程时,JVM 会将父线程的 inheritableThreadLocals 复制到子线程中

触发时机

当父线程通过 new Thread() 创建一个新线程时,JVM 会在子线程的初始化阶段进行检测。如果发现父线程的 inheritableThreadLocals 不为空,就会触发数据复制逻辑。

数据的浅拷贝

这里的复制是浅拷贝。对于对象类型的数据,父子线程共享同一个对象引用。如果子线程修改了对象内部的属性,父线程获取到的对象也会受到影响。

不支持线程池

InheritableThreadLocal 的数据复制仅发生在线程被 new 出来的那一瞬间。而线程池的核心机制是“线程复用”,池子里的工作线程早就被创建好了。当父线程向线程池提交新任务时,工作线程并不会重新创建,因此根本不会触发数据复制的逻辑。

简单示例:
// 创建 InheritableThreadLocal InheritableThreadLocal<String> context = new InheritableThreadLocal<>(); // 在父线程中设置值 context.set("父线程的数据"); // 创建子线程,子线程会自动继承这个值 Thread childThread = new Thread(() -> { // 子线程中可以获取到父线程设置的值 System.out.println(context.get()); // 输出: 父线程的数据 }); childThread.start();

TransmittableThreadLocal(支持线程池)

//先在pom.xml加入下列依赖 <dependencies> <!-- TransmittableThreadLocal 依赖 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>transmittable-thread-local</artifactId> <version>2.14.5</version> </dependency> </dependencies> import java.util.concurrent.*; import com.alibaba.ttl.TransmittableThreadLocal; import com.alibaba.ttl.threadpool.TtlExecutors; public class Main { private static final TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>(); public static void main(String[] args) throws InterruptedException { // 设置上下文数据 context.set("主线程数据"); // 创建线程池并用 TTL 包装 ExecutorService executor = Executors.newFixedThreadPool(3); ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(executor); // 提交任务 - 子线程能正确获取父线程的值 ttlExecutor.submit(() -> { String value = context.get(); System.out.println("子线程获取到的值: " + value); }); // 关闭线程池 ttlExecutor.shutdown(); // 等待线程池关闭 try { if (!ttlExecutor.awaitTermination(5, TimeUnit.SECONDS)) { ttlExecutor.shutdownNow(); // 再次等待,确认强制关闭 if (!ttlExecutor.awaitTermination(5, TimeUnit.SECONDS)) { System.out.println("错误:线程池未能正常关闭"); } } else { System.out.println("所有任务已完成"); } } catch (InterruptedException e) { System.out.println("等待过程中被中断,强制关闭线程池"); ttlExecutor.shutdownNow(); Thread.currentThread().interrupt(); } finally { // 清理 TTL 上下文,防止内存泄漏 context.remove(); } } }

五、并发工具类

1、 CountDownLatch

作用:允许一个或多个线程等待其他线程完成一组特定的操作。

核心方法:await():阻塞当前线程,直到计数器归零。

countDown():将计数器减 1(通常在工作线程的 finally 块中调用)。

底层原理:基于 AQS的共享模式实现,内部维护一个 volatile int state 作为计数器,通过 CAS 操作保证线程安全。

特点:一次性使用,计数器归零后无法重置。

应用场景:主线程等待多个子任务(如初始化资源、并行查询接口)全部完成后,再继续执行后续逻辑。

示例:

CountDownLatch latch = new CountDownLatch(nodes.size()); while (true) { String nodeId = readyQueue.poll(); if (nodeId == null) break; // 队列为空,说明所有可执行任务已提交 WorkflowNode node = nodes.stream().filter(n -> n.getId().equals(nodeId)).findFirst().orElse(null); if (node == null) continue; // 提交任务到线程池 executorService.submit(() -> { try { // --- 执行业务逻辑 --- NodeExecutor executor = new ExecutorFactory().getExecutor(node.getType()); // 假设工厂类已实现 // 从全局上下文收集输入 (这里简单传入所有,实际可按需过滤) Map<String, Object> input = new HashMap<>(globalContext); Map<String, Object> output = executor.execute(node, input); // 结果写入全局上下文 globalContext.put(node.getId(), output); System.out.println("节点 " + node.getId() + " 执行完成"); } catch (Exception e) { e.printStackTrace(); // 生产环境需处理失败逻辑(如阻断下游) } finally { // --- 调度逻辑:通知下游节点 --- List<String> successors = reverseDeps.get(node.getId()); if (successors != null) { for (String successorId : successors) { // 入度减 1,如果变为 0 则加入就绪队列 if (inDegreeMap.get(successorId).decrementAndGet() == 0) { readyQueue.offer(successorId); } } } latch.countDown(); } }); } // 7. 等待所有任务执行完毕 latch.await(); System.out.println("DAG 全部执行完毕!"); executorService.shutdown(); } }

---------------------------------------------------------------------------------------------------------------------------------本篇完。

http://www.cnnetsun.cn/news/2744249.html

相关文章:

  • HFSS仿真效率翻倍:巧用Floquet端口分析天线阵列,一个单元搞定整个周期结构
  • HFSS新手避坑指南:波端口和集总端口到底怎么选?手把手教你设置(附尺寸估算技巧)
  • AI工具链断裂导致虚拟主播“失语”?一文讲透RAG+TTS+VAD+ASR四层协同架构(含可运行Docker Compose配置)
  • 深度学习中过拟合的统一机制与DOM框架解析
  • 如何快速构建Go语言网络自动化工具:终极完整指南
  • OpenBCI Cyton/Ganglion/WiFi板的Python即用型数据采集工具包,含UDP/串口/MNE接口
  • PSINS工具箱入门第一步:手把手教你用glvf函数初始化地球参数(附完整参数表)
  • 医疗问答系统毕设包:Django前后端+MySQL用户数据+Neo4j疾病关系图谱(含部署文档、论文与演示PPT)
  • 告别玄学调试:用CubeMX仿真一步步揪出Boot跳转App跑飞的元凶
  • mcu内存
  • 告别Redis?用C语言写的LMDB内存数据库,在嵌入式场景下到底有多快?
  • 锂电SOC实时预测代码包:Informer-LSTM混合模型+多工况数据+可视化结果
  • 多通路炎症因子同步精准检测Luminex检测多因子重构免疫研究新生态,武汉云克隆多因子树立行业新标杆
  • 告别OPC!用Snap7和Visual Studio 2022轻松搞定西门子PLC通信(附避坑指南)
  • Claude智能工作台:Projects+Memory+Skills全栈配置指南
  • 极路由2 HC5761救砖记:TTL线救活‘认证失败’变砖机,保姆级刷机教程
  • 51单片机实现实时自适应温控:神经元PID算法+电炉仿真+LCD显示
  • 生命周期实际业务用法
  • 水果翻牌游戏新特性接入
  • 从一次HTTPS握手失败排查说起:JDK8默认加密限制如何“坑”了你的Spring Boot应用
  • 别再手动拼接了!CAPL脚本中整型数组与Hex字符串互转的通用函数库(附完整源码)
  • 告别地址冲突!I3C总线动态地址分配(ENTDAA)保姆级流程与实战避坑
  • Surface Pro4电池鼓包别慌!手把手教你用吹风机+塑料板安全拆屏换电池(附SSD升级指南)
  • RAG系统实战:从Elasticsearch到混合检索与重排序落地
  • Grok-3技术解析与API实战指南
  • 如何用快马AI在5分钟内为你的软件搭建一个girigo式下载页面原型
  • 2026 年 AI 数字人直播系统全面测评:技术、成本与转化的深度博弈
  • 2026年6月Claude Code新技能:安装使用全指南
  • 从‘锅盖’到星链:一文读懂卫星天线角度的演变与底层原理(附极化角图解)
  • AI Mock 数据生成:Schema 解析与自动校验策略