java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > ThreadLocal的用法及原理

Java中ThreadLocal的用法及原理详解

作者:你好世界wxx

这篇文章主要介绍了Java中ThreadLocal的用法及原理详解,在并发编程中,如果一个类变量被多个线程操作,会造成线程安全问题,使用ThreadLocal可以让每个线程拥有线程内部的变量,防止多个线程操作一个类变量造成的线程安全问题,需要的朋友可以参考下

1 ThreadLocal简介

ThreadLocal中文是:线程局部变量。

2 ThreadLocal用法

2.1 用法一:线程独享对象

请创建1000个格式化打印时间的任务并执行。

做法:使用线程池,线程池中开辟10个线程,用这10个线程执行这1000个任务,为了防止出现线程安全问题,使用 ThreadLocal 保证每个线程独享一个 SimpleDateFormat 对象,代码如下:

/**
 * 典型场景1:每个线程需要一个独享的对象
 * 利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用了内存
 */
public class Main1 {
    public static ExecutorService tp = Executors.newFixedThreadPool(10);
    public String date(int seconds) {
        SimpleDateFormat df = TSF.df.get();  // 获取当前线程拥有的 SimpleDateFormat 对象
        return df.format(new Date(1000 * seconds));
    }
    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            tp.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new Main1().date(finalI);
                    System.out.println(date);
                }
            });
        }
        tp.shutdown();
    }
}
class TSF {  // ThreadSafeFormatter
    // 本类中定义的类变量都是线程内部的,可以定义多个
    // 每个类变量的用法都是类似的,即:TSF.类变量名.get()    根据类变量名可以知道返回哪个对象
    // 底层map中存在键值对:(UTSF.df, 该函数的返回值)
    public static ThreadLocal<SimpleDateFormat> df = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };
}

结果会打印出1000个不同的时间。

2.2 用法二:线程全局变量

每个线程都会牵涉到三个服务类:Service1、Service2、Service3,这三个类中都会使用到同一个对象。同一个进程内部这是一个对象,不同进程之间对象不同,请实现该需求。

/**
 * 每个线程内需要保存全局变量
 * 同一个线程内该全局信息相同,不同线程间该全局信息不同
 * 如下两个线程,线程1保存全局用户"wxx",线程2保存全局用户"she"
 */
public class Main2 {
    public static void main(String[] args) throws Exception {
        new Thread(() -> new Service1().process("wxx")).start();
        Thread.sleep(100);
        new Thread(() -> new Service1().process("she")).start();
    }
}
class Service1 {  // Service1 调用 Service2
    public void process(String name) {
        User user = new User(name);
        UserContextHolder.holder.set(user);  // 底层map中存在键值对:(UserContextHolder.holder, user)
        System.out.println("Service1:" + user.name);
        new Service2().process();
    }
}
class Service2 {  // Service2 调用 Service3
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service2:" + user.name);
        new Service3().process();
    }
}
class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3:" + user.name);
    }
}
class UserContextHolder {  // 本类中定义的类变量都是线程内部的,可以定义多个
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
    String name;
    public User(String name) {
        this.name = name;
    }
}

结果:

Service1:wxx
Service2:wxx
Service3:wxx
Service1:she
Service2:she
Service3:she

3 ThreadLocal原理

在这里插入图片描述

现在搞清楚了ThreadLocal、ThreadLocalMap之间的关系,那这两个和Thread是什么关系呢?答案是:Thread中有一个 ThreadLocal.ThreadLocalMap 的变量。如下图:

在这里插入图片描述

public class Thread implements Runnable {
    // ...
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // ....
}

接下来我们就可以探究ThreadLocal到底是如何获取属于线程内部的变量的,关键在于探究ThreadLocal的 get() 方法。该函数如下:

public class ThreadLocal<T> {
    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();
    }
}

该函数中使用到了 getMap 和 setInitialValue 两个函数,这两个函数的定义如下:

public class ThreadLocal<T> {
    private T setInitialValue() {
        T value = initialValue();  // 用法一 重写了该方法,由多态可知,返回重写的该函数的返回值
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);  // 得到当前线程t的成员变量 threadLocals
        if (map != null)
            map.set(this, value);  // 向 threadLocals 中放入键值对, 关键!!!
        else
            createMap(t, value);
        return value;
    }
    public void set(T value) {  // 用法二调用了该方法
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);  // 向 threadLocals 中放入键值对, 关键!!!
        else
            createMap(t, value);
    }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
}

分析 get() 函数的执行流程:

(1)获取当前线程 t ,然后调用 getMap(t) ,从而得到属于当前线程 t 的ThreadLocalMap变量 map ;

(2)然后判断属于当前线程 t 的 map 是否为空,不空的话从 map 中取出当前键值对,这里的键是this,也就是说调用get()方法的变量。对应于用法一的 TSF.df ,对应于用法二的 UserContextHolder.holder 。为空的话则调用 setInitialValue() ,该函数会将this作为键,重写的 initialValue() 返回值作为值存入到 map 中。

(3)返回 this 对象对应的值。

无论是用法一,还是用法二,其实本质上都在操纵 当前线程 t 的成员变量 threadLocals 。

根据上述 get() 分析的第(2)点,当我们 new ThreadLocal<>(); 时并没有向 ThreadLocalMap 中存入键值对,只有当调用 get()、set() 方法时才放入键值对,这是懒加载的一种体现。

4 ThreadLocal注意点

ThreadLocalMap

解决哈希冲突

ThreadLocalMap中节点的键值对

如果弱引用对象只与弱引用关联,则这个弱引用对象可以被回收。

ThreadLocalMap中的Entry继承自WeakReference,是弱引用;

每一个Entry都是对key的弱引用;

每个Entry都包含了一个对value的强引用;

value为强引用的原因:因为JVM认为这个引用十分重要,是程序员定义的,不能随意回收,回收之后可能发生异响不到的错误;

因为值value是强引用,所以可能导致内存泄露,最终导致OOM,这是因为:如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,存在以下调用链:Thread---->ThreadLocalMap---->Entry(key为null)---->value。导致value无法回收,日积月累可能造成OOM。

JDK已经考虑到了这个问题,所以在Entry的set,remove,rehash方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收。但是这样做还不足够,因为我们必须调用这些方法才能达到上述效果。

为了避免产生内存泄露问题,我们在使用完ThreadLocal之后,就应该调用remove方法(阿里规约)。例如用法二中 Service3 应该改为:

class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3:" + user.name);
        UserContextHolder.holder.remove();  // 防止内存泄露
    }
}

我们可不可以在新建ThreadLocal并在没有重写initialValue()方法后,直接调用 ThreadLocal 的 get()方法?

可以,只不过会返回 null 。

如下代码演示了上述描述的问题:

public class ThreadLocalNPE {
    ThreadLocal<Long> tl = new ThreadLocal<>();
//    public void set() {
//        tl.set(Thread.currentThread().getId());
//    }
    public long get() {  // 返回值改为 Long 就没有NPE异常了
        return tl.get();  // tl.get() 为 null
    }
    public static void main(String[] args) {
        ThreadLocalNPE main = new ThreadLocalNPE();
        // 不进行set,直接get
        main.get();
    }
}

上述代码会抛出java.lang.NullPointerException异常,这不是因为get()的原因,而是因为:拆箱时null不能转为基本类型。当返回值改为 Long 就没有NPE异常了。

到此这篇关于Java中ThreadLocal的用法及原理详解的文章就介绍到这了,更多相关ThreadLocal的用法及原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文