一、前言
多线程是什么?
Java的多线程是指:允许程序同时执行多个线程的编程技术。在深入了解Java线程知识点之前,我们先熟悉几个基本概念,线程和进程、并行和并发。
多线程的关键特点:
- 轻量级:线程是进程的子任务,共享同一内存空间和资源。
- 并发:多个线程可以独立执行,从而提高程序效率。
- 共享资源:线程可以共享同一个进程内的变量和数据。
多线程的优缺点:
优点:
- 提高程序并发性。
- 更高效地利用多核处理器。
缺点:
- 增加了开发复杂性。
- 容易出现线程安全问题,例如死锁和资源争用。
进程和线程的关系:
进程(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对象,FutureTask是Runnable的实现类,可以用于提交到线程中执行。 - 使用
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;
}
线程状态之间的变化如下图所示:
-
新建
- 当一个线程对象被创建,但还未调用
start方法时处于新建状态; - 此时未与操作系统底层线程关联;
- 当一个线程对象被创建,但还未调用
-
可运行
- 调用了 start 方法,就会由新建进入可运行;
- 此时与底层线程关联,由操作系统调度执行;
-
终结
- 线程内代码已经执行完毕,由可运行进入终结;
- 此时会取消与底层线程关联;
-
阻塞
- 当获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,此时不占用 cpu 时间;
- 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态;
-
等待
- 当获取锁成功后,但由于条件不满足,调用了
wait()方法,此时从可运行状态释放锁进入 Monitor 等待集合等待,同样不占用 cpu 时间; - 当其它持锁线程调用
notify()或notifyAll()方法,会按照一定规则唤醒等待集合中的等待线程,恢复为可运行状态;
- 当获取锁成功后,但由于条件不满足,调用了
-
限时等待
- 当获取锁成功后,但由于条件不满足,调用了
wait(long)方法,此时从可运行状态释放锁进入 Monitor 等待集合进行有时限等待,同样不占用 cpu 时间 - 当其它持锁线程调用
notify()或notifyAll()方法,会按照一定规则唤醒等待集合中的有时限等待线程,恢复为可运行状态,并重新去竞争锁; - 如果等待超时,也会从有时限等待状态恢复为可运行状态,并重新去竞争锁;
- 还有一种情况是调用
sleep(long)方法也会从可运行状态进入有时限等待状态,但与 Monitor 无关,不需要主动唤醒,超时时间到自动恢复为可运行状态 ;
- 当获取锁成功后,但由于条件不满足,调用了
线程创建和管理相关关键点:
Runable和Callable的区别?
Runnable的run()方法没有返回值,而Callable的call()方法有返回值,是一个泛型,和Futrue、FutureTask配合使用来获取异步执行的结果。Callable接口支持返回执行结果,需要调用FutrueTask.get()方法得到,此方法会阻塞主进程继续往下执行。Callable的call()方法允许抛出异常,而Runnable接口的run()方法异常只能在内部消化,不能往上抛出。
run()方法和start()方法的区别?
start()方法用来启动线程,通过该线程调用run()方法,执行run()方法中所定义的逻辑代码。start()方法只能被调用一次。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的作用:
- 确保线程安全:当多个线程访问同一个共享资源时,
synchronized可以通过锁定资源来保证只有一个线程在某一时刻能访问资源。 - 避免数据竞争:多个线程同时操作共享变量时,使用
synchronized可以避免对变量的并发修改,从而导致数据不一致。
Synchronized的使用方式:
- 修饰实例方法(对象锁):
- 当方法声明为
synchronized时,表示该方法的实例级锁(每个实例都有一个锁)。 - 同一时刻,只有一个线程可以访问该实例的同步方法。
- 当方法声明为
public class SynchronizedExample {
private int counter = 0;
public synchronized void increment() {
counter++;
}
public synchronized int getCounter() {
return counter;
}
}
- 修饰类方法(类锁):
- 如果方法是
static的,那么锁是类级别的,所有实例共享同一个锁。
- 如果方法是
public class SynchronizedExample {
private static int counter = 0;
public static synchronized void increment() {
counter++;
}
public static synchronized int getCounter() {
return counter;
}
}
-
修饰代码块(显式锁):
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,反编译效果如下:
- monitorenter 开始上锁的位置;
- monitorexit 解锁的位置;
- 被monitorenter和monitorexit包围住的指令就是上锁的代码;
- 第二个monitorexit存在的原因是:防止上锁的代码抛出异常后不能及时释放锁;
monitor主要和Synchronized锁住的对象关联,如下图:
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) :每个线程都有自己独立的工作内存,线程对共享变量的操作首先在工作内存中进行,然后再同步到主内存。
关于JMM的特点:
- 所有的共享变量都存储于主内存(计算机的RAM)这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
- 每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。
- 线程对变量的所有的操作(读,写)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。
现在回过头来,细说关于线程之间的通信方式。
- 共享内存: 线程之间通过访问相同的共享变量来实现通信,如上述所说的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关键字修饰之后,具备两层含义:
- 保证线程间的可见性:
volatile 关键字主要保证了变量的可见性。在没有 volatile 的情况下,线程对共享变量的修改可能无法立即反映到其他线程中。具体来说,每个线程可能会在本地缓存(寄存器或者线程的工作内存)中读取共享变量的副本,而不是直接从主内存中读取。如果线程 A 修改了某个共享变量,线程 B 可能无法立即看到线程 A 对该变量的修改,导致数据不一致。
使用 volatile 修饰变量后,保证了以下两点:
- 每次读写操作都会直接与主内存交互,保证了其他线程能够看到最新的值。
- 禁止线程对
volatile变量进行缓存,即强制从主内存中读取最新的值。
- 禁止进行指令重排序
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() 钩子方法执行完成后,线程池进入此状态,生命周期结束。 |
生命周期状态的转换: 线程池的状态转换:
-
初始为
RUNNING。 -
调用
shutdown()后,状态从RUNNING转换为SHUTDOWN。 -
调用
shutdownNow()后,状态从RUNNING或SHUTDOWN转换为STOP。 -
当任务完成并且线程池关闭后,状态从
SHUTDOWN或STOP转换为TIDYING。 -
在
TIDYING状态时,terminated()方法执行完成后,状态最终转换为TERMINATED。
线程池的执行原理
线程池的核心参数: 线程池核心参数主要参考ThreadPoolExecutor这个类的7个参数的构造函数;
- corePoolSize------核心线程数;
- maximumPoolSize------最大线程数目 = (核心线程+救急线程的最大数目);
- keepAliveTime------生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放;
- unit------时间单位 - 救急线程的生存时间单位;
- workQueue------当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务;
- threadFactory 线程工厂------可以定制线程对象的创建;
- handler------拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略;
线程池的工作流程:
- 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
- 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
- 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
- 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
- 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
为什么不建议用Executors创建线程池:
线程池的使用场景
具体线程池相关技术可以参考美团技术团队--# Java线程池实现原理及其在美团业务中的实践;
五、ThreadLocal
ThreadLocal是什么?
ThreadLocal 是 Java 提供的一个类,它为每个线程提供独立的变量副本。每个线程在访问 ThreadLocal 变量时,都会得到自己线程内的副本,而不是共享的静态变量。这种方式避免了多线程环境下的竞争和同步问题。
ThreadLoacl底层实际是由ThreadLoaclMap实现,每个Thread对象中都存在一个ThreadLoaclMap,其中key为ThreadLocal对象,value为需要缓存的值。综上所述,ThreadLocal是用来操作当前线程中ThreadLocalMap的一个工具类。
- 实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题;
- 实现了线程内的资源共享;
示例:使用JDBC操作数据库时,会将每一个线程的Connection放入各自的ThreadLocal中,从而保证每个线程都在各自的 Connection 上进行数据库的操作,避免A线程关闭了B线程的连接。
ThreadLocal的生命周期
-
ThreadLocal的生命周期与创建它的线程密切相关。每个线程会保存自己的一份副本,而副本仅在该线程存活期间有效。 -
一旦线程结束,这些局部变量将会被垃圾回收。
ThreadLocal的隔离特性
-
每个线程都有自己独立的变量副本,因此不会发生多线程之间的数据竞争。
-
但是,它也带来了一些内存管理问题,特别是在使用线程池时,可能导致内存泄漏。在使用线程池时,应在合适的时机调用
ThreadLocal的remove()方法来手动清理。
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());