Java多线程相关

189 阅读23分钟

一、前言

多线程是什么?

Java的多线程是指:允许程序同时执行多个线程的编程技术。在深入了解Java线程知识点之前,我们先熟悉几个基本概念,线程和进程、并行和并发。

多线程的关键特点:

  1. 轻量级:线程是进程的子任务,共享同一内存空间和资源。
  2. 并发:多个线程可以独立执行,从而提高程序效率。
  3. 共享资源:线程可以共享同一个进程内的变量和数据。

多线程的优缺点:

优点:

  • 提高程序并发性。
  • 更高效地利用多核处理器。

缺点:

  • 增加了开发复杂性。
  • 容易出现线程安全问题,例如死锁和资源争用。

进程和线程的关系:

进程(Process):

  • 程序由指令和数据组成,这些指令要运行,数据要读写,就必须将指令加载进CPU,数据加载至内存。指令运行还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的;
  • 当一个程序被运行,从磁盘加载这个程序的代码到内存,这时开启了一个进程;
  • 进程可以视为一个程序的实例,大部分程序可以运行多个实例进程(浏览器),也有的程序只能启动一个进程(网易云音乐);

线程(Thread):

  • 一个进程内可以分为一到多个线程;
  • 一个线程就是一个指令流,将指令流中的一条指令以一定顺序交给CPU执行;
  • 在Java中,线程作为最小的调度单位,进程作为资源分配的最小单位;

两者对比:

  • 进程之间相互独立,线程存在于进程内,是进程的一个子集;
  • 进程拥有共享资源,如内存空间,供其内部的线程共享;
  • 线程的通信是通过共享进程的内存,多个线程可以共同访问一个共享变量;

并行和并发:

在单核CPU,线程实际还是串行执行的,操作系统的任务调度器将CPU的时间片分配给不同的线程使用,由于CPU在线程间切换非常快(时间片很短),所以给我们的感觉是同时运行的。综上所述:宏观并行,微观串行

  • 并行(Parallel): 同一时间动手做多少事情的能力;
  • 并发(ConCurrent):同一时间应对多少事情的能力;

二、线程的创建和管理

线程的创建主要有四种方式:继承Thread类、实现Runnable接口、实现Callable接口、线程池的方式创建线程。

继承Thread类:

  • 创建一个类继承Thread类;
  • 重写run()方法,该方法线程要执行的任务;
  • 调用start()方法启动线程;
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running...");
    }
}
public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

实现Runnable接口:

  • 创建一个类实现Runnable接口;
  • 重写run()方法,该方法内是线程要执行的任务;
  • 使用 Thread 类包装该对象并调用 start() 方法。
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread is running...");
    }
}
public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}

实现Callable接口:

  • 创建一个类实现Callable接口;
  • 重写call()方法;
  • 使用FutureTask类来包装Callable对象,FutureTaskRunnable 的实现类,可以用于提交到线程中执行。
  • 使用 Thread 或线程池来执行任务,并通过 FutureTask 获取结果。
public class MyCallable implements Callable<String> {
 
     @Override
     public String call() throws Exception {
         System.out.println("MyCallable...call...");
         return "OK";
     }
 
     public static void main(String[] args) throws ExecutionException, InterruptedException {
 
         // 创建MyCallable对象
         MyCallable mc = new MyCallable() ;
 
         // 创建F
         FutureTask<String> ft = new FutureTask<String>(mc) ;
 
         // 创建Thread对象
         Thread t1 = new Thread(ft) ;
         Thread t2 = new Thread(ft) ;
 
         // 调用start方法启动线程
         t1.start();
 
         // 调用ft的get方法获取执行结果
         String result = ft.get();
 
         // 输出
         System.out.println(result);
 
     }
 
 }

线程的生命周期:

关于线程的状态,我们参考JDK中Thread类的枚举State

public enum State {
         /**
          * 尚未启动的线程的线程状态
          /
          NEW,

         /**
          * 可运行线程的线程状态。处于可运行状态的线程正在 Java 虚拟机中执行,但它可能正在等待来自        
          * 操作系统的其他资源,例如处理器。
          /
          RUNNABLE,

         /**
          * 线程阻塞等待监视器锁的线程状态。处于阻塞状态的线程正在等待监视器锁进入同步块/方法或在调          
          * 用Object.wait后重新进入同步块/方法。
          /
          BLOCKED,

         /**
          * 等待线程的线程状态。由于调用以下方法之一,线程处于等待状态:
         * Object.wait没有超时
          * 没有超时的Thread.join
          * LockSupport.park
          * 处于等待状态的线程正在等待另一个线程执行特定操作。
          * 例如,一个对对象调用Object.wait()的线程正在等待另一个线程对该对象调用Object.notify()         
          * 或Object.notifyAll() 。已调用Thread.join()的线程正在等待指定线程终止。
          /
          WAITING,

         /**
          * 具有指定等待时间的等待线程的线程状态。由于以指定的正等待时间调用以下方法之一,线程处于定          * 时等待状态:
         * Thread.sleep
         * Object.wait超时
         * Thread.join超时
         * LockSupport.parkNanos
         * LockSupport.parkUntil
          * </ul>
          /
          TIMED_WAITING,
         
         /**
          * 已终止线程的线程状态。线程已完成执行
          */
         TERMINATED;
     }

线程状态之间的变化如下图所示:

image.png

  • 新建

    • 当一个线程对象被创建,但还未调用 start 方法时处于新建状态;
    • 此时未与操作系统底层线程关联;
  • 可运行

    • 调用了 start 方法,就会由新建进入可运行
    • 此时与底层线程关联,由操作系统调度执行;
  • 终结

    • 线程内代码已经执行完毕,由可运行进入终结
    • 此时会取消与底层线程关联;
  • 阻塞

    • 当获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,此时不占用 cpu 时间;
    • 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态;
  • 等待

    • 当获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行状态释放锁进入 Monitor 等待集合等待,同样不占用 cpu 时间;
    • 当其它持锁线程调用 notify()notifyAll() 方法,会按照一定规则唤醒等待集合中的等待线程,恢复为可运行状态;
  • 限时等待

    • 当获取锁成功后,但由于条件不满足,调用了 wait(long) 方法,此时从可运行状态释放锁进入 Monitor 等待集合进行有时限等待,同样不占用 cpu 时间
    • 当其它持锁线程调用 notify()notifyAll() 方法,会按照一定规则唤醒等待集合中的有时限等待线程,恢复为可运行状态,并重新去竞争锁;
    • 如果等待超时,也会从有时限等待状态恢复为可运行状态,并重新去竞争锁;
    • 还有一种情况是调用 sleep(long) 方法也会从可运行状态进入有时限等待状态,但与 Monitor 无关,不需要主动唤醒,超时时间到自动恢复为可运行状态 ;

线程创建和管理相关关键点:

RunableCallable的区别?

  1. Runnablerun()方法没有返回值,而Callablecall()方法有返回值,是一个泛型,和FutrueFutureTask配合使用来获取异步执行的结果。
  2. Callable接口支持返回执行结果,需要调用FutrueTask.get()方法得到,此方法会阻塞主进程继续往下执行。
  3. Callablecall()方法允许抛出异常,而Runnable接口的run()方法异常只能在内部消化,不能往上抛出。

run()方法和start()方法的区别?

  1. start()方法用来启动线程,通过该线程调用run()方法,执行run()方法中所定义的逻辑代码。start()方法只能被调用一次。
  2. run()方法封装了要被线程执行的代码,可以被调用多次。

notify()notifyAll()的区别?

  • notifyAll:唤醒所有wait的线程;
  • notify:只随机唤醒一个 wait 线程;

Java中 wait()方法和 sleep()方法的区别?

共同点:

  • wait()wait(long)sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态;

不同点:

  • 方法归属不同

    • sleep(long)Thread 类的静态方法;
    • wait()wait(long) 都是 Object 的成员方法,每个对象都有;
  • 醒来时机不同

    • 执行 sleep(long)wait(long) 的线程都会在等待相应毫秒后醒来;
    • wait(long)wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去;
    • 它们都可以被打断唤醒;
  • 锁特性不同(重点)

    • wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制;
    • wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用);
    • sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了);

三、Java中的并发安全问题

出现并发问题的根源:并发三要素

在Java中,并发问题出现主要体现在三个方面:共享数据、并发执行和线程调度。总结来说就是三个关键字:可见性、原子性和有序性;

  • 可见性:在多线程环境中,一个线程对共享变量的修改,能否立即被其他线程看到。可见性问题通常发生在多个线程同时访问共享数据时,尤其在没有适当的同步机制的情况下。

    解决方案:使用volatile关键字;
  • 原子性:操作在执行过程中不可被中断,要么全部完成,要么全部不做。对于多线程操作共享数据时,如果一个操作不是原子的,那么在执行过程中可能会被其他线程打断,导致数据的不一致或错误。

    解决方案:使用synchronized关键字、使用Atomic类。
  • 有序性:程序执行的顺序是否符合程序员的预期。在多线程程序中,由于 CPU 和编译器的优化,程序的执行顺序可能会发生变化,这种行为称为 指令重排。指令重排可能导致线程在执行时产生意外的结果,违反了程序的执行顺序。

    解决方案:使用volatile关键字;

Synchronized关键字:

在Java中,Synchronized 是一种用于实现一种线程同步的机制,旨在解决多线程环境下的共享资源访问问题。使用 synchronized 可以确保多个线程在访问同一资源时,只有一个线程能访问该资源,从而避免数据的不一致性和竞争条件。

Synchronized的作用:

  1. 确保线程安全:当多个线程访问同一个共享资源时,synchronized 可以通过锁定资源来保证只有一个线程在某一时刻能访问资源。
  2. 避免数据竞争:多个线程同时操作共享变量时,使用 synchronized 可以避免对变量的并发修改,从而导致数据不一致。

Synchronized的使用方式:

  1. 修饰实例方法(对象锁):
    • 当方法声明为 synchronized 时,表示该方法的实例级锁(每个实例都有一个锁)。
    • 同一时刻,只有一个线程可以访问该实例的同步方法。
public class SynchronizedExample {
    private int counter = 0;

    public synchronized void increment() {
        counter++;
    }
    
    public synchronized int getCounter() {
        return counter;
    }
}
  1. 修饰类方法(类锁):
    • 如果方法是 static 的,那么锁是类级别的,所有实例共享同一个锁。
public class SynchronizedExample {
    private static int counter = 0;

    public static synchronized void increment() {
        counter++;
    }
    
    public static synchronized int getCounter() {
        return counter;
    }
}

  1. 修饰代码块(显式锁):

    • synchronized 还可以用来修饰代码块,限定同步的代码范围。通过传入对象来指定锁的范围。
    • 这种方式更为灵活,通常用于提高性能,锁定较小的代码段。
public class SynchronizedExample {
    private int counter = 0;

    public void increment() {
        synchronized (this) {
            counter++;
        }
    }
}

Sychronized的基本使用案例:

// 如下抢票的代码,如果不加锁,就会出现超卖或者一张票卖给多个人

public class TicketDemo {

    static Object lock = new Object();
    int ticketNum = 10;


    public synchronized void getTicket() {
        synchronized (this) {
            if (ticketNum <= 0) {
                return;
            }
            System.out.println(Thread.currentThread().getName() + "抢到一张票,剩余:" + ticketNum);
            // 非原子性操作
            ticketNum--;
        }
    }

    public static void main(String[] args) {
        TicketDemo ticketDemo = new TicketDemo();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                ticketDemo.getTicket();
            }).start();
        }
    }


}

Monitor

synchronized 底层使用的JVM级别中的Monitor 来决定当前线程是否获得了锁,如果某一个线程获得了锁,在没有释放锁之前,其他线程是不能获得到锁的。synchronized 属于悲观锁。synchronized 因为需要依赖于JVM级别的Monitor ,相对性能也比较低。

Monitor 被翻译为监视器,是由jvm提供,c++语言实现。 在代码中想要体现monitor需要借助javap命令查看clsss的字节码,比如以下代码:

public class SyncTest {

    static final Object lock = new Object();
    static int counter = 0;
    public static void main(String[] args) {
        synchronized (lock) {
            counter++;
        }
    }
}

找到这个类的class文件,在class文件目录下执行javap -v SyncTest.class,反编译效果如下:

image.png

  • monitorenter 开始上锁的位置;
  • monitorexit 解锁的位置;
  • 被monitorenter和monitorexit包围住的指令就是上锁的代码;
  • 第二个monitorexit存在的原因是:防止上锁的代码抛出异常后不能及时释放锁;

monitor主要和Synchronized锁住的对象关联,如下图:

image.png

Monitor内部存储结构:

  • WaitSet:关联调用了wait()方法的线程,处于 Waiting状态的线程;
  • EntryList:关联没有抢到锁的线程,处于Blocked状态的线程;
  • Owner:存储当前获取锁的线程,只有一个线程可以获取;

相关流程:

  • 代码进入synchorized代码块,先让lock(对象锁)关联的monitor,然后判断Owner是否有线程持有;
  • 如果没有线程持有,则让当前线程持有,表示该线程获取锁成功;
  • 如果有线程持有,则让当前线程进入entryList进行阻塞,如果Owner持有的线程已经释放了锁,在EntryList中的线程去竞争锁的持有权(非公平);
  • 如果代码块中调用了wait()方法,则会进去WaitSet中进行等待;

线程间的通信:

在 Java 中,线程之间的通信主要是为了实现协作,即使得多个线程能够协调工作,以完成特定任务。线程通信主要分为三种方式:共享内存消息队列(等待-通知)管道流。 1 在进行探讨线程通信之前,我们先熟悉一个非常重要的概念:JMM(Java Memory Model)

JMM(Java Memory Model):

  • JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型;

  • Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节;

JMM的核心概念:

主内存与工作内存

  • 主内存(Main Memory) :所有线程共享的内存区域,包括静态变量和实例变量。

  • 工作内存(Working Memory) :每个线程都有自己独立的工作内存,线程对共享变量的操作首先在工作内存中进行,然后再同步到主内存。

image.png

关于JMM的特点:

  1. 所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
  2. 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
  3. 线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。

现在回过头来,细说关于线程之间的通信方式。

  • 共享内存: 线程之间通过访问相同的共享变量来实现通信,如上述所说的JMM就是其底层原理。线程通过对共享变量的读写操作来实现对数据的交换和共享。其中还有一个重要的关键字: volatile(主要目的是保证线程间的可见性防止指令重排序。在本文末尾会有详细解释); 示例:
public class SharedMemoryExample {
    // 共享变量
    private int counter = 0; 

    public synchronized void increment() {
        // 增加
        counter++; 
    }

    public synchronized void decrement() {
        // 减少
        counter--; 
    }

    public synchronized int getCounter() {
        return counter;
    }

    public static void main(String[] args) throws InterruptedException {
        SharedMemoryExample example = new SharedMemoryExample();

        Thread incrementThread = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        Thread decrementThread = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.decrement();
            }
        });

        incrementThread.start();
        decrementThread.start();

        incrementThread.join();
        decrementThread.join();

        System.out.println("Final Counter Value: " + example.getCounter());
    }
}
  • 消息传递: 线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,比如  wait()/ notify()/ notifyAll()join() 方式。 示例:
package com.xieq.message;

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * 消息队列
 */
public class MessageQueueExample {
    private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);

    public static void main(String[] args) {

        // 生产者线程
        Thread producer = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                String message = "Message-" + i;
                queue.add(message);
                System.out.println("Produced: " + message);
                try {
                    // 模拟生产时间
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            while (true) {
                try {
                    String message = queue.take();
                    System.out.println("Consumed: " + message);
                    // 模拟处理时间
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        producer.start();
        consumer.start();
    }

}
  • 管道通信: 管道流是是一种使用比较少的线程间通信方式,管道输入/输出流和普通文件输入/输出流或者网络输出/输出流不同之处在于,它主要用于线程之间的数据传输,传输的媒介为管道**。比如 管道输入/输出

Volatile关键字

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile关键字修饰之后,具备两层含义:

  1. 保证线程间的可见性:

volatile 关键字主要保证了变量的可见性。在没有 volatile 的情况下,线程对共享变量的修改可能无法立即反映到其他线程中。具体来说,每个线程可能会在本地缓存(寄存器或者线程的工作内存)中读取共享变量的副本,而不是直接从主内存中读取。如果线程 A 修改了某个共享变量,线程 B 可能无法立即看到线程 A 对该变量的修改,导致数据不一致。

使用 volatile 修饰变量后,保证了以下两点:

  • 每次读写操作都会直接与主内存交互,保证了其他线程能够看到最新的值。
  • 禁止线程对 volatile 变量进行缓存,即强制从主内存中读取最新的值。

  1. 禁止进行指令重排序

volatile 还具有禁止指令重排的作用。现代 CPU 会对代码执行进行优化,以提高性能,例如将指令进行重排。指令重排会改变程序执行的顺序,但它不会改变程序的单线程执行结果。在多线程环境下,这种优化可能会导致线程间的操作顺序错乱。

当一个变量被声明为 volatile 时,Java 保证:

  • 程序在写 volatile 变量时,前面的所有操作会在写操作之前完成
  • 程序在读 volatile 变量时,后面的所有操作会在读取操作之后开始执行

这就避免了线程间的操作顺序不一致的情况。

// volatile确保线程间的可见性

public class VolatileExample {
    private volatile boolean flag = false;

    public void producer() {
        // 修改共享变量
        flag = true; 
    }

    public void consumer() {
        while (!flag) {
            // 如果 flag 为 false,消费者线程会一直在此循环等待
        }
        System.out.println("Consumer thread sees flag is true.");
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileExample example = new VolatileExample();
        
        // 生产者线程
        Thread producer = new Thread(example::producer);

        // 消费者线程
        Thread consumer = new Thread(example::consumer);

        consumer.start();
        // 确保消费者先启动
        Thread.sleep(1000); 
        producer.start();
    }
}

在上述代码中,若flag变量未使用volatile修饰,则可能会导致消费者线程一直没看到flag的变化,从而导致死循环。

四、线程池

线程池是什么?

线程池(Thread Pool)是一种用来管理和复用线程的机制。线程池通过预先创建一定数量的线程,减少了频繁创建和销毁线程的开销,提高了程序的性能和响应速度,同时也可以有效地控制线程的数量,避免系统资源的过度消耗。

使用线程池的优势:

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

线程池的生命周期管理

ThreadPoolExecutor 中的生命周期状态通过一个内部变量表示,包括以下几种状态:

状态描述
RUNNING默认状态,线程池可以接收新任务并处理队列中的任务。
SHUTDOWN调用 shutdown() 后进入此状态,线程池不再接收新任务,但会继续执行队列中已提交的任务。
STOP调用 shutdownNow() 后进入此状态,线程池不再接收新任务,同时会尝试停止正在执行的任务,并清空队列中的任务。
TIDYING线程池在完全停止后进入此状态,此时所有的任务都已完成,线程池中没有活动线程,terminated() 钩子方法会在进入该状态时被调用。
TERMINATED线程池完全终止的状态。terminated() 钩子方法执行完成后,线程池进入此状态,生命周期结束。

生命周期状态的转换: 线程池的状态转换:

  1. 初始为 RUNNING

  2. 调用 shutdown() 后,状态从 RUNNING 转换为 SHUTDOWN

  3. 调用 shutdownNow() 后,状态从 RUNNINGSHUTDOWN 转换为 STOP

  4. 当任务完成并且线程池关闭后,状态从 SHUTDOWNSTOP 转换为 TIDYING

  5. TIDYING 状态时,terminated() 方法执行完成后,状态最终转换为 TERMINATED

image.png

线程池的执行原理

线程池的核心参数: 线程池核心参数主要参考ThreadPoolExecutor这个类的7个参数的构造函数;

image.png

  1. corePoolSize------核心线程数;
  2. maximumPoolSize------最大线程数目 = (核心线程+救急线程的最大数目);
  3. keepAliveTime------生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放;
  4. unit------时间单位 - 救急线程的生存时间单位;
  5. workQueue------当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务;
  6. threadFactory 线程工厂------可以定制线程对象的创建;
  7. handler------拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略;

线程池的工作流程:

image.png

  1. 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
  2. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
  3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
  4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
  5. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

为什么不建议用Executors创建线程池:

image.png

线程池的使用场景

具体线程池相关技术可以参考美团技术团队--# Java线程池实现原理及其在美团业务中的实践

五、ThreadLocal

ThreadLocal是什么?

ThreadLocal 是 Java 提供的一个类,它为每个线程提供独立的变量副本。每个线程在访问 ThreadLocal 变量时,都会得到自己线程内的副本,而不是共享的静态变量。这种方式避免了多线程环境下的竞争和同步问题。

ThreadLoacl底层实际是由ThreadLoaclMap实现,每个Thread对象中都存在一个ThreadLoaclMap,其中keyThreadLocal对象,value为需要缓存的值。综上所述,ThreadLocal是用来操作当前线程中ThreadLocalMap的一个工具类。

  • 实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题;
  • 实现了线程内的资源共享;

示例:使用JDBC操作数据库时,会将每一个线程的Connection放入各自的ThreadLocal中,从而保证每个线程都在各自的 Connection 上进行数据库的操作,避免A线程关闭了B线程的连接。

image.png

ThreadLocal的生命周期

  • ThreadLocal 的生命周期与创建它的线程密切相关。每个线程会保存自己的一份副本,而副本仅在该线程存活期间有效。

  • 一旦线程结束,这些局部变量将会被垃圾回收。

ThreadLocal的隔离特性

  • 每个线程都有自己独立的变量副本,因此不会发生多线程之间的数据竞争。

  • 但是,它也带来了一些内存管理问题,特别是在使用线程池时,可能导致内存泄漏。在使用线程池时,应在合适的时机调用 ThreadLocalremove() 方法来手动清理。

ThreadLocal的基本使用

ThreadLocal 提供了几个关键方法来操作线程局部变量:

  • ThreadLocal<T> threadLocal = new ThreadLocal<>():创建一个 ThreadLocal 实例。
  • T get():获取当前线程的局部变量副本。如果该线程第一次访问该变量,则会调用 initialValue() 方法来初始化它。
  • void set(T value):设置当前线程的局部变量副本。
  • void remove():移除当前线程的局部变量副本。

ThreaLocal示例

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

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            // 获取当前线程的值
            Integer value = threadLocal.get(); 
            System.out.println(Thread.currentThread().getName() + " Value: " + value);
            // 设置新值
            threadLocal.set(value + 1); 
        };

        Thread thread1 = new Thread(task, "Thread-1");
        Thread thread2 = new Thread(task, "Thread-2");
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }
}

阿里开发手册推荐使用场景

import java.text.DateFormat;
import java.text.SimpleDateFormat; 
public class DateUtils { 
           public static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
            @Override 
            protected DateFormat initialValue() { 
            return new SimpleDateFormat("yyyy-MM-dd"); 
} }; 
}

// 在需要用到的地方按如下所示调用
DateUtils.df.get().format(new Date());