来聊一聊ThreadLocal

来聊一聊ThreadLocal,第1张

来聊一聊ThreadLocal 从两种应用场景来介绍一下ThreadLocal 场景一(线程不安全的工具类的使用) 简介

我们都知道SimpleDateFormat是一个线程不安全的日期格式化工具,接下来笔者就一一个层层递进的代码示例重现这个工具类的线程不安全的问题

代码示例 第一版

如下代码所示,我们简单的实现了一个多线程间使用dateFormat工具,可以看出这一版多线程使用的很不优雅,我们不妨使用线程池来改造一下这段代码

package threadlocal;

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


public class MyThreadLocalDemo {

    public static void main(String[] args) {
        new Thread(()->{
            new MyThreadLocalDemo().caclData(10);
        }).start();

        new Thread(()->{
            new MyThreadLocalDemo().caclData(1047);
        }).start();
    }

    
    public String caclData(int second){
        Date date=new Date(1000*second);
        SimpleDateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateStr = dateFormat.format(date);
        System.out.println("**************************"+dateStr);
        return dateStr;
    }
}



第二版(使用线程池便于调度管理线程)

这版相比第一版来说,线程池解决线程复用减小开销、提高响应速度以及便于管理的问题。但是我们还是在频繁的创建日期格式化对象,所以我们需要复用这个对象,所以我们再来看看下文的第三版

public class MyThreadLocalDemo2 {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 10; i++) {
            int finalI = i;
            threadPool.submit(()->{
              new MyThreadLocalDemo2().caclData(finalI);

          });
        }

        threadPool.shutdown();

    }

    
    public String caclData(int second){
        Date date=new Date(1000*second);
        SimpleDateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateStr = dateFormat.format(date);
        System.out.println("**************************"+dateStr);
        return dateStr;
    }
}
第三版(复用日期格式化类)

很明显看到输出的结果都一样,说明这个类出现了线程安全问题,对此我们可能第一时间想到的是加锁,好的,我们下一版就用sync锁尝试一下问题是否解决



public class MyThreadLocalDemo3 {

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


    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 10; i++) {
            int finalI = i;
            threadPool.submit(()->{
              new MyThreadLocalDemo3().caclData(finalI);

          });
        }

        threadPool.shutdown();
        

    }

    
    public String caclData(int second){
        Date date=new Date(1000*second);
        String dateStr = dateFormat.format(date);
        System.out.println("**************************"+dateStr);
        return dateStr;
    }
}
第四版(基于以上基础加锁)

经过调试发现这个问题被解决了,但是缺点也很明显,假如使用sync锁的话,在高并发情况下,很可能会出现大量线程等待进而导致OOM问题。所以我们必须再进行一次迭代,使用ThreadLocal

package threadlocal;

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




public class MyThreadLocalDemo4 {

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


    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 10; i++) {
            int finalI = i;
            threadPool.submit(() -> {
                new MyThreadLocalDemo4().caclData(finalI);

            });
        }

        threadPool.shutdown();


    }

    
    public String caclData(int second) {
        Date date = new Date(1000 * second);
        String dateStr;

        synchronized (MyThreadLocalDemo4.class) {
            dateStr = dateFormat.format(date);
        }

        System.out.println("**************************" + dateStr);
        return dateStr;
    }
}



第五版(使用ThreadLocal) 工具类
public class ThreadLocalDftUtils {
//    private static ThreadLocal dft = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    private static ThreadLocal dft = new ThreadLocal(){
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
};

    public static ThreadLocal getDft() {
        return dft;
    }
}
使用示例

可以看到问题也被解决了

public class MyThreadLocalDemo5 {


    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 10; i++) {
            int finalI = i;
            threadPool.submit(() -> {
                new MyThreadLocalDemo5().caclData(finalI);

            });
        }

        threadPool.shutdown();


    }

    
    public String caclData(int second) {
        Date date = new Date(1000 * second);
        String dateStr;
        dateStr = ThreadLocalDftUtils.getDft().get().format(date);

        System.out.println("**************************" + dateStr);
        return dateStr;
    }
}

场景二(同一线程间参数共享) 场景简介

假如我们有一个service他需要调用其他服务,服务之间都需要共享一个参数,假如使用方法的参数传递,这可能会导致各个服务之间耦合度过高,对此我们可以使用threadLocal解决问题,具体参照下述代码

代码示例 工具类
package threadlocal;

import java.text.SimpleDateFormat;

public class MyUserContextHolder {
    private static ThreadLocal holder = new ThreadLocal<>();

    public static ThreadLocal getHolder() {
        return holder;
    }
}

示例代码
package threadlocal;

public class MyThreadLocalGetUserId {
    public static void main(String[] args) {
        MyService1 service1=new MyService1();
        service1.doWork1("username1");


        MyService1 service11=new MyService1();
        service1.doWork1("username2");
    }
}


class MyService1 {

    public void doWork1(String name) {
        User user = new User(name);
        System.out.println("service2 userName:" + user.name);
        ThreadLocal holder = MyUserContextHolder.getHolder();
        holder.set(user);
        MyService2 service2 = new MyService2();
        service2.doWork2();
    }

}

class MyService2 {
    public void doWork2() {
        ThreadLocal holder = MyUserContextHolder.getHolder();
        User user = holder.get();
//        holder.remove();
        System.out.println("service2 userName:" + user.name);
        MyService3 service3 = new MyService3();
        service3.doWork3();
    }
}


class MyService3 {
    public void doWork3() {
        ThreadLocal holder = MyUserContextHolder.getHolder();
        User user = holder.get();
        System.out.println("service3 userName:" + user.name);

//        避免oom问题
        holder.remove();
    }
}

输出结果
service2 userName:username1
service2 userName:username1
service3 userName:username1
service2 userName:username2
service2 userName:username2
service3 userName:username2
ThreadlLocal工作原理解析 图解

如下图所示,之所以threadLocal可以保证线程安全,是以为每个线程内部都会维护一个ThreadLocal的map对象,他就负责存储我们上述 *** 作的对象。
如下源码所示

  
    ThreadLocal.ThreadLocalMap threadLocals = null;

通过源码来查看ThreadLocal如何完成get、set、初始化 threadLocal如何完成初始化的

如下所示,笔者以上文的MyThreadLocalDemo5进行一个断点,通过debug看到下文所示位置

通过debug我们看到get内部,可以发现他的逻辑也很简单,具体可以参照笔者写的注释

 public T get() {
        Thread t = Thread.currentThread();
        //通过当前线程获取一个map
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        //通过当前对象去map中拿一个entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            //若有值则取值返回
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //否则走set逻辑
        return setInitialValue();
    }

可以看到上文的set不如就走到我们初始化时候的initialValue,通过赋值然后返回

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如何如何完成set的

回答这个问题我们不妨对上文的MyThreadLocalGetUserId.java进行如下图所示的debug

可以看到set逻辑也很简单,以当前线程作为key,值作为value存到map中,这个map就是上文中的user

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是底层实现是什么?

看到上文的实现,我们就发现,无论何种 *** 作,threadLocald大部分逻辑都是依靠一个map实现的,这map我们不妨通过源码查看一下它的具体实现
可以看到这个类就是在Thread类中的一个内部类,而且我们可以看到这个map处理冲突的方式是通过线性探测法,而不是像hashmap一样使用拉链法转红黑树

 private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }
ThreadLocal使用注意事项 OOM问题

可以看到我们创建entry时key是若引用,而value是强引用,假如我们现在所使用的线程是线程池中的线程的话,反复复用且我们并没有去用remove方法去清理value很可能导致OOM

 static class Entry extends WeakReference> {
            
            Object value;

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

当然jdk似乎也一试到了这个问题,如下源码所示,我们经常会在set或者其他源码中看到jdk会时不时检查没用的强引用将其设置为null,交给GC回收

 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();
        }
 private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    //核心逻辑
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

如下所示将value设置为null

 private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal k = e.get();
                if (k == null) {
                //核心逻辑,将value设置为null
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }
空指针问题

如下所示,假如我们没有对threadLocal进行set,而且泛型指明为包装类,而返回值是long,很可能导致拆箱进而出现控制针

package threadlocal;

public class MyThreadLocalNpe {

    private ThreadLocal threadLocal = new ThreadLocal<>();

    public ThreadLocal getThreadLocal() {
        return threadLocal;
    }


    
    public Long get() {
        return threadLocal.get();
    }

    public void set() {
        threadLocal.set(Thread.currentThread().getId());
    }

    public static void main(String[] args) {
        MyThreadLocalNpe threadLocalNpe = new MyThreadLocalNpe();
        ThreadLocal threadLocal = threadLocalNpe.getThreadLocal();
        System.out.println(threadLocalNpe.get());
        new Thread(() -> {
            threadLocalNpe.set();
        }).start();
    }

}

ThreadLocal在Spring中的运用 RequestContextHolder
private static final ThreadLocal requestAttributesHolder =
			new NamedThreadLocal<>("Request attributes");

	private static final ThreadLocal inheritableRequestAttributesHolder =
			new NamedInheritableThreadLocal<>("Request context");
DateTimeContextHolder
	private static final ThreadLocal dateTimeContextHolder =
			new NamedThreadLocal<>("DateTimeContext");

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/zaji/5719108.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-12-18
下一篇2022-12-18

发表评论

登录后才能评论

评论列表(0条)

    保存