volatile

volatile关键字在Java中确保了变量的改变对所有线程都是可见的,这意味着一个线程中对这个变量的改变立即对其他线程可见。以下是一个简单的例子来演示volatile如何用于保证内存可见性。
假设我们有一个简单的程序,一个线程用来更新标志,另一个线程用来监听这个标志,一旦标志变为true,监听线程将打印一条消息并终止。

  1. public class VolatileExample {
  2. // volatile变量用于保证内存可见性
  3. private boolean flag = false;
  4. public void updateFlag() {
  5. // 这里可能有一些代码
  6. this.flag = true; // 修改flag的值
  7. System.out.println("Flag updated to true");
  8. }
  9. public boolean checkFlag() {
  10. return flag;
  11. }
  12. public static void main(String[] args) {
  13. VolatileExample example = new VolatileExample();
  14. // 创建一个线程用于更新flag变量
  15. Thread updateThread = new Thread(() -> {
  16. try {
  17. Thread.sleep(1000); // 假设有一些处理需要一段时间
  18. } catch (InterruptedException e) {
  19. Thread.currentThread().interrupt();
  20. }
  21. example.updateFlag();
  22. });
  23. // 创建一个线程用于检查flag变量
  24. Thread checkThread = new Thread(() -> {
  25. while (!example.checkFlag()) {
  26. // 循环直到flag的值变为true
  27. }
  28. System.out.println("Flag has been set to true");
  29. });
  30. // 启动线程
  31. checkThread.start();
  32. updateThread.start();
  33. }
  34. }

在这个例子中,flag变量是volatile的,这保证了当updateThread线程将flag设置为true后,checkThread线程可以立即看到这个改变。如果flag不是volatile的,Java内存模型允许线程在自己的工作内存中缓存变量的值,而不是每次都从主内存中读取。这可能会导致checkThread线程永远看不到flag变量的更新,因此它可能会无限循环下去。
通过将flag标记为volatile,我们确保了每次访问flag变量时都会从主内存重新读取其值,而对flag的每次写入也都会立即同步回主内存,从而确保了不同线程对该变量访问的内存可见性。
请注意,volatile变量不是万能的,它只能用于某些特定情况,不能替代其他同步机制以保证复合操作的原子性。

synchronized

在Java中,synchronized关键字是用来控制对共享资源的并发访问的一种机制,从而保证线程安全。synchronized可以用来修饰一个方法或者一个代码块,当一个线程访问这段同步代码时,它会自动获得一个锁,其他线程如果尝试访问这段同步代码,将会被阻塞直到当前线程释放锁。
以下是synchronized如何确保并发安全的几个关键点:

  1. 互斥锁(Mutual Exclusion):
    • synchronized关键字背后的核心机制是内部锁或监视器锁(在Java对象头中实现)。
    • 当一个线程进入一个同步的方法或同步代码块时,它会自动获取这个锁。
    • 任何其他试图访问同步代码的线程都会被阻塞,直到持有锁的线程退出同步代码块或方法,从而释放锁。
  2. 可见性(Visibility):
    • synchronized关键字确保释放锁之前对共享变量所做的更改对接下来获取锁的线程是可见的。
    • 当线程释放锁时,它做的更改会刷新回主内存,而当线程获取锁时,它会看到之前线程所做的所有更改。
  3. 重入性(Reentrancy):
    • synchronized锁是可重入的,这意味着同一个线程可以多次获得同一把锁。
    • 如果一个 synchronized方法或代码块调用另一个 synchronized方法,那么线程将再次获取锁,因为它已经持有这个锁。
    • 这避免了死锁,并允许同一个线程在一个对象的 synchronized上下文中多次获得锁。
  4. 阻塞和唤醒:
    • 线程在试图获取不能立即获得的锁时将会被阻塞。
    • 当锁被释放时,Java虚拟机负责唤醒一个或多个正在等待这个锁的线程(具体取决于锁的策略)。
    • 被唤醒的线程将再次竞争锁,一旦获取到锁,线程就可以继续执行。

下面是synchronized关键字的两种常见用法:

  1. 同步实例方法:

    1. public synchronized void doSomething() {
    2. // 访问或修改共享资源
    3. }

    这种情况下,锁定的是调用该方法的对象实例。

  2. 同步代码块:

    1. public void doSomethingElse() {
    2. synchronized (this) {
    3. // 访问或修改共享资源
    4. }
    5. }

    在这种情况下,也是锁定调用该方法的对象实例。如果需要锁定不同的对象,可以将this替换为那个对象的引用。
    还可以同步一个类方法,这会锁定整个类的Class对象,适用于静态变量的同步操作。
    synchronized关键字是实现并发安全的基本工具,但它可能会引入性能开销,因为它会阻止线程并发执行同步代码。因此,在设计并发程序时,应该尽量减少同步区域的范围,只在必要时才进行同步。

    TreadLocal

    threadLocal实例

    在Java中,线程局部变量(Thread-Local Variables)是那些每个线程都有自己独立实例的变量。每个线程访问一个线程局部变量时,它们操作的是自己的、独立的副本。这些变量通常使用java.lang.ThreadLocal类来实现。
    线程局部变量的一个典型应用场景是在需要避免共享资源的情况下保存线程的上下文信息,如用户身份认证、事务信息等。使用线程局部变量可以避免同步的需求,因为每个线程都操作自己的独立副本。
    以下是应用线程局部变量的一个简单例子:

    1. public class ThreadLocalExample {
    2. // 创建一个ThreadLocal实例
    3. private static final ThreadLocal<Integer> threadLocalCounter = new ThreadLocal<>();
    4. static class MyRunnable implements Runnable {
    5. private final int value;
    6. MyRunnable(int value) {
    7. this.value = value;
    8. }
    9. @Override
    10. public void run() {
    11. // 将当前线程的值设置到ThreadLocal
    12. threadLocalCounter.set(value);
    13. // 有一些处理过程
    14. doSomeWork();
    15. // 获取当前线程的ThreadLocal值
    16. System.out.println(Thread.currentThread().getName() + ": " + threadLocalCounter.get());
    17. }
    18. private void doSomeWork() {
    19. // 模拟一些处理,可能会更改ThreadLocal变量的值
    20. threadLocalCounter.set(threadLocalCounter.get() + 1);
    21. }
    22. }
    23. public static void main(String[] args) throws InterruptedException {
    24. Thread thread1 = new Thread(new MyRunnable(1), "Thread-1");
    25. Thread thread2 = new Thread(new MyRunnable(5), "Thread-2");
    26. thread1.start();
    27. thread2.start();
    28. thread1.join();
    29. thread2.join();
    30. }
    31. }

    在这个例子中,我们有一个ThreadLocal实例,它用于存储每个线程的计数器值。MyRunnable类在每个线程开始时设置自己的threadLocalCounter值,并在完成一些处理工作后打印它。
    由于threadLocalCounter是一个ThreadLocal对象,因此每个线程对它的访问都相互独立。这意味着即使多个线程同时执行这段代码,它们访问的threadLocalCounter值也不会发生冲突。
    一些常见的使用线程局部变量的场景包括:

  • 在Web应用中,存储与请求相关的用户身份信息,使得在处理请求的过程中,不同的部分(如过滤器、servlet、服务层等)都可以方便地访问用户信息。
  • 在数据库连接管理中,使用线程局部变量存储与当前线程相关的数据库连接对象,确保每个线程都有自己的数据库连接,从而避免同步问题。
  • 在使用ORM框架时,比如Hibernate,在会话管理中使用线程局部变量来存储当前线程的会话对象。 :::info 需要注意的是,虽然线程局部变量减少了对同步的需求,但它们也需要适当的管理,特别是在使用线程池的情况下。因为线程池中的线程是被重用的,如果不正确地清理线程局部变量,可能会导致内存泄漏问题。通常,在线程即将结束之前,应当使用ThreadLocal.remove()方法来清除该线程局部变量的值。 :::

join方法

:::info 在编程语境下,“join”这个词通常表示将多个部分连接或者合并成一个整体。在多线程编程中,join 方法的含义是类似的:它使调用者能够等待另一个线程完成其执行,从而“加入”到调用者线程的执行流程中去。

当你调用某个线程的 join() 方法时,调用这个方法的线程会被阻塞,直到目标线程完成其执行。换句话说,目标线程加入到调用者线程的执行流程中去,二者“合并”了执行路径。只有当目标线程完成了它的任务,调用者线程才会继续执行,就好像两个线程的执行在这一点上合并了一样。

这个术语源自操作系统和并发编程的概念,其中线程(或进程)需要在某个点上同步它们的执行。join 方法是线程同步的一种形式,确保了线程可以按照特定的顺序执行。例如,一个计算由多个阶段组成,只有前一个阶段完成后,下一个阶段才能开始。在这种情况下,你可能会让每个阶段在不同的线程中运行,并在每个阶段的线程开始之前调用上一个阶段线程的 join() 方法,以确保按正确的顺序进行。

因此,join() 方法的命名反映了它在多线程编程中的作用和目的,即等待目标线程“加入”并完成,然后继续执行。 :::

Executor框架

Java的Executor框架是自Java 5开始引入的一个强大的框架,用来简化并发编程。该框架提供了线程池的管理功能,包括线程的生命周期管理、任务提交与执行等,从而允许开发者将焦点放在任务的实现上,而不是线程的管理上。
Executor框架主要由以下几个核心接口和类组成:

  1. Executor接口:最基本的接口,用于提交可执行的任务。它定义了一个方法 execute(Runnable command)。
  2. ExecutorService接口:继承自Executor,是一个完整的异步任务执行框架。提供了管理任务生命周期和线程池的方法,如启动、关闭线程池,提交、执行任务,检查任务状态等。
  3. ScheduledExecutorService接口:继承自ExecutorService,添加了时间调度的功能。允许你在给定的延迟之后或定期执行任务。
  4. ThreadPoolExecutor类:是最常用的线程池实现类,实现了ExecutorService接口。它允许你创建具有可扩展的线程池的服务,提供了丰富的构造函数来自定义线程池的各种属性。
  5. ScheduledThreadPoolExecutor类:继承自ThreadPoolExecutor,并实现了ScheduledExecutorService接口。除了执行任务外,还可以在指定延时之后或定期执行任务。
  6. Future接口:代表异步计算的结果,提供方法来检查计算是否完成,等待计算完成,并检索计算结果。
  7. Callable接口:类似于Runnable,但它可以返回一个值,并且能够抛出异常。

使用Executor框架的一个简单例子:

  1. import java.util.concurrent.ExecutorService;
  2. import java.util.concurrent.Executors;
  3. import java.util.concurrent.Future;
  4. public class ExecutorExample {
  5. public static void main(String[] args) {
  6. // 创建一个固定大小的线程池
  7. ExecutorService executorService = Executors.newFixedThreadPool(2);
  8. // 通过线程池提交任务
  9. Future<?> future = executorService.submit(() -> {
  10. System.out.println("Asynchronous task");
  11. });
  12. // 检查任务是否已经完成
  13. if (future.isDone()) {
  14. System.out.println("Task completed");
  15. }
  16. // 关闭线程池
  17. executorService.shutdown();
  18. }
  19. }

在这个例子中,我们创建了一个固定大小的线程池,提交了一个简单的异步任务,然后关闭了线程池。Future对象可以用来检查任务是否完成或等待任务结束,并获取返回结果。
Executor框架极大地简化了线程池的使用和任务提交流程,是现代Java并发编程的首选方式。

Executors

Java的Executors类提供了几种不同类型的线程池,每种都有其特定的用途。以下是Executors类中提供的一些常见线程池工厂方法:

  1. newFixedThreadPool(int nThreads) 创建一个固定大小的线程池。每当提交一个任务时,如果池中有空闲线程,则会立即执行,否则会在工作队列中等待,直到有线程可用。
  2. newSingleThreadExecutor() 创建一个单线程的Executor。这个线程池保证所有任务都在同一个线程中按顺序执行,因此不需要处理线程同步的问题。
  3. newCachedThreadPool() 创建一个可缓存的线程池。如果线程池的当前大小超过了处理需求,那么会回收空闲的线程。当任务数量增加时,此线程池会自动增加线程数量。
  4. newScheduledThreadPool(int corePoolSize) 创建一个大小无限制的线程池,但是会重用先前构造的线程,这些线程在空闲时会被存放在工作队列中。此外,它可以在给定的延迟后运行任务,或者定期执行任务。
  5. newWorkStealingPool() 创建一个使用了工作窃取算法的线程池,这种类型的线程池通常用于并行处理任务。这个方法在Java 8中被引入,创建的线程池大小默认为CPU的可用核心数。
  6. newSingleThreadScheduledExecutor() 创建一个单线程的定时任务Executor。这个线程池可以在给定的延迟后运行任务,或者定期执行任务,所有任务都在同一个线程中按顺序执行。

每种线程池都有其使用场景,选择合适的线程池可以提高程序的性能和资源利用率。例如,如果你的应用程序执行许多短暂的异步任务,那么newCachedThreadPool可能是个不错的选择。与此相反,如果你需要限制并发线程的数量,那么newFixedThreadPool会是更好的选择。
使用Executors类中的工厂方法创建线程池的一个重要注意点是,这些方法默认的拒绝策略是AbortPolicy,这意味着当任务无法提交到线程池时(例如,线程池已关闭或达到其容量),会抛出一个RejectedExecutionException异常。此外,某些线程池(尤其是newCachedThreadPool和newScheduledThreadPool)默认允许创建的线程数量几乎没有限制,这可能会导致创建过多的线程,耗尽系统资源。因此,在生产环境中,建议根据具体需求手动配置线程池的参数,而不是使用这些工厂方法。

如何创建一个线程

在Java中,创建和启动一个新线程可以通过两种主要方式来实现:

方式1:扩展 Thread 类

你可以通过扩展 Thread 类并重写其 run() 方法来创建一个新的线程。在 run() 方法中,你需要定义线程执行时要完成的任务。

  1. public class MyThread extends Thread {
  2. @Override
  3. public void run() {
  4. // 在这里放置线程执行的代码
  5. System.out.println("线程正在执行...");
  6. }
  7. public static void main(String[] args) {
  8. MyThread myThread = new MyThread();
  9. myThread.start(); // 启动线程
  10. }
  11. }

在上面的代码中,MyThread 类继承了 Thread 类,并覆盖了 run() 方法。你可以通过调用 start() 方法来启动这个线程,这将使 JVM 调用 run() 方法。

方式2:实现 Runnable 接口

另一种创建线程的方法是实现 Runnable 接口并将其实例传递给 Thread 类的构造函数。Runnable 接口仅有一个无参数的方法 run(),你需要实现这个方法来指定线程的行为。

  1. class MyRunnable implements Runnable {
  2. public void run() {
  3. // 在这里放置需要并行执行的代码
  4. System.out.println("Runnable is running.");
  5. }
  6. }
  7. public class Main {
  8. public static void main(String[] args) {
  9. MyRunnable myRunnable = new MyRunnable();
  10. Thread thread = new Thread(myRunnable);
  11. thread.start(); // 启动新线程
  12. }
  13. }

在这个例子中,我们创建了一个实现 Runnable 接口的 MyRunnable 类的实例,并将其传递给 Thread 的构造函数。然后,我们通过调用 Thread 实例的 start() 方法来启动线程。
使用 Runnable 接口通常比直接扩展 Thread 类更加灵活,因为它允许你的类继承其他类,同时还可以运行在一个线程中。此外,Runnable 也可以用在Java的高级并发API中,例如 ExecutorService。

方式3:使用 Executor 框架 (Java 5+)

从Java 5开始,你还可以使用 Executor 框架来创建和管理线程。以下是一个简单的例子,演示如何使用 Executor 框架来执行 Runnable 任务:

  1. import java.util.concurrent.ExecutorService;
  2. import java.util.concurrent.Executors;
  3. public class Main {
  4. public static void main(String[] args) {
  5. ExecutorService executor = Executors.newSingleThreadExecutor();
  6. executor.submit(() -> {
  7. // 在这里放置需要并行执行的代码
  8. System.out.println("Executor Runnable is running.");
  9. });
  10. executor.shutdown(); // 关闭线程池
  11. }
  12. }

以上就是在Java中创建和启动线程的基本方法。选择合适的方法取决于具体的应用场景和个人偏好。

线程池的关键参数

基本参数

在Java中使用线程池时,了解其关键参数对于合理配置和高效使用线程池非常重要。以下是线程池的一些关键参数:

  1. 核心线程数(Core Pool Size):
    • 这个参数代表线程池中的基本线程数。
    • 即使线程是空闲的,线程池也会尽量维护至少这么多的线程数。
    • 如果所有的核心线程都在忙,新任务会被放入工作队列中等待。
  2. 最大线程数(Maximum Pool Size):
    • 线程池允许创建的最大线程数。
    • 如果工作队列已满,并且当前运行的线程数小于最大线程数,线程池会尝试创建新的线程来处理任务。
  3. 工作队列(Work Queue):
    • 用于保存等待执行的任务的阻塞队列。
    • 当核心线程都在忙时,新任务会被放在这个队列中,直到有线程空闲下来。
    • 常见的队列有:无界队列(如LinkedBlockingQueue),有界队列(如ArrayBlockingQueue)等。
  4. 线程保持活跃时间(Keep-Alive Time):
    • 如果线程池中的线程数超过了核心线程数,那么这些多余的线程在空闲时会等待新任务的最长时间。
    • 超过这个时间,非核心线程会被回收。
  5. 时间单位(Time Unit):
    • 与线程保持活跃时间的参数一起使用,用来指定keep-alive时间的单位,如毫秒、秒等。
  6. 线程工厂(Thread Factory):
    • 用于创建新线程的工厂。
    • 可以定制线程的名字、优先级等属性。
  7. 拒绝策略(Rejected Execution Handler):
    • 当工作队列满并且线程池中的线程数已达到最大值时,对新提交的任务采取的策略。
    • 常见的拒绝策略包括:
      • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
      • ThreadPoolExecutor.CallerRunsPolicy:调用任务的run()方法绕过线程池直接执行。
      • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
      • ThreadPoolExecutor.DiscardOldestPolicy:丢弃最早的未处理任务,然后重试执行当前任务。

要正确配置线程池以满足应用需求,需要根据任务的性质、系统资源和期望的性能指标来合理设定这些参数。过大的核心线程数和最大线程数可能会导致系统过度切换上下文和资源竞争,而过小的话可能会导致CPU资源未能充分利用。工作队列的容量设定也会影响系统的吞吐量和内存使用情况。因此,根据实际场景合理配置线程池参数是非常重要的。

调参实践

调整corePoolSize、maximumPoolSize和workQueue的大小是一个需要细致考量的过程,因为最佳实践取决于具体的应用场景和负载特性。以下是一些调参的一般性建议:

  1. 核心线程数 (corePoolSize):
    • 这个值的设置应当反映出服务器可用的CPU资源以及你想让这些核心线程保持忙碌的程度。
    • 对于CPU密集型的任务,理想的corePoolSize通常设置为处理器的数量加一,这样可以保持处理器的高效利用,同时避免过多的上下文切换。
    • 对于IO密集型的任务,由于线程会阻塞等待IO,可以设置更高的corePoolSize。一个常用的公式是核心线程数 = CPU核心数 * (1 + 阻塞系数),其中阻塞系数代表阻塞时间与计算时间的比例。
  2. 最大线程数 (maximumPoolSize):
    • 最大线程数决定了线程池可以同时运行的最大线程数量。
    • 这个数值通常比corePoolSize大,以便在队列满的时候还能处理更多的任务。
    • 一个过大的maximumPoolSize可能会造成内存消耗过多和上下文切换频繁,因此需要根据具体应用的负载和资源限制来设定。
  3. 工作队列 (workQueue):
    • 工作队列用于存放等待被线程执行的任务。队列的大小会直接影响系统的吞吐量和资源使用。
    • 对于需要立即执行任务的场景,可以使用较小的队列。
    • 如果需要缓冲大量的请求,可以使用较大的队列。但是注意,一个过大的队列可能导致内存消耗过多,且任务的响应时间变长。
  4. 调参的最优实践:
    • 监控和指标: 使用监控工具来收集线程池的性能指标,如队列大小,活跃线程数,任务拒绝次数等,可以帮助你做出更好的调整决策。
    • 负载测试: 在模拟真实负载的情况下进行压力测试,观察应用的性能表现和资源消耗情况。
    • 逐步调整: 逐步调整参数并观察应用的表现,同时考虑到负载的波动,确保在高负载时应用仍然能稳定运行。
    • 文档和经验: 参考官方文档和社区的最佳实践。由于不同的应用可能有截然不同的需求和表现,借鉴他人的经验同时结合自己的实际情况来调整参数。

总结来说,没有一套固定的参数设置适合所有场景,最佳实践是基于应用的实际需要和行为来调整参数,并持续监控其性能和资源消耗。

ConcurrentHashMap

ConcurrentHashMap 是 Java 中的一个线程安全的哈希表,它是 java.util.concurrent 包的一部分。与传统的 Hashtable 和同步的 HashMap(即通过 Collections.synchronizedMap 包装的 HashMap)相比,ConcurrentHashMap 提供了更好的并发性能,同时确保了线程安全。
下面是 ConcurrentHashMap 实现原理的基本概述:

  1. 分段锁(Segmentation): 在 Java 7 及以前的版本中,ConcurrentHashMap 使用了一种分段锁(Segmentation)的机制。它将数据结构分成一些部分(segment),每个部分拥有自己的锁。当一个线程需要访问某个段的数据时,它只需要获取这个段的锁,这样其他线程可以并发地访问其他段。这种方式减少了锁的粒度,提高了并发访问的能力。每个段都是一个独立的 HashMap。
  2. 锁分离: ConcurrentHashMap 中的一些操作是通过锁分离技术来实现更高的并发性的。例如,读操作通常是无锁的(在 Java 8 之后完全无锁),而写操作通过锁定一个小的范围(Java 8 之后是桶的头结点)来完成,而不是像 Hashtable 那样锁定整个结构。
  3. 无锁读取: 在 Java 8 及其后续版本中,ConcurrentHashMap 去掉了分段锁的概念,而是引入了一种新的节点(Node)结构,并且使用了 CAS(compare-and-swap)操作来实现无锁的读取和更新,大大提升了并发读取时的性能,因为大多数读取操作都不需要加锁。
  4. Node结构和CAS: 每个桶头部的节点使用 volatile 变量来保证内存可见性。如果一个线程需要更改节点,它将使用 CAS 操作来确保更新的原子性。如果 CAS 操作失败,说明有其他线程正在同时修改这个节点,当前线程会在尝试了一定次数后进行自旋或阻塞。
  5. 树化(Treeification): 与 HashMap 类似,当链表长度超过一定阈值时,ConcurrentHashMap 中的链表会转换成红黑树,从而提供更快的查找性能。这在高哈希冲突场景下是非常有用的。
  6. 大小控制和扩容: ConcurrentHashMap 会根据存储的元素数量来进行自动扩容。扩容过程是多线程并发执行的,不同于早期版本需要锁定整个结构,Java 8 中的扩容是通过一种称为 “forwarding nodes” 的技术来实现局部性的数据迁移,而不影响其他线程对未迁移部分的读写操作。

ConcurrentHashMap 的这些设计确保了它可以在高并发环境中有效工作,同时提供比其他线程安全的 Map 实现更高的性能。在使用 ConcurrentHashMap 时,开发者不仅能享受到线程安全的好处,而且由于其高效的并发策略,通常无需担心性能瓶颈。