mac_girl

ThreadLocal线程局部变量的使用

ThreadLocal: 线程局部变量

一)、ThreadLocal的引入

用途:解决多线程间并发访问的方案,不是解决数据共享的方案。

特点:每个线程提供变量的独立副本,所有的线程使用同一个ThreadLocal,

​ 通过ThreadLocal来创建自己的独立副本。

好处: 解决多个线程使用同一个引用可能出现的问题。

例如:使用SimpleDateFormat进行日期装换

​ 声明 public static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")将日期转换对象声明为所有线程共有。

​ SimpleDateFormat内部维护一个Calendar对象,该对象用于存储 sdf.parse(dateStr)或sdf.format(date)的信息,通过clear()来清除设置信息,通过getTime()来获取日期信息,因为sdf为所有的线程共有,因此所有的线

​ 均指向同一个Calendar引用,当线程A执行完Calendar.clear(),后线程的执权到达线程B中,线程B也执行了Calendar.claer(),之后执行Calendar.getTime()得到装换的日期类,此时线程的执行权到达线程A中

​ 此时线程A打印的是线程B的时间,如果线程B执行完Calendar.claer()后执行权被线程A抢夺,就会出现java.lang.NumberFormatException异常。

解决:每一个线程使用一个自己的SimpleDateFomat副本,每一个线程有自己的

​ Calendar

二)、使用ThreadLocal创建独立副本的步骤

1).创建ThreadLocal

//T: 创建独立副本的变量类型。

ThreadLocal<t> threadLocal = new ThreadLocal<T>();

 public ThreadLocal() {
    }

2).通过set()设置当前线程的独立副本

threadLocal.set(T t)

    public void set(T value) {
        //1.获取当前的线程对象
        Thread t = Thread.currentThread();
        //2.获取当前线程对象的map集合
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

   ThreadLocalMap getMap(Thread t) {
       // threaLocals是一个ThreadLocalMap对象。ThreadLocal.ThreadLocalMap threadLocals 
        return t.threadLocals;
    }

        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }




3.通过get()获取当前线程的独立副本

threadLocal.get()

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

三)、使用普通方法实现多线程日期转换

package com;

/**
 * author:chenxuebing
 * Date:2019/11/1
 * Time:13:51
 */

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 不使用ThreadLocal进行时间装换时出现的问题
 *     多个线程共同使用一个sdf对象,SimpleDataFormat内部维护着一个Caledar对象
 *     Caledar对象存储着parse(dateStr)或format(date)中dateStr、date的信息
 *     Caledar执行两个操作
 *       claer(); 清楚维护的date或dateStr的信息
 *       getTime(); 获取date
 *
 *     当所有的线程共用一个SimpleDateFormat对象时,使用同一个Calendar引用
 *     此时,出现问题:
 *     线程A调用parse(), 并没有getTime(),此时轮到了线程B执行parse(),线程B执行完后也clear(),线程A, getTime()
 *     最后得到的是线程B的时间
 *
 *     ThreadLocal: 解决了static修饰的共享变量内部操作不一致的问题。
 */
public class NormalParseDate implements Runnable{
    /**
     * 多个线程共享的变量
     */
    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");

    int i = 0;

    public NormalParseDate(int i) {
        this.i = i;
    }

    /**
     * 日期装换方法
     */

    @Override
    public void run() {
        try {
            //所有的线程使用同一个sdf
            Date date = sdf.parse("2017-08-02 11:22:"+ i % 60);
            System.out.println(i+" : "+date);
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
    /**
     * 创建线程
     */
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        for(int i = 0; i < 100; i++){
            executor.execute(new NormalParseDate(i));
        }
    }
}

结果:

68 : Mon Jan 02 11:22:08 CST 2017
Exception in thread "pool-1-thread-2" java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)

结论:因为多个线程使用同一个SimpldeDateFormat对象,指向同一个Calendar引

​ 用,在多线程调用时会出现数据交替问题。

四)、使用ThreadLocal实现多线程日期转换:

/**
 * 使用ThreadLocal线程局部变量来解决线程间共享变量内部操作不一致问题
 */
public class ThreadLocalParseData implements Runnable{
    //线程共用一个threadLocal对象
    private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();
    int i = 0;

    public ThreadLocalParseData(int i) {
        this.i = i;
    }

    @Override
    public void run() {
        //判断当前线程是否有SimpleDateFormat对象,有则使用,没有则创建
        if(threadLocal.get() == null){
            //如果没有就给当前线程创建一个, 每一个线程都有一个自己的SimpleDateFormat对象
            threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        }
        //有,则直接使用
        try {
            Date date = threadLocal.get().parse("2017-2-21 14:29:" + i % 60);
            System.out.println(i+":"+date);
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        //创建线程对象,每个线程都有一个SimpleDateFormat对象
        ExecutorService executor = Executors.newFixedThreadPool(10);
        for(int i = 0; i < 100; i++){
            executor.execute(new ThreadLocalParseData(i));
        }
    }
}

posted on 2019-10-31 17:25  宇宙美少女  阅读(526)  评论(0编辑  收藏  举报

导航