15.ThreadLocal的作用

1 什么是ThreadLocal

在多线程环境中,如果多个线程同时访问某个变量,如果希望每个线程对共享变量的相关操作仅对自己可见 ,该如何做呢?对应的现实场景就是各有各家的炉灶,相互之间生火做饭互不影响,这就是ThreadLocal要干的事。

ThreadLocal为每个线程提供一个独立的空间,用来存储共享变量的副本,每个副本只会对共享变量的副本进行操作,线程之间互不影响。

我们先看一个例子:

public class ThreadLocalExample {
    public final static ThreadLocal<String> string = ThreadLocal.withInitial(() -> "DEFAULT VALUE");

    public static void main(String[] args) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + ":INITIAL_VALUE->" + string.get());
        string.set("Main Thread Value");
        System.out.println(Thread.currentThread().getName() + ":BEFORE->" + string.get());
        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":T1->" + string.get());
            string.set("T1 Thread Value");
            System.out.println(Thread.currentThread().getName() + ":T1->" + string.get());
        }, "t1");
        Thread t2 = new Thread(() -> {
        //第一个关注点
            System.out.println(Thread.currentThread().getName() + ":T2->" + string.get());
            string.set("T2 Thread Value");
            System.out.println(Thread.currentThread().getName() + ":T2->" + string.get());
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        //第二个关注点
        System.out.println(Thread.currentThread().getName() + ":AFTER->" + string.get());
    }
}

这段代码虽然比较长,但是功能很简单, 首先定义了一个全局变量string,并初始化为"DEFAULT VALUE"。

在main()方法中,主要是main()线程和t1、t2两个子线程,分别获得和修改string变量的值。我们重点关注上面注释标注的两个位置,输出对应的分别是下面的①和②:

main:INITIAL_VALUE->DEFAULT VALUE
main:BEFORE->Main Thread Value
t1:T1->DEFAULT VALUE
t2:T2->DEFAULT VALUE//①第一个关注点输出结果,为什么是DEFAULT VALUE?
t1:T1->T1 Thread Value
t2:T2->T2 Thread Value
main:AFTER->Main Thread Value//②第二个关注点输出结果,为什么是Main Thread Value

我们发现,不同线程通过 string.set()方法设置的值,仅对当前线程可见,各个线程之间不会相互影响,这就是ThreadLocal的作用,它实现了不同线程之间的隔离,从而保证多线程对于共享变量操作的安全性。

2 ThreadLocal的应用

ThreadLocal最经典的应用是在日期方法里,SimpleDateFormat是非线程安全的,例如:

public class SimpleDateFormatExample {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static Date parse(String strDate) throws ParseException {
        return sdf.parse(strDate);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 9; i++) {
            executorService.execute(() -> {
                try {
                    System.out.println(parse("2022-08-18 16:35:20"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

这里构建了一个线程池,通过9次循环让这个线程池去执行一个解析字符串的任务,运行上面的程序,会抛出multiple points的异常,为什么会这样呢?就是因为SimpleDateFormat是非线性安全的,SimpleDateFormat在使用的时候,需要为每个线程单独创建示例,如果有多个线程同时访问,则必须通过外部的同步机制来保护。

SimpleDateFormat继承了DateFormat类,而在DateFormat中定义了两个全局成员变量Calendar和NumberFormat,分别用来进行日期和数字的转化,而DateFormat本身也不是线程安全的。

public class SimpleDateFormat extends DateFormat {
  protected Calendar calendar;
  protected NumberFormat numberFormat;
 }

在SimpleDateFormat类的subParse中 ,会用到numberFormat进行分析操作

if (obeyCount) {
      ...
    number = numberFormat.parse(text.substring(0, start+count), pos);
} else {
    number = numberFormat.parse(text, pos);
}

再看numberFormat.parse的实现类的部分代码:

//DecimalFormat类中
public Number parse(String text, ParsePosition pos) {
 if (!subparse(text, pos, positivePrefix, negativePrefix, digitList, false, status)) {
     return null;
   }
   ...
 if (digitList.fitsIntoLong(status[STATUS_POSITIVE], isParseIntegerOnly())) {
    gotDouble = false;
    longResult = digitList.getLong();
   } else {
      doubleResult = digitList.getDouble();
    }  

注意上面的digitList是一个全局变量。

再看上面的subparse()方法,该方法非常长,我们看几个关键位置:

private final boolean subparse(...){
    ...
    digits.decimalAt = digits.count = 0;
    ....
    backup = -1;
    ....
     if (!sawDecimal) {
         digits.decimalAt = digitCount; // Not digits.count!
    }
    digits.decimalAt += exponent;
}

导致报错的原因就是这里的subparse()方法,对全局变量digits的更新操作没有加锁,不满足原子性。假设ThreadA和B同时进入该方法,就会导致处理冲突。

为了解决该问题,我们可以通过ThreadLocal来保护该值。代码如下:

public class SimpleDateFormatSafetyExample {
    private static final String DATEFORMAT = "yyyy-MM-dd HH:mm:ss";
    private static ThreadLocal<DateFormat> dateFormatThreadLocal = new ThreadLocal<>();

    private static DateFormat getDateFormat() { //每次从threadlocal中获取SimpleDateFormat实例
        DateFormat df = dateFormatThreadLocal.get();
        if (df == null) {
            df = new SimpleDateFormat(DATEFORMAT);
            dateFormatThreadLocal.set(df);
        }
        return df;
    }

    public static Date parse(String strDate) throws ParseException {
        return getDateFormat().parse(strDate);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 9; i++) {
            executorService.execute(() -> {
                try {
                    System.out.println(parse("2021-06-16 16:35:20"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

这里,每个线程通过parse()方法做格式转换时,都可以获得一个完全独立的SimpleDateFormat实例,由于线程不存在对于同一个共享实例的竞争,也就不存在线程安全的问题了。

        

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

纵横千里,捭阖四方

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值