Java并发是Java语言中非常重要的一个话题,也是面试和实际应用中最常接触到的。作为JAVA工程师,掌握并发编程知识是非常重要的。在多核CPU的环境下,使用多线程并发执行任务可以显著提高程序的执行效率,从而提高系统的性能。
目录
线程的基本概念
线程和进程
线程创建的三种方式
线程的状态
线程的通知与等待
wait()方法
wait(long timeout)
notify()
notifyAll()
join()方法
sleep()
yield()
线程的上下文切换
线程死锁
死锁的产生的四个条件
如何避免死锁
守护线程和用户线程
ThreadLocal
使用示例
实现原理
ThreadLocalMap
ThreadLocal不支持继承性
InheritableThreadLocal类
总结
参考资料
线程的基本概念
线程和进程
在操作系统中,一个进程就是一个正在运行的程序实例,它由程序代码和程序执行时所需的资源组成。一个进程可以包含多个线程,线程是进程中的一个执行路径,是CPU调度和分配的基本单位。
每个线程都有自己的程序计数器(PC)、栈、局部变量和方法参数。多个线程可以共享进程中的资源,例如全局变量、静态变量和方法等。多线程可以共享数据,从而使得程序更加灵活高效。
线程创建的三种方式
- 继承Thread类,重写run方法
/**
* 方式一:继承Thread类的方式创建线程
*/
public class ThreadExtendTest extends Thread{ //步骤 1
@Override
public void run() { //步骤 2
//run方法内为具体的逻辑实现
System.out.println("create thread by thread extend");
}
public static void main(String[] args) {
new ThreadExtendTest(). start();
}
}
thread.start()
后线程启动,转化为就绪态
问题: Java是单继承的,如果继承了Thread类,就无法继承其他类;任务与代码没有分离,当多个线程执行同样的任务时需要多份任务代码;
- 实现Runnable接口的run方法
/**
* 方式二:实现java.lang.Runnable接口
*/
public class ThreadRunnableTest implements Runnable{//步骤 1
@Override
public void run() {//步骤 2
//run方法内为具体的逻辑实现
System.out.println("create thread by runnable implements");
}
public static void main(String[] args) {
ThreadRunnableTest task = new ThreadRunnableTest();
new Thread(task). start();
new Thread(task). start();
}
}
缺点:任务没有返回值
- 使用FutureTask方式
/**
* 方式三:实现Callable接口
*/
public class ThreadCallableTest implements Callable<String> {//步骤 1
@Override
public String call() throws Exception { //步骤 2
//call 方法的返回值类型是 String
//call 方法是线程具体逻辑的实现方法
return "create thread by implements Callable";
}
public static void main(String[] args) throws ExecutionException, InterruptedException{
//创建异步任务
FutureTask<String> future1 = new FutureTask<String>(new ThreadCallableTest());
Thread thread1 = new Thread(future1);
thread1. start();
System.out.println(future1.get());
}
}
- 步骤 1:实现
Callable
接口,implements Callable
; - 步骤 2:复写
call()
方法,call()
方法是线程具体逻辑的实现方法。
小结
- 使用继承的方式可以方便传参,可以在子类中添加成员变量,但子类无法继承其他类
- 使用Runnable方式,则只能使用主线程中被final修饰的变量
- FutureTask方式能够获取返回结果
线程的状态
线程在不同的生命周期中会有不同的状态。JAVA中的线程有以下五种状态:
- 新建(New):线程对象被创建后,但还没有调用start()方法启动线程时的状态。
- 运行(Runnable):线程被start()方法启动后,进入可运行状态。在可运行状态中,线程正在等待CPU的调度,以便进入运行状态。
- 阻塞(Blocked):线程在等待某个条件时,进入阻塞状态。例如等待I/O操作完成,等待锁释放等。
- 等待(Waiting):线程等待其他线程执行完特定的操作,进入等待状态。例如等待某个线程完成、等待某个对象的notify()或notifyAll()方法。
- 终止(Terminated):线程运行完run()方法后,线程会进入终止状态。线程的状态转换如下图所示:
线程的通知与等待
wait()方法
wait()是Object里面的方法,Object是所有对象的父类,即所有对象都可以调用wait()方法。当一个线程调用一个共享变量的wait()
方法时, 该调用线程会被阻塞挂起,释放锁并进入等待状态, 直到发生下面几件事情之一才返回:
- 其他线程调用了该共享对象(同一个监视器)的
notify()
或者notifyAll()
方法; - 其他线程调用了该线程的
interrupt()
方法, 该线程抛InterruptedException
异常返回。
另外需要注意的是,如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException
异常。
如何获取一个共享变量的监视器锁?
- 执行
synchronized
同步代码块时, 使用该共享变量作为参数。
synchronized (共享变量){
//doSomething
}
2. 调用该共享变量的方法,并且该方法使用了synchronized
修饰。
synchronized void add ( int a , int b ) {
//doSomething
}
注:synchronized 修饰方法锁的是什么,取决于方法是静态的还是非静态的。
- 如果synchronized 修饰非静态方法,那么锁的是调用该方法的对象,也就是说,只有同一个对象的同步方法才会互相阻塞,不同对象的同步方法可以同时执行。这种锁又叫对象锁。
- 如果synchronized 修饰静态方法,那么锁的是该类的Class对象,也就是说,只有同一个类的静态同步方法才会互相阻塞,不同类的静态同步方法可以同时执行。这种锁又叫类锁。
所以上面synchronized修饰方法锁的不是共享变量,而是调用方法的对象。
例:假设有两个线程A和B,A负责生产数据,B负责消费数据。A和B共享一个队列queue作为数据缓冲区。当队列为空时,B需要等待A生产数据;当队列满时,A需要等待B消费数据。
其中queue
为共享变量,生产者线程在调用queue
的wait()
方法前,使用synchronized
关键字拿到了该共享变量queue
的监视器锁,所以调用wait()
方法才不会抛出IllegalMonitorStateException
异常。如果当前队列没有空闲容量则会调用queue
的wait()
方法挂起当前线程。
注:释放的是当前共享变量上的锁,如果还持有其他锁,这些锁并不会被释放
// A thread that produces data and puts it on the queue
//生产线程
class Producer extends Thread {
private Queue queue;
public Producer(Queue queue) {
this.queue = queue;
}
public void run() {
while (true) {
synchronized (queue) {
// wait if the queue is full
//消费队列满,则等待队列空闲
while (queue.isFull()) {
try {
//挂起当前线程, 并释放通过同步块获取的queue上的锁,
//让消费者线程可以获取该锁,然后获取队列里面的元素
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// produce data and notify the consumer
// 空闲则生成元素, 并通知消费者线程
Object data = produceData();
queue.put(data);
queue.notify();
}
}
}
private Object produceData() {
// some logic to produce data
return new Object();
}
}
// A thread that consumes data from the queue
//消费者线程
class Consumer extends Thread {
private Queue queue;
public Consumer(Queue queue) {
this.queue = queue;
}
public void run() {
while (true) {
synchronized (queue) {
// wait if the queue is empty
// 消费队列为空
while (queue.isEmpty()) {
try {
//挂起当前线程,并释放通过同步块获取的queue上的锁,
//让生产者线程可以获取该锁,将生产元素放入队列
queue.wait();
} catch (InterruptedException) {
e.printStackTrace();
}
}
//消费元素,并通知唤醒生产者线程
queue.take( );
queue.notify();
}
}
}
这里使用循环就是为了避免虚假唤醒问题。假如当前线程被虚假唤醒了,但是队列还是没有空余容量,那么当前线程还是会调用wait()方法把自己挂起。
虚假唤醒可参考博客:什么是虚假唤醒?为什么会产生虚假唤醒?_橙不甜橘不酸的博客-CSDN博客
wait(long timeout)
该方法相比wait()
方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后, 没有在指定的timeout
ms 时间内被其他线程调用该共享变量的notify()
或者notifyAll()
方法唤醒,那么该函数还是会因为超时而返回 。如果将timeout
设置为0 则和wait
方法效果一样,因为在wait
方法内部就是调用了wait(0)
。需要注意的是,如果在调用该函数时, 传递了一个负的timeout 则会抛出IllegalArgumentException 异常。
notify()
一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait 系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
被唤醒的线程不能马上从wait 方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁 , 只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
类似wait 系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的notify() 方法,否则会抛出IllegalMonitorStateException 异常。
notifyAll()
不同于在共享变量上调用notify()
函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()
方法则会唤醒所有在该共享变量上由于调用wait
系列方法而被挂起的线程。
join()方法
在项目实践中经常会遇到一个场景,就是需要等待某几件事情完成后才能继续往下执行, 比如多个线程加载资源, 需要等待多个线程全部加载完毕再汇总处理。Thread
类中有一个join
方法就可以做这个事情,前面介绍的等待通知方法是Object 类中的方法, 而join方法则是Thread 类直接提供的。join
是无参且返回值为void
的方法。
public static void main(Str 工ng[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable( ) {
@Override
public void run() {
try {
Thread.sleep (1000);
) catch (Interrupted.Exceptio e) {
e.printStackTrace();
}
System.out.println("child threadOne over !");
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run(){
try {
Thread.sleep (1000);
) catch (Interrupted.Exceptio e) {
e.printStackTrace();
}
System.out.println( "child threadTwo over") ;
}
});
//启动子线程
threadOne.start() ;
threadTwo.start() ;
System.out.println("wait all child thread over !");
//等待子线程执行完毕,返回
threadOne.join ();
threadTwo.join ();
System.out.println ( " all child thread over ") ;
}
如上代码在主线程里面启动了两个子线程,然后分别调用了它们的join()
方法,那么主线程首先会在调用threadOne.join()
方法后被阻塞, 等待threadOne
执行完毕后返回。
threadOne
执行完毕后threadOne.join()
就会返回, 然后主线程调用threadTwo.join()
方法后再次被阻塞, 等待threadTwo
执行完毕后返回。
sleep()
Thread
类中有一个静态的sleep
方法,当一个执行中的线程调用了Thread
的sleep
方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU 的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与CPU 的调度,获取到CPU 资源后就可以继续运行了。如果在睡眠期间其他线程调用了该线程的interrupt()
方法中断了该线程,则该线程会在调用sleep
方法的地方抛出IntermptedException
异常而返回。
例:
public class SleepTest {
// 创建一个独占锁
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(new Runnable(){
public void run(){
lock.lock(); // 获取独占锁
try{
System.out.println("child threadA is in sleep");
Thread.sleep(10000);
System.out.println("child threadA is awaked");
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock(); // 释放锁
}
}
});
Thread threadB = new Thread(new Runnable(){
public void run(){
lock.lock(); // 获取独占锁
try{
System.out.println("child threadB is in sleep");
Thread.sleep(10000);
System.out.println("child threadB is awaked");
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock(); // 释放锁
}
}
});
// 启动线程
threadA.start();
threadB.start();
}
}
运行结果:
child threadA is in sleep
child threadA is awaked
child threadB is in sleep
child threadB is awaked
在线程A 睡眠的这10 s 内那个独占锁lock 还是线程A 自己持有,线程B 会一直阻塞直到线程A 醒来后执行unlock
释放锁。
yield()
Thread 类中有一个静态的yield
方法,当一个线程调用yield
方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU 使用,但是线程调度器可以无条件忽略这个暗示。我们知道操作系统是为每个线程分配一个时间片来占有CPU 的, 正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了Thread 类的静态方法yield
时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。
当一个线程调用yield
方法时, 当前线程会让出CPU 使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU 的那个线程来获取CPU 执行权。
例:
public class YieldTest implements Runnable {
YieldTest() {
//创建并启动线斗呈
Thread t =new Thread(this);
t.start() ;
}
public void run() {
for (int i = 0 ; i < 5; i++) {
//当i=0 时让出CPU执行权,放弃时间片,进行下一轮调度
if ((i % 5) == 0 ) {
System.out.println (Thread.currentThread() + "yield cpu . . .");
//当前线程让出CPU执行权,放弃时间片,进行下一轮调度
//Thread.yield ();
}
}
System .out . println(Thread.currentThread() + " is over ");
}
public static void main(String [] args) {
new YieldTest();
new YieldTest();
new YieldTest();
}
}
输出结果:
Thread[Thread-0,5 ,main]yield cpu ...
Thread[Thread-0,5 ,main] is over
Thread[Thread-1,5 ,main]yield cpu ...
Thread[Thread-1,5,main] is over
Thread[Thread-2,5 ,main]yield cpu ...
Thread[Thread-2,5 ,main] is over
如上代码开启了三个线程,每个线程的功能都一样,都是在for 循环中执行5 次打印。运行多次后,上面的结果是出现次数最多的。解开Thread.yield()
注释再执行,结果如下:
Thread[Thread-0,5 ,main]yield cpu ...
Thread[Thread-2,5 ,main]yield cpu ...
Thread[Thread-1,5 ,main]yield cpu ...
Thread[Thread-0,5 ,main] is over
Thread[Thread-2,5,main] is over
Thread[Thread-1,5 ,main] is over
从结果可知, Thread .yield()
方法生效了,三个线程分别在i=0
时调用了Thread. yield()
方法,所以三个线程自己的两行输出没有在-起,因为输出了第一行后当前线程让出了CPU 执行权。
yield() 与sleep()的区别
当线程调用sleep
方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用yield
方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。
线程的上下文切换
在多线程编程中,线程个数一般都大于CPU 个数,而每个CPU 同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU 执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU 让其他线程占用, 这就是上下文切换,从当前线程的上下文切换到了其他线程。那么就有一个问题,让出CPU 的线程等下次轮到自己占有CPU 时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场, 当再次执行时根据保存的执行现场信息恢复执行现场。
线程上下文切换时机有: 当前线程的CPU 时间片使用完处于就绪状态时,当前线程被其他线程中断时。
线程死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去,如图所示,线程A 己经持有了资源2 , 它同时还想申请资源1 , 线程B 已经持有了资源1,它同时还想申请资源2 , 所以线程A和线程B就因为相互等待对方已经持有的资源,而进入了死锁状态。
死锁的产生的四个条件
- 互斥条件: 线程对己经获取到的资源进行排它性使用, 即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
- 请求并持有条件: 一个线程己经持有了至少一个资源, 但又提出了新的资源请求,而新资源己被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己己经获取的资源。
- 不可剥夺条件: 线程获取到的资源在自己使用完之前不能被其他线程抢占, 只有在自己使用完毕后才由自己释放该资源。
- 环路等待条件: 指在发生死锁时, 必然存在一个线程→资源的环形链, 即线程集合{P0 , P1,P2 ,…, Pn }中的P0 正在等待一个P1 占用的资源, P1 正在等待P2 占用的资源,……Pn 正在等待己被P0 占用的资源。
死锁产生的原因
- 进程顺序不当
- PV操作使用不妥
- 同类资源分配不均
- 对某些资源的使用未加限制
……
如何避免死锁
既然死锁要满足四个条件,那破坏掉至少一个就可以避免死锁了。但四个条件中,只有请求与保持和环路等待条件可以被破坏(互斥条件是必须要有的,不然多个线程同时操作一个共享资源,会出现脏读等问题,一般资源是不可剥夺的,当然也可能会有可剥夺的情况,这里不做考虑)
死锁具体相关的知识点可参考下面这篇文章,我就不赘述了。
死锁的产生、防止、避免、检测和解除 - 知乎
守护线程和用户线程
Java 中的线程分为两类,分别为daemon 线程(守护线程〉和user 线程(用户线程)。在JVM 启动时会调用main 函数, main 函数所在的钱程就是一个用户线程,其实在JVM内部同时还启动了好多守护线程, 比如垃圾回收线程。那么守护线程和用户线程有什么区别呢?
区别之一是当最后一个非守护线程结束时, JVM 会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响JVM 的退出。言外之意,只要有一个用户线程还没结束, 正常情况下JVM 就不会退出。
如何创建守护线程?
public static void main(String[] args ) {
Thread daemonThread = new Thread (new Runnable() {
public void run( ) {
}
});
//设置为守护线程
daemonThread.setDaemon(true) ;
daemonThread.start();
}
用户线程与守护线程的区别
public static vroid main (String [] args ) {
Thread thread= new Thread(new Runnable() {
publiC vroid run(){
for(;;) {}
}
});
//启动子线程
thread.start() ;
System.out.print ( "main thread is over ");
}
输出结果:
如上代码在main 线程中创建了一个thread 线程,在thread 线程里面是一个无限循环。从运行代码的结果看, main 线程已经运行结束了,那么JVM 进程己经退出了吗?在IDE的输出结果右上侧的红色方块说明, JVM 进程并没有退出。
这个结果说明了当父线程结束后,子线程还是可以继续存在的,也就是子线程的生命周期并不受父线程的影响。这也说明了在用户线程还存在的情况下JVM 进程并不会终止。
那么我们把上面的thread 线程设置为守护线程后,再来运行看看会有什么结果:
//设置为守护线程
thread.setDaemo 口( true) ;
//启动子线程
thread.start();
输出结果:
在启动线程前将线程设置为守护线程,执行后的输出结果显示,JVM 进程己经终止了,执行ps -eaf | grep java
也看不到JVM 进程了。
在这个例子中, main 函数是唯一的用户线程, thread 线程是守护线程,当main 线程运行结束后, JVM 发现当前己经没有用户线程了,就会终止JVM 进程。由于这里的守护线程执行的任务是一个死循环,这也说明了如果当前进程中不存在用户线程,但是还存在正在执行任务的守护线程,则JVM不等守护线程运行完毕就会结束JVM 进程。
main 线程运行结束后, JVM 会自动启动一个叫作DestroyJavaVM
的线程, 该线程会等待所有用户线程结束后终止JVM 进程。
ThreadLocal
在多线程编程中,线程间数据共享是非常常见的问题,但是容易出现并发问题,特别是多个线程对同一共享变量进行写入时。为了保证线程安全,一般使用者在访问成员变量时需要进行适当的同步。
我们通常会使用synchronized
或者Lock
等加锁方式进行线程同步,但是这些方式在性能方面是有一定的损耗的。那么有没有一种方式可以在保证线程安全的前提下,又不会带来太大的性能问题呢?ThreadLocal
就是这样一种机制。
ThreadLocal
是一种线程封闭的机制,它可以将数据隔离在每个线程中,每个线程都拥有一份独立的数据副本, 实际操作的是自己本地内存的变量。这样一来,不同的线程之间就不会出现数据共享的问题了。
使用示例
public class ThreadLocalDemo {
//创建ThreadLocal变量
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
//创建线程
new Thread(() -> {
//设置threadLocal本地变量的值
threadLocal.set("hello world");
//使用get方法获取该值
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
}, "thread1").start();
//创建线程
new Thread(() -> {
threadLocal.set("你好 世界");
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
}, "thread2").start();
}
}
在这个示例代码中,我们创建了两个线程,分别向ThreadLocal
中设置了不同的值。由于ThreadLocal
是线程封闭的机制,因此每个线程都拥有自己的独立的数据副本,两个线程之间不会互相影响。输出结果如下:
thread1 hello world
thread2 你好 世界
实现原理
ThreadLocal
的实现原理其实比较简单,它主要依赖于ThreadLocalMap
字段实现,其类图结构如下:
ThreadLocal
的类结构比较简单,主要包含以下几个方法:
set
:向当前线程的ThreadLocalMap
中存储值get
:获取当前线程的ThreadLocalMap
中的值remove
:清除当前线程的ThreadLocalMap
中的值initialValue
:初始化值,可在子类中覆盖此方法以提供默认值
ThreadLocal源码:
public class ThreadLocal<T> {
/**
* 返回当前线程对应的ThreadLocalMap。
* 这里的threadLocals是ThreadLocalMap类型的变量,作为存储多个ThreadLocal变量的容器,后面再具体解析
*/
private ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 在当前线程的ThreadLocalMap中设置值。
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
//更新当前ThreadLocal变量的值,注意,这里this值ThreadLocal类的当前实例对象
map.set(this, value);
else
//第一次调用就创建当前线程对应的HashMap
createMap(t, value);
}
/**
* 从当前线程的ThreadLocalMap中获取值。
*/
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//将当前线程作为key,查找对应的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//通过this:当前实例对象获取ThreadLocal
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T) e.value;
}
//ThreadLocalMap为空则初始化当前线程的ThreadLocal变量值
return setInitialValue();
}
/**
* 从当前线程的ThreadLocalMap中清除值。
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* 初始化值。
*/
protected T initialValue() {
return null;
}
/**
* 创建一个新的ThreadLocalMap并初始化值。
*/
void createMap(Thread t, T firstValue) {
//传入的key值是当前ThreadLocal的this引用
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* 获取初始值,如果子类未提供初始值,则返回null。
*/
T setInitialValue() {
//初始化为空
T value = initialValue();
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap变量
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//第一次调用就创建当前线程对应的HashMap
createMap(t, value);
return value;
}
}
注:一开始被
ThreadLocal
、threadLocals
、ThreadLocalMap
搞晕了,后面发现其实很简单,直接把threadLocals
给踢了,只剩下ThreadLocal
、ThreadLocalMap
,这样就好理解多了。threadLocals
这玩意只是个变量名而已,不用关心。记住
ThreadLocal
类对应一个变量,里面有一些对应的方法,而ThreadLocalMap
是一个Map,里面有很多ThreadLocal
, 取的时候通过当前线程和ThreadLocal
对象的哈希码,找到对应的Entry对象,然后返回它的value字段。
在ThreadLocal
类中,主要是通过对当前线程的ThreadLocalMap
的操作来实现线程本地变量的存储和获取,具体实现细节可以通过源码来了解。
ThreadLocalMap
Thread类中有一个ThreadLocalMap
类型的字段threadLocals
,它是线程本地变量的存储容器。每个ThreadLocal
对象都有一个对应的entry在threadLocals
中 ,用来存储线程对应的值。
ThreadLocalMap
是一个自定义的HashMap
,它的键为ThreadLocal
对象,值为线程保存的数据对象。在每个线程中, ThreadLocalMap
都是独立的,因此不同的线程之间不会出现数据共享的问题。由于ThreadLocalMap
是线程独立的,因此在多线程环境下不会出现竞争的问题,从而保证了线程安全性。
ThreadLocalMap
中的键值对的生命周期是跟随线程的,当线程结束后,ThreadLocalMap
中的键值对也会被回收。如何线程一直不消亡,会导致这些本地变量一直存在,所以可能造成内存溢出,因此,使用结束后记得要调用ThreadLocal
的remove
方法删除对应线程ThreadLocalMap
中存储的本地变量。
ThreadLocal
对象通过调用ThreadLocal
的set
方法将值存储到当前线程的ThreadLocalMap
中,通过调用ThreadLocal
的get
方法可以获取当前线程的值。ThreadLocal
的remove
方法可以清除当前线程中的ThreadLocalMap
中的键值对,从而释放资源。
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
在Thread类中使用到两个变量:threadLocals
和 inheritableThreadLocals
,都是 ThreadLocalMap
类型的变量。
ThreadLocals
是线程本地变量的存储容器。每个ThreadLocal
对象都有一个对应的entry在ThreadLocals
中,用来存储线程对应的值。这里也就刚好引出另一个问题了:为什么threadLocals
被设计成map结构?
很明显,ThreadLocals
要作为容器存储多个ThreadLocal
本地变量,并通过每个线程进行关联,自然我们就想到了map了。
ThreadLocal不支持继承性
ThreadLocal
不支持继承性,也就是说,子线程无法访问父线程中的ThreadLocal
变量,因为子线程中的ThreadLocal
变量是子线程自己的,而不是从父线程中继承的。
举个例子,假设有一个父线程和一个子线程,父线程中有一个ThreadLocal变量,当父线程启动子线程时,子线程无法访问父线程中的ThreadLocal变量,因为子线程中的ThreadLocal变量是子线程自己的,而不是从父线程中继承的。
public class ThreadLocalExample {
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Hello, World!");
Thread thread = new Thread(() -> {
System.out.println("Child thread value: " + threadLocal.get());
});
thread.start();
System.out.println("Main thread value: " + threadLocal.get());
}
}
运行结果如下:
Main thread value: Hello, World!
Child thread value: null
可以看到,子线程中的ThreadLocal变量值为null,而不是从父线程中继承的值。
那有没有办法可以让子线程能够访问到父线程中的值呢?当然!
InheritableThreadLocal类
InheritableThreadLocal
类是Java中的一个线程本地变量类,它继承了ThreadLocal类,扩展了ThreadLocal以提供从父线程到子线程的值继承。
下面是一个使用InheritableThreadLocal
类的例子:
public class InheritableThreadLocalExample {
public static void main(String[] args) {
InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
inheritableThreadLocal.set("Hello World");
Thread thread = new Thread(() -> {
System.out.println(inheritableThreadLocal.get());
});
thread.start();
}
}
在这个例子中,我们创建了一个InheritableThreadLocal
对象,然后在主线程中设置了一个值。接着,我们创建了一个子线程,并在子线程中获取了这个值。由于InheritableThreadLocal
类提供了从父线程到子线程的值继承,所以子线程中可以获取到这个值,输出结果为"Hello World"。
使用场景
InheritableThreadLocal
类的应用场景是当线程需要从父线程继承某些值时,可以使用InheritableThreadLocal
类。例如,当需要在多个线程之间共享用户ID或事务ID等线程本地变量时,可以使用InheritableThreadLocal
类。InheritableThreadLocal
类提供了从父线程到子线程的值继承,因此子线程可以获取父线程中的值。
源码分析
Thread类中定义了叫inheritableThreadLocals
的变量,类型为ThreadLocalMap
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
在InheritableThreadLocal
类中继承了ThreadLocal
,并重写了三个方法,替换了原有的threadLocals
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* 用于在子线程中创建一个新的InheritableThreadLocal对象时,为该对象提供一个初始值。
* 在默认情况下,该方法返回父线程中的值,因此子线程中的InheritableThreadLocal对象的初始值与父线程中的值相同。
* @param parentValue the parent thread's value
* @return the child thread's initial value
*/
protected T childValue(T parentValue) {
return parentValue;
}
/**
* 获取的是inheritableThreadLocals变量
* 而不再是ThreadLocals变量
* @param t the current thread
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* 创建当前线程的inheritableThreadLocals变量实例
* @param t the current thread
* @param firstValue value for the initial entry of the table.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
再具体看一下childValue方法的执行流程,以及如何访问父线程的本地变量
当我们在主线程中start一个子线程时,会new 一个Thread。创建线程时发生了什么才让父子线程的InheritableThreadLocal可以传递?
在Thread类中,有多个默认构造方法, 经过重载,都最终调用了init方法
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
//最终调用
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//获取当前线程(父线程)
Thread parent = currentThread();
……
/* 如果inheritThreadLocals为true并且父线程中存在InheritableThreadLocal对象,
* 则使用ThreadLocal.createInheritedMap方法创建一个新的Map对象,
* 该对象包含父线程中所有InheritableThreadLocal对象的值。这个新的Map对象将作为
* 子线程的InheritableThreadLocalMap对象的初始值。
*/
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
//inheritThreadLocals变量的值复制一份到新的//创建一个新的Map对象
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
//将父线程的inheritThreadLocals变量的值复制一份到新的对象中
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
//这里调用的是InheritableThreadLocal重写后的childValue方法
Object value = key.childValue(e.value);//返回e.value
Entry c = new Entry(key, value);
//计算hash值
int h = key.threadLocalHashCode & (len - 1);
//找到一个空的位置添加Entry实例,就是hashmap添加元素的过程
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
总结
ThreadLocal
是一种线程封闭的机制,它可以将数据隔离在每个线程中,每个线程都拥有一份独立的数据副本。ThreadLocal
主要是通过ThreadLocalMap
和Thread类中的ThreadLocalMap
字段实现,ThreadLocalMap
是一个自定义的HashMap,用来存储线程本地变量的键值对,而Thread类中的ThreadLocalMap
字段threadLocals
则是用来存储每个线程的ThreadLocalMap
。通过使用ThreadLocal
,我们可以在保证线程安全的前提下,又不会带来太大的性能问题。
参考资料
《Java并发编程之美》