【Java进阶】深入理解ThreadLocal

初识ThreadLocal

  首先有一个问题,在多线程环境中,我们的多个线程需要同时访问某个共享变量,但我们只希望每个线程对共享变量的相关操作仅对当前线程可见,应该如何操作呢?

        其实ThreadLocal就是用来解决该问题的工具,它为我们的每个线程提供了一个独立的存储空间,这个空间用来存储共享变量的副本,此后每个线程只会对共享变量的副本进行操作,并且该操作对其他线程而言是不可见的。

所以我们可以称ThreadLocal为线程本地变量,相当于说我们创建了一个ThreadLocal变量之后,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝。多个线程操作这个变量的时候,实际上是操作自己本地内存的变量,从而起到线程隔离的作用,避免了线程安全的问题。

        通过一个具体的代码例子,我们来探讨一下:

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

    public static void main(String[] args) throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+":INITIAL_VALUE->"+STRING_THREAD_LOCAL.get());
        STRING_THREAD_LOCAL.set("Main Thread Value");
        System.out.println(Thread.currentThread().getName()+":BEFORE->"+STRING_THREAD_LOCAL.get());
        Thread t1 = new Thread(()->{
            String value = STRING_THREAD_LOCAL.get();
            if (value == null){
                STRING_THREAD_LOCAL.set("T1 Thread Value");
            }
            System.out.println(Thread.currentThread().getName()+":T1->"+STRING_THREAD_LOCAL.get());
        },"t1");
        Thread t2 = new Thread(()->{
            String value = STRING_THREAD_LOCAL.get();
            if (value == null){
                STRING_THREAD_LOCAL.set("T2 Thread Value");
            }
            System.out.println(Thread.currentThread().getName()+":T2->"+STRING_THREAD_LOCAL.get());
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(Thread.currentThread().getName()+":AFTER->"+STRING_THREAD_LOCAL.get());
    }
}

        这个案例的主要逻辑是:

  • 先定义一个全局的String类型的ThreadLocal对象STRING_THREAD_LOCAL,初始值是DEFAULT_VALUE。
  • 在main线程的第一行,通过STRING_THREAD_LOCAL.get()方法默认的value。
  • 在main线程中,通过STRING_THREAD_LOCAL.set("Main Thread Value")设置了一个值并保存到ThreadLocal中。
  • 接着定义t1和t2两个线程,分别在两个线程中使用了STRING_THREAD_LOCAL.set设置新的value,并且在每个线程中打印设置之后的结果。
  • 当t1和t2两个线程执行结束后,继续在main线程中通过STRING_THREAD_LOCAL.get()打印当前的ThreadLocal中的值。

        案例的结果如下:

main:INITIAL_VALUE->DEFAULT VALUE
main:BEFORE->Main Thread Value
t1:T1->DEFAULT VALUE
t2:T2->DEFAULT VALUE
main:AFTER->Main Thread Value

        我们从结果中惊奇地发现,在不同线程中通过STRING_THREAD_LOCAL.set()方法设置的值,仅对当前线程可见,各线程之间也不会相互影响。这就是ThreadLocal的作用,他能够实现不同线程之间的数据隔离,从而保证多线程对于共享变量操作的安全性。

ThreadLocal的内存结构图

 ThreadLocal的应用场景分析

        在Spring-JDBC的TransactionSynchronizationManager类中,通过ThreadLocal来保证数据库连接和事务资源的隔离性,从而避免了不同线程之间事务和连接混乱问题。

        在实际开发中,用户登录之后,拦截器会获得用户的基本信息,这些信息会在后续的方法中用到,如果设置了HttpServletRequest中,是很灵活,而且还依赖服务器对象,这时候我们也可以用到ThreadLocal中。在拦截器中,经过校验之后,我们把User的信息保存到ThreadLocal中,后续的代码可以直接通过get来获取User的信息。

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("2021-06-15 16:35:20"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

       我们用SimpleDataFormatExample类中构建了一个线程池,通过9次循环让这个线程池去执行一个解析字符串任务。运行上面的程序,可能会得到如下的异常信息。

java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:601)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at Example.SimpleDateFormatExample.parse(SimpleDateFormatExample.java:13)
	at Example.SimpleDateFormatExample.lambda$main$0(SimpleDateFormatExample.java:20)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:750)

        之所以会产生异常,是因为SimpleDateFormat是非线程安全的,SimpleDateFormat的官方文档说明如下。

Date formats are not synchronized.
It is recommended to create separate format instances for each thread.
If multiple threads access a format concurrently, it must be synchronized externally.
    DateFormats不是一个满足同步机制的类,因此建议为每个线程创建单独的实例,如果有多个线程同时访问,则必须要通过外部的同步机制来进行保护,保证线程安全。

        我们来看一下SimpleDateFormat的类关系图:

         SimpleDateFormat继承了DateFormat类,而在DateFormat中定义了两个全局的成员变量:

Calendar,NumberFormat,分别用来进行日期和数字的转化。

public abstract class DateFormat extends Format {

    protected Calendar calendar;
    protected NumberFormat numberFormat;
}

        在DateFormat的类注解中同样发现一段线程不安全的说明。

Date formats are not synchronized. It is recommended to create separate format instances
for each thread. If multiple threads access a format concurrently, it must be synchronized
externally.

        Calendar和NumberFormat这两个成员变量都是线程不安全的,也就是在多线程环境下对于两个成员变量的操作都会存在线程安全问题,我们可以通过具体的错误信息定位到问题的根源。

        在SimpleDateFormat类的SimpleDateFormat.subParse(SimpleDateFormat.java:1869)中。会用到numberFormat进行parse操作。

private int subParse(String text, int start, int patternCharIndex, int count,
                         boolean obeyCount, boolean[] ambiguousYear,
                         ParsePosition origPos,
                         boolean useFollowingMinusSignAsDelimiter, CalendarBuilder calb) {
    //省略
    //内部调用numberFormat的parse()方法,转化数字
    //这里的numberFormat就是DateFormat中的成员变量,默认实例是DecimalFormat
     number = numberFormat.parse(text, pos);
    if(number != null){
     value = number.intValue();
}
    return -1;
}

        继续看numberFormat.parse()方法实现。

 public Number parse(String text, ParsePosition pos) {
       //内部调用subparse()方法,将text的内容“set”到digitList上
        if (!subparse(text, pos, positivePrefix, negativePrefix, digitList, false, status)) {
            return null;
        }
        //将digitList转为目标格式
       
            if (digitList.fitsIntoLong(status[STATUS_POSITIVE], isParseIntegerOnly())) {
                gotDouble = false;
                longResult = digitList.getLong();
                if (longResult < 0) {  // got Long.MIN_VALUE
                    gotLongMinimum = true;
                }
            } else {
                doubleResult = digitList.getDouble();
            }

           
            return gotDouble ?
                (Number)new Double(doubleResult) : (Number)new Long(longResult);
        }
    }

这里的digitList是一个全局变量。

 private transient DigitList digitList = new DigitList();

继续看DecimalFormat中的subparse()方法。

 private final boolean subparse(String text, ParsePosition parsePosition,
                   String positivePrefix, String negativePrefix,
                   DigitList digits, boolean isExponent,
     
          //digitList在这个方法里面叫digits,先对digits进行清零处理
            digits.decimalAt = digits.count = 0;
            

            backup = -1;
           //还要对digits继续操作
            if (!sawDecimal) {
                digits.decimalAt = digitCount; // Not digits.count!
            }

          
            digits.decimalAt += exponent;

        return true;
    }

        从这里我们可以看出,导致错误的根本原因在subParse()方法中,对全局变量digits的更新操作没有加锁,不满足原子性。假设ThreadA,ThreadB同时进入subParse()方法中,同时对全局变量digitList进行更新操作,有可能ThreadA执行到CASE2 位置,而ThreadB正好执行到CASE 1位置,那么在DecimalFormat.parse()方法中获取的digitList就有问题,导致异常的发生。

参考书籍:

《Java并发编程深度解析与实战》

《Java编程思想》

《Java并发编程实战》

猜你喜欢

转载自blog.csdn.net/weixin_43918614/article/details/123805145