ThreadLocal解决SimpleDateFormat非线程安全
您目前处于:编程  2014年10月07日

SimpleDateFormat

SimpleDateFormat是非线程安全,测试Demo:

package com.jd.api.admin.common;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Test {
    public static void main(String[] args) {

        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

        Date today = new Date();
        Date tomorrow = new Date(today.getTime() + 1000 * 60 * 60 * 24);

        System.out.println(today); // 今天是2014-10-17
        System.out.println(tomorrow); // 明天是2014-10-18

        Thread thread1 = new Thread(new Thread1(dateFormat, today));
        thread1.start();
        Thread thread2 = new Thread(new Thread2(dateFormat, tomorrow));
        thread2.start();
    }

}

class Thread1 implements Runnable {
    private SimpleDateFormat dateFormat;
    private Date date;

    public Thread1(SimpleDateFormat dateFormat, Date date) {
        this.dateFormat = dateFormat;
        this.date = date;
    }

    public void run() {
        for (; ; ) {// 一直循环到出问题为止吧。
            String strDate = dateFormat.format(date);
            // 如果不等于2014-10-17,证明出现线程安全问题了!!!!
            if (!"2014-10-17".equals(strDate)) {
                System.err.println("today=" + strDate);
                System.exit(0);
            }
        }
    }
}

class Thread2 implements Runnable {
    private SimpleDateFormat dateFormat;
    private Date date;

    public Thread2(SimpleDateFormat dateFormat, Date date) {
        this.dateFormat = dateFormat;
        this.date = date;
    }

    public void run() {
        for (; ; ) {
            String strDate = dateFormat.format(date);
            // 如果不等于2014-10-18,证明出现线程安全问题了!!!!
            if (!"2014-10-18".equals(strDate)) {
                System.err.println("tomorrow=" + strDate);
                System.exit(0);
            }
        }
    }
}

输出结果:

today=2014-10-18

由于创建一个 SimpleDateFormat 实例的开销比较昂贵,解析字符串时间时频繁创建生命周期短暂的实例导致性能低下。所以将 SimpleDateFormat 定义为静态类变量。但是,但是 SimpleDateFormat 是非线程安全的。如果用 synchronized 线程同步同样面临问题,同步导致性能下降。

使用 Threadlocal 解决了此问题,对于每个线程 SimpleDateFormat 不存在影响他们之间协作的状态,为每个线程创建一个 SimpleDateFormat 变量的拷贝或者叫做副本。

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 使用ThreadLocal以空间换时间解决SimpleDateFormat线程安全问题。
 */
public class DateFormatUtils {

    // 创建一个ThreadLocal类变量,这里创建时用了一个匿名类,覆盖了initialValue方法,主要作用是创建时初始化实例。
    private static final ThreadLocal threadLocal = new ThreadLocal() {
        protected synchronized Object initialValue() {
            return new SimpleDateFormat();
        }
    };

    // 获取线程的变量副本,如果不覆盖initialValue,第一次get返回null,故需要初始化一个SimpleDateFormat,并set到threadLocal中
    public static DateFormat getDateFormat(String pattern) {
        DateFormat dateFormat = (DateFormat) threadLocal.get();
        if (dateFormat == null) {
            dateFormat = new SimpleDateFormat(pattern);
            threadLocal.set(dateFormat);
        }
        return dateFormat;
    }

    public static Date parse(String pattern, String textDate) throws ParseException {
        return getDateFormat(pattern).parse(textDate);
    }

    public static String format(String pattern, Date date) {
        return getDateFormat(pattern).format(date);
    }
}

ThreadLocal

谈到 ThreadLocal 的使用,我们先来了解一下 ThreadLocal 是什么?ThreadLocal 是在 JDK1.2 的版本中开始提供的,他不是一个线程,而是一个线程的本地化对象。当某个变量在使用 ThreadLocal 进行维护时,ThreadLocal 为使用该变量的每个线程分配了一个独立的变量副本,每个线程可以自行操作自己对应的变量副本,而不会影响其他线程的变量副本。

研究ThreadLocal的最根本的实现原理,一共有三个问题。

  • 1. 每个线程的变量副本是存储在哪里的?

  • ThreadLocal 是怎么实现了多个线程之间每个线程一个变量副本的?它是如何实现共享变量的。

    ThreadLocal 提供了 set 和 get 访问器用来访问与当前线程相关联的线程局部变量。可以从 ThreadLocal 的 get 函数中看出来,其中 getmap 函数是用 t 作为参数,这里 t 就是当前执行的线程。

    从而得知,get 函数就是从当前线程的 threadlocalmap 中取出当前线程对应的变量的副本(注意,变量是保存在线程中的,而不是保存在 ThreadLocal 变量中)。当前线程中,有一个变量引用名字是 threadLocals,这个引用是在 ThreadLocal 类中 createmap 函数内初始化的。

    每个线程都有一个这样的 threadLocals 引用的 ThreadLocalMap,以 ThreadLocal 和 ThreadLocal 对象声明的变量类型作为参数。这样,我们所使用的 ThreadLocal 变量的实际数据,通过 get 函数取值的时候,就是通过取出 Thread 中 threadLocals 引用的 map,然后从这个 map 中根据当前 threadLocal 作为参数,取出数据。现在,变量的副本从哪里取出来的已经确认解决了。

    ThreadLocal 整体感觉就是,一个包装类。声明了这个类的对象之后,每个线程的数据其实还是在自己线程内部通过 threadLocals 引用到的自己的数据。只是通过 ThreadLocal 访问这个数据而已

  • 2. 变量副本是怎么从共享的那个变量赋值出来的?源码中的threadlocal的初始值是什么时机设置的?

  • 看下面 set 函数。当线程中的 threadlocalmap 是 null 的时候,会调用 createmap 创建一个 map。同时根据函数参数设置上初始值。也就是说,当前线程的 threadlocalmap 是在第一次调用 set 的时候创建 map 并且设置上相应的值的。

    在代码中声明的ThreadLocal对象,实际上只有一个。在每个线程中,都维护了一个 threadlocals 对象,在没有 ThreadLocal 变量的时候是 null 的。一旦在 ThreadLocal 的 createMap 函数中初始化之后,这个 threadlocals 就初始化了。以后每次那个 ThreadLocal 对象想要访问变量的时候,比如 set 函数和 get 函数,都是先通过 getMap(t) 函数,先将线程的 map 取出,然后再从这个在线程(Thread)中维护的map中取出数据(以当前threadlocal作为参数)。

    看 Thread 中 threadlocals 的定义:

    ThreadLocal.ThreadLocalMap threadLocals = null;

    从这个函数中可以看出来,Thread 中的 threadlocals 变量是在 ThreadLocal 对象中调用 createMap 函数来初始化的。

    下面是 ThreadLocal 截取的部分源码。

    public class ThreadLocal<T> {
       
        protected T initialValue() {
            return null;
        }
    
        public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null)
                    return (T)e.value;
            }
            return setInitialValue();
        }
    
        private T setInitialValue() {
            T value = initialValue();
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
            return value;
        }
    
        public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
    
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    
        void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    
        static class ThreadLocalMap {
    
            static class Entry extends WeakReference<ThreadLocal> {
                Object value;
    
                Entry(ThreadLocal k, Object v) {
                    super(k);
                    value = v;
                }
            }
    
            private static final int INITIAL_CAPACITY = 16;
            private Entry[] table;
    
            ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
                table = new Entry[INITIAL_CAPACITY];
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
                table[i] = new Entry(firstKey, firstValue);
                size = 1;
                setThreshold(INITIAL_CAPACITY);
            }
        }
    }
  • 3. 不同的线程局部变量,比如说声明了 n 个 (n>=2) 这样的线程局部变量 threadlocal,那么在 Thread 中的 threadlocals 中是怎么存储的呢?threadlocalmap 中是怎么操作的?

  • 在 ThreadLocal 的 set 函数中,可以看到,其中的 map.set(this, value); 把当前的 threadlocal 传入到 map 中作为键,也就是说,在不同的线程的 threadlocals 变量中,都会有一个以你所声明的那个线程局部变量 threadlocal 作为键的 key-value。假设说声明了 N 个这样线程的局部变量,那么在线程的 ThreadLocalMap 中就会有 n 个分别以你线程的局部变量作为 key 的键值对。


转载请并标注: “本文转载自 linkedkeeper.com ”