美文网首页
初识ThreadLocal理解和使用场景—小白学习篇

初识ThreadLocal理解和使用场景—小白学习篇

作者: 魂之挽歌w | 来源:发表于2019-09-28 22:32 被阅读0次

为什么使用ThreadLocal?
初识ThreadLocal时,第一句话就是说其是为了解决线程安全问题的,这句话可以说是正确的但又不正确,为什么说它不正确呢,它会误导我们初学者,我们对线程安全的第一印象往往就是其帮助解决了资源共享的问题,比如sychronized、volatile,多个线程在同一时刻只能有一个访问共享资源,这样来解决共享资源混乱的问题。而ThreadLocal简单理解就是我们在每个线程中都使用了单独的资源(变量等),互不影响。借用一位老哥的图展示一下(我比较懒~):

ThreadLocal理解图
只看图比较抽象,我们用实践来证明下:
我们在类中定义一个volitaile修饰的同步Integer变量 sync_num和threadLocal包含的线程本地Integer变量threadLocal_num,起三个线程,在线程中进行三次+1操作。
/**
 * ThreadLocal初步理解
 */
public class ThreadLocalTest {
    
    private volatile static Integer sync_num =0;//同步变量
    //threadLocal局部变量
    private static ThreadLocal<Integer> threadLocal_num = ThreadLocal.withInitial(()->new Integer(0));
    //lamda表达式方便了我们的使用,其代替的是initialValue方法,如下为原理方法
    private ThreadLocal<Integer> threadLocal_num2 = new ThreadLocal<Integer>(){
        //该方法提供初始化值,当我们第一次调用get方法时执行,若使用set方法设置了则不会执行该方法
        @Override
        protected Integer initialValue() {
            return new Integer(0);
        }
    };
    public static void main(String[] args) {
        for(int i=0;i<3;i++){
            new Thread(()->{
                Integer integer = threadLocal_num.get();
                for (int j=0 ;j<3;j++){
                    System.out.print(++sync_num+"*  ");
                    System.out.println(++integer+"%   ");
                }
            }).start();
        }
    }
}

执行结果如下:

1*  1%   
2*  2%   
3*  3%   
4*  1%   
5*  2%   
6*  3%   
7*  1%   
8*  2%   
9*  3% 

可以看到带*号的整数同步变量每次都加一所以加到了9,而线程本地整数变量每次都是自己加到3。
那回到标题上来,我们为啥要使用ThreadLocal呢?第一点,我们都知道同步虽然可以保证数据的准确性,但会导致线程的阻塞,特别是并发量较大的情况下,性能会降低,而线程本地变量就没有这个问题了,缺点是每个线程会增加内存的使用,就是用空间换时间。举个例子,我们都使用过SimpleDateFormat这个类进行时间转换,如:

 String dateFormat ="yyyy-MM-dd HH:mm:ss:SSS";
 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
 String dateString = simpleDateFormat.format(new Date());

首先,它是线程不安全的,dateFormat使用的内部数据结构可能会被并发的访问所破坏,如果在不同线程里使用同一个SimpleDateFormat变量极有可能会造成生成Date格式的混乱,第一个解决方案就是使用同步,但这样每个线程在其被加锁之后都需要等待,效率低,开销大;其次,如果我有的线程需要使用不同dateFormat来获取不同格式的dateString呢?现在想想是不是ThreadLocal可以完美解决这两个问题,代码如下:

 private ThreadLocal<SimpleDateFormat> format = ThreadLocal.withInitial(()->{
        String dateFormat ="yyyy-MM-dd HH:mm:ss:SSS";
        return new SimpleDateFormat(dateFormat);
    });
     new Thread(new Runnable() {
            @Override
            public void run() {
                //在线程自己内部设置具体的格式与其他线程不相关
                SimpleDateFormat simpleDateFormat = format.get();
                String dateFormat ="yyyy-MM-dd ";
                simpleDateFormat.applyPattern(dateFormat);
            }
        }).start();

同样,在多个线程中生成随机数也存在类似的问题。只是java.util.Random类是线程安全的。但是如果多个线程需要等待一个共享的随机数生成器,还是会很低效。Java7中提供了一个便利类,只需要如下调用即可:
int random = ThreadLocalRandom.current().nextInt(upperBound);
上面说本地ThreadLocal比sychornized同步快,口说无凭,我们实地测试一下上面所说的ThreadLocal比synchornized效率高的论点:
测试代码如下:

 private  volatile static long totalTime =0;
  public static void main(String[] args) {
        //创建一个核心数为100的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        for(int i=0;i<100;i++){
           executorService.execute( new Runnable() {
               @Override
               public void run() {
                   Integer integer = threadLocal_num.get();
                   long start_time = System.nanoTime();
                   for (int j=0 ;j<3;j++){
                       System.out.print(++sync_num+"*  ");
//                    System.out.println(++integer+"%   ");
                   }
                   System.out.println("线程"+Thread.currentThread().getName()+"使用时间:"+(System.nanoTime()-start_time));
                   totalTime= totalTime+System.nanoTime()-start_time;
                   System.out.println("总时间"+ totalTime);
               }
           });
        }
    }

我们创建一个线程池来模拟下高并发场景,将代码运行一遍打印出时间(这里totalTime是同步的,不然计时可就不准了哈哈)
同步变量sync_num执行结果如下:

1*  2*  3*  线程pool-1-thread-1使用时间:721942
总时间1386282
4*  5*  6*  线程pool-1-thread-2使用时间:757783
总时间2223427
7*  8*  9*  线程pool-1-thread-3使用时间:193285
总时间2491595
10*  11*  12*  线程pool-1-thread-4使用时间:207366
总时间2775124
13*  14*  15*  线程pool-1-thread-5使用时间:183045
总时间3034332
16*  17*  18*  线程pool-1-thread-6使用时间:225927
总时间3338341
19*  20*  21*  线程pool-1-thread-7使用时间:219527
总时间3653871
22*  23*  24*  线程pool-1-thread-8使用时间:1804215
总时间5829937
25*  26*  27*  线程pool-1-thread-9使用时间:200326
总时间6108346
.....
28*  29*  30*  线程pool-1-thread-99使用时间:215687
总时间613659414

线程本地变量执行结果:

.....
1%   2%   3%   线程pool-1-thread-64使用时间:13067913
总时间264541078
1%   2%   3%   线程pool-1-thread-65使用时间:14975171
总时间279849059
1%   2%   3%   线程pool-1-thread-48使用时间:815384
总时间280720125
1%   2%   3%   线程pool-1-thread-50使用时间:500495
总时间281532309
1%   2%   3%   线程pool-1-thread-51使用时间:555537
总时间282350254
1%   2%   3%   线程pool-1-thread-49使用时间:496015
总时间283067076
1%   2%   3%   线程pool-1-thread-45使用时间:719381
总时间288843890
1%   2%   3%   线程pool-1-thread-43使用时间:437133
总时间289501190
1%   2%   3%   线程pool-1-thread-47使用时间:485774
总时间290230172
1%   2%   3%   线程pool-1-thread-46使用时间:648979
总时间291238842

结果显而易见吧,差了一倍。

ThreadLoca使用注意点

  • 使用完ThreadLoca最好使用remove方法移除不然有可能内存溢出
  • 最好不要将ThreadLocal设置成静态的

相关文章

网友评论

      本文标题:初识ThreadLocal理解和使用场景—小白学习篇

      本文链接:https://www.haomeiwen.com/subject/qfmjyctx.html