Java并发工具之ThreadLocal

1. ThreadLocal的两大使用场景

  1. 每个线程需要一个独享的对象(通常是工具类),每个线程内有自己的实例副本,不共享;
  2. 每个线程内需要保持全局变量,可以让不同的方法直接使用,避免传递参数的麻烦;
  3. 总之,就是解决多个线程的共享变量的线程安全问题;

2. 解决SimpleDateFormat的线程安全问题

  • 问题代码:
public class MyThreadLocal {

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

    public static String date(int seconds) {
        Date date = new Date(1000 * seconds);
        return sdf.format(date);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    String date = date(finalI);
                    System.out.println(date);
                }
            });
        }
        executorService.shutdown();
    }
}
1970-01-01 08:15:22
1970-01-01 08:15:19
1970-01-01 08:15:19

由结果可以看出,打印出了两个相同的时间,说明发生了运行结果错误,问题代码就发生在sdf.format(date),这行代码不是线程安全的。

  • 用synchronized加锁解决
public class MyThreadLocal {

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

    public static String date(int seconds) {
        Date date = new Date(1000 * seconds);
        String s = null;
        synchronized (MyThreadLocal.class) {
            s = sdf.format(date);
        }
//        Date date = new Date(1000 * seconds);
        return s;
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    String date = date(finalI);
                    System.out.println(date);
                }
            });
        }
        executorService.shutdown();
    }
}
  • 用ThreadLocal解决
public class MyThreadLocal1 {

//    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));

    public static String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat sdf = simpleDateFormatThreadLocal.get();
        return sdf.format(date);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    String date = date(finalI);
                    System.out.println(date);
                }
            });
        }
        executorService.shutdown();
    }
}

synchronized加锁的方法可以解决线程安全问题,但是由于同一时刻只有一个线程执行,所以效率低下,ThreadLocal方法每个线程内都有自己独享的对象,也不会有线程安全问题。

3. 每个线程内获取全局变量,可以让不同的方法直接使用,避免传递参数的麻烦

public class MyThreadLocal2 {

    ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public void service1() {
        threadLocal.set("用戶A");
    }

    public void service2() {
        System.out.println(threadLocal.get());
    }

    public static void main(String[] args) {
        MyThreadLocal2 myThreadLocal2 = new MyThreadLocal2();
        myThreadLocal2.service1();
        myThreadLocal2.service2();
    }
}
用戶A

4. ThreadLocal总结

作用:

  1. 让对象在线程之间隔离;
  2. 在任何方法中都可以直接获取到对象;

场景:

  1. 在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机由我们控制;
  2. 保存在ThreadLocal里对象的生成时机不由我们控制,我们用set方法放进去,再用get方法取出来;

好处:

  1. 线程安全;
  2. 不需要加锁;
  3. 高效利用内存;
  4. 避免传参的麻烦;

5. ThreadLocal原理

  • 每一个Thread里面都有一个ThreadLocalMap类型的threadlocals成员变量,它可以存储很多的ThreadLocal对象,因为一个线程可能有多个ThreadLocal对象,其中对象引用名称作为key;
  • ThreadLocalMap:也就是Thread.threadLocals,是Thread里的一个成员变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对;键:这个ThreadLocal;值:实际需要的成员变量;

6. ThreadLocal内存泄漏

内存泄漏:某个对象不再有用,但是占用的内存不能被回收;

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

上述代码是ThreadLocal里面的静态内部类ThreadLocalMap中的静态内部类Entry,里面存储ThreadLocal对象作为key,我们定义的成员变量作为value,所以这个Entry就是kv的键值对。
在构造函数赋值的时候,super(k)继承了WeakReference,k的赋值是个弱引用。
什么是弱引用?
如果这个对象只被弱引用关联,那么这个对象就可以被回收,所以弱引用不会阻止GC,这就是弱引用的好处。
但是value是强引用,只要强引用在,则没法GC,就可能导致内存泄漏。正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了,但是如果线程不终止(比如线程需要保持很久),发生GC时,key被回收了,key变成了null,key对应的value是强引用而不能被回收,因为有以下的调用链:
Thread->ThreadLocalMap->Entry(key=null)->Value
比如使用线程池,线程池里面的线程始终是存在的,而且value和Thread之间存在这个强引用链路,所以导致value无法回收,就可能会出现OOM。
所以最终原因是:ThreadLocalMap的生命周期和Thread一样长,只要Thread不结束,那么ThreadLocalMap中的Value就一直存在于内存中,即使发生GC时,key被回收了,但是value还存在。
如何避免内存泄漏: 当使用完了对应的ThreadLocal,主动调用remove方法删除

7. ThreadLocal总结

  1. 如果每个线程中set进去的东西本来就是多线程共享的对象,比如static对象,那么ThreadLocal.get的还是共享对象本身,还有线程安全问题;
  2. 优先使用框架的支持,而不是自己创造,例如在Spring中,如果可以使用RequestContextHolder,那么久不需要自己去维护ThreadLocal,因为自己可能会忘记调用remove方法,造成内存泄漏;
  3. 每次HTTP请求都对应一个线程,线程之间相互隔离,这就是ThreadLocal的典型应用场景;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值