1–50面
java并发编程基础
什么是线程
进程:
是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位
线程:
是进程的一次执行路径,一个进程至少有一个线程,进程中的多个线程共享进程的资源.
线程是CPU分配的基本单位
栈:
每个线程都有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程无法访问,除此之外栈还可以用来存放线程的调用栈帧.
堆:
堆是一个进程中最大的一个内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用new操作创建的对象实例.
方法区:
存放JVM加载的类,常量及静态变量等信息,也是线程共享的.
线程的创建与运行
java一共三种创建线程的方式
-
- 实现Runnable接口的run方法
- 继承Thread类并重写run方法
- 使用FutureTask方式
/**
* @author xingchen
* @version V1.0
* @Package com.并发.线程的创建
* @date 2023/4/2 15:53
*
*
*
*
*
*
*
*
* 1.使用继承的好处是方便传参,可以在子类里面添加成员方法
* 通过set或者构造函数进行参数传递
* 2.使用Runnable则只能使用主线程里面被声明的final变量.不好的地方是java不支持多继承
* 3.继承Thread那么子类不能再继承其他类 而Runnable没有这个限制
* 4.前两个方法都不支持返回参数,最后的Futuretask方式可以
*
*/
public class ThreadTest {
/**
* 第一种实现多线程的方式
*
* 优点:
* 在run()方法内获取当前线程不需要使用Thread.currentThread()方法,直接使用this就可以获取当前线程
* 缺点:
* java不支持多继承,如果继承Thread就不能继承其他父类
* 任务和代码没分离,当多个线程执行一样的任务时候需要多份认任务代码
* 任务方法没有返回值
*/
public static class MyThread extends Thread{
@Override
public void run() {
System.out.println("I am a Thread");
}
}
/**
* 第二种创建线程的方法
*
* 优点:
* 任务跟代码分离开
* 缺点:
* 任务方法没有返回值
*/
public static class RunableTask implements Runnable{
@Override
public void run() {
System.out.println("I am a Thread2");
}
}
public static class CallTask implements Callable<String>{
@Override
public String call() throws Exception {
return "hello Thread";
}
}
public static void main(String[] args) throws InterruptedException{
/**
*创建线程的时候其实还没有成功启动,只有执行了线程的start方法的时候才算真正启动成功
* 用操作系统的角度来看创建线程的时候其实处在了就绪态(已经获取了除了CPU之外的其他资源)
* 执行start会执行线程的run方法,就获得了cpu从就绪态转到了运行态
* run方法结束标志该线程运行完成处于终止状态
*
*/
MyThread myThread = new MyThread();
//线程启动
myThread.start();;
RunableTask runableTask = new RunableTask();
new Thread((runableTask)).start();;
new Thread((runableTask)).start();
FutureTask<String> futureTask = new FutureTask<>(new CallTask());
new Thread(futureTask).start();
try {
String string = futureTask.get();
System.out.println(string);
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
线程的通知和等待
wait()函数
当一个线程调用共享线程的wait()方法的时候,该线程就会被组赛挂起,知道发生以下几种才会返回
-
-
- 其他线程调用了该共享线程的notify()或者notifyAll()
- 其他线程调用了该线程的interrupt(),该线程抛出了中断异常返回
-
注意:调用wait()方法的时候如果没有提前获取该对象的监视器锁,则调用该方法的时候会抛出IllegalMonitorException异常
获取共享变量的监视器锁的方法
执行synchronized同步代码块时候,使用该共享变量作为参数
synchronized (共享变量){
//代码逻辑
}
调用该共享变量方法的时候,并且该方法使用了synchronized来进行修饰
synchronized void add(int a,int b){
//代码逻辑
}
虚假唤醒
某个线程没有被其他线程调用notify(),notifyAll()方法进行通知,并且没有被中断,等待超时,就从挂起态变成了运行状态
要避免出现这种虚假唤醒
方法: 不断的测试该线程被唤醒的条件是否满足,不满足则继续等待,满足唤醒条件了就退出循环
synchronized (obj){
while (条件不满足){
obj.wait();
}
}
注意:当前线程调用共享变量的wait()方法后只会释放当前共享线程变量上的锁,如果当前线程还持有其他线程的锁是不会被释放的.
当一个线程调用共享对象的wait()方法被阻塞挂起的时候,如果其他线程中断了该线程,则该线程会抛出InterruptedException异常并返回
/**
* @author xingchen
* @version V1.0
* @Package com.并发
* @date 2023/4/2 20:16
*/
public class WaitNotifyInterupt {
static Object object=new Object();
public static void main(String[] args) throws InterruptedException{
Thread threadA=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("--------begin--------");
synchronized (object){
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
threadA.start();
Thread.sleep(1000);
System.out.println("begin interrput threadA");
threadA.interrupt();
System.out.println("end interrupt threadA---");
}
}
wait(long timeout)函数
相比之前的外滩()方法多了一个超时参数
不同点:
如果一个线程调用共享对象的该方法挂起时,没有在指定的超时时间内被其他线程调用该共享线程的notify()和notifyAll()方法唤醒,就会因为超时而返回
如果调用一个负数则会抛出IllegalArgumentException异常
wait(long timeout,int nanos)函数
public final void wait(long timeout,int nanos) throws InterruptedException{
if(nanos<0){
throw new IllegalAccessException("timeout value is negative");
}
if(nanos<0||nanos>999999){
throw new IllegalAccessException("nanoscond timeout value out of range");
}
if(nanos>0){
timeout++;
}
wait(timeout);
}
notify()函数
会唤醒一个在共享变量上调用的wait()系列方法后被挂起的线程.一个共享变量上可能会有很多个被阻塞的线程在等待,具体唤醒那个线程是随机的
被唤醒的线程不是立马就可以从wait()方法返回并运行,该线程必须在获取了对共享对象的监视器锁后才可以返回
没有获取到监视器锁的话则会抛出IllegalMonitorStateException异常
notifyAll()函数
该方法相比于上一个方法来说,该方法会释放该共享变量上面所有的由于调用wait()系列方法而被挂起的线程
public class NotifyAllTest {
private static volatile Object resourceA=new Object();
public static void main(String[] args) throws InterruptedException{
Thread threadA=new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA){
System.out.println("threadA get resocuseA lock");
try {
resourceA.wait();
System.out.println("threadA end wait");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread threadB=new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA){
System.out.println("threadB get resocuseA lock");
try {
resourceA.wait();
System.out.println("threadB end wait");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread threadC=new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA){
System.out.println("thread begin notify");
resourceA.notifyAll();
}
}
});
threadA.start();
threadB.start();
Thread.sleep(1000);
threadC.start();
threadA.join();
threadB.join();
threadC.join();
System.out.println("over");
}
}
注意:在共享变量上调用notifyAll()只会唤醒该方法前调用了wait系列函数而被放入共享变量等待集合里面的线程。而不会唤醒在该方法之后调用wait系列函数。
等待线程6373等待
等待线程执行终止的join方法
在需要等待多个事件完成后才螚继续往下执行的时候就可以使用Thread方法提供的join()方法
该方法是无参无返回值的方法。
/**
* @author xingchen
* @version V1.0
* @Package com.并发.Test
* @date 2023/4/12 23:31
*/
public class JoinTest {
public static void main(String[] args) throws InterruptedException{
Thread threadOne=new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("child threadOne over");
}
});
Thread threadTwo =new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("child threanTwo over");
}
});
threadOne.start();
threadTwo.start();
System.out.println("wait all child thread over ");
threadOne.join();
threadTwo.join();
System.out.println("all child over");
}
}
线程A调用线程B的join方法后会被阻塞,当其他线程调用线程A的interrupt()方法中断线程A,则A会抛出一个InterruptedException异常
public class InterruptedExceptionTest {
public static void main(String[] args) throws InterruptedException{
Thread threadOne =new Thread(new Runnable() {
@Override
public void run() {
System.out.println("threadOne begin run!!!");
for(; ; ){
}
}
});
final Thread mainThread=Thread.currentThread();
Thread threadTwo =new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
mainThread.interrupt();
}
});
threadOne.start();
threadTwo.start();
try {
threadOne.join();
} catch (InterruptedException e) {
System.out.println("main thread "+e);
}
}
}
让线程睡眠都sleep方法
sleep是Thread的一个静态方法,调用该方法会暂时让出指定时间的执行权,也就是不参与CPU调度,但是该线程所拥有的监视器资源(锁)还是持有不让出的,指定的休眠时间到了就会正常返回,相当于从阻塞态变成了就绪态,参与CPU的调度,如果获取到CPU资源之后就可以进入运行态。但是如果在睡眠期间其他线程调用了该线程的Interrupt()方法中断了该线程,那么该方法调用sleep的时候会抛出InterruptedException异常而返回。
/**
* @author xingchen
* @version V1.0
* @Package com.并发.Test
* @date 2023/4/12 23:38
*/
public class InterruptedExceptionTest {
//创建一个独占锁
private static final Lock lock=new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
System.out.println("child threadA is in sleep");
Thread.sleep(1000);
System.out.println("child threadA is in awaked");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
System.out.println("child threadB is in sleep");
Thread.sleep(1000);
System.out.println("child threadB is in awake");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});
threadOne.start();
threadTwo.start();
}
}
让出cpu执行权的yield方法
yield是Thread的一个静态方法,当线程调用yield方法时,就是按时线程调度器当前线程请求让出自己的cpu使用 但是线程调度器可以忽略这个暗示。
使用的时候意味着该线程自己占用都时间片没有使用完的部分不想使用了,暗示线程调度器现在就可以进行下一轮的线程调度。当前线程交出cpu使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,也有可能会调度到刚刚让出cpu的那个线程来获取cpu执行权。
总结:
sleep与yield方法的区别
当线程调用sleep方法时会调用线程会被阻塞挂起指定时间,在这期间线程调度器不会去调度该程。
当调用yield时候,线程会让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器在下一次调度的时候就有可能调度刀当前线程执行。
线程中断
线程中断是一种线程间协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。
- void Interrupt() :中断线程 例如当线程a运行时,线程b可以调用线程a的interrupt()方法设置a的中断标志为true并返回。设置标志仅仅是设置标志,线程a并没有被中断,他会继续往下执行,如果线程a调用了wait系列函数,join方法或者sleep等方法被阻塞挂起,这时候若是线程b调用a的interrupt()方法,线程a会在调用这些方法的时候抛出InterruptedException异常
- boolean isInterrupted() :检测当前线程是否被中断。如果是返回true,否则返回false
public boolean isInterrupted(){
//传递false,说明不清除中断标志
return isIntrrrupted( false);
}
- boolean interrupted() :检测当前线程是否被中断,如果是返回true。不同点该方法发现当前线程被中断,会清楚中断标志,并且该方法是static方法,可以通过Thread直接调用在interrupted()内部是获取当前线程的中断标志而不是调用interrupted()方法的实例对象的中断标志。
public static boolean Interrupted{
//清除中断标志
return currentThread.isInterrupted(true);
}
一段Interrupt优雅退出的例子
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()&&more work to do){
//do more work
}
}catch (InterruptedException e){
//thread was interrupted during sleep or wait
}finally {
//cleanup if required
}
}
根据中断标志判断线程是否终止的例子
public static void main(String[] args) throws InterruptedException{
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread()+"hello");
}
}
});
thread.start();
Thread.sleep(1000);
System.out.println("main thread interrupt thread");
thread.interrupt();
thread.join();
System.out.println("main is over");
}
当线程为了等待一些特定条件到来,一般会调用sleep函数,wait系列函数或者join函数来阻塞挂起当前线程,如果提前满足由阻塞到激活态的条件,这时候可以调用该线程的interrupt方法,强制sleep方法抛出InterruptedException异常而返回,线程恢复到激活状态
public class InterruptedOrIsInterrupted {
public static void main(String[] args) throws InterruptedException{
Thread threadOne=new Thread(new Runnable() {
@Override
public void run() {
for(; ;){
}
}
});
threadOne.start();
threadOne.interrupt();
System.out.println("isInterrupted:"+threadOne.isInterrupted());
// System.out.println("isInterrupted"+threadOne.interrupted());
System.out.println("isInterrupted:"+Thread.interrupted());
System.out.println("isInterrupted:"+threadOne.isInterrupted());
threadOne.join();
System.out.println("main thread is over");
}
}
/**
* @author xingchen
* @version V1.0
* @Package com.并发
* @date 2023/4/13 9:11
*/
public class InterruptedOrIsInterruptedTwo {
public static void main(String[] args) throws InterruptedException{
Thread threadOne=new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
}
System.out.println("threadTwo isInterrupted:" + Thread.currentThread().isInterrupted());
}
});
threadOne.start();
threadOne.interrupt();
threadOne.join();
System.out.println("main thread is over");
}
}
理解线程上下文切换
多线程里面,线程数一般是大于CPU个数的,而每个CPU同一时刻只能被一个线程所使用,为了让用户感觉到多个线程在同时使用的,CPU的分配采用了时间片轮转的策略.每一个线程分配一个时间片,线程在该分配的时间片内占用CPU执行任务,当线程片用完之后,就会处于就绪状态并让出CPU让其他线程占用使用.
上下文切换的时候需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场
切换时机:
-
- 当前线程的CPU时间片使用完处于就绪状态
- 当前线程被其他线程中断的时候
线程死锁
什么是线程死锁
死锁是指两个或者两个以上的线程在执行过程中,因抢夺资源而造成的互相等待的现象,在无外力的作用下,这些线程会一直等待下去而无法运行
产生死锁的四个必要条件
- 互斥条件
线程对已经获取到的资源进行排他性使用,资源同时只能由一个线程占用,如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占用的资源被释放
- 请求持有条件
指一个线程已经占有至少一个资源,但又提出新的资源请求,而新的资源已经被其他线程占用,所以当先线程被阻塞,但阻塞的同时并并不释放自己已经获得的资源
- 不可剥夺条件
指线程获取的资源在自己使用完之前别的线程不能抢占,只有在自己使用完之后释放了才能被其他线程使用
- 环路等待条件
指发生死锁的时候必然存在一个线程一个资源的环形链
/**
* @author xingchen
* @version V1.0
* @Package com.并发.Test
* @date 2023/4/13 9:35
*/
public class DeadLockTest2 {
private static Object resourceA=new Object();
private static Object resourceB=new Object();
public static void main(String[] args) throws InterruptedException{
Thread threadA=new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA){
System.out.println((Thread.currentThread() + "get ResourceA"));
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"wait get sourceB");
synchronized (resourceB){
System.out.println(Thread.currentThread()+"get ResourceB");
}
};
}
});
Thread threadB=new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceB){
System.out.println((Thread.currentThread() + "get ResourceB"));
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"wait get sourceA");
synchronized (resourceB){
System.out.println(Thread.currentThread()+"get ResourceA");
}
};
}
});
}
}
避免线程死锁
只需要破坏至少一个死锁的必要条件即可,但是目前其实能被破坏的只有请求并持有和环路等待条件是可以被破坏的
造成死锁其实也和申请资源的顺序有很大的关系,使用资源的有序性原则就可以破坏避免死锁
举例:对上述代码的线程B进行修改
Thread threadB=new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA){
System.out.println((Thread.currentThread() + "get ResourceB"));
try{
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"wait get sourceA");
synchronized (resourceB){
System.out.println(Thread.currentThread()+"get ResourceA");
}
};
}
});
守护线程与用户线程
- daemon线程(守护线程)
- user线程(用户线程)
main函数就是属于用户线程JVM启动的时候会调用main线程,但其实还调用了许多其他的守护线程
两者的区别
当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否由守护线程,也就是说守护线程不影响JVM的退出.也就是说只要有一个用户线程没有结束,正常情况下JVM就不会退出
/**
* @author xingchen
* @version V1.0
* @Package com.并发.Test
* @date 2023/4/13 16:44
*/
public class DaemonTest {
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
}
});
/**
* 只需要设置参数为true就开启了守护线程
*
*/
thread.setDaemon(true);
thread.start();
}
}
举例理解守护线程和用户线程的区别
public class DaemonOrUserTest {
/**
*thread线程里面是一个无限循环,运行之后主线程已经结束,但是jvm还没有退出
* 说明父线程结束后,子线程还可以继续存在,也就是子线程的生命周期并不受父线程的影响
* 也说明了在用户线程还存在的情况下JVM进程并不会终止
*
*
*
*
* 设置成用户线程之后一旦主线程停止之后,jvm发现已经不存在用户线程了,就会终止JVN进程
* 如果用户进程已经结束,但是守护线程还说运行,这个时候JVM不需要等待守护线程停止就直接结束JVM进程
*
*
*
*
* main线程运行结束之后,JVM会自动启动一个DestroyJavaVm 的线程,该线程等待所有的用户线程结束后
* 终止JVM进程
*
*
*/
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
for (; ;){
}
}
});
thread.setDaemon(true);
thread.start();
System.out.println("main thread is over");
}
}
总结
如果希望在主线程结束后JVM立马结束,那么在创建进程的时候可以将其设置为守护线程,否则的话就设置为用户线程
ThreadLocal
多线程访问退役个共享变量的时候容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时,为了保证线程安全,一般使用访问者在访问共享变量时候需要进行适当的同步
同步一般情况下使用加锁, 但是这种方式加重了使用者的负担.
那么可以使用创建一个变量后,每一个线程对其访问的时候访问的是自己线程的变量,本节的ThreadLocal就是这个作用.
ThreadLocal
是由JDK提供的,提供了线程本地变量,如果创建了一个ThreadLocal变量,那么访问这个变量的每一个线程都会有这个变量的本地副本
当多个线程操作这个变量的时候,实际操作的是自己本地内存的变量,从而避免了线程安全问题
使用实例
public class ThreadLocalTest {
static void print(String str){
System.out.println(str+":"+localVariable.get());
//localVariable.remove();
}
static ThreadLocal<String> localVariable=new ThreadLocal<>();
public static void main(String[] agrs){
Thread threadOne=new Thread(new Runnable() {
@Override
public void run() {
localVariable.set("threadOne local variable");
print("threadOne");
System.out.println("threadOne remove after"+":"+localVariable.get());
}
});
Thread threadTwo=new Thread(new Runnable() {
@Override
public void run() {
localVariable.set("threadTwo local variable");
print("threadOne");
System.out.println("threadTwo remove after"+":"+localVariable.get());
}
});
threadOne.start();
threadTwo.start();
}
}
实现原理
set方法
public void set(T value){
Thread t=Thread.currentThread();
ThreadLoalMap map=getMap(t);
if(map!=null){
map.set(this,value);
}else {
createMap(t,value);
}
}
ThreadLocalMap getMap(Thread t){
return t.threadLocals;
}
getMap(t)
- 方法的作用是获取线程自己的变量threadLocals,threadlocal变量被绑定到线程的成员变量上面
- 如果个体Map(T)返回值不为空,则把value设置到threadLocals
- 如果返回值为空则说明第一次调用的set方法,这时候创建当前线程的threadLocals变量
createMap(t,value)
创建当前线程的threadLocals变量
void createMap(Thread t,T firstValue){
t.threadLocals=new ThreadLocalMap(this,firstValue);
}
T get()方法
public void get(){
//获取当前线程
Thread t=Thread.currentThread();
//获取当前线程的threadLocxals变量
ThreadLocalMap map=getMap(t);
//如果ThreadLocals不为空,则返回对应的本地变量
if(map!=null){
ThreadLocalMap.Entry e=map.getEntry(this);
}
if(e!=null){
@SuppressWarnings("unchecked")
T result=(T) e.value;
return result;
}
//为空初始化当前线程的ThreadLocals成员变量
return setInitialValue;
}
private T setInitialValue(){
//初始化为null
T value =initialValue;
Thread t=Thread.currentThread();
ThreadLocalMap map=getMap(t);
//如果当前线程的本地变量不为空
if(map!=null){
map.set(this,value);
}else {
//如果当前线程的本地变量为空 则创建
createMap(t,value);
}
return value;
}
private T initialValue(){
return null;
}
void remove()
/**
* 如果当前线程的threadLocals变量不为空,则删除当前线程中指定ThreadLocal实例的本地变量
*/
public void remove(){
ThreadLocalMap map=getMap(Thread.currentThread());
if(m!=null){
m.remove(this);
}
}
不支持继承性
public class TestThreadLocal1 {
public static ThreadLocal<String> threadLocal=new ThreadLocal<>();
public static void main(String[] args){
threadLocal.set("hello world");
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread:"+threadLocal.get());
}
});
thread.start();
System.out.println("main:"+threadLocal.get());
}
}
同一个ThreadLocal变量在父线程中被设置后,在子线程是获取不到的
在子线程里面调用的get方法是当前线程,而调用的set方法是设置的main线程
两者是不同的线程
InheritableThreadLocal类
继承自ThreadLocal,提供了一个特性,让子线程可以访问在父线程中设置的本地变量
public class InheritableThreadLocal<T> extends ThreadLocal{
protected T childValue(T parentValue){
return parentValue;
}
ThreadLocalMap.getMap(Thread t){
return t.inheritableThreadLocals;
}
void create(Thread t,T firstValue){
t.inheritableLocals=new TestThreadLocalMap(this,firstValue);
}
}
总结:
InheritableThreadLocal类重写代码让本地变量保存到了具体线程的inheritableThreadLocals变量里面,那么线程在通过InheritableThreadLocal类实例的set或者get()方法设置变量时,就会创建当前线程的InheritableThreadLocals变量,当父线程创建子线程时,构造函数就会把父线程的InheritableThreadLocals变量里面的本地变量复制一份保存到子线程的InheritableThreadLocals变量里面.