Java并发系列:ThreadLocal的用法和坑

本文主要讲解:

本文源码地址:E01_ThreadLocal, 欢迎star我的Github

正文开始

ThreadLocal的用法

  • 多线程竞争同一个变量

当我们使用线程池来复用线程的时候,对于同一个变量的竞争使用,一般会导致线程安全问题,因此建议放入到ThreadLocal中使用。

public class RightThreadLocalDemo {
    public static ExecutorService tpool = Executors.newFixedThreadPool(10);

    @Test//一千个线程
    public void ThreadLocalDemo_right() throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int fi = i;
            tpool.submit(() -> {
                String date = new RightThreadLocalDemo().date(fi);
                System.out.println(Thread.currentThread().getName() + ":     " + date);
            });
        }
        Thread.sleep(100);
        tpool.shutdown();
    }

    public String date(int second) {
        Date date = new Date(1000 * second);
        SimpleDateFormat df = datefmt1.get();
        //此处证明了ThreadLocal即使是static 对象,其在线程中也不仅仅是一个,而是以副本的形式存在于线程中
        System.out.println(Thread.currentThread().getName()+"========"+System.identityHashCode(df));
        return df.format(date);
    }

    //此处是新建 ThreadLocal
    public static ThreadLocal<SimpleDateFormat> datefmt1 =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
}
  • 注意到一个问题:为什么ThreadLocal可以写在很多地方,比如写在不同的类中,用的时候确在另一个类或者方法里面,但是依旧是线程安全的?
    • 因为ThreadLocal是属于线程的,使用了 Thread.currentThread() 来获取当前线程。
public void set(T value) {
      Thread t = Thread.currentThread();
      ThreadLocalMap map = getMap(t);
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
}
  • 同一个线程无需显式调用

当一个线程需要经历很多类的很多方法,一般是将需要的对象当作参数,每一步都带下去,此时可以使用ThreadLocal,就像web里面的session一下,做到随取随用。

public class RightThreadLocalDemo2 {
    static ThreadLocal<User> tl = new ThreadLocal<>();

    @Test
    public void deal(String nn){
        new Service1().service(nn);
    }
    public static ExecutorService tpool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            int fg = i;
            tpool.submit(()->{
                new Service1().service("ljfirst---"+fg);
            });
        }
        //Thread.sleep(100);
        tpool.shutdown();
    }
}

class Service1{
    public void service(String n){
        User u = new User(n);
        RightThreadLocalDemo2.tl.set(u);
        new Service2().service();
    }
}
class Service2{
    public void service(){
        User u = RightThreadLocalDemo2.tl.get();
        System.out.println(Thread.currentThread().getName()+"   ---Service2().service():  "+u.name);
        new Service3().service();
    }
}
class Service3{
    public void service(){
        User u = RightThreadLocalDemo2.tl.get();
        System.out.println(Thread.currentThread().getName()+"   ---Service3().service():  "+u.name);
        new Service4().service();
    }
}
class Service4{
    public void service(){
        User u = RightThreadLocalDemo2.tl.get();
        System.out.println(Thread.currentThread().getName()+"   ---Service4().service():  "+u.name);
        //不用的时候记得remove
        RightThreadLocalDemo2.tl.remove();
    }
}

class User{
    String name ;
    public User(String name){
        this.name = name;
    }
}

ThreadLocal的原理

  • 数据结构

    由下图可以看出ThreadLocal其实是一个Entry,每个ThreadLocal组成一个Entry的数组,被ThreadLocalMap管理。

在这里插入图片描述
图片来源:用了三年 ThreadLocal 今天才弄明白其中的道理

  • set、get 和 remove方法

    • set方法
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 从 Thread 中获取 ThreadLocalMap
    ThreadLocalMap map = getMap(t);

    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
  • 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();
}
  • remove方法
在这里插入代码片public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

上述提及的三个方法,只是表象,需要看具体的实现,还是进ThreadLocalMap中看。源码解析来自于:ThreadLocal有没有内存泄漏?源码给你安排得明明白白

ThreadLocal的问题

  • 线程不安全的场景

    在上述样例中,展示了使用线程池复用线程的方法,但是复用线程的同时,ThreadLocal没有被清除,也会被复用,因此造成污染。
public class threadlocal复用污染 {
    public static ExecutorService tpool = Executors.newFixedThreadPool(10);

    @Test
    public void test() {
        Thread t = new Thread(()->{
            Tools.tl.set("bbb");
            System.out.println(Thread.currentThread().getName() + ":======" + Tools.tl.get());
            //这句话不加会导致后续的线程复用时,threadlocal也被复用,因此造成线程不安全
            Tools.tl.remove();
        });
        tpool.submit(t);
        Thread t1 = new Thread(()->{
            Tools.dd();
            System.out.println(Thread.currentThread().getName() + ":======" + Tools.tl.get());
            /*
            ddd.tl.remove():这句话不加会导致后续的线程复用时,threadlocal也被复用,因此造成线程不安全
            现象:所有的pool-1-thread-1 只会出现一次pool-1-thread-1:======bbb,
            pool-1-thread-2没有清除threadlocal,因此会出现多次 pool-1-thread-2aaaa
            */
        });
        tpool.submit(t1);

        for (int i = 0; i < 100; i++) {
            tpool.submit(() -> {
                System.out.println(Thread.currentThread().getName() + Tools.tl.get());
            });
        }
        tpool.shutdown();
    }
}

class Tools {
    static ThreadLocal<String> tl = new ThreadLocal<>();
    public static void dd() {
        tl.set("aaaa");
    }
}
  • 内存溢出问题

    如果ThreadLocal在使用后,不删除,虽然ThreadLocalMap的key会被清空(因为它是弱引用),但是其Value并不会,在高并发场景下,很容易出现OOM。
public class ThreadLocal内存泄漏 {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Task().calc(10);
            //通过下方的Evaluate Expression 可以看出在80前后回收了 线程里面的内容
            //但是仅仅回收了map 的 key(当前的ThreadLocal),并不是回收Value。
            //因此存在内存溢出的问题。
            if (i == 80) {
                System.gc();
            }
        }
    }

    static class Task {
        ThreadLocal<Integer> value;
        public int calc(int i) {
            value = new ThreadLocal();
            value.set((value.get() == null ? 0 : value.get()) + i);
            return value.get();
        }
    }
}
  • 延伸:强弱虚软引用

    • 强引用:
      • 类似于Student s = new Student();这里的s就是一个强引用,当线程、对象消失,或者手动s=null;就在恰当的时间,被GC掉。
    • 弱引用:
      • 当一个对象同时被强、弱引用指向时,它不会被回收,但是当强引用消失,那么弱引用就会在恰当的时间,被GC掉,有种狐假虎威的感觉。
      • 下方代码的Entry,在线程销毁,或者线程池销毁的时候,将被GC掉。
      • 弱引用的特点是不管内存是否足够,只要发生GC,都会被回收
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
  • 虚引用:
    • 在NIO中,就运用了虚引用管理堆外内存
  • 软引用:
    • 软引用就是把对象用SoftReference包裹一下,当我们需要从软引用对象获得包裹的对象,只要get一下就可以了。
    • 当内存不足,会触发JVM的GC,如果GC后,内存还是不足,就会把软引用的包裹的对象给干掉,也就是只有在内存不足,JVM才会回收该对象
SoftReference<Student>studentSoftReference=new SoftReference<Student>(new Student());  
Student student = studentSoftReference.get();  
System.out.println(student);  

强软弱虚的验证代码:强软弱虚
参考博客:强软弱虚引用

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值