ThreadLocal应用及原理解析

分析SimpleDateFormat线程不安全的原因

   static private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


    public static void main(String[] args) throws InterruptedException {

        for(int i=0;i<100;i++){
            new Thread(() -> {
                try {
                    Date date=simpleDateFormat.parse("2020-04-04 12:00:00");
                    System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
                }catch (Exception e){
                    e.printStackTrace();
                }
            }).start();
        }



    }

在这里插入图片描述

我们可以看到在多线程并发的情况下SimpleDateFormat 是线程不安全的
原因是因为DateFormat类中,有个protected的属性calendar,用于存储设置的时间

/**
 * The {@link Calendar} instance used for calculating the date-time fields
 * and the instant of time. This field is used for both formatting and
 * parsing.
 *
 * <p>Subclasses should initialize this field to a {@link Calendar}
 * appropriate for the {@link Locale} associated with this
 * <code>DateFormat</code>.
 * @serial
 */
 protected Calendar calendar;

DateFormat中提供了很多共有方法去更改此属性,如下:

public void setCalendar(Calendar newCalendar)
{
	this.calendar = newCalendar;
}
public void setTimeZone(TimeZone zone)
{
    calendar.setTimeZone(zone);
}

即DateFromat中存有当前将要转换的时间信息,而不只是格式化相关的信息。

如将DateFormat设置为一个全局变量。时间转换都使用此变量进行转换,每次转换时间,都将更改此全局对象中的calendar信息。

多线程情况下会导致线程不安全的问题,即一个线程更改此属性后,在计算格式化或者换行成日期的过程中,其他线程对此属性进行了更改,导致计算报错,或者计算不正确。

多线程环境下使用SimpleDateFormat进行日期转换,会出现两种错误信息:
1.抛出异常
2.计算错误

解决方案(只讲其中一种使用ThreadLocal解决)

    private static final ThreadLocal<SimpleDateFormat> THREAD_LOCALE=new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
    

    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<100;i++){
            new Thread(() -> {
                try {
                    Date date=THREAD_LOCALE.get().parse("2020-04-04 12:00:00");
                    System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    THREAD_LOCALE.remove();
                }
            }).start();
        }
    }

ThreadLocal的作用

线程之间隔离
解决多线程的并发问题,是Thread的局部变量,使用它维护变量,会使该变量的线程提供一个独立的副本,可以独立修改,不会影响其他线程的副本、

方法

initialValue()

该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候才会触发
当线程第一次调用get方法访问变量时,调用此方法,如果线程先前调用了set方法,在这种请求下,不会为线程调用本initialValue方法
通常,每个线程最多调用一次此方法,但如果已经调用了remove()后,再调用get(),则可以再次调用此方法
如果不重写该方法,这个方法会返回null,一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象

Set(T value)

为这个线程设置一个新值

get()

得到这个线程对应的value。如果是首次调用get(),则会调用initialValue()方法来得到这个值

remove()

删除对应线程的值

源码解析

在这里插入图片描述
因为一个线程可能会访问多个ThreadLocal 所以一个线程下会维护 ThreadLocalMap集合 会存在多个 每个 ThreadLocalMap 的key 是Threadlocal实例 value是值

set

   public void set(T value) {
        //拿到当前线程
        Thread t = Thread.currentThread();
        //根据当前线程找当前线程的 ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //不等于空 直接set
        if (map != null) key ThreadLocal的弱引用的实例
            map.set(this, value);
        else
            //直接创建
            createMap(t, value);
    }
   //t是当前线程 t.threadLocals   表示当前线程的ThreadLocalMap    
   // ThreadLocalMap   的key 是this 表示当前对象 当前对象是 ThreadLocal      value 是输入的值
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

线性探索

用来解决hash冲突的一种策略 是一种开放寻址的策略
通过hash 函数把key映射到hash 表中的一个位置来访问记录 从而加快查找的速度 存放记录的数据就是hash表(散列表)
当我们针对一个key通过hash函数计算产生一个位置 而这个位置在hash表中已经被另外一个键值对占比时 线性探索就可以解决这种情况
写入:查找hash表中冲突单元最近的空闲单元 把新的键值插入这个空闲单元
查找:从根据 hash函数计算得到的位置开始往后查找 直到找到与key对应的value或找到空的单元
就是个map key是指向当前 ThreadLocal实例的弱引用 value就是数据

  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;
            //根据hash code找数组的下标
            int i = key.threadLocalHashCode & (len-1);
 
            //从i开始遍历 直到最后一个 线性探索
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                 //如果key相等则覆盖
                if (k == key) {
                    e.value = value;
                    return;
                }
                //如果key 为null 用新key  value覆盖
                if (k == null) {
                    //覆盖 并清理 key== null的数据
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            //超过阈值不需要扩容  
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

向前查找失效的entry向后查找可以被覆盖的entry
向前查找有失效的enrty 向后未找到可以覆盖的Entry
向前未查找到失效的entry 向后找到可以覆盖的Entry
向前未查找到失效的enrty 向后未查找到失效的entry
找到无效的数据进行清理

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            //向前扫描 查找最前面无效的key 
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    //循环遍历 定位最前面无效的key
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            //从i开始向后查找 遍历数组的最后一个Entry
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                //找到匹配的key之后
                if (k == key) {
                    e.value = value;
                    //更新 value值
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    //如果最前面无效的key 和当前的key相同 责任将 i作为起点开始清理
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    //清理
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                //如果当前的slot已经无效 并且在向前扫描的过程中没有无效的slot 则更新slotToExpunge当前位置
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            //如果key对应的value不存在 则直接放一个新的Entry
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            //如果有任何无效的slot 则做一次清理 
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

get

public T get() {
        Thread t = Thread.currentThread();
        //根据当前线程拿到一个 ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //根据当前的ThreadLocal实例的软引用 拿value
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        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;
    }

ThreadLocal内存泄漏问题

引用类型

强引用

只要强引用存在,则垃圾回收器就不会回收这个对象

软引用

当内存足够时不会被回收

弱引用

引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记

虚引用

虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。

原因

ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。

   static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
}

在这里插入图片描述
通过上述解决会发现 因为是弱引用 所以会导致key 已经丢了 但是value却还存在 所以会造成内存泄漏

解决方案

每次使用完记得 remove

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("张三");
    ……
} finally {
    localName.remove();
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值