Thread、ThreadPool、ThreadLocal详解
一.Thread(线程)
1.线程的几种创建方式
-
继承Thread类,重写run()方法,本身实例可作为一个线程
-
实现Runnable接口,重写run()方法,实例需要作为参数传入到Thread对象(作为Thread对象的构造参数)
-
实现Callable接口,通过实现Callable接口并重写call方法,并把Callable实例传给FutureTask对象,再把FutureTask对象传给Thread对象。它与Thread、Runnable最大的不同是Callable能返回一个异步处理的结果Future对象并能抛出异常,而其他两种不能
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; //新建一个类实现callable接口 class MyCallable implements Callable<String>{ private Integer ticket=5; @Override public String call() throws Exception{ synchronized (ticket){ while (ticket>0){ System.out.println(Thread.currentThread().getName()+"卖出了第"+(ticket--)+"张票"); Thread.sleep(1000); } return "票卖完了"; } } } public class Test{ int ticket = 5; public static void main(String[] args){ MyCallable mc = new MyCallable(); FutureTask<String> task1=new FutureTask<String>(mc); FutureTask<String> task2=new FutureTask<String>(mc); Thread t1 = new Thread(task1); Thread t2 = new Thread(task2); t1.start(); t2.start(); try { System.out.println("A线程返回结果:"+task1.get()); System.out.println("B线程返回结果:"+task2.get()); } catch (Exception e) { e.printStackTrace(); } } }
2.start()方法和run()方法的区别
只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。
3.给线程加锁的几种方式:synchornized和lock
1.synchronized
java的对象锁和类锁:java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
/**
模拟synchornized同步锁
*/
//创建一个任务类,里面有三个同步方法,其中两个模拟类锁,另一个模拟对象锁
public class Task {
//执行任务的方法
public static void task() {
System.out.println("name = " + Thread.currentThread().getName() + ", begin");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("name = " + Thread.currentThread().getName() + ", end");
}
//给静态方法加锁,锁当前类(类锁),即使该类不同的对象实例分别在每一个线程中调用该方法时也会阻塞(同步),是因为静态方法属于类方法,加了静态后,该类不同对象实例共享该方法,因此任一对象调用都会触发同步操作产生阻塞。而且静态方法通常用类名直接调用而不是通过对象调用,这也是为什么给静态方法加锁就相当于给类加锁,即使多个线程同时调用该方法也都是通过类名调用该方法。
public synchronized static void doTaskA() {
task();
}
//给静态方法加锁,锁当前类(类锁),即使该类不同的对象实例分别在每一个线程中调用该方法时也会阻塞(同步)
public synchronized static void doTaskB() {
task();
}
//给普通成员方法加锁,锁当前对象this(对象锁),该类同一个对象在多个线程中调用该方法时会阻塞(同步),但该类不同的对象实例分别在每个线程中调用该方法时则不会阻塞(异步)。
public synchronized void doTaskC() {
task();
}
//线程ThreadA用来执行doTaskA()方法
class ThreadA extends Thread{
@Override
public void run() {
Task.doTaskA();
}
}
//线程ThreadB用来执行doTaskB()方法
class ThreadB extends Thread{
@Override
public void run() {
Task.doTaskB();
}
}
//线程ThreadC用来执行doTaskC()方法
class ThreadC extends Thread{
private Task mTask;
public ThreadC(Task tk){
mTask = tk;
}
@Override
public void run() {
mTask.doTaskC();
}
}
}
//多个ThreadA线程执行任务类Task中的doTaskA()方法,无需传入任务类Task的实体对象,因为doTaskA()为静态方法属于类
public static void main(String[] args) {
ThreadA t1 = new Thread();
ThreadA t2 = new Thread();
t1.setName("A1");
t2.setName("A2");
t1.start();
t2.start();
}
运行效果如图:因为加了类锁,所以任一线程执行任务时,其他要执行同样任务的线程会进入阻塞状态,直到占有锁的线程执行完毕释放锁
//线程ThreadA、B、C分别执行各自的任务
public static void main(String[] args) {
Task t = new Task();
ThreadA ta = new ThreadA();
ThreadB tb = new ThreadB();
//线程ThreadC有一个参数为Task类型的构造器,且需要通过Task的实例来调用doTaskC()方法
ThreadC tc = new ThreadC(t);
ta.setName("A");
tb.setName("B");
tc.setName("C");
ta.start();
tb.start();
tc.start();
}
运行效果图如下:一个类的所有静态方法公用一把锁,因此A线程执行任务会使得B线程进入阻塞状态
2.lock
Lock锁与synchronized一样,都是可以用来控制同步访问的。
那就要谈到synchronized的缺点,主要是三个方面:
A 有时候用synchronized修饰的代码,访问它需要很长时间,下一个要访问同一代码块的线程就要等待阻塞很长的时间。如果我想要下一个线程在等待一段时间后,如果还没有得到锁的话,就放弃等待,这就可以使用Lock锁,来设置等待时间。
B synchronized 是互斥锁,同一时间只能有一个线程可以访问被它修饰的代码块。而Lock锁可以实现互斥锁,也可以实现共享锁(同一时间支持多条线程访问)。
C 有些情况下,获取与释放锁的情况比较复杂。比如:用于遍历并发访问的数据结构的一些算法需要使用“手动”或“链锁定”:您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。在这种场景中synchronized关键字就不那么容易实现了,使用Lock接口容易很多
-
Lock锁的缺点:
相比于synchronized,Lock需要手动的获取锁与释放锁。
-
Lock接口常用API
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
1.lock(): lock 方法可能是平常使用最多的一个方法,就是用来获取锁。如果锁被其他线程获取,则进行等待。
如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。
Lock lock = new ReentrantLock();
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
2.tryLock() :方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
Lock lock = new ReentrantLock();
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
3.tryLock(long time, TimeUnit unit) :方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
Lock lock = new ReentrantLock();
//100毫秒内拿不到锁就返回false
if(lock.tryLock(100,TimeUnit.MILLISECONDS)) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
4.lockInterruptibly() : 此方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就是说,当两个线程同时通过 lock.lockInterruptibly() 想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用 threadB.interrupt() 方法能够中断线程B的等待过程。
由于 lockInterruptibly() 的声明中抛出了异常,所以 lock.lockInterruptibly() 必须放在try块中或者在调用lockInterruptibly() 的方法外声明抛出 InterruptedException。一般形式如下:
public void method() {
lock.lockInterruptibly();
try {
//处理任务
}catch(InterruptedException e){
//处理异常
}finally {
lock.unlock();
}
}
测试代码如下:
public class LockTest {
private Lock lock = new ReentrantLock();
public void doBussiness() {
String name = Thread.currentThread().getName();
try {
System.out.println(name + " 开始获取锁");
lock.lockInterruptibly();
System.out.println(name + " 拿到锁");
System.out.println(name + " 开工干活");
for (int i=0; i<5; i++) {
Thread.sleep(1000);
System.out.println(name + " : " + i);
}
} catch (InterruptedException e) {
System.out.println(name + " 被中断");
System.out.println(name + " 做些别的事情");
} finally {
try {
lock.unlock();
System.out.println(name + " 释放锁");
} catch (Exception e) {
System.out.println(name + " : 没有得到锁的线程运行结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
LockTest lockTest = new LockTest();
Thread t0 = new Thread(
new Runnable() {
public void run() {
lockTest.doBussiness();
}
}
);
Thread t1 = new Thread(
new Runnable() {
public void run() {
lockTest.doBussiness();
}
}
);
// 启动线程t1
t0.start();
Thread.sleep(10);
// 启动线程t2
t1.start();
Thread.sleep(100);
// 线程t1没有得到锁,中断t1的等待
t1.interrupt();
}
}
运行效果:
5.newCondition():生成一个Condition实例,可以实现比 wait()和notify/notifyAll()方法更高级的 等待/通知机制
一个Lock对象中可以创建多个Condition实例,一个Condition可以注册多个线程,从而可以有选择性的进行线程通知
而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。
Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
public class ConditionTask{
private Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
public void test1(){
lock.lock();
try{
System.out.println("我是用condition1注册的方法");
condition1.await();
System.out.println("1我执行完了");
condition2.signal();
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
public void test2(){
lock.lock();
try{
System.out.println("我是用condition2注册的方法");
//唤醒注册在此Condition上所有等待的线程,signal()只能唤醒一个等待最久的线程
condition1.signalAll();
//等待被唤醒
condition2.await();
System.out.println("2我执行完了");
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
public class ConditionDemo{
public static void main(String[] args){
ConditionTask t = new ConditionTask();
new Thread(new Runnable(){
@Override
public void run(){
t.test1();
}
}).start();
new Thread(new Runnable(){
@Override
public void run(){
t.test1();
}
}).start();
new Thread(new Runnable(){
@Override
public void run(){
t.test2();
}
}).start();
}
}
Condition condition1 = lock.newCondition(); //创建Condition
condition1.await(); //使当前线程释放锁,构造成节点,加入等待队列尾部,进入等待状态。
condition1.signal(); //唤醒注册在此Condition上等待时间最长的线程。即唤醒在等待队列首节点的线程
condition1.signalAll(); //唤醒注册在此Condition上所有等待的线程。
二.ThreadPool线程池
1.为什么要使用线程池
- 反复创建和销毁线程太消耗系统资源,开销大
- 过多的线程会太占用内存
2.线程池的优势
- 线程和任务分离,提升线程重用性;
- 控制线程并发数量,降低服务器压力,统一管理所有线程;
- 提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间;
3.线程池的几个核心参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue){
}
//创建一个自定义线程池
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();
ThreadPoolExecutor threadpool = new ThreadPoolExecutor(5,10,10,TimeUnit.SECODES,workQueue);
corePoolSize(必需) --核心线程数
maximumPoolSize(必需) --最大线程数
keepAliveTime(必需) -- 如果线程池当前的线程多余corePoolSize,那么多余的线程空闲时间超过keepAliveTime就会被终
unit(必须) --指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS (秒)、TimeUnit.MINUTES(分)。
workQueue(必须) -- 常用的阻塞队列有三种:
-- SynchronousQueue 一个不存储元素的队列,核心线程数满后每来一个任务都创建一个新的线程直到达到最大线程数
-- LinkedBlockingQueue 无界队列,新进来的任务会存放到该队列中,队列容量Integer.MAX_VALUE
-- ArrayBlockingQueue 有界队列,实例化时需要传入队列容量
4.线程池添加线程的规则
5.java内置线程池
- 在JDK1.5之后,JDK内置了线程池,我们可以直接使用,java.util.concurrent.Executors:线程池的工厂类,用来生成线程池,Executor家族关系如下。
-
线程池的种类与创建
1.newFixedRhreadPool创建固定数量线程池
//创建一个固定容量为5的线程池 ExecutorService threadPool = Executors.newFixedThreadPool(5);
2.newCachedThreadPool创建一个可缓存的线程池
特点:
- 可缓存线程池
- 具有自动回收多余线程的功能
- 队列没有容量,因为最大线程数为Integer.MAX_VALUE,可创建很多线程
ExecutorService threadPool = Executors.newCachedThreadPool();
3.newSingleThreadExecutor创建只有一个线程的线程池,相当于定长线程池李的参数为1
ExecutorService threadPool = Executors.newSingleThreadExecutor();
4.newScheduleThreadPool支持定时及周期性执行任务的线程池
ExecutorService threadPool = Executors.newScheduleThreadPool();
-
四种线程池的创建方式底层依然是通过ThreadPoolExecutor来创建。
-
ThreadPoolExecutor 的4种拒绝策略
ThreadPoolExecutor的拒绝策略是在工作队列满并且线程个数达到max时,再次添加时触发。
通过设置RejectedExecutionHandler,RejectedExecutionHandler有四个已有的实现- AbortPolicy:默认的策略,拒绝任务,并抛出异常
- CallerRunsPolicy:在调用线程执行任务
- DiscardPolicy:抛弃当前任务
- DiscardOldestPolicy:抛弃最老的任务
三.ThreadLocal
1.为什么要使用ThreadLocal
1.场景一
当多个线程需要都需要使用某个对象例如SimpleDateFormat,而不希望他们互相影响,可以为每个任务开启一个线程,每个线程都新建一个SimpleDateFormat对象。但是当任务变多,需要使用大量线程来执行时,就需要使用到线程池,而用线程池执行大量任务,每个任务又会新建一个SimpleDateFormat对象,造成内存的浪费。而如果将SimpleDateFormat设为静态变量作为全局共享时,又会引发线程安全问题,于是可以给任务加上synchornized类锁,这样做保证了线程安全,但是同时又会导致线程的阻塞等待,效率太低,而ThreadLocal则能很好的解决上述问题。ThreadLocal没有像用synchonized那样带来的性能问题,因为是可以并发执行的,每个线程都有属于自己的一份副本,互不干扰,也是线程安全的。
/**
* 描述: 利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
*/
public class ThreadLocalNormalUsage {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
// SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//从ThreadLocal中获取SimpleDateFormat对象的副本,无需每次都new一个SimpleDateFormat对象
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
//不同线程输出的是同一个dateFormatThreadLocal地址
System.out.println(Thread.currentThread().getName() + ThreadSafeFormatter.dateFormatThreadLocal);
SimpleDateFormat simpleDateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
//不同线程输出的是同一个simpleDateFormat地址
System.out.println(Thread.currentThread().getName() + simpleDateFormat.toString());
System.out.println(
Thread.currentThread().getName() + System.identityHashCode(simpleDateFormat));
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
//该方法是初始化要用到的实例对象,此时是SimpleDateFormat对象
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
//初始化SimpleDateFormat对象的另一种方式,使用Lambda表达式(更方便)
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
2.场景二:
当一个变量需要被一个线程内的多个业务共享,一个比较繁琐解决方法是将该变量作为参数一层一层的传递,但这样会显得代码冗余不易维护
在此基础上演进,可以使用UserMap,第一个业务Service-1生成了一个user对象,然后将其放到一个User Map里,后面方法想要用到该对象可直接到User Map里面取。
可是当多个线程要用到user对象时,就要保证线程的安全,可以使用synchonized或ConcurrentHashMap(ConcurrentHashMap是线程安全的),但无论用什么,都会对性能造成一定影响。
这时一个最好的办法就是使用ThreadLocal,这样无需synchornized,可以在影响性能的情况下,也无需层层传递参数,就可以达到保存当前线程用户信息的目的,并且可以在需要的时候随时拿出来。这些信息在同一个线程内是相同的,但不同的线程使用的业务内容是不同的(例如多个用户并发访问时,每个线程保存的用户信息是不同的,这也是为什么需要手动set)。在线程的生命周期内,都通过这个静态的ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象作为参数传递的麻烦。与场景一不同的是,场景二不需要重写initialValue()方法,但必须手动调用set()方法
/**
* 描述: 演示ThreadLocal用法2:避免传递参数的麻烦
*/
public class ThreadLocalNormalUsage {
public static void main(String[] args) {
new Service1().process("");
}
}
class Service1 {
public void process(String name) {
//在业务1里设置user对象,set到ThreadLocal里,然后当前线程的其他业务就可以通过ThreadLocal的get()方法获取到user对象
User user = new User("小胡");
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
ThreadSafeFormatter.dateFormatThreadLocal.get();
System.out.println("Service2拿到用户名:" + user.name);
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名:" + user.name);
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
运行效果:
3.场景一和场景二的区别
-
场景一:initialValue()
在ThreadLocal第一次get的时候把对象初始化出来,对象的初始化时机由我们自己控制,initialValue()必须要重写,否则返回的对象是一个null。
-
场景二:set()
需要保存到ThreadLocal的对象的时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set直接放到我们的ThreadLocal中去,以便后续使用。
2.ThreadLocal的原理,源码分析
- 一个线程可能有多个ThreadLocal,所以需要一个ThreadLocalMap来存储这些ThreadLocal对象,取的时候调用get()方法,get方法原理如下
/**
ThreadLocal中get()方法源码解析
*/
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//调用getMap()传入当前线程t,获取当前线程的ThreadMap
ThreadLocalMap map = getMap(t);
//判断map是否为空
if (map != null) {
//将当前ThreadLocal对象的应用作为参数传入,取出map中属于本ThreadLocal的value(可以是场景一的SimpleDateFormat对象,也可以是场景二中的User对象)
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//map为空时调用setInitialValue()方法初始化
return setInitialValue();
}
- ThreadLocal中的set()方法
/**
ThreadLocal中set()方法源码解析
*/
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//调用getMap()传入当前线程t,获取当前线程的ThreadMap
ThreadLocalMap map = getMap(t);
//判断map是否为空
if (map != null)
//将当前ThreadLocal对象的引用this作为参数传入到set()方法里作为key,value作为值
map.set(this, value);
//map为空则说明是第一次设置,就调用createMap()方法,新建ThreadLocalMap对象
else
createMap(t, value);
}
ThreadLocalMap是Thread类的一个成员变量,而不是ThreadLocal中的,要注意区分。
-
ThreadLocal中的initialValue()方法没有默认实现,要使用的话需要自己使用匿名内部类的方式实现
/* ThreadLocal中的initialValue()方法如果不重写则会返回一个null */ protected T initialValue() { return null; }
/*
initialValue()方法的实现
*/
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
//该方法是初始化要用到的实例对象,此时是SimpleDateFormat对象
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
-
ThreadLocal中的remove()方法
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) //删除当前ThreadLocal对象引用所对应的value(该对象的引用就是ThreadLocalMap中的key,可以通过key删除value) m.remove(this); }
3.ThreadLocalMap
-
ThreadLocalMap是ThreadLocal类的一个静态内部类
这个静态内部类是Thread类的一个成员变量
4.使用ThredLocal需要注意的点
1.内存泄露:某个对象不再有用,但是占用的内存却不能被回收,一直堆积可能导致内存溢出OOM,下图为ThreadLocalMap源码
弱引用的特点:如果这个对象只被弱引用关联(没有被任何强引用关联),那么这个对象就可以被回收
不过java也帮我们想到了这点,ThreadLocalMap中resize()会把key值为null的value也设置成null
可是这样是我们在调用ThreadLocal中set(),remove(),resize()等方法时会触发的,实际中当一个ThreadLocal不再被使用,我们可能也不会去调用这些方法,此时如果这个线程一直不停止,那就没人帮我们把为null的key的value设置成null。
如何避免内存泄露(阿里规约)
调用remove()方法就会删除对应的Entry对象,可以避免内存泄露,所以使用完ThreadLocal后,应该调用remove()方法
拿上述场景二的例子来说,当最后一个业务用完ThreadLocal,就需要使用remove()方法进行删除
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名:" + user.name);
//最后一个业务,因此需要remove()防止内存泄露
UserContextHolder.holder.remove();
}
}
以上就是本人关于Thread、ThreadPool、ThreadLocal的全部见解,如果考虑不周的地方,欢迎指正。