[Java Advanced] In-depth understanding of ThreadLocal

Getting to know ThreadLocal first

  First of all, there is a problem. In a multi-threaded environment, our multiple threads need to access a shared variable at the same time, but we only hope that the related operations of each thread on the shared variable are only visible to the current thread. How should we do it?

        In fact, ThreadLocal is a tool used to solve this problem. It provides an independent storage space for each of our threads. This space is used to store a copy of the shared variable. After that, each thread will only operate on the copy of the shared variable. And the operation is invisible to other threads .

So we can call ThreadLocal a thread local variable, which is equivalent to saying that after we create a ThreadLocal variable, each thread that accesses this variable will have a local copy of this variable . When multiple threads operate on this variable, they are actually operating on their own local memory variables , thus playing the role of thread isolation and avoiding thread safety issues.

        Through a specific code example, let's explore:

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());
    }
}

        The main logic of this case is:

  • First define a global String type ThreadLocal object STRING_THREAD_LOCAL, the initial value is DEFAULT_VALUE.
  • In the first line of the main thread, the default value is passed through the STRING_THREAD_LOCAL.get() method.
  • In the main thread, a value is set through STRING_THREAD_LOCAL.set("Main Thread Value") and saved in ThreadLocal.
  • Then define two threads t1 and t2, respectively use STRING_THREAD_LOCAL.set to set a new value in the two threads, and print the result after setting in each thread.
  • After the execution of the two threads t1 and t2 ends, continue to print the value in the current ThreadLocal through STRING_THREAD_LOCAL.get() in the main thread.

        The result of the case is as follows:

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

        From the results, we were surprised to find that the value set by the STRING_THREAD_LOCAL.set() method in different threads is only visible to the current thread, and the threads will not affect each other. This is the role of ThreadLocal, which can achieve data isolation between different threads, thereby ensuring the security of multi-threaded operations on shared variables.

Memory structure diagram of ThreadLocal

 Application Scenario Analysis of ThreadLocal

        In Spring-JDBC's TransactionSynchronizationManager class, ThreadLocal is used to ensure the isolation of database connections and transaction resources , thereby avoiding the confusion of transactions and connections between different threads.

        In actual development, after the user logs in, the interceptor will obtain the basic information of the user, which will be used in subsequent methods. If HttpServletRequest is set, it is not very flexible, and it also depends on the server object. At this time , we also Can be used in ThreadLocal. In the interceptor, after verification, we save the User's information in ThreadLocal, and the subsequent code can directly obtain the User's information through get.

ThreadLocal solves the thread safety problem of SimpleDateFormat

        Let's simulate it with a piece of code:

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();
                }
            });
        }
    }
}

       We use the SimpleDataFormatExample class to build a thread pool, and let the thread pool perform a string parsing task through 9 cycles. Run the above program, you may get the following exception information.

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)

        The reason for the exception is that SimpleDateFormat is not thread-safe. The official documentation of SimpleDateFormat explains as follows.

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不是一个满足同步机制的类,因此建议为每个线程创建单独的实例,如果有多个线程同时访问,则必须要通过外部的同步机制来进行保护,保证线程安全。

        Let's take a look at the class diagram of SimpleDateFormat:

         SimpleDateFormat inherits the DateFormat class, and defines two global member variables in DateFormat:

Calendar and NumberFormat are used to convert dates and numbers respectively.

public abstract class DateFormat extends Format {

    protected Calendar calendar;
    protected NumberFormat numberFormat;
}

        A thread-unsafe statement was also found in the class annotation of 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.

        The two member variables of Calendar and NumberFormat are not thread-safe, that is, there will be thread safety issues in the operation of the two member variables in a multi-threaded environment. We can locate the root of the problem through specific error messages.

        In SimpleDateFormat.subParse(SimpleDateFormat.java:1869) of the SimpleDateFormat class. The numberFormat will be used for the parse operation.

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;
}

        Continue to see the implementation of the numberFormat.parse() method.

 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);
        }
    }

Here digitList is a global variable.

 private transient DigitList digitList = new DigitList();

Continue to look at the subparse() method in DecimalFormat.

 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;
    }

        From here we can see that the root cause of the error is that in the subParse() method, the update operation of the global variable digits is not locked, which does not satisfy the atomicity. Assuming that ThreadA and ThreadB enter the subParse() method at the same time, and update the global variable digitList at the same time, it is possible that ThreadA executes to the CASE2 position, and ThreadB just executes to the CASE 1 position, then the digitList obtained in the DecimalFormat.parse() method There is a problem that causes an exception to occur.

Reference books:

"Java Concurrent Programming In-depth Analysis and Practice"

"Java Programming Thoughts"

"Java Concurrent Programming Practice"

Guess you like

Origin blog.csdn.net/weixin_43918614/article/details/123805145