概述
Java对多线程编程(multithreaded programming)提供了内置支持。多线程程序包含同时运行的两个或更多个部分。这种程序的每一部分被称为一个线程,并且每个线程定义了单独的执行路径。因此,多线程是特殊形式的多任务处理。
Java运行时系统在许多依赖于线程,并且所有类库在设计时考虑了多线程。事实上,Java通过利用线程使得整个环境能够异步执行。这有助于通过防止浪费CPU时钟周期来提高效率。
单线程系统使用一种称为轮询事件循环(event loop with polling)的方法。在这种模型中,单个线程在一个无线循环中控制运行,轮询一个事件队列以决定下一步做什么。一旦轮询返回一个信号,比如准备读取网络文件的信号,事件循环就将控制调度至适当的事件处理程序。在这个事件处理程序返回之前,程序不能执行任何其他工作。这浪费了CPU时间,并且会导致程序的一部分支配着系统而阻止对任何其他部分进行处理。通常,在单线程环境中,当线程因为等待某些资源而阻塞(即挂起执行)时,整个程序都会停止运行。
Java多线程的优点消除了主循环/轮询机制。可以暂停一个线程而不会停止程序的其他部分。例如,由于线程从网络读取数据或等待用户输入数据或等待用户输入而造成的空闲时间,可以在其他地方得以利用。多线程允许当前激活的循环在两帧之间休眠,而不会造成整个系统暂停。当Java程序中的线程阻塞时,只有被阻塞的线程会暂停,所有其他线程仍将继续允许。
在单核系统中,并发执行的线程共享CPU,每个线程得到一片CPU时钟周期。所以,在单核系统中,两个或更多个线程不是真正同时运行的,但是空闲时间被利用了。然而,在多行系统中,两个或更多个线程可能是真正同步执行的。在许多情况下,这会进一步提高程序的效率并提高特定操作的速度。
线程有多种状态。线程可以处于运行(running)状态,只要获得CPU时间就准备运行。运行的线程可以被挂起(suspended),这会临时停止线程的活动。挂起的线程可以被恢复(resumed),从而允许线程从停止处恢复执行。当等待资源时,线程会被阻塞(blocked)。在任何时候,都可以终止线程,这回立即停止线程的执行。线程一旦终止,就不能再恢复。
线程优先级
Java为每个线程都指定了优先级,优先级决定了相对于其他线程应当如何处理某个线程。线程优先级是一些整数,他们指定了一个线程相对于另一个线程的优先程度。优先级的绝对值没有意义;如果只有一个线程运行,优先级高的线程不会比优先级低的线程运行快。反而,线程的优先级用于决定何时从一个运行的线程切换到下一个,这称为上下文(context switch)。决定上下文切换发生时机的规则比较简单:
- 线程自愿地放弃控制。线程显示地放弃控制权、休眠或在I/O之前阻塞,都会出现这种情况。在这种情况下,检查所有其他线程,并且准备运行的线程中优先级最高的那个线程会获得CPU资源。
- 线程被优先级更高的线程取代。对于这种情况,没有放弃控制权的低优先级线程不管正在做什么,都会被高优先级线程简单地取代。基本上,只要高优先级线程希望运行,他就会取代低优先级线程,这称为抢占式多任务处理(preemptice multitasking)。
如果具有相同优先级的两个线程竞争CPU资源,这种情况有些复杂。对于windows这类操作系统,优先级相等的线程以循环方式自动获得CPU资源。对于其他类型的操作系统,优先级相等的线程必须资源地向其他线程放弃控制权,否则其他线程就不能运行。
线程调度程序根据线程优先级决定每个线程应当何时运行。理论上,优先级更高的线程比优先级更低的线程会获得更多的CPU时间。时间上,线程得到的CPU时间除了依赖优先级外,通常还依赖其他几个因素(例如,操作系统实现多任务的方式可能会影响CPU时间的相对可用性)。具有更高优先级的线程还可能取代更低优先级的线程,例如,当一个低优先级的线程正在运行时,需要恢复一个更高优先级的线程(例如,从睡眠或等待I/O中恢复)时,高优先级的线程将取代低优先级的线程。
理论上,具有相同优先级的线程应当得到相等的CPU时间。但是,这需要谨慎对待。请记住,Java被设计为在范围广泛的环境中运行。有些环境实现多任务方式与其他环境不同。为了安全起见,具有相同优先级的线程应当时不时释放控制权。这样可以确保所有线程有机会运行,因为大部分线程不可避免地会遇到一些阻塞的情况,例如I/O等待。当发生这种情况时,阻塞的线程被挂起,其他线程就可以运行。但是,如果希望平滑多个线程的执行,最好不要依赖于这种情况。此外,某些类型的任务是CPU密集型的。这种线程会支配CPU。对于这类线程,您会希望经常地释放控制权,以使其他线程能够运行。
在Java,线程的优先级的值必须在MIN_PRIORITY和MAX_PRIORITY之间选择,分别是1和10。NORM_PRIORITY表示的是默认优先级。
不同的Java实现对于人物调度可能有很大的区别。如果线程依赖于抢占式行为,而不是协作性地放弃CPU,那么经常会引起不一致性。使用Java实现可预测、跨平台行为的最安全方法是使用自愿放弃CPU控制权的线程。
同步
当两个或多个线程需要访问共享的资源时,它们需要以某种方式确保每次只有一个线程使用资源。实现这一目的的过程称为同步。
因为多线程为程序引入了异步行为,所以必须提供一种在需要时强制同步的方法。例如,如果希望两个线程进行通信并共享某个复杂的数据结构,如链表,就需要以某种方式确保它们相互之前不会发生冲突。也就是说,当一个线程正在读取数据结构时,必须阻止另外一个线程向该数据结构写入数据。为此,Java以监视器这一年代久远的进程间同步模型为基础,实现了一种乔木的方案。监视器最初是由C.A.R.Hoare定义的一种控制机制,可以将监视器看做非常小的只能包含一个线程的盒子。一旦某个线程进入监视器,其他所有线程就必须等待,直到该线程退出监视器。通过这种方式,可以将监视器用于保护共享的数据,以防止多个线程同时对资源进行操作。
同步的关键是监视器的概念,监视器是用作互斥所的对象。在给定时刻,只有一个线程可以拥有监视器。当线程取得锁时,也就是进入监视器。其他所有企图进入加锁监视器的线程都会被挂起,知道第一个线程退出监视器。也就是说,这些等待的其他线程在等待监视器。如果需要的话,拥有监视器的线程可以再次进入监视器。
Java没有提供“Monitor”类;相反,每个对象都有自己的隐式监视器。如果调用对象的同步方法,就会自动进入对象的隐式监视器。一旦某个线程位于一个同步方法中,其他线程就不能调用同一个对象的任何其他同步方法。因为语言本身内置了同步支持,所以可以编写出非常清晰并且简明的多线程代码。
同步的规则定义
- 对 volatile 变量 v 的写入,与所有其他线程后续对 v 的读同步
- 对于监视器 m 的解锁与所有后续操作对于 m 的加锁同步
- 对于每个属性写入默认值(0, false, null)与每个线程对其进行的操作同步
- 启动线程的操作与线程中的第一个操作同步
- 线程 T2的最后操作与线程 T1 发现线程 T2 已经结束同步。( isAlive ,join可以判断线程是否终结)
- 如果线程 T1 中断了 T2,那么线程 T1 的中断操作与其他所有线程发现 T2 被中断了同步
- 通过抛出 InterruptedException 异常,或者调用 Thread.interrupted 或 Thread.isInterrupted
通信
多线程通过将任务分隔到独立的逻辑单元来替换事件循环变成。线程还提供了第二个有点:消除了轮询检测。轮询检测通常是通过重复检查某些条件的循环实现的。一点条件为true,就会发生恰当的动作,这会浪费CPU时间。例如,分析经典的队列问题,对于这种问题,一个线程产生一些数据,另外一个线程使用这些数据。为了使问题更有趣,假定生产者在产生更多数据之前,必须等待消费结束。在轮询检测系统中,消费者在等待生产者生产时需要消耗许多的CPU时间。一旦生产者结束生成数据,就会开始轮询,在等待消费者结束的过程中,会浪费更多CPU时间。显然,这种情况不是我们所期望的。
为了避免轮询检测,Java通过wait()、notify()以及notifyAll()方法,提供了一种线程间通信机制,这些方法在Object中是作为final方法实现的,因此所有类都具有这些方法。所有这3个方法都只能在同步上下文中调用。
- wait()方法通知调用线程放弃监视器并进入休眠,直到其他一些线程进入同一个监视器并调用notify()方法或notifyAll()方法。
- notify()方法唤醒调用相同对象的wait()方法的线程。
- notifyAll()方法唤醒调用相同对象的wait()方法的所有线程,其中一的一个线程将得到访问权限。
wait()方法还有另外一种形式,允许指定等待的时间间隔。
尽管在正常情况下,wait()方法会等待直到调用notify()或notifyAll()方法,但是还有一种几率很小却可能会发生的情况,等待线程由于假唤醒(spurious wakeup)而被唤醒。对于这种情况,等待线程也会被唤醒,然而却没有调用notify()或notifyAll()方法(本质上,线程在没有什么明显理由的情况下被恢复了)。因为存在这种极小的可能,Oracle推荐应当在一个检测线程等待的循环中调用wait()方法。
happens-before先行发生原则
happens-before 关系用于描述两个有冲突的动作之间的顺序,如果一个action happends before 另一个
action,则第一个操作被第二个操作可见, JVM需要实现如下happens-before规则:
- 某个线程中的每个动作都 happens-before 该线程中该动作后面的动作。
- 某个管程上的 unlock 动作 happens-before 同一个管程上后续的 lock 动作
- 对某个 volatile 字段的写操作 happens-before 每个后续对该 volatile 字段的读操作
- 在某个线程对象上调用 start()方法 happens-before 被启动线程中的任意动作
- 如果在线程t1中成功执行了t2.join(),则t2中的所有操作对t1可见
- 如果某个动作 a happens-before 动作 b, 且 b happens-before 动作 c, 则有 a happens-before c.
当程序包含两个没有被 happens-before 关系排序的冲突访问时,就称存在数据竞争。
遵守了这个原则,也就意味着有些代码不能进行重排序,有些数据不能缓存!
消息传递
将程序分隔到独立的线程之后,需要定义它们之间相互通信的方式。当使用某些其他语言编程时,必须依赖操作系统建立线程之间的通信。当然,这会增加系统开销。相反,通过调用所有对象都具有的预定义的方法,Java为两个或更多个线程之间的相互通信提供了一种简洁的低成本方式。Java的消息传递系统允许某个线程进入对象的同步方法,然后进行等待,知道其他线程显示地通知这个线程退出为止。
主线程
当Java程序启动时,会立即开始运行一个线程,因为它是程序开始时执行的线程,所以这个线程通常称为程序的主线程。主线程很重要,有以下两个原因:
- 其它子线程都是从主线程产生的
- 通常,主线程必须是最后才结束执行的线程,因为它要执行各种关闭动作。
尽管主线程是在程序启动时自动创建的,但是可以通过Thread对象对其进行控制。为此,必须调用currentThread()方法获取对主线程的一个引用。该方法是Thread()类的共有静态成员static Thread currentThread()
1 | public static void main(String args[]) |
线程封闭
多线程访问共享可变数据时,涉及到线程间数据同步的问题。
并不是所有时候,都要用到共享数据,若数据都被封闭在各自的线程之中,就不需要同步,
这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭。
线程池
- 线程不仅java中是一个对象, 每个线程都有自己的工作内存,
- 线程创建、 销毁需要时间, 消耗性能。
- 线程过多, 会栈用很多内存
- 操作系统需要频繁切换线程上下文(大家都想被运行) , 影响性能。
- 如果创建时间+ 销毁时间 > 执行任务时间 就很不合算
线程池管理器: 用于创建并管理线程池, 包括创建线程池, 销毁线程池, 添加新任务;
工作线程: 线程池中线程, 可以循环的执行任务, 在没有任务时处于等待状态;
任务接口: 每个任务必须实现的接口, 以供工作线程调度任务的执行, 它主要规定了任务的入口, 任务执行完后的收尾工作, 任务的执行状态等;
任务队列: 用于存放没有处理的任务。 提供一种缓冲机制。
线程数量
计算型任务: cpu数量的1-2倍
*IO型任务: *相对比计算型任务, 需多一些线程, 要根据具体的IO阻塞时长进行考量决定。也可考虑根据需要在一个最小数量和最大数量间自动增减线程数。
如tomcat中默认的最大线程数为: 200。
共享变量
可以在线程之间共享的内存称为共享内存或堆内存
所有实例字段、 静态字段和数组元素都存储在堆内存中,这些字段和数组都是标题中提到的共享变量。
冲突:如果至少有一个访问是写操作,那么对同一个变量的两次访问是冲突的。
这些能被多个线程访问的共享变量是内存模型规范的对象。
interrupt说明
- 1.interrupt方法并不会中断线程,只是给线程打上中断标记
- 2.如 果 目 标 线 程 在 调 用 wait() 、wait(long) 方法 、join() 、join(long, int) 、join(long, int)、sleep(long, int)或sleep(long, int)等方法后,处于WAITING、Timed Waiting状态时,该线程被调用interrupt方法后,线程的WAITING、Timed Waiting状态将被清除,并抛出InterruptedException异常。
- 3.park()\parkNanos方法执行后, 线程也处于 WAITING、Timed Waiting,也会被唤醒,但是不会抛异常,且有很诡异的情况发生,再次进入part()的时候,线程将不会在进入等待状态,直接往后part()后面的代码运行。
- 4.如果目标线程是被I/O 或者NIO中的Channel所阻塞, 同样, I/O操作会被中断或者返回特
殊异常值。 达到终止线程的目的。 - 5.如果以上条件都不满足(也就是RUNNABLE状态),则会设置此线程的中断状态。
线程类型
守护线程:是指在程序运行的时候在后台提供一种通用服务的线程,进程结束时,会杀死所有守护线程。
用户线程:非守护线程就是用户线程线,进程结束时,也不会结束。
使用注意
Word Tearing字节处理
有些处理器(尤其是早期的 Alphas 处理器)没有提供写单个字节的功能。在这样的处理器上更新 byte 数组,若只是
简单地读取整个内容,更新对应的字节,然后将整个内容再写回内存,将是不合法的。
这个问题有时候被称为“字分裂(word tearing)”,更新单个字节有难度的处理器,就需要寻求其它方式来解决问题。
因此, 编程人员需要注意,尽量不要对byte[]中的元素进行重新赋值,更不要在多线程程序中这样做
double和long的特殊处理
由于《Java语言规范》 的原因,对非 volatile 的 double、 long 的单次写操作是分两次来进行的,每次操作
其中32位,这可能导致第一次写入后,读取的值是脏数据,第二次写完成后,才能读到正确值。
读写volatile 修饰的 long、 double是原子性的。
商业JVM不会存在这个问题,虽然规范没要求实现原子性,但是考虑到实际应用,大部分都实现了原子性。
《Java语言规范》 中说道:建议程序员将共享的64位值(long、 double)用volatile修饰或正确同步其程序以避
免可能的复杂的情况。
伪唤醒
在进行等待条件判断的时候,不建议使用if语句, 官方建议应该在循环中检查等待条件, 原因是处于等待状态的线程可能会收到错误警报和伪唤醒, 如果不在循环中检查等待条件, 程序就会在没有满足结束条件的情况下退出。
伪唤醒是指线程并非因为notify、 notifyall、 unpark等api调用而意外唤醒, 是更底层原因导致的。
1 | // park |
deamon线程
- 1.thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程
- 2.在Daemon线程中产生的新线程也是Daemon的。
- 3.守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
Thread类
Java的多线程系统是基于Thread类,Thread类的方法及其伴随接口Runnable而构建的。Thread类封装了线程的执行。因为不能直接引用正在运行的线程的细微状态,所以需要通过代理进行处理,Thread实例就是线程的代理。为了创建新线程,程序可以扩展Thread类或实现Runnable接口。
Thread类创建新的执行线程。
- 构造函数
方法 | 描述 |
---|---|
Thread() | |
Thread(Runnable threadOb) | |
Thread(Runnable threadOb, String threadName) | |
Thread(String threadName) | |
Thread(ThreadGroup groupOb, Runnable threadOb) | |
Thread(ThreadGroup groupOb, Runnable threadOb, String threadName) | |
Thread(ThreadGroup groupOb, String threadName) |
threadOb是一个类的实例,该类实现了Runnable接口并定义了线程执行从何处开始。线程的名称由threadName指定。如果没有指定名称,Java虚拟机会创建一个名称。groupOb指定新线程属于哪个线程组。如果没有指定线程组,那么新线程与父线程将属于相同的线程组。
- 常量
常量 | 描述 |
---|---|
MAX_PRIORITY | 线程优先级最大值 |
MIN_PROPRITY | 线程优先级最小值 |
NORM_PRIORITY | 线程优先级默认值 |
- Thread类定义的一些方法
方法 | 含义 |
---|---|
static int activeCount() | 返回线程所属线程组中活动线程的大概数量 |
final void checkAccess() | 导致安全管理器核实当前线程是否能够访问和/或修改对之调用checkAccess()方法的线程 |
static Thread currentThread() | 返回的Thread对象封装了调用该方法的线程 |
static void dumpStack() | 显示线程的调用堆栈 |
static int enumerate(Thread threads[]) | 将当前线程组中所有Thread对象的副本放入threads中,返回线程的数量 |
static Map<Thread, StackTraceElement[]> getAllStackTraces() | 返回的Map对象包含所有活动线程的堆栈追踪。在映射中,每一项都包含一个键和对应的值。其中,键是Thread对象,它的值是元素为StackTraceElement的数组 |
ClassLoader getContextClassLoader() | 返回用于该线程加载类和资源的上下文类加载器 |
static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() | 返回默认的未捕获的异常处理程序 |
long getID() | 返回线程的ID |
final String getName() | 返回线程的名称 |
final int getPriority() | 返回线程的优先级的设置 |
StackTraceElement[] getStackTrace() | 返回的数组包含调用线程的堆栈跟踪 |
Thread.State getState() | 返回调用线程的状态 |
final ThreadGroup getThreadGroup() | 返回一个ThreadGroup对象,调用线程是该对象的一个成员 |
Thread.UncaughtExceptionHandler getUncaughtExceptionHandler() | 返回调用线程的未捕获的异常处理程序 |
static boolean holdsLock(Object ob) | 如果调用线程拥有ob上的锁,就返回true;否则返回false |
void interrupt() | 中断线程 |
static boolean interrupted() | 如果当前执行的线程已经被中断,就返回true;否则返回false |
final boolean isAlive() | 如果线程仍然在运行,就返回true;否则返回false |
final boolean isDaemon() | 如果线程是守护线程,就返回true;否则返回false |
boolean isInterrupted() | 如果线程被中断,就返回true;否则返回false |
final void join() | 进行等待,直到线程终止 |
final void join(long milliseconds) | 等待调用线程终止,等待的最长时间为milliseconds毫秒 |
final void join(long milliseconds, int nanoseconds) | 等待调用线程终止,等待的最长时间为milliseconds毫秒加上nanoseco纳秒 |
void run() | 开始执行线程 |
void setContextClassLoader(ClassLoader cl) | 把调用线程将使用的上下文类加载器设置为cl |
final void setDaemon(boolean state) | 将线程标记为守护线程 |
static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler e) | 将默认的未捕获异常处理程序设置为e |
final void setName(String threadName) | 将线程的名称设置为threadName |
final void setPriority(int priority) | 将线程的优先级设置为priority |
void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler e) | 将调用线程默认的未捕获异常处理程序设置为e |
static void sleep(long milliseconds) | 将线程执行挂起指定的毫秒数 |
static void sleep(long milliseconds, int nanoseconds) | 将线程执行挂起指定的毫秒数加纳秒数 |
void start() | 开始线程执行 |
String toString() | 返回线程的等价字符串 |
static void yield() | 调用线程将CPU让给其他的线程 |
- 线程状态
值 | 描述 |
---|---|
BLOCKED | 线程因为正在等待需要的锁而挂起执行 |
NEW | 线程还没有开始运行 |
RUNNABLE | 线程要么当前正在执行,要么在获得CPU的访问权限之后执行 |
TERMINATED | 线程以及完成执行 |
TIMED_WAITING | 线程挂起执行一段时间,例如当调用sleep()方法时就会处于这种状态。当调用wait()或join()方法的暂停版本时,也会进入这种状态 |
WAITING | 线程因为等待某些动作而挂起执行。例如,因为调用非暂停版本的wait()或join()方法而等待时,会处于这种状态 |
1 | package learn; |
Runnable接口
启动某个单独线程执行的任何类都必须实现Runnable接口。Runnable接口只定义了抽象方法run(),该方法是线程的入口点。创建的线程必须实现该方法。
1 | package learn; |
isAlive()和join()
通过sleep()方法延迟来确保所有子线程在主线程之前终止是一个不好的方式,并且会造成一个更大的问题:一个线程如果知道另外一个线程的结束。为此Java的线程提供了isAlive()方法来获取指定的线程是否在运行。但是通常使用join()来等待线程结束,该方法会一直等待,直到调用线程终止。如果命名该方法的原因是:调用线程一直等待,直到指定的线程加入(join)其中为止。join()方法的另外一种形式,允许指定希望等待指定线程终止的最长时间。
1 | class NewThread implements Runnable |
ThreadGroup
ThreadGroup用于创建一组线程。线程组为管理一组线程提供了一种便利的方式,可以将一组线程作为一个单位进行管理。对于希望挂起或恢复大量相关线程的情况,线程组特别有用。
若非真需要使用,是不建议使用ThreadGroup的,因为这会带来线程安全问题- 构造函数
方法 | 描述 |
---|---|
ThreadGroup(String groupName) | 创建一个新组,该组将当前线程作为父线程 |
ThreadGroup(ThreadGroup parentOb, String groupName) | 线程新租,父线程由parentOb指定 |
- ThreadGroup常用方法
方法 | 描述 |
---|---|
int activeCount() | 返回调用线程组(包括子线程组)中处于活动状态的线程的大概数量 |
int activeGroupCount() | 返回以调用线程为父线程的活动线程组的数量(包括子线程组) |
final void checkAccess() | 导致安全管理器核实调用线程是否可以访问和/或修改对之调用checkAccess()方法的线程组 |
final void destroy() | 销毁在对之调用该方法的线程组(及其所有子线程组) |
int enumerate(Thread group[]) | 将调用线程组(包括子线程组)包含的活动线程放入group数组中 |
int enumerate(Thread group[], boolean all) | 将调用线程组包含的活动线程放入group数组中。如果all为true,那么子线程组中的所有线程也被放入到group数组中 |
int enumerate(ThreadGroup group[]) | 将调用线程组的活动子线程组(包括子线程组的子线程组,等等)放入group数组中 |
int enumerate(ThreadGroup group[], boolean all) | 将调用线程组的活动子线程组放入group数组中。如果all为true,那么子线程组的所有子线程组也将被放入group数组中 |
final int getMaxPriority() | 返回线程组的最大优先级设置 |
final String getName() | 返回线程组的名称 |
final ThreadGroup getParent() | 如果调用线程组没有父对象,就返回null;否则返回调用对象的父对象 |
final void interrupt() | 调用线程组(以及所有子线程组)中所有线程的interrupt()方法 |
final boolean isDaemon() | 如果线程组是守护线程组,就返回true;否则返回false |
boolean isDestroyed() | 如果线程组已经被销毁,就返回true;否则返回false |
void list() | 显示有关线程组的信息 |
final boolean parentOf(ThreadGroup group) | 如果调用线程是group的父线程(或是group本身),就返回true;否则返回false |
final void setDaemon(boolean isDaemon) | 如果isDaemon为true,那么调用线程组将被表示为守护线程组 |
final void setMaxPriority(int priority) | 将调用线程组的最大优先级设置为priority |
String toString() | 返回线程组的等价字符串 |
void uncaughtException(Thread thread, Throwable e) | 当某个异常未被捕获时,调用该方法 |
1 | class NewThread extends Thread |
ThreadLocal
ThreadLocal用于创建线程局部变量,每个线程将具有线程局部变量的一个副本。
变量值的共享可以使用public static的形式,所有线程都使用同一个变量,如果想实现每一个线程都有自己的共享变量该如何实现呢?JDK中的ThreadLocal类正是为了解决这样的问题。
ThreadLocal类并不是用来解决多线程环境下的共享变量问题,而是用来提供线程内部的共享变量,在多线程环境下,可以保证各个线程之间的变量互相隔离、相互独立。在线程中,可以通过get()/set()方法来访问变量。ThreadLocal实例通常来说都是private static类型的,它们希望将状态与线程进行关联。这种变量在线程的生命周期内起作用,可以减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
ThreadLocal的实现离不开ThreadLocalMap类,ThreadLocalMap类是ThreadLocal的静态内部类。每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。这样的设计主要有以下几点优势:
- 这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能;
- 当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。
- 方法
方法 | 描述 |
---|---|
get() | 获取与当前线程关联的ThreadLocal值。 |
set(T value) | 设置与当前线程关联的ThreadLocal值。 |
initialValue() | 设置与当前线程关联的ThreadLocal初始值。 |
remove() | 将与当前线程关联的ThreadLocal值删除。 |
1 | package learn; |
ThreadLocalMap
ThreadLocalMap是用来存储与线程关联的value的哈希表,它具有HashMap的部分特性,比如容量、扩容阈值等,它内部通过Entry类来存储key和value,Entry类的定义为:
1 | static class Entry extends WeakReference<ThreadLocal<?>> { |
Entry继承自WeakReference,通过上述源码super(k);可以知道,ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。弱引用对象在Java虚拟机进行垃圾回收时,就会被释放。
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部关联的强引用,那么在虚拟机进行垃圾回收时,这个ThreadLocal会被回收,这样,ThreadLocalMap中就会出现key为null的Entry,这些key对应的value也就再无妨访问,但是value却存在一条从Current Thread过来的强引用链。因此只有当Current Thread销毁时,value才能得到释放。
InheritableThreadLocal
InheritableThreadLocal继承自ThreadLocal,使用InheritableThreadLocal类可以使子线程继承父线程的值
使用InheritableThreadLocal类需要注意的一点是,如果子线程在取得值的同时,主线程将InheritableThreadLocal中的值进行更改,那子线程获取的还是旧值。
1 | package learn; |