并发编程之美(1)并发编程基础二_会写代码的花城的博客-CSDN博客_并发编程之美

1…9线程死锁

1.9.1什么是线程死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去

在这里插入图片描述
在这里插入图片描述

线程A 己经持有了资源2 , 它同时还想申请资源l , 线程B 已经持有了资源l ,它同时还想申请资源2 , 所以线程l 和线程2 就因为相互等待对方已经持有的资源,而进入了死锁状态。

为什么会产生死锁呢?用原书(并发编程之美)中的解释

死锁的产生必须具备以下四个条件。互斥条件: 指线程对己经获取到的资源进行排它性使用, 即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。请求并持有条件: 指一个线程己经持有了至少一个资源, 但又提出了新的资源请求,而新资源己被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己己经获取的资源。不可剥夺条件: 指线程获取到的资源在自己使用完之前不能被其他线程抢占, 只有在自己使用完毕后才由自己释放该资源。环路等待条件: 指在发生死锁时, 必然存在一个线程→资源的环形链, 即线程集合{T0 , T1 T2 ,…, Tn }中的T0 正在等待一个Tl 占用的资源, T1正在等待T2 占用的资源,……Tn 正在等待己被T0 占用的资源。

死锁例子

public class DedLock {
    //创建资源
    private static Object resourceA = new Object();
    private static Object resourceB = new Object();

    public static void main(String[] args) {
        new Thread(()->{
            //先给我们的资源A加锁
            synchronized (resourceA){
                System.out.println(Thread.currentThread()+"获得资源A");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread()+"等待获取资源B");
                //在对资源B加锁
                synchronized (resourceB)
                {
                    System.out.println(Thread.currentThread()+"获得资源B");
                }
            }
        }).start();
        new Thread(()->{
            //先给我们的资源A加锁
            synchronized (resourceB){
                System.out.println(Thread.currentThread()+"获得资源B");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread()+"等待获取资源A");
                //在对资源B加锁
                synchronized (resourceA)
                {
                    System.out.println(Thread.currentThread()+"获得资源B");
                }
            }
        }).start();
    }
}
结果
Thread[Thread-0,5,main]获得资源A
Thread[Thread-1,5,main]获得资源B
Thread[Thread-1,5,main]等待获取资源A
Thread[Thread-0,5,main]等待获取资源B

这就是典型的死锁例子,我们先锁了资源A,在资源A中又对资源B加锁,但是因为我们线程A睡了1s,期间我们的线程B已经对资源B先加锁了,线程B先对资源B加锁但是又想获取资源A,就导致两者互相争抢,又不释放自己的锁,导致死锁

他们满足上面我们说的死锁产生具备的条件么?

1.resourc eA 和re sourc eB 都是互斥资源,当线程A 调synchronized(resource A)方法获取到resourceA 上的监视器锁并释放前, 线程B 再调用synchronized(resourceA) 方法尝试获取该资源会被阻塞,只有线程A 主动释放该锁, 线程B 才能获得, 这满足了资源互斥条件

2.线程A 首先通过synchronized(resourceA) 方法获取到resourceA 上的监视器锁资源,然后通过synchronized(resourceB) 方法等待获取resourceB 上的监视器锁资源, 这就构成了请求并持有条件。

也就是请求B但是我还持有A

3.构成了资源的不可剥夺条件,就是我们线程A只要不是自己主动释放资源A的监视器锁,那么其他线程(线程B)是不会掠夺走的

4.环路等待条件就是,线程A锁了资源A但是请求资源B,但是线程B锁了资源B,又去请求资源A,形成闭合

那么如何避免线程死锁呢?

1.9.2如何避免线程死锁

字需要破坏形成死锁的一个必要条件即可。

目前我们只有请求并持有环路等待条件是可以被破坏的

造成死锁的原因其实和申请资源的顺序有很大关系, 使用资源申请的有序性原则就可以避免死锁,那么什么是资源申请的有序性呢?我们对上面线程B 的代码进行如下修改

new Thread(()->{
    //先给我们的资源A加锁
    synchronized (resourceA){
        System.out.println(Thread.currentThread()+"获得资源B");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread()+"等待获取资源A");
        //在对资源B加锁
        synchronized (resourceB)
        {
            System.out.println(Thread.currentThread()+"获得资源A");
        }
    }
}).start();

结果

Thread[Thread-0,5,main]获得资源A
Thread[Thread-0,5,main]等待获取资源B
Thread[Thread-0,5,main]获得资源B
Thread[Thread-1,5,main]获得资源B
Thread[Thread-1,5,main]等待获取资源A
Thread[Thread-1,5,main]获得资源A

也就是说资源B没有被加锁,虽然两个线程都对资源A加锁了,但是某个线程先执行,好后获得B之后就是放了资源A,另一个线程能拿到了

这就是资源的有序分配,资源的有序性破坏了资源的请求并持有条件和环路等待条件, 因此避免了死锁。

1.10守护线程与用户线程

Java 中的线程分为两类,分别为daemon 线程(守护线程〉和user 线程(用户线程)。

在JVM启动的时候会调用main函数,main函数所在的线程就是一个用户线程

当然JVM内部同时还启动了好多守护线程,比如垃圾回收线程

关于垃圾回收线程后续会在JVM的笔记中详细提到

如何创建守护线程

public class daemon {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{});
        //设置为守护线程
        thread.setDaemon(true);
        thread.start();
    }
}

用户线程与守护线程的区别

区别之一是当最后一个非守护线程结束时, NM 会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响NM 的退出。言外之意,只要有一个用户线程还没结束, 正常情况下NM 就不会退出。

public class daemon {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            for (;;){}
        });
        //启动子线程
        thread.start();
        System.out.println("主线程完毕");
    }
}
结果是
    主线程完毕

如上代码在main 线程中创建了一个thread 线程,在thread 线程里面是一个无限循环。从运行代码的结果看, main 线程已经运行结束了,那么JVM进程己经退出了吗?在IDE的输出结果右上侧的红色方块说明,JVM 进程并没有退出。另外,我们亦可以通过jps来查看

这个结果说明了当父线程结束后,子线程还是可以继续存在的,也就是子线程的生命周期并不受父线程的影响。这也说明了在用户线程还存在的情况下JVM 进程并不会终止。那么我们把上面的thread 线程设置为守护线程后,再来运行看看会有什么结果:

public class daemon {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            for (;;){}
        });
        //设置为守护线程
        thread.setDaemon(true);
        //启动子线程
        thread.start();
        System.out.println("主线程完毕");
    }
}
结果是
    主线程完毕

    进程已结束,退出代码 0

在启动线程前将线程设置为守护线程,执行后的输出结果显示,JVM进程己经终止了,执行***ps -eaf |grep java*** 也看不到JVM 进程了。在这个例子中, main 函数是唯一的用户线程, thread 线程是守护线程,当main 线程运行结束后, JVM 发现当前己经没有用户线程了,就会终止JVM 进程。

由于这里的守护线程执行的任务是一个死循环,这也说明了如果当前进程中不存在用户线程,但是还存在正在执行任务的守护线程,则JVM不等守护线程运行完毕就会结束JVM进程。

main 线程运行结束后, JVM会自动启动一个叫作DestroyJava VM 的线程, 该线程会等待所有用户线程结束后终止JVM进程。下面通过简单的JVM代码来证明这个结论

int JNICALL
JavaMain(void * args)
    //执行Java 中的ma 工n函数
    (*env) - >CallStaticVoidMethod(env , mainClass, mainID, mainArgs) ;
    //main 函数返回值
    ret = (*env)->ExceptIonOccurred(env) == NULL ? 0: 1 ;
    //等待所有非守护线程结束, 然后销毁♂月4进程
    LEAVE();
}

LEAVE 是C 语言里面的一个宏定义,具体定义如下。

#define LEAVE () \
    do {\
    if ( (*vm) >DetachCurrentThread(vm) 1= JNI_OK ) 	{ \
        JLI_ReportErrorMessage(JVM_ERROR2) ; \
        ret = l ; \
    }\
    if (JNI_TRUE) { \
        ( *vm)->DestroyJavaVM (vm); \
        return ret; \
    }\
} while_(JNI FALSE)

该宏的作用是创建一个名为DestroyJava VM 的线程,来等待所有用户线程结束

在Tomcat 的NIO 实现NioEndpoint 中会开启一组接受线程来接受用户的连接请求,以及一组处理线程负责具体处理用户请求,那么这些线程是用户线程还是守护线程呢?

下面我们看一下NioEndpoint 的startlntemal 方法。

public void startInternal() throws Exception{
    if(!running){
        running = true;
        paused = false;
        ...
        //创建处理线程
        pollers = new Poller[get PollerThreadCount () ] ;
        for (int i=O ; i<pollers . length; i++) {
            pollers [i] = new Poller () ;
            Thread pollerThread =new Thread (pollers [i],getName () +"- Client Poller-"+ i ) ;
            pollerThread.setPriority (threadPriority ) ;
            pollerThread.setDaemon(true );
            //声明为守护线程
            pollerThread.start() ;
    }
}
protected final void startAcceptorThreads() {
    int count= getAcceptorThre adCount();
    acceptors= new Acceptor[count];
    for (int i = O;i<count; i ++) {
        acceptors [i] = createAcceptor () ;
        String threadName = getName ()+"- Acceptor-" +i;
        acceptors[i].setThreadName(threadName) ;
        Thread t =new Thread(acceptors[i],threadName) ;
        t.setPriority(getAcceptorThreadPriority()) ;
        t.setDaemon(getDaemon());//设置是否为守护线程,默认为守护线程
        t . start() ;
    }
}
    private boolean daemon = true;
    public void setDaemon(boolean b) { daemon= b ; }
    public boolean getDaemon () { return daemon ; }

在如上代码中,在默认情况下, 接受线程和处理线程都是守护线程, 这意味着当tomcat 收到shutdown 命令后并且没有其他用户线程存在的情况下tomcat 进程会马上消亡,而不会等待处理线程处理完当前的请求。

总结

如果你希望在主线程结束后JVM 进程马上结束,那么在创建线程时可以将其设置为守护线程,如果你希望在主线程结束后子线程继续工作,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程。

1.11 ThreadLocal

多钱程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步,如下图所示

在这里插入图片描述
在这里插入图片描述

我们的同步方式一般的都是用加锁的方式来实现同步,那么有没有一种方式可以做到,当创建一个变量后, 每个线程对其进行访问的时候访问的是自己线程的变量呢?====>ThreadLocal

什么是ThreadLocal

ThreadLocal 是JDK 包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个ThreadLocal 变量后,每个线程都会复制一个变量到自己的本地内存,如下图所示。

在这里插入图片描述
在这里插入图片描述

1.11.1 ThreadLocal使用示例

public class ThreadLocalTest {
    static void print(String str){
        //1.1打印当前线程本地内存中localVariable变量的值
        System.out.println(str+":"+localVairable.get());
        //1.2清除当前线程池本地内存中的localVairable变量
        //localVairable.remove();
    }

    //2.创建ThreadLocal变量
    static ThreadLocal<String> localVairable = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(()->{
            //3.1设置线程A中本地变量localVairable的值
            localVairable.set("线程A的localVairable");
            //3.2调用打印函数
            print("线程A");
            //3.3打印本地变量的值
            System.out.println("线程A的localVairable清除后:"+localVairable.get());
        }).start();
        new Thread(()->{
            //3.1设置线程A中本地变量localVairable的值
            localVairable.set("线程B的localVairable");
            //3.2调用打印函数
            print("线程B");
            //3.3打印本地变量的值
            System.out.println("线程B的localVairable清除后:"+localVairable.get());
        }).start();
    }
}
结果是:

线程A:线程A的localVairable
线程B:线程B的localVairable
线程A的localVairable清除后:线程A的localVairable
线程B的localVairable清除后:线程B的localVairable

我们使用了set设置了localVariable 的值,这其实是设置A线程本地内存的一个副本,这个副本B是访问不了的。

上面的例子是我们没有执行清除本地内存副本的操作,我们放开注释执行,结果变为

线程A:线程A的localVairable
线程B:线程B的localVairable
线程A的localVairable清除后:null
线程B的localVairable清除后:null

1.11.2 ThreadLocal的实现原理

相关类图

image
image

书中说道

Thread 类中有一个threadLocals 和一个inheritableThreadLocals , 它们都是ThreadLocalMap 类型的变量, 而ThreadLocalMap 是一个定制化的Hashmap 。在默认情况下, 每个线程中的这两个变量都为null ,源码中就有体现

protected T initialValue() {
    return null;
}

只有当前线程第一次调用ThreadLocal 的set 或者get 方法时才会创建它们。

其实每个线程的本地变量不是存放在ThreadLocal 实例里面,而是存放在调用线程的threadLocals 变量里面。也就是说, **ThreadLocal 类型的本地变量存放在具体的线程内存空间中。**ThreadLocal 就是一个工具壳,

它通过set 方法把value 值放入调用线程的threadLocals 里面并存放起来, 当调用线程调用它的get 方法时,再从当前线程的threadLocals 变量里面将其拿出来使用。如果调用线程一直不终止, 那么这个本地变量会一直存放在调用线程的threadLocals 变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal 变量的remove 方法,从当前线程的threadLocals 里面删除该本地变量。

另外, Thread 里面的threadLocals 为何被设计为map 结构?很明显是因为每个线程可以关联多个ThreadLocal 变量。

1 void set(T value)

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //将当前线程作为key,去查找对应的线程变量
    ThreadLocalMap map = getMap(t);
    //如果存在这个线程对应的变量
    if (map != null)
        map.set(this, value);
    else
        //如果对应的线程变量不存在,就新建,将key,value保存
        createMap(t, value);
}

getMap(Thread t)的源码如下。

ThreadLocalMap getMap(Thread t) {
    //传进来一个线程,返回这个线程的变量threadLocals
    return t.threadLocals;
}

可以看到, getMap(t)的作用是获取线程自己的变量threadLocal s, threadlocal 变量被绑定到了线程的成员变量上

如果getMap(t)的返回值不为空,则把value 值设置到threadLocals 中,也就是把当前变量值放入当前线程的内存变量threadLocals 中

threadLocals 是一个HashMap 结构, 其中key 就是当前ThreadLocal 的实例对象引用, value 是通过set 方法传递的值。

如果getMap(t)返回空值则说明是第一次调用set 方法,这时创建当前线程的threadLocals 变量。下面来看**createMap(t, value)**做什么。

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

也就是说当前线程没有threadLocals这个变量时,她就new一个

ThreadLocalMap线程作为键,设置的内容为值

  1. T get()

public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //查找当前线程存不存在对应的threadLocals变量
    ThreadLocalMap map = getMap(t);
    //如果threadLocals变量不为null,则返回对应本地变量的值
    if (map != null) {
        //
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            //取出我们的值 @SuppressWarnings正压警告注解
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //如果没有threadLocals这个变量,则初始化一个
    return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}
private T setInitialValue() {
    //初始化为null
    T value = initialValue();
    //获得当前线程
    Thread t = Thread.currentThread();
    //查找当前线程存不存在对应的threadLocals变量
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //有的话值设置为null
        map.set(this, value);
    else
        //没有的话创建,
        createMap(t, value);
    return value;
}

如果当前线程的threadLocal s 变量不为空, 则设置当前线程的本地变量值为null , 否则调用createMap 方法创建当前线程的createMap 变量。

3.void remove()

public void remove() {
    //查询该线程有没有绑定的threadLocals变量
    ThreadLocalMap m = getMap(Thread.currentThread());
    //如果有,移除
    if (m != null)
        m.remove(this);
}

总结

​ 在每个线程内部都有一个名为threadLocals 的成员变量, 该变量的类型为Hash Map , 其中key 为我们定义的ThreadLocal 变量的this 引用, value 则为我们使用set 方法设置的值。每个线程的本地变量存放在线程自己的内存变量threadLocals 中,如果当前线程一直不消亡, 那么这些本地变量会一直存在, 所以可能会造成内存溢出, 因此使用完毕后要记得调用ThreadLocal 的remove 方法删除对应线程的threadLocals 中的本地变量。在高级篇要讲解的只JC 包里面的ThreadLocalRandom , 就是借鉴ThreadLocal 的思想实现的, 后面会具体讲解。

1.11.3 ThreadLocal不支持继承性

首先看一个例子

public class TestThreadLocal {
    //创建线程变量
    public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    public static void main(String[] args) {
        //2.设置线程变量
        threadLocal.set("你好,world");
        //3.启动子线程
        new Thread(()->{
            System.out.println("子线程:"+threadLocal.get());
        }).start();
        //4.输出主线成的变量值
        System.out.println("main:"+threadLocal.get());
    }
}

结果是:

main:你好,world
子线程:null

也就是说,同一个ThreadLocal 变量在父线程中被设置值后, 在子线程中是获取不到的。根据上节的介绍,这应该是正常现象,因为在子线程thread 里面调用get 方法时当前线程为thread 线程,而这里调用s et 方法设置线程变量的是main 线程,两者是不同的线程,自然子线程访问时返回null 。

那如果我们把set放到子线程的函数体里面,结果就会是

main:null
子线程:你好,world

那么有没有办法让子线程能访问到父线程中的值? 答案是有。

1.11.4 lnheritableThreadLocal 类

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

下面看一下InheritableThreadLocal 的代码。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    //1
    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);
    }
}

Inheritab I e ThreadLocal 继承了ThreadLocal ,并重写了三个方法

InheritableThreadLocal 重写了createMap 方法, 那么现在当第一次调用set 方法时,创建的是当前线程的inheritableThreadLocals 变量的实例而不再是threadLocals 。

当调用get 方法获取当前线程内部的map 变量时, 获取的是inheritableThreadLocals 而不再是threadLocals 。

也就是说我们用了lnheritableThreadLocal 类的话,threadLocals 变成了inheritableThreadLocals

childValue

下面我们看一下重写的代码( 1 )何时执行, 以及如何让子线程可以访问父线程的本地变量。这要从创建Thread 的代码说起,打开Thread 类的默认构造函数,代码如下。

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init (ThreadGroup g , Runnable target , String name,long stacksize , AccessControlContext ace) {
//(4 )获取当前线程  这里获取的是我们的main线程
Thread parent = currentThread( );
//(5 )如采父线程的inheritableThreadLocals变量不为null
if (parent.inheritableThreadLocals != null )
    //(6 )设置子线程中的inheritableThreadLocals变量
    this.inheritableThreadLocals =
ThreadLocal.createinheritedMap(parent.inheritableThreadLocals);
this .stackSize = stackSize;
tid = nextThreadID() ;
}

代码在创建线程时,在构造函数里面会调用in it 方法。代码( 4 )获取了当前线程(这里是指main 函数所在的线程,也就是父线程〉。

然后代码( 5 )判断main 函数所在线程里面的inheritableThreadLocals 属性是否为null 。

前面我们讲了InheritableThreadLocal 类的get 和set 方法操作的是inheritableThreadLocals ,所以这里的inheritableThreadLocal 变量不为null ,因此会执行代码( 6 )。下面看一下createlnh eritedMap 的代码

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

可以看到,在createlnheri tedMap 内部使用父线程的inheritableThreadLocals 变量作为构造函数创建了一个新的ThreadLocalMap 变量, 然后赋值给了子线程的inheritableThreadLocals 变量

下面我们看看在ThreadLocalMap 的构造函数内部都做了什么事情

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) {
                //调用重写的方法
                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++;
            }
        }
    }
}

在该构造函数内部把父线程的inheritabl eThreadLoca l s 成员变量的值复制到新的ThreadLoca!Map 对象中,其中l代码( 7 )调用了Inheri tab leThreadLocal 类重写的代码( 1 ) 。

总结

InheritableThreadLocal 类通过重写代码。〉和( 3 ) 让本地变量保存到了具体线程的inheritableThreadLocal s 变量里面,那么线程在通过InheritableThreadLocal 类实例的set 或者get 方法设置变量时,就会创建当前线程的inheritableThreadLocals 变量。当父线程创建子线程时,构造函数会把父线程inheritableThreadLocals 变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals 变量里面。

把1.11.3 节中的代码( 1 )修改为

public class TestThreadLocal {
    //创建线程变量
    public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        //2.设置线程变量
        threadLocal.set("你好,world");
        //3.启动子线程
        new Thread(()->{

            System.out.println("子线程:"+threadLocal.get());
        }).start();
        //4.输出主线成的变量值
        System.out.println("main:"+threadLocal.get());
    }
}
结果为
    main:你好,world
    子线程:你好,world

那么在什么情况下需要子线程可以获取父线程的threadlocal 变量呢?

情况还是蛮多的,比如子线程需要使用存放在threadlocal 变量中的用户登录信息,再比如一些中间件需要把统一的id 追踪的整个调用链路记录下来。

其实子线程使用父线程中的threadlocal 方法有多种方式, 比如创建线程时传入父线程中的变量,并将其复制到子线程中,或者在父线程中构造一个map 作为参数传递给子线程,但是这些都改变了我们的使用习惯,所以在这些情况下InheritabI e ThreadLocal 就显得比较有用。