J.U.C Review - Java线程间的通信

news2024/9/25 11:11:33

文章目录

  • Java线程间的通信
    • 无锁的程序
    • 锁与同步
    • 等待/通知机制
    • 信号量
    • 管道
  • 其它通信相关
    • join方法
      • join 方法概述
      • 底层实现细节
      • 小结
    • sleep方法
      • 1. `Thread.sleep` 方法详解
      • 2. `sleep` 和 `wait` 的主要区别
      • 3. 实际代码示例
    • ThreadLocal类
      • `ThreadLocal` 的基本概念
      • `ThreadLocal` 的主要方法
      • `ThreadLocal` 的使用场景
      • 示例代码
      • 为什么使用 `ThreadLocal`
      • 注意事项
      • 小结
    • `InheritableThreadLocal`
      • 基本概念
      • `InheritableThreadLocal` 的使用场景
      • 示例代码
      • 自定义继承行为
      • 注意事项
      • 小结
    • TransmittableThreadLocal
      • 背景
      • `TransmittableThreadLocal` 的特点
      • 使用方法
      • 适用场景
      • 总结

在这里插入图片描述


Java线程间的通信

无锁的程序

public class NoneLock {

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("Thread A " + i);
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println("Thread B " + i);
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new ThreadA()).start();
        new Thread(new ThreadB()).start();
    }
}

线程A和线程B各自独立工作,输出自己的打印值。

在这里插入图片描述

现在有一个需求,我想等A先执行完之后,再由B去执行,怎么办呢?最简单的方式就是使用一个“对象锁”


锁与同步

在Java中,锁(Lock)是用来保护资源的。当一个线程拿到锁后,其他线程就必须等待,直到这个线程释放锁。我们可以用婚姻来比喻:一个锁一次只能被一个线程“持有”或“结婚”,其他线程必须等待,直到这个锁被“释放”或“离婚”。

为了确保多个线程能按顺序执行,我们可以使用锁来实现线程同步。比如,如果我们不使用锁,两个线程可能会在控制台同时输出,导致结果是混乱的。但如果我们希望线程A先完成,线程B再开始,我们就可以用锁来确保顺序。

示例代码:

public class ObjectLock {
    private static Object lock = new Object();

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100; i++) {
                    System.out.println("Thread A " + i);
                }
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100; i++) {
                    System.out.println("Thread B " + i);
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        // 在主线程里使用sleep方法睡眠了10毫秒,是为了防止线程B先得到锁。因为如果同时start,线程A和线程B都是出于就绪状态,操作系统可能会先让B运行。这样就会先输出B的内容,然后B执行完成之后自动释放锁,线程A再执行
        Thread.sleep(10);
        new Thread(new ThreadB()).start();
    }
}

在这个代码中,声明了一个名字为lock的对象锁。我们在ThreadAThreadB内需要同步的代码块里,都是用synchronized关键字加上了同一个对象锁lock. 线程A和线程B都使用同一个对象锁lock。当一个线程持有锁时,另一个线程必须等待锁被释放。这样,我们就确保了线程A和线程B不会同时执行。


等待/通知机制

锁虽然能确保线程同步,但可能会浪费资源,因为线程需要不停尝试获取锁。Java提供了另一种方式:等待/通知机制。

Java多线程的等待/通知机制是基于Object类的wait()方法和notify(), notifyAll()方法来实现的。

notify()方法会随机叫醒一个正在等待的线程,而notifyAll()会叫醒所有正在等待的线程。

一个锁同一时刻只能被一个线程持有。而假如线程A现在持有了一个锁lock并开始执行,它可以使用lock.wait()让自己进入等待状态。这个时候,lock这个锁是被释放了的。

这时,线程B获得了lock这个锁并开始执行,它可以在某一时刻,使用lock.notify(),通知之前持有lock锁并进入等待状态的线程A,说“线程A你不用等了,可以往下执行了”。

需要注意的是,这个时候线程B并没有释放锁lock,除非线程B这个时候使用lock.wait()释放锁,或者线程B执行结束自行释放锁,线程A才能得到lock锁。

代码示例:

public class WaitAndNotify {
    private static Object lock = new Object();

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    try {
                        System.out.println("ThreadA: " + i);
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    try {
                        System.out.println("ThreadB: " + i);
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(1000);
        new Thread(new ThreadB()).start();
    }
}

在这里插入图片描述

在这个例子中,线程A和线程B交替执行。每次一个线程执行完后都会使用notify()唤醒另一个线程,然后自己使用wait()方法陷入等待并释放lock


信号量

JDK提供了一个类似于“信号量”功能的类Semaphore。这里我们介绍一种基于volatile关键字的自己实现的信号量通信。

信号量是一种用于控制多个线程访问公共资源的机制。我们可以使用volatile关键字来实现简单的信号量。volatile保证变量的更新对其他线程可见。

volatile关键字能够保证内存的可见性,如果用volatile关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其它线程是立马可见更改后的值的。

代码示例:

public class Signal {
    private static volatile int signal = 0;

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            while (signal < 5) {
                if (signal % 2 == 0) {
                    System.out.println("threadA: " + signal);
                    signal++;
                }
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            while (signal < 5) {
                if (signal % 2 == 1) {
                    System.out.println("threadB: " + signal);
                    signal++;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(1000);
        new Thread(new ThreadB()).start();
    }
}

在这里插入图片描述

signal变量控制线程A和线程B的交替执行。线程A在signal为偶数时执行,线程B在signal为奇数时执行。

我们使用了一个volatile变量signal来实现了“信号量”的模型。这里需要注意的是,volatile变量需要进行原子操作。

需要注意的是,signal++并不是一个原子操作,所以在实际开发中,会根据需要使用synchronized给它“上锁”,或者是使用AtomicInteger等原子类。并且上面的程序也并不是线程安全的,因为执行while语句后,可能当前线程就暂停等待时间片了,等线程醒来,可能signal已经大于等于5了


管道

管道是一种线程间通信的方式,主要用于I/O流。

JDK提供了PipedWriterPipedReaderPipedOutputStreamPipedInputStream。其中,前面两个是基于字符的,后面两个是基于字节流的。

Java提供了基于字符和字节的管道流,我们可以使用这些管道在线程之间传递数据。

示例代码:

package com.artisan.thread;

import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;

/**
 * 定义Pipe类,用于演示PipedInputStream和PipedOutputStream如何进行数据传输
 */
public class Pipe {

    /**
     * 定义ReaderThread类,负责从PipedReader中读取数据
     */
    static class ReaderThread implements Runnable {
        private PipedReader reader;

        /**
         * 构造函数,初始化PipedReader
         *
         * @param reader PipedReader对象
         */
        public ReaderThread(PipedReader reader) {
            this.reader = reader;
        }

        /**
         * 实现Runnable接口的run方法,在新线程中执行读取操作
         */
        @Override
        public void run() {
            System.out.println("this is reader");
            int receive = 0;
            try {
                // 循环读取数据,直到没有数据可读
                while ((receive = reader.read()) != -1) {
                    System.out.print((char) receive);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 定义WriterThread类,负责向PipedWriter中写入数据
     */
    static class WriterThread implements Runnable {

        private PipedWriter writer;

        /**
         * 构造函数,初始化PipedWriter
         *
         * @param writer PipedWriter对象
         */
        public WriterThread(PipedWriter writer) {
            this.writer = writer;
        }

        /**
         * 实现Runnable接口的run方法,在新线程中执行写入操作
         */
        @Override
        public void run() {
            System.out.println("this is writer");
            int receive = 0;
            try {
                // 向PipedWriter写入测试数据
                writer.write("test");
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                // 关闭PipedWriter
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 主函数,用于启动ReaderThread和WriterThread的示例
     */
    public static void main(String[] args) throws IOException, InterruptedException {
        PipedWriter writer = new PipedWriter();
        PipedReader reader = new PipedReader();
        // 确保PipedWriter和PipedReader连接,以便数据可以传输
        writer.connect(reader);

        // 启动ReaderThread
        new Thread(new ReaderThread(reader)).start();
        // 等待一段时间后启动WriterThread,以确保线程启动顺序
        Thread.sleep(1000);
        // 启动WriterThread
        new Thread(new WriterThread(writer)).start();
    }
}

输出

this is reader
this is writer
test
  1. 线程ReaderThread开始执行,
  2. 线程ReaderThread使用管道reader.read()进入”阻塞“,
  3. 线程WriterThread开始执行,
  4. 线程WriterThread用writer.write(“test”)往管道写入字符串,
  5. 线程WriterThread使用writer.close()结束管道写入,并执行完毕,
  6. 线程ReaderThread接受到管道输出的字符串并打印,
  7. 线程ReaderThread执行完毕。

ReaderThread从管道中读取数据,而WriterThread则向管道中写入数据。管道用于在两个线程之间传递信息。


其它通信相关

除了上面介绍的通信方式,还有一些其他的方法可以用来实现线程间的通信。

join方法

join()方法可以让一个线程等待另一个线程执行完成。例如,如果主线程希望等子线程执行完后再继续执行,就可以使用join()方法。

示例代码:

package com.artisan.thread;

/**
 * Join类用于演示Thread类的join方法的使用
 */
public class Join {

    /**
     * ThreadA是一个实现了Runnable接口的线程类
     * 它在run方法中模拟了一个长时间运行的任务
     */
    static class ThreadA implements Runnable {

        /**
         * run方法定义了线程执行时的行为
         * 它模拟了一个子线程,在运行时先睡眠1秒,然后醒来
         */
        @Override
        public void run() {
            try {
                System.out.println("我是子线程,我先睡一秒");
                Thread.sleep(1000); // 子线程睡眠1秒,模拟长时间运行的任务
                System.out.println("我是子线程,我睡完了一秒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * main方法是程序的入口
     * 它创建并启动了一个ThreadA线程,并使用join方法确保主线程等待子线程执行完毕后再继续执行
     * @param args 命令行参数
     * @throws InterruptedException 如果主线程等待被中断
     */
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new ThreadA()); // 创建子线程
        thread.start(); // 启动子线程
        thread.join(); // 确保主线程等待子线程执行完毕后再继续
        System.out.println("如果不加join方法,我会先被打出来,加了就不一样了");
    }
}


输出

我是子线程,我先睡一秒
我是子线程,我睡完了一秒
如果不加join方法,我会先被打出来,加了就不一样了

join 方法概述

  • join(long millis): 使当前线程等待调用此方法的线程终止,最长等待时间由 millis 参数指定,单位为毫秒。
  • join(long millis, int nanos): 除了接受一个毫秒数外,还接受一个额外的纳秒数(范围在0到999999之间),用于更细粒度的时间控制。但需注意,这个纳秒参数并不能保证绝对的精度。

底层实现细节

不论是 join(long) 还是 join(long, int),它们实际上都依赖于对象的监视器(monitor)来实现等待逻辑,具体是通过调用 Object 类的 wait(long timeout) 方法或其变体。这意味着调用 join 的线程会释放它当前持有的任何锁,并进入等待状态,直到目标线程执行完毕或者超时。

关于 join(long, int) 方法中的纳秒参数处理:JDK 实现中确实没有直接按纳秒精度去精确控制等待时间,这部分更多是一个兼容性设计,以保持 API 的一致性和向后兼容性。实际上,这个纳秒值会被合并到毫秒数中进行计算,然后传递给底层的等待方法,这导致了纳秒级别的精度无法直接体现。

小结

join 方法提供了让一个线程等待另一个线程完成的能力,主要通过两个重载形式实现不同时间精度的等待控制。尽管存在纳秒级别的参数输入,实际精度受限于 Java 虚拟机对线程调度和监控器操作的实现,通常并不保证达到纳秒级的精确控制。这些方法的底层机制涉及线程间的同步与等待,是通过监视器对象和 wait/notify 机制实现的。


sleep方法

sleep()方法让当前线程暂停一段时间。它是Thread类的静态方法,常用于模拟延迟或暂停执行。

需要注意的是,sleep()方法不会释放锁,而wait()方法会。


1. Thread.sleep 方法详解

Thread.sleepThread 类的一个静态方法,用于让当前线程暂停执行一段指定的时间。它有两个重载版本:

  • Thread.sleep(long millis):使当前线程暂停执行指定的毫秒数 (millis)。
  • Thread.sleep(long millis, int nanos):使当前线程暂停执行指定的毫秒数 (millis) 加上指定的纳秒数 (nanos)。

注意: sleep 方法并不精确到纳秒。JDK 1.8 的实现中,Thread.sleep(long millis, int nanos) 通过对纳秒数 (nanos) 的简单处理,仍然调用的是 Thread.sleep(long millis),即实际暂停时间是一个接近于(但不一定精确)的值。

2. sleepwait 的主要区别

1. 锁的释放:

  • Thread.sleep 方法不会释放当前线程持有的锁。这意味着如果线程在一个同步块或同步方法中调用了 sleep,其他线程无法获得这个锁,直到该线程重新获得CPU资源并退出同步块或同步方法。

  • Object.wait 方法会释放当前线程持有的锁。这意味着调用 wait 的线程会放弃锁,允许其他线程进入同步块或同步方法。线程会进入等待状态,直到被其他线程通过 notifynotifyAll 唤醒,并重新获得锁。

2. CPU资源的释放:

  • 两者都会释放 CPU 资源,意味着这段时间内不会执行任何代码。

  • wait 方法会将线程放入对象的等待池中,并且必须配合 notifynotifyAll 来唤醒。

  • sleep 只是单纯地让线程休眠一段时间,时间到后自动进入可运行状态(Runnable)。

3. 使用位置和使用场景:

  • Thread.sleep 可以在任意位置调用,并不要求在同步块或同步方法中。它是一个线程控制的方法,常用于在多线程编程中制造延迟。

  • Object.wait 必须在同步块或同步方法中调用。这是因为 wait 需要当前线程持有对象的锁,它通常用于线程间的通信。

3. 实际代码示例

Thread.sleep 示例:

public class SleepExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Thread is going to sleep");
            try {
                Thread.sleep(2000);  // 睡眠2秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread has woken up");
        });
        thread.start();
    }
}

Object.wait 示例:

public class WaitExample {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("Thread 1 is waiting");
                try {
                    lock.wait();  // 线程1等待,释放锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 1 is resumed");
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("Thread 2 is notifying");
                lock.notify();  // 唤醒等待的线程1
            }
        });

        thread1.start();
        try {
            Thread.sleep(1000);  // 确保thread1先启动并等待
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.start();
    }
}

在这个例子中,thread1lock 对象上调用了 wait(),使其进入等待状态并释放锁。随后,thread2 在相同的 lock 对象上调用了 notify(),唤醒了 thread1


ThreadLocal类

ThreadLocal为每个线程提供独立的变量副本,确保各线程互不干扰。它常用于保存线程独立的数据,如数据库连接或Session。

package com.artisan.thread;
public class ThreadLocalDemo {

    // 实现Runnable接口的内部类ThreadA,用于演示ThreadLocal的使用
    static class ThreadA implements Runnable {
        private ThreadLocal<String> threadLocal;

        // 构造函数,初始化ThreadLocal实例
        public ThreadA(ThreadLocal<String> threadLocal) {
            this.threadLocal = threadLocal;
        }

        @Override
        public void run() {
            // 在当前线程中,为ThreadLocal变量设置一个值
            threadLocal.set("A");
            try {
                // 让线程睡眠1秒,模拟一段时间内的操作
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 打印当前线程中ThreadLocal变量的值
            System.out.println("ThreadA输出:" + threadLocal.get());
        }
    }

    // 实现Runnable接口的内部类ThreadB,与ThreadA类似,但设置不同的值
    static class ThreadB implements Runnable {
        private ThreadLocal<String> threadLocal;

        // 构造函数,初始化ThreadLocal实例
        public ThreadB(ThreadLocal<String> threadLocal) {
            this.threadLocal = threadLocal;
        }

        @Override
        public void run() {
            // 在当前线程中,为ThreadLocal变量设置一个值
            threadLocal.set("B");
            try {
                // 让线程睡眠1秒,模拟一段时间内的操作
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 打印当前线程中ThreadLocal变量的值
            System.out.println("ThreadB输出:" + threadLocal.get());
        }
    }

    // 主函数,创建ThreadLocal实例,并启动两个使用该实例的线程
    public static void main(String[] args) {
        // 创建ThreadLocal实例,用于在不同线程中传递数据
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        // 创建并启动ThreadA实例所在的线程
        new Thread(new ThreadA(threadLocal)).start();
        // 创建并启动ThreadB实例所在的线程
        new Thread(new ThreadB(threadLocal)).start();
    }
}

输出

ThreadB输出:B
ThreadA输出:A

ThreadLocal 的基本概念

ThreadLocal 是 Java 中提供的一种机制,用于在多线程环境下实现线程局部变量。简单来说,ThreadLocal 为每个线程提供了一个独立的变量副本,这意味着每个线程都可以独立地访问和修改自己的副本,而不会影响其他线程的副本。

  • 线程局部变量:每个线程都有自己的 ThreadLocal 变量副本,其他线程无法访问或修改这个副本。
  • 隔离性ThreadLocal 通过提供线程专属的变量副本,确保了多线程环境中的数据隔离,避免了线程间的数据冲突。

ThreadLocal 的主要方法

  • set(T value): 设置当前线程的 ThreadLocal 变量的值。
  • get(): 获取当前线程的 ThreadLocal 变量的值。如果当前线程没有设置过值,且 ThreadLocal 变量有初始值,会返回初始值。
  • remove(): 删除当前线程的 ThreadLocal 变量值,释放资源。

ThreadLocal 的使用场景

ThreadLocal 常用于在多线程编程中需要保持线程独立状态的场景。典型的使用场景包括:

  1. 用户会话管理:在 Web 应用中,每个请求都可能在不同的线程中处理,可以使用 ThreadLocal 来存储每个线程独立的用户会话信息。

  2. 数据库连接:在多线程环境下,通过 ThreadLocal 可以为每个线程创建一个独立的数据库连接,避免线程间的资源冲突。

  3. 事务管理:当处理事务时,ThreadLocal 可以用来存储每个线程的事务上下文信息,确保线程独立处理事务。

示例代码

以下是一个简单的 ThreadLocal 使用示例:

public class ThreadLocalDemo {
    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        Runnable task = () -> {
            int value = threadLocal.get();
            value += 1;
            threadLocal.set(value);
            System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
        };

        Thread thread1 = new Thread(task, "Thread 1");
        Thread thread2 = new Thread(task, "Thread 2");

        thread1.start();
        thread2.start();
    }
}

输出示例:

Thread 1: 1
Thread 2: 1

在这个示例中,threadLocal 为每个线程维护了一个独立的整数副本。即使它们使用了同一个 ThreadLocal 实例,两个线程仍然拥有独立的值。

为什么使用 ThreadLocal

在某些情况下,使用 ThreadLocal 是更为优雅和方便的解决方案。例如:

  • 避免参数传递的复杂性:当多个方法或类需要共享某个变量时,通过 ThreadLocal 可以隐式地传递该变量,而不需要在每个方法中显式地传递参数。

  • 确保线程安全:在并发环境中,ThreadLocal 可以确保每个线程操作的变量是独立的,从而避免了线程安全问题。

注意事项

  • 内存泄漏:如果没有在适当的时机调用 remove() 方法,ThreadLocal 可能会导致内存泄漏,特别是在使用线程池的情况下,因为线程池中的线程是重用的。

小结

ThreadLocal 是一种为每个线程提供独立变量副本的机制,用于在多线程环境中实现线程间数据隔离。它简化了线程独立状态的管理,常用于会话管理、数据库连接等场景,但使用时需注意内存管理和合理性。


InheritableThreadLocal

基本概念

InheritableThreadLocal 是 Java 中的一个特殊版本的 ThreadLocal,用于在子线程中继承父线程的 ThreadLocal 变量值。与普通的 ThreadLocal 不同,InheritableThreadLocal 允许一个线程的子线程访问父线程中已经设置的本地变量副本。

  • 继承机制:当一个线程创建了子线程时,子线程会自动继承父线程中 InheritableThreadLocal 的值。这使得子线程可以共享父线程的上下文信息,例如用户身份、事务上下文等。

  • ThreadLocal 的区别:普通的 ThreadLocal 变量在子线程中是不可见的,而 InheritableThreadLocal 使得子线程能够访问父线程在 InheritableThreadLocal 中设置的值。

InheritableThreadLocal 的使用场景

InheritableThreadLocal 主要用于在父线程和子线程之间传递一些需要共享的上下文信息,例如:

  1. 用户身份信息:在父线程中保存用户身份信息,子线程可以直接访问而不需要显式传递。

  2. 事务管理:在父线程中开始一个事务,并在子线程中继续使用相同的事务上下文。

  3. 日志跟踪:在多线程环境中,传递和记录与父线程相关的上下文信息,如日志中的追踪ID。

示例代码

package com.artisan.thread;

/**
 * InheritableThreadLocalDemo 类演示了如何使用 InheritableThreadLocal
 * 在父线程和子线程之间传递值
 */
public class InheritableThreadLocalDemo {
    // 创建一个 InheritableThreadLocal 对象,用于在不同线程之间传递字符串值
    private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

    /**
     * 程序的入口点
     * 在主线程中设置 InheritableThreadLocal 的值,并启动一个子线程来展示值的继承
     *
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        // 在主线程中设置 InheritableThreadLocal 的值
        inheritableThreadLocal.set("Parent Thread Value");

        // 创建并启动一个子线程,子线程会继承主线程的 InheritableThreadLocal 值
        Thread childThread = new Thread(() -> {
            // 子线程中打印 InheritableThreadLocal 的值
            System.out.println("Child Thread InheritableThreadLocal Value: " + inheritableThreadLocal.get());
        });
        childThread.start();

        // 主线程中打印 InheritableThreadLocal 的值
        System.out.println("Main Thread InheritableThreadLocal Value: " + inheritableThreadLocal.get());
    }
}

输出示例:

Main Thread InheritableThreadLocal Value: Parent Thread Value
Child Thread InheritableThreadLocal Value: Parent Thread Value

在这个示例中,父线程设置了 InheritableThreadLocal 的值 "Parent Thread Value"。当创建并启动子线程时,子线程能够继承并访问这个值。

自定义继承行为

如果希望在子线程中修改继承的值或进行特殊处理,可以通过重写 InheritableThreadLocalchildValue(T parentValue) 方法来定制子线程的值。例如:

InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<String>() {
        @Override
        protected String childValue(String parentValue) {
            return parentValue + " - Modified in Child Thread";
        }
    };

Demo:

public class InheritableThreadLocalDemo {
    // 创建一个 InheritableThreadLocal 对象,用于在不同线程之间传递字符串值
//    private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();


    private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<String>() {
        @Override
        protected String childValue(String parentValue) {
            return parentValue + " - Modified in Child Thread";
        }
    };

    /**
     * 程序的入口点
     * 在主线程中设置 InheritableThreadLocal 的值,并启动一个子线程来展示值的继承
     *
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        // 在主线程中设置 InheritableThreadLocal 的值
        inheritableThreadLocal.set("Parent Thread Value");

        // 创建并启动一个子线程,子线程会继承主线程的 InheritableThreadLocal 值
        Thread childThread = new Thread(() -> {
            // 子线程中打印 InheritableThreadLocal 的值
            System.out.println("Child Thread InheritableThreadLocal Value: " + inheritableThreadLocal.get());
        });
        childThread.start();

        // 主线程中打印 InheritableThreadLocal 的值
        System.out.println("Main Thread InheritableThreadLocal Value: " + inheritableThreadLocal.get());
    }
}

输出:

Main Thread InheritableThreadLocal Value: Parent Thread Value
Child Thread InheritableThreadLocal Value: Parent Thread Value - Modified in Child Thread

注意事项

  • 继承限制:虽然子线程可以继承父线程的 InheritableThreadLocal 值,但子线程对值的修改不会影响父线程的值。

  • 内存泄漏风险:和 ThreadLocal 类似,如果不适当地使用 InheritableThreadLocal,也可能会导致内存泄漏问题,尤其是在线程池中使用时,需谨慎处理 remove() 操作。

小结

InheritableThreadLocal 扩展了 ThreadLocal 的功能,使得父线程的本地变量可以被其子线程继承。这对于需要在多个线程之间共享上下文信息的场景非常有用,如用户身份管理、事务管理等。然而,使用时需要注意避免内存泄漏和其他潜在问题。


TransmittableThreadLocal

背景

在 Java 的并发编程中,ThreadLocalInheritableThreadLocal 提供了线程本地变量的功能,但它们在一些复杂场景中存在局限性:

  • 线程池复用问题:在使用线程池时,线程被复用,导致子线程执行时可能使用的是之前其他任务的线程局部变量,InheritableThreadLocal 不能保证每次任务的隔离性。
  • 异步任务问题:在线程池或异步框架(如 CompletableFutureForkJoinPool)中,子线程可能在不同的时间段被执行,父子线程之间的上下文传递变得复杂且不可靠。

为了解决这些问题,阿里巴巴开发了 TransmittableThreadLocal(简称 TTL),它能够确保在线程池复用和异步任务场景下,线程上下文信息能够正确传递。

TTL(TransmittableThreadLocal)是阿里巴巴开源的一个增强版 ThreadLocal,专门用于解决在使用线程池、异步任务或其他线程复用场景下,InheritableThreadLocal 失效的问题。它在跨线程任务时,能够自动传递线程上下文信息,确保父线程的 ThreadLocal 变量在子线程中也能正确获取。

TransmittableThreadLocal 的特点

  1. 线程池复用安全TTL 可以正确地将父线程的 ThreadLocal 变量值传递给子线程,即使子线程是从线程池中复用的。
  2. 跨线程传递:即使线程在不同时间点执行,TTL 也能确保父线程的上下文信息被子线程正确继承。
  3. 自动清理机制:避免了 ThreadLocal 可能导致的内存泄漏问题。

使用方法

import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.TtlRunnable;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TTLExample {
    private static TransmittableThreadLocal<String> ttl = new TransmittableThreadLocal<>();

    public static void main(String[] args) {
        ttl.set("Parent Value");

        ExecutorService executorService = Executors.newFixedThreadPool(2);

        // 通过 TtlRunnable 包装 Runnable,以传递 ThreadLocal 变量
        Runnable task = TtlRunnable.get(() -> {
            System.out.println(Thread.currentThread().getName() + ": " + ttl.get());
        });

        executorService.submit(task);
        executorService.submit(task);

        executorService.shutdown();
    }
}

输出示例:

pool-1-thread-1: Parent Value
pool-1-thread-2: Parent Value

TransmittableThreadLocal 确保了即使线程池复用线程,每个线程仍然能够获取到父线程设置的 ThreadLocal 变量值。

适用场景

  • 分布式系统上下文传递:在微服务或分布式系统中,传递上下文(如请求追踪ID、用户身份信息)是非常常见的需求,TTL 可以确保这些上下文信息在异步和多线程场景下被正确传递。
  • 日志跟踪:可以确保在不同的线程中,日志记录的信息具有一致性,比如同一个请求的所有日志都包含相同的追踪ID。
  • 异步任务处理:在使用异步框架或多线程处理任务时,TTL 能确保子任务能够继承父任务的上下文。

总结

TransmittableThreadLocal 是阿里巴巴为了解决线程池复用、异步任务上下文传递问题而开发的工具,扩展了 ThreadLocal 的功能,使得在复杂并发场景下也能保持上下文信息的一致性。它适用于需要在父子线程间传递信息的场景,如分布式追踪、日志管理等,但使用时应注意可能的性能影响和复杂性。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2089829.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

STM32——看门狗(独立/窗口)

程序运行的保障措施&#xff0c;需要在程序中定期喂狗实现&#xff0c;如果某次没有喂&#xff0c;表示程序出现卡死或者其他状态&#xff0c;此时看门狗就会自动复位电路&#xff0c;防止程序长时间卡死。相当于自动复位电路。 独立看门狗&#xff1a;有单独的时钟LSI 窗口看…

谷歌发布新AI GameNGen:AI也能实时生成游戏画面!

有关 GameNGen 的帖子 又有一则消息直接让全网为之狂欢&#xff01;Google 推出了一个实时AI生成的游戏引擎 GameNGen。目前 GameNGen 生成3D游戏的祖宗《BOOM》的视频已经火遍 X 平台&#xff0c;在视频中&#xff0c;游戏画面每一个画面都是由AI实时生成&#xff0c;可以说是…

应用层协议(下)Https加密Http的秘密(含逻辑图解 简单易学 通俗易懂!)

绪论​ “如今我努力奔跑&#xff0c;不过是为了追上那个曾经被寄予厚望的自己 —— 约翰丶利文斯顿”&#xff0c;本章承接上章Http&#xff0c;没看过强烈建议看后再看本章&#xff0c;本章主要就是学习Https是干什么的并且去底层的学习Http的原理&#xff0c;将会讲到Https的…

pdf转dwg怎么转换?5个软件教你轻松转换文件

pdf转dwg怎么转换&#xff1f;5个软件教你轻松转换文件 将PDF文件转换为DWG格式可以帮助你将静态的图像和矢量图形转换为可编辑的CAD图纸。这在建筑、工程和设计领域尤为重要&#xff0c;因为它可以让你在CAD软件中进一步编辑和利用这些图纸数据。以下是五款能够帮助你轻松将P…

查看显卡cuda版本

1.命令行窗口 打开cmd&#xff0c;输入下列语句 nvidia-smi 如下图红框所示&#xff1a; 2.查看cuda版本&#xff0c;打开英伟达控制面板&#xff0c;桌面右键或者系统右下角&#xff0c;然后点击系统信息&#xff0c;之后点击组件

锻炼思考力的有效实践

1. 意识觉醒 意识觉醒是提升思考力最重要的一个点&#xff0c;我认为。只要形成了这种意识&#xff0c;就已经成功了一半。 很多同学思维能力没有上去&#xff0c;是没有意识到思考力这个概念&#xff0c;只是机械地做事情&#xff0c;做事情&#xff0c;做事情……每次都在同…

C++ Vector容器操作

vector赋值操作 提供三种方式进行赋值 assign函数是STL中提供的一个成员函数&#xff0c;assign() 函数可以重新定义向量的内容&#xff0c;可以用于设置特定数量的重复元素&#xff0c;或者直接用另一个容器的元素来替换当前向量的元素。 //直接赋值&#xff0c;类似于拷贝 …

软考高级证书拿到手了,怎么评职称?

软考&#xff0c;是以考代评&#xff0c;以一个考试来代替提交若干材料的评职称的过程。 但是评聘是分开的。就是指评审或考试后取得软考证书之后&#xff0c;也需要你们单位聘任你&#xff0c;才可以享受相应职称工资待遇&#xff0c;担任相应专业技术职位。 也就是说&#…

【FPGA】入门学习路线

文章目录 编程语言语法RTL设计RTL验证学习方法仿真工具 基础知识&#xff08;1&#xff09;专业基础课&#xff08;2&#xff09;FPGA相关专业知识&#xff08;3&#xff09;FPGA开发流程 开发工具动手实验 编程语言 硬件描述语言HDL&#xff08;Hardware Description Languag…

项目型企业管理系统有哪些?这10款值得研究

国内外主流的 10 款企业项目管理系统对比&#xff1a;PingCode、Worktile、泛微 e-office、金蝶云星瀚、用友项目管理软件、Jira、Monday.com、Asana、Trello、Basecamp。 在面对复杂的项目管理需求时&#xff0c;许多企业经常感到手头的工具不足以支撑高效的运作。有效的项目型…

代码随想录刷题day17丨654.最大二叉树,617.合并二叉树,700.二叉搜索树中的搜索,98.验证二叉搜索树

代码随想录刷题day17丨654.最大二叉树&#xff0c;617.合并二叉树&#xff0c;700.二叉搜索树中的搜索&#xff0c;98.验证二叉搜索树 1.题目 1.1最大二叉树 题目链接&#xff1a;654. 最大二叉树 - 力扣&#xff08;LeetCode&#xff09; 视频讲解&#xff1a;又是构造二叉…

【GeoscenePro】Pro连接本机Portal

打开自己的portal下面框住的就是链接的地址 证书的话点击是即可

嵌入式数据库

概述 1.作用&#xff1a;存储大量数据&#xff0c;专业存储数据 存储在内存&#xff08;数组&#xff0c;变量&#xff0c;链表&#xff09;上的特点&#xff1a;程序运行结束&#xff0c;或者掉电&#xff0c;数据会丢失。 存储在硬盘&#xff08;文件&#xff09;上的特点…

恺英网络:有业绩,无“游戏”

2024年上半年&#xff0c;恺英网络的业绩依然很好&#xff0c;但有些不讲逻辑了。 8月22日晚&#xff0c;恺英网络发布了2024年半年度财报。 报告显示&#xff0c;上半年公司实现营业收入25.55亿元&#xff0c;同比增长29.28%&#xff1b;归母净利润和扣非净利润分别为8.09亿…

鸿蒙开发5.0【基于Search组件实现搜索栏】

场景一&#xff1a;自定义搜索图标 方案 基于Search组件的searchIcon属性设置搜索图标&#xff0c;searchIcon可以设置size、color。注意&#xff1a;当前搜索图标不支持隐藏且不支持位置调整。基于searchButton属性设置搜索框末尾搜索按钮。基于该属性可更改文本内容及文本样…

Pepper佩盼尔wordpress模板

Pepper佩盼尔WordPress模板是一款专为追求简洁、现代和专业外观的网站设计者和开发者打造的高品质主题。它以简站为主题&#xff0c;强调“让建网站更简单”的理念&#xff0c;旨在为用户提供一个易于使用、功能丰富的平台来构建他们的在线业务或个人网站。 模板特点包括&…

【JavaEE】网络原理 UDP协议

&#x1f525;个人主页&#xff1a; 中草药 &#x1f525;专栏&#xff1a;【Java】登神长阶 史诗般的Java成神之路 &#x1f383;一.运输层 运输层&#xff08;Transport Layer&#xff09;是OSI七层模型中的第四层&#xff0c;也是TCP/IP四层模型中的第三层。它的主要职责是…

在3D Slicer中使用 Monai Bundle 和 Model Zoo 标注医学影像数据-全身CT器官分割

在3D Slicer中使用 Monai Bundle 和 Model Zoo 标注医学影像数据-全身CT器官分割 文章持续更新&#xff0c;可以关注微公【医学图像人工智能实战营】获取最新动态。人手有限&#xff0c;文中涉及的链接前往微公对应文章查看。关注Tina姐&#xff0c;一起学习进步~ 导读 本系列…

云计算基础之Docker

云计算是什么 云计算的本质是计算资源的虚拟化。 与磁盘逻辑卷的分配类似&#xff0c;云计算是在负载波动时动态调整服务器计算资源&#xff0c;避免资源浪费的做法&#xff0c;生产环境中&#xff0c;厂商一般将服务器组成一个池&#xff0c;实现逻辑管理计算资源&#xff0…

实时图像编辑大革新!Adobe发布TurboEdit:可以通过文本来编辑图像,编辑时间<0.5秒!

今天给大家介绍Adobe研究院新的研究TurboEdit&#xff0c;可以通过文本来编辑图像&#xff0c;通过一句话就能改变图像中的头发颜色、衣服、帽子、围巾等等。而且编辑飞快&#xff0c;<0.5秒。简直是图像编辑的利器。 相关链接 项目&#xff1a;betterze.github.io/TurboE…