up:: ThreadLocal部分内容概述
说明:
(1) 本篇博客内容:介绍ThreadLocal的第一种使用场景;
(2) ThreadLocal第一种使用场景,通常会用在多线程调用工具类的场合;每一个正在执行的线程,拥有一个只属于该线程的工具类对象,从而保证线程安全;
(3) 声明:本篇博客有演进关系,最好像看故事一样,从头看到尾;
一:ThreadLocal第一种使用场景:场景介绍;
(1) 先看一个案例,我们有一个类,这个类可以完成某种功能;
● 【比如我们定义了一个处理日期的工具类DateFormat类;】→【那么,假设现在突然有10000个日期需要处理,很自然我们需要实例化DateFormat类,然后调用该类中定义的方法去处理日期;】→【同时,因为有10000个任务需要处理,自然可以使用多线程来提高处理效率;】→【假设,我们创建了一个核心线程数和最大线程数都是10的线程池,来帮助处理这10000个任务】→【那么,可以认为,线程池在处理这10000个任务的时候,在同一时间会有10个线程同时运行;】→【那么,为了保证数据不乱,这10个同时在运行的线程,需要各自实例化自己的DateFormat对象】→【也就是,既然我们有10个线程在同时运行,那么我们就创建10个SimpleDateFormat对象,一个线程分一个;】;
● 利用这个案例,能够帮助理解ThreadLocal第一种使用场景,是什么意思;
(2) ThreadLocal第一种使用场景:每个Thread线程类中,有自己的实例副本;同时,各个线程之间的实例是不共享的;
● 结合上面的案例,这句话很容易理解哈;
● 其实,通过ThreadLocal的命名也能感受到,Local的意思就有点【本线程的实例,只能被当前线程使用】的意思;
这儿,我们使用SimpleDateFormat的进化之路,逐步迭代,来说明ThreadLocal在第一种场景中的用处;
二:ThreadLocal第一种使用场景:演示;
1.初始情况:有两个任务,我们创建两个线程去处理;
(1)情况介绍;
● 我们想把两个日期,装换为对应的格式的日期;那么,我们使用两个线程来处理这两个任务;
(2)创建ThreadLocalNormalUsage00类,来演示;
ThreadLocalNormalUsage00类:
说明:
(1) 类内容说明;
2.第二种情况:有30个任务,我们创建30个线程去处理;
(1)情况介绍;
还是遵循上面的思路,如果有30个任务的话,我们就创建30个线程去处理;
(2) 创建ThreadLocalNormalUsage01类,来演示;
说明:
(1) 类内容说明;
(2) 附加说明:for循环的循环变量,不允许在循环内被修改,所以使用finalI来应对;
3.第三种情况:有1000个任务,我们利用线程池去处理;
(1)情况介绍;
● 如果我们有1000个任务,显然我们不能循环1000次,创建1000个线程去处理这1000个任务;
● 这是很不好的,我们创建1000个线程,就需要承担创建、销毁1000个线程所带来的开销;
● 很容易就能想到,我们可以利用线程池来帮我们,把【线程的生命周期】和【线程所执行的任务】解耦;这样一来,面对1000任务,我们就不用傻傻的去创建1000个线程了;
(2) 创建ThreadLocalNormalUsage02类,来演示;
说明:
(1) 类内容说明;
(2) 即,这种方式有一个缺点:【我们要执行1000次任务】→【每次任务,都会创建一个SimpleDateFormat对象】→【那么,面对1000次任务,我们需要创建、销毁SimpleDateFormat对象1000次;这个开销是很大的;】
(1)情况介绍;
● 我们线程池来处理1000个任务;
● 同时,为了降低创建、销毁1000次SimpleDateFormat对象的开销,我们让1000任务,共享同一个SimpleDateFormat对象;
(2) 创建ThreadLocalNormalUsage03类,来演示;
说明:
(1) 类内容说明;
(2) 即,这种方式有一个问题:由于10个线程共享了同一个SimpleDateFormat对象,这就会导致线程安全问题;
(1)情况介绍;
● 既然,在第四种情况中,存在线程池的10个核心线程“同时”使用SimpleDateFormat对象,从而引发线程安全的问题;
● 那么,为了保证在同一时间只能够有一个线程在使用SimpleDateFormat对象,很自然可以想到使用synchronized关键字;
(2) 创建ThreadLocalNormalUsage04类,来演示;
说明:
(1) 类内容说明;
(2) 问题说明;
使用【synchronized关键字后,可以保证在同一时间,只能有一个线程使用SimpleDateFormat对象;诚然,这可以避免线程安全问题】→【我们使用线程池创建了10个核心线程,我们原本是希望,面对1000个任务,我们能同时有10个线程去处理,从而能加快处理1000个任务的速度】→【但是,如果我们使用synchronized关键字后,就会导致在同一时间,只能有一个线程使用SimpleDateFormat对象;而这,就完全发挥不出“10个线程,能同时处理任务,从而提高处理1000个任务的速度”的优点了】;
(3) 附加说明;
synchronized关键字可以使用在语句块上,如有需要可以参考【Java线程四:线程同步】;
(4) 为此,为了既能【发挥出线程池的,“10个线程能并发的处理任务”的有点】,又能【防止,不同的线程在同一时间,共享同一个SimpleDateFormat对象,从而产生线程安全问题】;ThreadLocal的第一个应用场景,就出来了;
6.第六种情况:有1000个任务,我们利用线程池去处理;同时,使用ThreadLocal;
(1)情况介绍;
● 现在我们的需求是:既想在使用线程池(比如这儿的线程池,设置为核心线程数和最大线程数都是10)的处理1000个任务时候,能够同时有10个线程并发的处理任务;又想,避免这10个线程同时使用同一个SimpleDateFormat对象,从而引发线程安全的问题;
● 那么,略加思考,可以发现这样是否可以:我们让这10个线程各自有一个SimpleDateFormat对象(即,因为线程池同时会有10个线程并发处理任务,那么我们就创建10个SimpleDateFormat对象,一个线程分一个);这种情况类似于,平均来看每个线程会处理100个任务,在处理这100个任务的时候,该线程持有的SimpleDateFormat对象是一直不变的;;;;;;这样一来,既能够让10个线程并发的处理,又能够避免不同的线程同时共享同一个SimpleDateFormat对象;
● 而这种思路,正是ThreadLocal的第一种使用场景;
(2) 创建ThreadLocalNormalUsage05类,来演示;
首先,创建ThreadSafeFormatter类,该类的主要作用就是作为SimpleDateFormat对象的来源;
说明:
(1) 前面我们创建SimpleDateFormat对象时候,是new出来的,而这是不好的;
(2) ThreadSafeFormatter类可以认为是一个工具类,这个类的主要作用是生产出线程安全的SimpleDateFormat对象;
然后,创建ThreadLocalNormalUsage05类,该类就可以利用上面的ThreadSafeFormatter类获取线程安全的SimpleDateFormat对象了;
说明:
(1) 类内容说明;
(2) 此时,由于不存在不同线程在同一时间共享同一个对象的情况,所以不会发生线程安全问题了;
(3)一点思考(不一定对哈):
【我们有1000个任务,需要执行】→【每个任务调用date方法时候,都会创建一个SimpleDateFormat对象,只是这个SimpleDateFormat对象是由ThreadLocal创建的】→【为了处理这1000个任务,我们创建了一个线程池;;;这个线程池的核心线程数和最大线程数都是10,也就说在一般情况下,线程池会有10个固定的线程并发的去处理这1000个任务】→【每个线程会拿一个任务去执行;;;在该任务执行到date()方法,需要创建SimpleDateFormat对象】→【那么此时,此时ThreadLocal就会检查下,当前携带这个任务的线程中,究竟已经有没有SimpleDateFormat对象;;;如果没有(基本就是线程第一次执行任务),那么就创建一个SimpleDateFormat对象,给这个任务用;同时,当前这个线程也会“留下”这个SimpleDateFormat对象;那么,当该线程执行下一个任务的时候,因为该线程已经有了SimpleDateFormat对象,所以ThreadLocal就不会再为下一个任务创建新的SimpleDateFormat对象了】;
所以,这样一看ThreadLocal就相当于是一个管理员,其会控制究竟需不需要【为当前这个线程携带的、将要执行的、任务】创建新的对象;
(3)附加,借助Lambda表达式,来简化ThreadLocal生产对象的过程;
创建ThreadSafeFormatterWithLambda类,该类的主要作用使用Lambda表达式,来简化ThreadLocal生产对象的过程;
说明:
(1) 类内容说明;
(2) 那么在ThreadLocalNormalUsage05中,还是采用同一种方法,去使用ThreadSafeFormatterWithLambda;
(3) 有关Lambda表达式的内容,如有需要可以参考【框架前置知识】专栏中的内容;