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类:

 
     package threadLocal;
 
     import java.text.SimpleDateFormat;
     import java.util.Date;
 
     public class ThreadLocalNormalUsage00 {
 
         public String date(int seconds) {
             //seconds单位是秒,而Date()的参数要求是毫秒;所这儿乘1000了;
             //【这个返回的日期】=【1970.1.1 00:00:00】+【1000*seconds】;(PS:如果在东八区的话,就是【1970.1.1 08:00:00】)
             Date date = new Date(1000 * seconds);
             //定义时间显示格式
             SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
             return dateFormat.format(date);
         }
 
 
         //只有两个任务时,目前看来没什么问题;一切很OK;
         public static void main(String[] args) {
             //第一个线程
             new Thread(new Runnable() {
                 @Override
                 public void run() {
                     String date = new ThreadLocalNormalUsage00().date(10);
                     System.out.println(date);
                 }
             }).start();
 
             //第二个线程
             new Thread(new Runnable() {
                 @Override
                 public void run() {
                     String date = new ThreadLocalNormalUsage00().date(104707);
                     System.out.println(date);
                 }
             }).start();
         }
     }

说明:

(1) 类内容说明;

2.第二种情况:有30个任务,我们创建30个线程去处理;

(1)情况介绍;

还是遵循上面的思路,如果有30个任务的话,我们就创建30个线程去处理;

(2) 创建ThreadLocalNormalUsage01类,来演示;

 
     package threadLocal;
 
     import java.text.SimpleDateFormat;
     import java.util.Date;
 
     public class ThreadLocalNormalUsage01 {
 
         public String date(int seconds) {
             //seconds单位是秒,而Date()的参数要求是毫秒;所这儿乘1000了;
             //【这个返回的日期】=【1970.1.1 00:00:00】+【1000*seconds】;(PS:如果在东八区的话,就是【1970.1.1 08:00:00】)
             Date date = new Date(1000 * seconds);
             //定义时间显示格式
             SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd  hh:mm:ss");
             return dateFormat.format(date);
         }
 
 
 
         //这儿有个缺点,有30个任务,我们就得创建30个线程;那么如果有一万个任务,我么还要创建一万个线程吗?这样
         // 就要承担【创建、销毁一万个线程所带来的开销】;
         //由此,就联想到可以利用【线程池】这个工具,来把【线程的生命周期】和【线程所执行的内容】解耦;
         public static void main(String[] args) throws InterruptedException {
             for (int i = 0; i <30 ; i++) {
                 int finalI = i;
                 new Thread(new Runnable() {
                     @Override
                     public void run() {
                         String date = new ThreadLocalNormalUsage01().date(finalI);
                         System.out.println(date);
                     }
                 }).start();
                 Thread.sleep(100);
             }
         }
     }

说明:

(1) 类内容说明;

(2) 附加说明:for循环的循环变量,不允许在循环内被修改,所以使用finalI来应对;

3.第三种情况:有1000个任务,我们利用线程池去处理;

(1)情况介绍;

● 如果我们有1000个任务,显然我们不能循环1000次,创建1000个线程去处理这1000个任务;

● 这是很不好的,我们创建1000个线程,就需要承担创建、销毁1000个线程所带来的开销;

● 很容易就能想到,我们可以利用线程池来帮我们,把【线程的生命周期】和【线程所执行的任务】解耦;这样一来,面对1000任务,我们就不用傻傻的去创建1000个线程了;

(2) 创建ThreadLocalNormalUsage02类,来演示;

 
     package threadLocal;
 
     import java.text.SimpleDateFormat;
     import java.util.Date;
     import java.util.concurrent.ExecutorService;
     import java.util.concurrent.Executors;
 
     public class ThreadLocalNormalUsage02 {
 
         public String date(int seconds) {
             //seconds单位是秒,而Date()的参数要求是毫秒;所这儿乘1000了;
             //【这个返回的日期】=【1970.1.1 00:00:00】+【1000*seconds】;(PS:如果在东八区的话,就是【1970.1.1 08:00:00】)
             Date date = new Date(1000 * seconds);
             //定义时间显示格式
             SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd  hh:mm:ss");
             return dateFormat.format(date);
         }
 
 
         //创建一个线程池
         public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
 
 
         //(1)但是,这儿有个缺点:这1000任务,我们每个任务都要调用一次date()方法;而每次调用date()方
         // 法,都会创建一个SimpleDateFormat对象;所以,为了完成1000次任务,我们需要创建1000
         // 个SimpleDateFormat对象;即SimpleDateFormat我们要创建、销毁1000次;
 
 //(2)由此,就想到:既然每个任务都需要用到SimpleDateFormat对象,那么为什么不只创建一次SimpleDateFormat对
         // 象,然后这1000次任务,都使用同一个SimpleDateFormat对象,不好吗?
         public static void main(String[] args) throws InterruptedException {
             for (int i = 0; i <1000 ; i++) {
                 int finalI = i;
                 //然后,把任务放到线程池中;
                 threadPool.submit(new Thread(new Runnable() {
                     @Override
                     public void run() {
                         String date = new  ThreadLocalNormalUsage02().date(finalI);
                         System.out.println(date);
                     }
                 }));
             }
             threadPool.shutdown();//最后,记得把线程池给关闭
         }
     }

说明:

(1) 类内容说明;

(2) 即,这种方式有一个缺点:【我们要执行1000次任务】→【每次任务,都会创建一个SimpleDateFormat对象】→【那么,面对1000次任务,我们需要创建、销毁SimpleDateFormat对象1000次;这个开销是很大的;】

4.第四种情况:有1000个任务,我们利用线程池去处理;同时,让这1000个任务,共享同一个SimpleDateFormat对象;

(1)情况介绍;

● 我们线程池来处理1000个任务;

● 同时,为了降低创建、销毁1000次SimpleDateFormat对象的开销,我们让1000任务,共享同一个SimpleDateFormat对象;

(2) 创建ThreadLocalNormalUsage03类,来演示;

 
     package threadLocal;
 
     import java.text.SimpleDateFormat;
     import java.util.Date;
     import java.util.concurrent.ExecutorService;
     import java.util.concurrent.Executors;
 
     public class ThreadLocalNormalUsage03 {
 
         public String date(int seconds) {
             //seconds单位是秒,而Date()的参数要求是毫秒;所这儿乘1000了;
             //【这个返回的日期】=【1970.1.1 00:00:00】+【1000*seconds】;(PS:如果在东八区的话,就是【1970.1.1 08:00:00】)
             Date date = new Date(1000 * seconds);
             return dateFormat.format(date);
         }
 
 
         //定义时间显示格式
         static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
         //创建一个线程池
         public static ExecutorService threadPool =  Executors.newFixedThreadPool(10);
 
 
 
         //因为所有的线程都共用了同一个SimpleDateFormat对象(去处理数字,得到date),其就会发生线程安全问题;
         //由此,就想到是不是可以通过加锁的方式,来应对【不同线程使用同一个资
         // 源(这儿被公用的资源就是SimpleDateFormat对象)时的线程安全问题?】
         public static void main(String[] args) throws InterruptedException {
             for (int i = 0; i <1000 ; i++) {
                 int finalI = i;
                 //然后,把任务放到线程池中;
                 threadPool.submit(new Thread(new Runnable() {
                     @Override
                     public void run() {
                         String date = new ThreadLocalNormalUsage03().date(finalI);
                         System.out.println(date);
                     }
                 }));
             }
             threadPool.shutdown();//最后,记得把线程池给关闭
         }
     }
 

说明:

(1) 类内容说明;

(2) 即,这种方式有一个问题:由于10个线程共享了同一个SimpleDateFormat对象,这就会导致线程安全问题;

5.第五种情况:有1000个任务,我们利用线程池去处理;同时,让这1000个任务,共享同一个SimpleDateFormat对象;同时,给调用SimpleDateFormat方法的语句块加锁,使用synchronized关键字;

(1)情况介绍;

● 既然,在第四种情况中,存在线程池的10个核心线程“同时”使用SimpleDateFormat对象,从而引发线程安全的问题;

● 那么,为了保证在同一时间只能够有一个线程在使用SimpleDateFormat对象,很自然可以想到使用synchronized关键字;

(2) 创建ThreadLocalNormalUsage04类,来演示;

 
     package threadLocal;
 
     import java.text.SimpleDateFormat;
     import java.util.Date;
     import java.util.concurrent.ExecutorService;
     import java.util.concurrent.Executors;
 
     public class ThreadLocalNormalUsage04 {
 
         public String date(int seconds) {
             //seconds单位是秒,而Date()的参数要求是毫秒;所这儿乘1000了;
             //【这个返回的日期】=【1970.1.1 00:00:00】+【1000*seconds】;(PS:如果在东八区的话,就是【1970.1.1 08:00:00】)
             Date date = new Date(1000 * seconds);
             String s = null;
             synchronized (ThreadLocalNormalUsage04.class) {
                 s = dateFormat.format(date);
             }
             return s;
         }
 
 
         //定义时间显示格式
         static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
         //创建一个线程池
         public static ExecutorService threadPool =  Executors.newFixedThreadPool(10);
 
 
         //这儿我们使用synchronized解决了线程安全问题;但,synchronized也会导致性能的降低;
         public static void main(String[] args) throws InterruptedException {
             for (int i = 0; i <1000 ; i++) {
                 int finalI = i;
                 //然后,把任务放到线程池中;
                 threadPool.submit(new Thread(new Runnable() {
                     @Override
                     public void run() {
                         String date = new ThreadLocalNormalUsage04().date(finalI);
                         System.out.println(date);
                     }
                 }));
             }
             threadPool.shutdown();//最后,记得把线程池给关闭
         }
     }

说明:

(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对象的来源;

 
     package threadLocal;
 
     import java.text.SimpleDateFormat;
 
     public class ThreadSafeFormatter {
         public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
                 new ThreadLocal<SimpleDateFormat>() {
             @Override
             protected SimpleDateFormat initialValue() {
                 return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
             }
         };
 
     }

说明:

(1) 前面我们创建SimpleDateFormat对象时候,是new出来的,而这是不好的;

(2) ThreadSafeFormatter类可以认为是一个工具类,这个类的主要作用是生产出线程安全的SimpleDateFormat对象;

然后,创建ThreadLocalNormalUsage05类,该类就可以利用上面的ThreadSafeFormatter类获取线程安全的SimpleDateFormat对象了;

 
     package threadLocal;
 
     import java.text.SimpleDateFormat;
     import java.util.Date;
     import java.util.concurrent.ExecutorService;
     import java.util.concurrent.Executors;
 
     public class ThreadLocalNormalUsage05 {
 
         public String date(int seconds) {
             //seconds单位是秒,而Date()的参数要求是毫秒;所这儿乘1000了;
             //【这个返回的日期】=【1970.1.1 00:00:00】+【1000*seconds】;(PS:如果在东八区的话,就是【1970.1.1 08:00:00】)
             Date date = new Date(1000 * seconds);
             //定义时间显示格式
     // SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd  hh:mm:ss");
             SimpleDateFormat dateFormat =  ThreadSafeFormatter.dateFormatThreadLocal.get();
     // SimpleDateFormat dateFormat =  ThreadSafeFormatterWithLambda.dateFormatThreadLocal.get();
             return dateFormat.format(date);
         }
 
 
         //创建一个线程池
         public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
 
 
 
         public static void main(String[] args) throws InterruptedException {
             for (int i = 0; i <1000 ; i++) {
                 int finalI = i;
                 //然后,把任务放到线程池中;
                 threadPool.submit(new Thread(new Runnable() {
                     @Override
                     public void run() {
                         String date = new ThreadLocalNormalUsage05().date(finalI);
                         System.out.println(date);
                     }
                 }));
             }
             threadPool.shutdown();//最后,记得把线程池给关闭
         }
     }

说明:

(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生产对象的过程;

 
     package threadLocal;
 
     import java.text.SimpleDateFormat;
 
     public class ThreadSafeFormatterWithLambda {
         public static ThreadLocal<SimpleDateFormat dateFormatThreadLocal =
                 ThreadLocal.withInitial(() - new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
 
 
     }

说明:

(1) 类内容说明;

(2) 那么在ThreadLocalNormalUsage05中,还是采用同一种方法,去使用ThreadSafeFormatterWithLambda;

(3) 有关Lambda表达式的内容,如有需要可以参考【框架前置知识】专栏中的内容;