java并发编程:ThreadLocal、InheritableThreadLocal、守护线程和用户线程

本文深入探讨Java的ThreadLocal,包括其作用、使用方式和源码解析,以及InheritableThreadLocal如何让子线程继承父线程的变量。同时,介绍了守护线程和用户线程的概念、区别及其在JVM中的实现,帮助理解Java多线程中的关键概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上次梳理了线程的创建和状态切换,本次将继续基础篇的整理,涉及JAVA的两类线程,以及ThreadLocal的一些知识、源码等。

1 ThreadLocal

1.1 概述

ThreadLocal是JDK包提供的,在java.lang包下,提供了线程本地变量。

它的作用是,当创建了一个ThreadLocal变量,每个访问这个变量的线程,都会有一个这个变量的本地副本,当多个线程操作这个变量时,实际操作的是自己本地内存的变量,互不干扰

如此一来,它的使用场景主要针对多线程,变量不共享的情况:

  • 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
  • 线程间数据隔离
  • 进行事务操作,用于存储线程事务信息。
  • 数据库连接,Session会话管理。

1.2 怎么用

通过声明ThreadLocal变量,不同的线程单独维护一个备份。

public class ThreadLocalTest {

    // 创建localVariable变量
    private static ThreadLocal<String> localVariable = new ThreadLocal<>();

    static void printAndRemove(String str){
        // 打印当前线程本地内存中的localVariable值
        System.out.println(str + ":" + localVariable.get());
        // 移出当前线程本地内存中的localVariable值
        localVariable.remove();
    }

    public static void main(String[] args) {
        // 线程1
        Thread threadOne = new Thread(() -> {
            localVariable.set("threadOne local variable 11");
            printAndRemove("threadOne");
            System.out.println("threadOne local variable remove" + ":" + localVariable.get());
        });
		// 线程2
        Thread threadTwo = new Thread(() -> {
            localVariable.set("threadTwo local variable 22");
            printAndRemove("threadTwo");
            System.out.println("threadTwo local variable remove" + ":" + localVariable.get());
        });
        // 开启线程
        threadOne.start();
        threadTwo.start();
    }
}

threadOne:threadOne local variable 11
threadOne local variable remove:null
threadTwo:threadTwo local variable 22
threadTwo local variable remove:null

1.3 源码实现

看一下相关的类图

在这里插入图片描述

ThreadLocalMap是一个定制化的HashMap,在Thread类中有两个ThreadLocalMap类型的变量:threadLocalsinheritableThreadLocals,在默认情况下,每个线程的这两个变量为null,只有当前线程第一次调用ThreadLocal的set或get方法,才会创建他们。

为什么会是一个map结构呢,从变量名可以看出,一个线程(threadLocals)可以关联多个threadLocal变量。

所以按上面的代码中,ThreadLocal变量实例里,并不存放具体某个线程的变量,只是一个工具壳,通过set方法将value值放到调用线程的threadLocals里,调用get时,再从当前线程中取出来。每个线程的本地变量存放在线程内存空间中,如果线程不终止,那么该本地变量将会一直存放在调用线程的threadLocals中,所以当不需要使用时,可以调用remove从当前线程中删除掉。

经过上面的分析,有了大致的概念,来看一下具体的源码实现

1.3.1 set()

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

如上是源码,比较简单:首先获取当前线程,然后以当前线程为KEY,查找对应的线程变量,找到变量则设置值,找不到则认为是第一次调用,创建当前线程对应的map。

其中getMap()方法,就是拿到threadLocals变量,createMap就是调用ThreadLocalMap的构造函数,实例化对象。源码如下:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

1.3.1 get()

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

总的体操作和set类似,首先拿到当前线程去取threadLocals,然后对这个map进行操作,值得注意的是,这个map的key是当前ThreadLocal实例(代码中的this),实现了一个线程对应多个ThreadLocal变量

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    ...

定义了一个Entry来保存数据,继承的弱引用。在Entry内部使用ThreadLocal作为key,使用传递的值作为value。

1.3.3 remove()

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

同样是获得当前线程之后,拿到线程对应的map,然后以当前ThreadLocal实例为key进行remove,删除当前ThreadLocal变量。

1.4 InheritableThreadLocal类

1.4.1 概述原理

通过上面的分析,知道了ThreadLocal变量,是每一个线程维护一个本地备份,从源码角度是对应了Thread的threadLocals。这样一来,不论两个线程之间是否存在父子关系,变量对对方来说,都是不可见的。

InheritableThreadLocal继承自ThreadLocal,提供了一个特性,就是让子线程可以访问父线程设置的本地变量

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    
    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

可以看出,主要重写了三个方法:

  • 重写了createMap之后,再次调用set的时候,将会创建当前线程的inheritableThreadLocals变量,而不是threadLocals变量。

  • 重写了getMap之后,返回的也是inheritableThreadLocals。

    可以看出来,在InheritableThreadLocal的中,使用inheritableThreadLocals代替了threadLocals变量。

  • 对childValue的重写,要看看Thread的默认构造函数:

public Thread() {
	init(null, null, "Thread-" + nextThreadNum(), 0);
}

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
	...
    // 获取当前线程
    Thread parent = currentThread();
    ...
    // 如果父线程的inheritableThreadLocals不为空
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        // 设置子线程中的inheritableThreadLocals变量
        this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        
    	/* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;
        /* Set thread ID */
        tid = nextThreadID();
}

// ThreadLocal.createInheritedMap
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

在创建线程的时候,构造函数调用init()方法,然后再init中,首先拿到了当前线程(这里指父线程),然后判断inheritableThreadLocals是否为null,不为空的话,将会执行createInheritedMap()方法,即使用父线程的inheritableThreadLocals变量作为构造函数参数,创建了一个新的ThreadLocalMap变量,然后复制给子线程的inheritableThreadLocals变量

整个流程就清楚了,即在创建子线程的时候(当前线程为父线程),就已经拿父线程的inheritableThreadLocals变量传参,复制到子线程的inheritableThreadLocals中。还剩一个方法源码没有看,就是ThreadLocalMap(parentMap),主要实现的就是将传入的值,复制出来。

private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    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) {
                // 使用到了重写的第三个方法childValue
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

1.4.2 使用

在实例化时,对象换成 InheritableThreadLocal。

场景应用于,子线程需要使用到父线程的本地变量。

    private static ThreadLocal<String> local = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        local.set("main");
        System.out.println("main" + ":" + local.get());
        
        Thread thread = new Thread(() -> {
            System.out.println("thread" + ":" + local.get());
        });
        thread.start();
    }

main:main
thread:main

2 守护线程和用户线程

2.1 概述

守护(daemom)线程,用户(user)线程,比较典型的例子是:守护线程:垃圾回收线程;用户线程:main主线程。

主要的区别就是,当非守护线程结束时,其余的线程(守护线程)不再等待,JVM退出。 即非守护线程不影响JVM的退出。

这样一来,可以利用这个规律,设置一些线程为守护线程,就不需要手动管理它的退出。

2.2 区别

创建守护线程:在开始线程前,通过设置标记位的方式即可。

Thread daemonThread = new Thread(new Runnable(){
    public void run(){
        // do something
    }
});
// 设置为守护线程
daemonThread.setDaemon(true);
daemonThread.start();

一个直观的例子,感受守护线程与用户线程的区别:

①首先看创建一个用户线程的情况:

public class DaemonThreadTest {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while(true){

            }
        });

        thread.start();
        System.out.println("main thread over");
    }
}

在这里插入图片描述

开启了一个用户线程类型的子线程是一个死循环,主线程执行完成之后,子线程还在进行,程序并没有执行结束。

②再来看看,创建的子线程是守护线程的情况下:

public class DaemonThreadTest {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(() -> {
            while(true){

            }
        });
        // 设置为守护线程
        daemonThread.setDaemon(true);
        daemonThread.start();
        System.out.println("main thread over");

    }
}

在这里插入图片描述

虽然子线程依旧是死循环,但是类型为守护线程时,主线程执行完成之后,用户线程全部执行完毕,JVM关闭,程序结束。

2.3 实现

main线程结束之后,JVM会自动启动一个叫作DestroyjavaVm的线程,该线程会等待所有用户线程结束后,终止JVM进程。

这里牵扯出的JVM代码如下:

int JNICALL
JavaMain(void *_args){
	...
	// 执行Java中的main函数
	(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
	// main函数返回值
	ret = (*env)->ExceptionOccurred(env) == NULL ? 0: 1;
	// 等待所有非守护线程结束,然后销毁JVM进程。
	LEAVE();
}

LEAVE()是C语言的宏定义,创建了一个名为DestroyjavaVm的线程,来等待所有用户线程结束。

#define LEAVE() 
	do{ 
		if ((*vm)->DetachCurrentThread(vm) != JNI_OK) {
		JLI_ReportErrorMessage(JVM_ERROR2);
		ret = 1;
		}
        if (JNI_TRUE){
            (*vm)->DestroyJavaVM(vm);
            return ret;
        }
    } while (JNI_FALSE)

相对的,在Tomcat的NIO实现NioEndpoint中会开启一组线程来接受用户的连接请求,以及一组处理线程负责处理具体的用户请求,查看源码,可以看到对daemon属性置为true的操作,故这两组接受和处理线程都属于守护线程。

简单的说,当希望主线程执行结束之后,JVM进行就马上结束,那么可以将创建的其他线程设置为守护线程;当希望主线程执行完成之后,子线程继续执行,那么将子线程设置为守护线程。

​参考文献:​《java并发编程之美》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值