文章目录
1. 线程池
1.1 定义
线程池是一种线程使用模式,是一种池化技术。线程过多会带来调度开销,进而影响缓存局部性能和整体性能。而线程池时提前创建若干线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。线程池维护着多个线程,等待着监督管理者分配可并发执行的任务,避免了频繁创建与销毁线程的代价。线程池不仅能保证CPU的充分利用,还能防止过度调度。
1.2 优点
- 降低资源消耗、提高响应速度
- 方便线程管理:线程可以复用、控制线程并发数量
1.3 线程池的基本框架
1.4 常见线程池
Executors类中提供了静态方法创建常用线程池。
-
newSingleThreadExector():单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务。
-
newFixedThreadPool(int n):固定工作线程数量的线程池,每提交一个任务就创建一个工作线程,如果达到线程池的最大数量,则将提交的任务存入到线程池队列中。
FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程所消耗开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,他不会释放工作线程,还会占用一定的系统资源。publilc static void main(String[] args){ ExecutorService threadPool = Executors.newFixedThreadPool(); }
-
newCacheThreadPool():可缓存线程池。如果线程长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认1分钟),则该工作线程将自动终止。终止后,如果又提交了新的任务,则线程池重新创建一个工作线程,在使用CachedThreadPool时,一定要注意控制任务数量,否则,由于大量线程同时运行,很可能会造成系统瘫痪。
-
newScheduleThreadPool():大小无限制的线程池,支持定时和周期性的执行线程。
这4种线程池底层都是有ThreadPoolExecutor对象实现的,阿里规范手册中规定线程池采用ThreadPoolExecutor自定义的,实际开发也是。
1.5 为什么要使用线程池
-
降低资源消耗:通过重复利用一创建的线程降低线程创建和销毁造成的资源消耗。
-
提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行。
-
提高线程的客观理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控。
1.6 底层工作原理
-
线程池刚创建时,里面没有任何线程,等到有任务过来的时候才会创建线程。当然也可以调用prestartAllCoreThreads()或者prestartCoreThread()方法与创建corePoolSize个线程。
-
调用execute()提交一个任务时,如果当前的工作线程数<corePoolSize,直接创建新的线程执行这个任务。
-
如果当时工作线程数量>=corePoolSize,会将任务放入任务队列中缓存。
-
如果队列已满,并且线程池中工作线程的数量<manimunPoolSize,还是会创建线程执行这个任务。
-
如果队列已满,并且线程池中的线程已达到maximunPoolSize,这个时候会执行拒绝策略,Java线程池默认的拒绝策略是AbortPolicy,即抛出RejectedExecutionException异常。
1.7 线程池的五种状态
线程池有五种状态,分别是RUNNING, SHUTDOWN, STOP, TIDYING, TERMINATED。
-
状态转换图
-
说明
-
RUNNING
- 状态说明:线程池处于RUNNING状态时,能够接受新任务,以及对已添加的任务及逆行处理。
- 状态切换:线程池的初始化状态是RUNNING。即线程池一旦被创建,就处于RUNNING状态,并且线程池中任务树为0。
private final AtomicInteger ctl = new AtomicTnteger(ctlof(RUNNING, 0));
-
SHUTDOWN
- 状态说明:处于SHUTDOWN状态时,不接受新任务,但能处理已添加的任务。
- 状态切换:调用线程池的shutdown()方法时,线程池由 RUNNING -> SHUTDOWN。
-
STOP
- 状态说明:线程池处于STOP状态时,不接受新任务,不处理已添加的任务,并且会中断正在处理的任务。
- 状态切换:调用线程池的shutdownNow()方式时,线程池由 RUNNING/SHUTDOWN -> STOP。
-
TIDYING
- 状态说明:当所有的任务已终止,ctl记录的”任务数量“为0,线程池会变成TIDYING状态。当线程池变为TIDYING状态时,hi执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TINDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
- 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。当线程池在STOP状态下,线程池中执行的任务为空时,就会由 STOP -> TIDYING。
-
TERMINATED
- 状态说明:线程池彻底终止,就会变成TERMINATED状态。
- 状态切换:线程池处于TIDYING状态时,执行完terminated()之后,就会由 TINDYING -> TERMINATED。
-
2. ThreadPoolExecutor
2.1 常见参数——七大参数
- corePoolSize:核心线程数
在ThreadPoolExecutor中有一个与它相关的配置:allowCoreThreadTimeOut(默认为false),当allowCoreThreadTimeOut为false时,核心线程会一直存活,哪怕是一直空闲着。默认情况下,核心池大小为0。而当allowCoreThreadTimeOut为true时核心线程空闲时间超过keepAliveTime时会被回收。 - maximumPoolSize:最大线程数
表示线程池中最大线程的数量。当工作队列已满并且池中的线程数小于最大池大小时,线程池会创建新的线程来处理任务,直到达到最大池大小。默认情况下,最大池大小为Integer.MAX_VALUE。 - keepAliveTime:线程存活时间
表示当线程池中的线程数量超过核心池大小时,空闲线程的存活时间。当线程空闲时间超过keepAliveTime时,多余的线程会被销毁,直到线程池中的线程数等于核心池大小。默认情况下,空闲线程会立即被销毁。 - unit:时间单位
用于指定keepAliveTime的时间单位,可以是纳秒(TImeUnit.NANOSECONDS)、微秒(TimeUnit.MICROSECONDS)、毫秒(TimeUnit.MILLISECINDS)、秒(TImeUnit>SECONDS)、分钟(TimeUnit.MINUTES)、小时(TimeUnit.HOURS)、天(TimeUnit.DAYS)。 - workQueue:工作队列
用于存储等待执行的任务。Java提供了多种类型的工作队列,如SynchronousQueue、LinkedBlockingQueue、ArrayBlockingQueue等。默认情况下,使用无界队列LinkedBlockingQueue。 - threadFactory:线程工厂
用于创建新线程。可以自定义线程工厂来指定线程的命名规则、优先级等。 - handler:拒绝策略
用于处理线程池队列已满且无法接受新任务时的情况。Java提供了几种内置的拒绝策略,也可以自定义拒绝策略。
2.2 拒绝策略
- AbortPolicy:丢弃任务并抛出RejectedExecutionException异常(默认拒绝策略)。
- DiscardPolicy:丢弃任务,但是不抛出异常。
- DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
- CallerRunsPolicy:由调用线程处理该任务。
2.3 Java线程池执行任务的过程
以ThreadPoolExecutor为例进行说明,ThreadPoolExecutor是Java中线程池的实现类,它实现了ExecutorService接口,用于执行任务的管理和调度。执行过程如下:
1. 检查状态:在执行任务之前,ThreadPoolExecutor会检查自身的状态,例如是否已经关闭。
2. 判断核心线程数:检查当前线程池中的线程数量,如果达到了核心线程数,则创建新的线程来执行任务。
3. 任务队列处理:如果达到了核心线程数,会将任务放入任务队列(通过BlockingQueue实现),等待执行。
4. 判断最大线程数:如果任务队列已满,并且当前线程池中的线程数量还未达到最大线程数,则会创建新的线程来执行任务。
5. 拒绝策略:如果任务队列已满,并且当前线程池中的线程数量已经达到最大线程数,会根据设置的拒绝策略来决定如何处理无法接受的新任务。
6. 执行任务:会从任务队列中去除任务,并将任务分配给工作线程来执行。工作线程会执行任务中的run()方法。
7. 结果返回和异常处理:任务执行完成后,可以通过Future对象获取任务的执行结果。如果任务执行过程中发生异常,ThreadPoolExecutor会捕获并处理异常。
8. 线程池关闭:当需要关闭线程池时,可以调用shutdown()方法。ThreadPoolExecutor会停止接受新任务,并尽量将已提交的任务执行完毕。如果需要立即停止所有的任务并返回尚未执行的任务列表,可以调用shutdownNow()方法。
2.4 核心线程池大小如何设置
- CPU密集型任务(N+1):这种任务消耗的主要是CPU资源,可以将线程数量设置为N(CPU核心数)+1,比CPU核心数多出来一个线程是为了防止线程偶发的缺页中断,或者其他原因导致的任务暂停而带来的映像。一旦任务暂停,CPU就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用CPU的空闲时间。
- I/O密集型任务(2N+1):这种任务应用起来系统会用大部分时间来处理I/O交互,而线程在处理I/O的时间段内不会占用CPU来处理,这是就可以将CPU交出来给其他线程使用。因此在I/O密集型任务的应用中,我们可以多配置一些线程,具体的计算方法时2N。
2.5 如何判断是CPU密集型还是IO密集型任务?
CPU密集型简单来说就是利用CPU计算能力的任务。例如在内存中能够对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是IO密集型,这类任务的特点是CPU计算耗时相比于等待IO操作完成的时间来说很少,大部分时间都花在了等到IO操作上。
3. Synchronized
3.1 结构图
3.2 底层实现原理
synchronized是Java中用于实现同步的关键字,它可以应用在方法或代码块上,用于控制多个线程对共享资源的访问。底层实现synchronized的机制主要涉及到对象头(Object Header)和监视器(Monitor)。
在Java中,每个对象都有一个对象头,对象头中包含了一些用于存储对象元数据的信息,其中就包括一个用于存储锁信息的字段。当一个线程进入synchronized方法或代码块时,它会尝试获取对象的锁。
具体来说,synchronized的底层实现主要有以下几个关键步骤:
- 获取锁:当一个线程进入synchronized方法或代码块时,会尝试获取对象的锁。如果该对象的锁没有被其他线程持有,则当前线程会成功获取锁,并继续执行;否则,当前线程会被阻塞,直到获取锁为止。
- 对象头标记:获取锁成功后,Java虚拟机会在对象头中的锁信息字段中做标记,以表示当前线程持有该对象的锁。
- 执行同步代码:一旦线程获取到锁,它可以执行synchronized方法或代码块中的代码。在同步代码执行期间,其他线程虽然可能会尝试获取该对象的锁,但由于锁已经被当前线程持有,他们将被阻塞等待。
- 释放锁:当线程退出synchronized方法或代码块时,它会释放锁对象。Java虚拟机会清除对象头中的锁信息字段,以表示当前线程已经不再持有该对象的锁。此时,其他被阻塞的线程可以竞争获取该对象的锁,进而继续执行。
总的来说,synchronized的底层实现涉及到对象头和监视器,通过获取对象的锁来实现多线程之间的同步访问,从而保证了共享资源的安全访问。
3.3 使用方式
- 修饰方法
- synchronized可以修饰普通方法,使用该关键字修饰的方法称为同步方法。在同步方法中,当线程访问该方法时,会自动获取该方法所在对象的监视器,其他线程则被阻塞,直到该线程执行完该方法释放锁。
- 同步方法是隐式获取锁的方式,它的锁是该方法所在对象的实例锁(即对象锁)。
- 修饰代码块
- synchronized还可以修饰代码块,使用该关键字修饰的代码块称为同步块。在同步块中,需要指定获取所得对象或者类。
- 当线程执行到同步代码块时,会尝试获取指定对象(或类)的监视器,其他线程则被阻塞,直到该线程执行完同步块,释放锁。
- 同步块的锁可以是任意对象,也可以使用this关键字表示当前对象的实例锁,还可以使用类名.class表示类锁。
3.4 特点
- synchronized保证了同一时间只有一个线程可以执行同步代码块或同步方法,从而保证了共享资源的安全访问。
- 原子性:即在同步代码块或同步方法内的操作不会被其他线程打断。
- 可见性:即一个线程对共享变量的修改会立即对其他线程可见。
- 重量级锁:获取和释放锁涉及到用户态和内核态之间的转换,会带来一定的性能开销。
3.5 锁升级
在Java中,锁是一种用于实现线程同步的机制。为了提高并发性能和减少内存开销,Java中的锁可以进行升级操作,将低级别的锁升级为高级别的锁,这个过程称为锁升级。
在Java中,锁级别分为4级,由低到高依次是:无锁状态,偏向锁,轻量级锁,重量级锁。
- 无锁状态(No lock):表示没有任何锁机制。多线程并发执行时,线程间不会相互干扰,就可能出现无序的、不一致的结果。
- 偏向锁(Biased Locking):偏向锁是一种针对多线程竞争情况下的常见场景进行优化的锁机制。当一个线程获取锁时,会在对象头部记录该线程的ID,并将锁标记为偏向锁。此后,如果再次执行相同的线程请求锁操作,就可以直接获取锁,而无需进行任何同步操作。这种情况下,锁的性能开销非常小。偏向锁适用于大部分情况下只有一个线程访问共享资源的场景。
- 轻量级锁(Lightweight Locking):也称为自旋锁。当多线程竞争同一锁时,偏向锁就会自动升级为轻量级锁。轻量级锁使用CAS(比较-交换)操作来实现,不涉及系统调用。它通过在对象头部的标记字段存储锁记录的指针以及线程ID的方式,来避免互斥同步操作。当线程获取轻量级锁时,通过CAS操作对象头不得标记字段修改为指向线程私有的锁记录。如果CAS操作成功,表示当前线程获取了锁;否则,表示其他线程竞争成功,锁升级为重量级锁。
- 重量级锁(Heavyweight Locking):当多个线程竞争同一个锁并且轻量级锁获取失败时,锁会升级为重量级锁。重量级锁依赖于操作系统的底层同步机制,如互斥量(mutex)或信息量(semaphore)。它会导致线程阻塞和切换,且性能开销较大。重量级锁适用于多个线程频繁争用锁的场景。
总的来说,Java中的锁升级过程是从无锁状态到偏向锁到轻量级锁,再到重量级锁。锁升级的过程是根据线程竞争的情况来判断和执行的。在大部分情况下,偏向锁和轻量级锁的性能开销较小,能够提升程序执行效率。只有在多线程竞争激烈的情况下,才hi升级为重量级锁以保证线程安全。因此,在设计高并发系统时,需要根据实际情况选择适当的锁机制,以提高系统性能和线程安全性。
4. Volatile
4.1 底层实现
在Java中,volatile的底层实现涉及到内存屏障(Memory Barrier)或者内存栅栏(Memory Fence)。内存屏障是一种硬件或者指令级别的机制,用于确保特定顺序的内存访问操作不会被重排序。
具体来说,volatile的底层实现包括以下几个方面:
-
写屏障(Write Barrier):当一个线程写入一个volatile变量时,会在写入操作之后插入一个写屏障。这个写屏障会确保该写入操作对其他线程可见,即写入操作的结果会立即被其他线程看到,而不会被缓存或者重排序到写入操作之后的某个时刻。
-
读屏障(Read Barrier):当一个线程读取一个volatile变量时,会在读取操作之前插入一个读屏障。这个读屏障会确保该读取操作能够看到其他线程对该变量的最新修改,而不会使用线程自己的缓存值。
-
禁止指令重排序:volatile关键字还会告诉编译器和处理器不要对被volatile修饰的变量的读写操作进行重排序。这样可以确保对volatile变量的写操作不会被重排序到之后,对volatile变量的读操作不会被重排序到之前。
-
内存可见性:通过写屏障和读屏障的作用,volatile变量的修改对其他线程是可见的。这意味着,当一个线程修改了volatile变量的值后,其他线程能够立即看到这个修改,而不会出现数据不一致的情况。
总体来说,volatile的底层实现依赖于内存屏障或者内存栅栏,通过写屏障、读屏障和禁止指令重排序等机制来确保volatile变量的可见性和禁止指令重排序,从而保证多线程环境下对volatile变量的安全访问。
4.2 特点
在Java中,volatile是一个关键字,用于修饰变量。具有以下3个特点:
- 可见性:volatile关键字保证了变量的可见性。当一个线程修改了volatile修饰的变量的值时,其他线程可以立即看到修改之后的值,而不会使用缓存中的旧值。
- 禁止指令重排序:volatile关键字禁止了对被修饰变量的指令重排序。指令重排序是编译器和处理器为了提高执行效率而进行的重排序操作,但可能会导致多线程程序出现问题。通过使用volatile关键字,可以确保特定的变量操作按照预期的顺序执行,避免了一些潜在的线程安全问题。
- 不保证原子性:volatile关键字并不能保证变量操作的原子性。如果一个操作涉及到多个步骤,且需要保证原子性的话,还需要使用其他的同步手段,例如synchronized关键字或java.util.concurrent包下的原子类。
注意:volatile关键字主要用于修饰多个线程之间共享的变量,在保证可见性和禁止指令重排序方面带来了便利。然而,他并不能解决所有的线程安全问题,例如符合操作的原子性。在编写多线程程序时,人需要根据具体场景来选择适当的线程同步手段。
4.3 volatile和synchronized的区别
- volatile本质是告诉JVM当前变量在寄存器中的是不确定的,需要从主存中获取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能用在变量级别,而synchronized可以使用在变量、方法、类级别。
- volatile仅能实现变量的修改和可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
- volatile不会造成线程的阻塞,synchronized可能会造成线程阻塞。
- volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化。
5. Lock
5.1 底层原理
在Java中,Lock接口及其实现类(如Reentrantlock)提供了一种显式的锁机制,用于多线程环境下对共享资源的安全访问。Lock的底层原理涉及到操作系统提供的底层同步机制,以及Java内部的实现方式。
Lock的底层原理的一般描述如下:
-
底层同步机制:Java中的Lock接口及其实现类在底层使用了操作系统提供的底层同步机制,通常是基于操作系统的互斥锁(Mutex)或信号量(Semaphore)实现的。这些底层同步机制能够确保在任意时刻只有一个线程能够持有锁,从而保证了共享资源的互斥访问。
-
自旋与阻塞:在使用Lock时,当一个线程尝试获取锁时,如果锁已经被其他线程持有,那么该线程可能会进入自旋等待或者被阻塞,直到获取到锁为止。自旋等待是指线程反复尝试获取锁,而不进入休眠状态,这可以减少线程上下文切换的开销。如果自旋等待一定时间后仍未获取到锁,线程可能会转为阻塞状态,等待锁释放。当锁被持有的时间较长或者线程数较多时,更倾向于使用阻塞等待,以避免资源浪费。
-
条件变量:Lock接口中提供了与底层同步机制配合使用的条件变量(Condition),用于支持线程之间的协调和通信。条件变量能够让等待某个条件的线程进入等待状态,并在条件满足时被唤醒。
-
可中断锁:Lock接口支持可中断的锁获取操作,即线程在等待锁的过程中可以被中断。这意味着在某些情况下,可以通过中断线程来取消对锁的获取操作,避免线程长时间阻塞。
-
公平性与非公平性:Lock实现类通常支持公平性和非公平性两种获取锁的方式。公平锁是指锁按照线程请求锁的顺序分配,而非公平性则允许请求的线程在旧线程之前获取锁。公平性通常会带来较高的系统开销,但能够避免线程饥饿。
总的来说,Lock的底层原理涉及到操作系统提供的底层同步机制、自旋与阻塞、条件变量、可中断性以及公平性和非公平性等方面,以实现多线程环境下的安全访问和线程协作。
5.2 synchronized和Lock的区别
都是用于实现线程同步和互斥访问共享资源的目的。主要区别有:
- 使用方式:synchronized是Java关键字,可以直接在方法或者代码块上使用,使用起来简单;而Lock是一个接口,需要通过具体的实现类(如ReentrantLock)创建实例,并且需要手动进行加锁和解锁的操作。
- 灵活性:Lock相当于synchronized提供了更多的灵活性,例如,Lock可以实现公平锁,而synchronized只能是非公平锁。此外,Lock还支持尝试获取锁、定时获取锁等功能,而synchronized没有提供这些特性。
- 性能:在低竞争情况下,synchronized的性能通常比Lock好,因为synchronized是由JVM底层进行优化的。在高竞争情况下,Lock的性能可能会更好,因为它提供了更细粒度的控制和更多的优化机会。
- 异常处理:使用synchronized时,如果发生异常,JVM会自动释放锁,而使用Lock时,需要手动在finally块中释放锁,以确保锁的释放,避免思索地发生。
synchronized使用于简单的线程同步场合,使用起来更加方便,而Lock适用于复杂的同步需求,提供了更多的高级特性。
6. AQS锁
6.1 概述
AQS(AbstractQueuedSynchronizer)是Java中用于构建同步器的框架,可以用来实现各种形式的同步控制。AQS提供了一种基于FIFO等待队列的机制,通过维护一个状态变量和一个等待队列来管理同步状态和线程的竞争。
AQS是一个抽象类,定义了两种锁:独占锁(Exclusive Lock)和共享锁(Shared Lock)。具体的同步器可以通过继承AQS并实现其中的几个方法来定义同步器,如ReentrantLock、Semaphore等都是基于AQS实现的。
AQS的核心思想是使用一个整型的state表示同步状态,当state为0时表示没有线程持有锁,大于0表示有线程持有锁。小于0表示有线程在等待获取锁。通过对state的修改和CAS操作,实现线程的加锁和释放锁。
AQS提供了两种主要的方法:acquire和release。acquire用来获取独占锁,如果获取失败则会将当前线程加入到等待队列中;release用来释放锁,并唤醒等待队列中的县城区竞争锁。
6.2 常见的AQS锁
-
ReentrantLock:可重入锁,时java.util.concurrent包中提供的基于AQS独占锁。它允许线程重复获取已持有的锁,同时支持公平性和非公平性两种模式。
-
ReentrantReadWriteLock:可重入读写锁,有一个读锁和一个写锁组成,支持多线程同时读取共享资源,但只允许一个线程写入共享资源。
-
StampeLock:信号量,基于AQS实现的共享锁,可以控制同时访问某个资源的线程数量。
-
CountDownLatch:倒计时门闩,用于等待其他线程执行完特定操作后再继续执行。
-
CyclicBarrier:循环屏障,用于同步多个线程,让它们在达到屏障点时互相等待,然后同时继续执行。
6.3 AQS为什么使用双向链表
双向链表用于存储等待获取锁的线程,即等待队列。原因:
-
有序性:双向链表可以保持等待线程的先后顺序,即按照线程等待获取锁的顺序进行排队。这样可以保证公平性,避免线程饥饿情况发生。
-
高效的插入与删除:双向链表在插入和删除节点时具有较高的效率,因为它可以通过修改前后节点的指针来完成操作,而无需像数组那样需要移动大量的元素。
-
易于管理节点状态:双向链表中的节点可以方便的记录线程的状态(比如是否被阻塞、是否已经获取锁等),这样可以更容易地管理线程在获取锁时地状态转换。
双向链表作为AQS中存储等待线程地数据结构,能够有效的保证线程等待锁地顺序,提高并发性,同时也更易于管理线程地状态,保证同步操作地正确性和高效性。
7. ThreadLocal变量的作用?使用场景
ThreadLocal是一个Java中的线程局部变量。它的主要作用是为了每个线程提供一个独立的变量副本,每个线程都可以独立的操作自己的副本,互不干扰,避免共享资源的竞争,从而实现线程安全。
-
独立副本:ThreadLocal为每个线程提供了一个独立的变量副本,每个线程都可以对该副本进行操作,而不影响其他线程的副本。这样就可以保证线程之间的数据隔开,每个线程都可以维护自己的状态。
-
线程隔离:由于每个线程拥有自己的副本,因此线程之间的变量不会相互冲突,在并发环境下,可以用来避免数据竞争和线程安全问题。
-
解决共享资源问题:在某些情况下,多个线程需要访问同一个变量,但是每个线程使用的值又不同,这时可以使用ThreadLocal来解决这个问题。每个线程可以通过ThreadLocal获取自己的副本,而无需使用锁或同步机制。
-
垃圾回收:ThreadLocal使用ThreadLocalMap来存储每个线程的变量副本,键是ThreadLocal实例本身,值是该线程对应的变量副本。当线程结束后,线程本地变量会自动被回收,不会造成内存泄漏。
- 使用场景
- 代替参数的显示传递:在复杂的系统调用链中,通常需要将某些参数从一个方法传递到另一个方法。通常使用ThreadLocal,可以将参数存储在当前线程的本地变量中,从而避免了在每个方法调用时显示传递参数的麻烦。
- 全局存储用户信息:在前后端分离的系统设计中,用户信息通常保存在Session或Token中。通过ThreadLocal可以在用户登录后将用户信息存储在当前线程的本地变量中,这样在后续的处理过程中,无论是哪个Service层或者Dao层,都可以轻松地获取用户信息,而不需要每次都从Session或Token中提取。
- 数据库连接和事务管理:在使用连接池和事务管理时,ThreadLocal可以用来存储每个线程特有的数据库连接或事务信息,确保线程在执行数据库操作时使用的是自己的资源,避免资源竞争和数据混乱。
- 性能计数和监控:在性能监控和诊断中,可以使用ThreadLocal来为每个线程独立记录性能指标,如请求处理时间、缓存命中率等,方便后续地性能分析和调优。
- 日志记录:在多线程应用中,可以使用ThreadLocal来存储线程特定的日志信息,如线程ID、用户ID等,以便在日志中追踪特定线程的行为。
oken中提取。
- 代替参数的显示传递:在复杂的系统调用链中,通常需要将某些参数从一个方法传递到另一个方法。通常使用ThreadLocal,可以将参数存储在当前线程的本地变量中,从而避免了在每个方法调用时显示传递参数的麻烦。