Java是如何通过ThreadLocal类来实现变量的线程独享

编程入门 行业动态 更新时间:2024-10-26 00:24:37

Java是如何通过ThreadLocal类<a href=https://www.elefans.com/category/jswz/34/1769578.html style=来实现变量的线程独享"/>

Java是如何通过ThreadLocal类来实现变量的线程独享

一 概述

Java中,如果一个变量要被多线程访问,可以使用volatile关键字将它声明为“易变的”;如果一个变量只要被某个线程独享时,我们可以通过java.lang.ThreadLocal类来实现线程本地存储的功能。每一个线程的Thread对象中都有一个ThreadLocalMap对象,这个对象存储了一组ThreadLocal<?>的实例化对象为键,以本地线程变量为值的K-V值对,ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,每一个ThreadLocal对象都包含了一个独一无二的threadLocalHashCode值,使用这个值就可以在线程K-V值对中找回对应的本地线程变量。

二 ThreadLocal,ThreadLocalMap和Thread的关系

ThreadLocal,ThreadLocalMap,Thread的关系图(图一):

Thread,ThreadLocal和ThreadLocalMap相关的源码:

//Thread类
public class Thread implements Runnable {ThreadLocal.ThreadLocalMap threadLocals = null;
}//ThreadLocal类与ThreadLocalMap类
public class ThreadLocal<T> {static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;//可能导致内存泄漏Entry(ThreadLocal<?> k, Object v) {//弱引用super(k);//强引用value = v;}}private static final int INITIAL_CAPACITY = 16;private Entry[] table;
}

三 ThreadLocal的使用场景与实例

场景一:每个线程都需要一个独享的对象,同时使用该对象是线程安全的,如SimpleDateFormat本身在多线程环境下不是线程安全的,我们利用ThreadLocal使得对象为一个线程独享,从而变得线程安全。

代码实例一:借助ThreadLocal通过大小为10的线程池完成1000个线程使用线程非安全的SimpleDateFormat类

/*** 描述:利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存*/
public class ThreadLocalExample {public static ExecutorService threadPool = Executors.newFixedThreadPool(10);public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 1000; i++) {int time = i;threadPool.submit(new Runnable() {@Overridepublic void run() {String date = new ThreadLocalExample().date(time);}});}threadPool.shutdown();}public String date(int seconds) {//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时Date date = new Date(1000 * seconds);SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();return dateFormat.format(date);}
}//ThreadLocal是可以并行的class ThreadSafeFormatter {public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new                 ThreadLocal<SimpleDateFormat>() {@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");}};public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

此时每个线程中的ThreadLocalMap中的key为ThreadLocal<SimpleDateFormat>实例化对象,value为SimpleDateFormat的对象实例。

场景二:每个对象中需要保存全局变量,使得统一请求中或同一线程中不同方法直接使用共享的变量,避免同一个参数被多次传递

代码实例二:通过ThreadLocal对象使得某个变量可以在同一个线程中被线程中的多个方法安全的共享。

public class ThreadLocalExample {public static void main(String[] args) {new Service1().process("");}
}class Service1 {public void process(String name) {User user = new User("ThreadLocal Example");//将变量保存在ThreadLocal对象中UserContextHolder.holder.set(user);new Service2().process();}
}class Service2 {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);UserContextHolder.holder.remove();}
}class UserContextHolder {public static ThreadLocal<User> holder = new ThreadLocal<>();
}class User {String name;public User(String name) {this.name = name;}
}

此时每个线程中的ThreadLocalMap中的key为ThreadLocal<User>实例化对象,value为SimpleDateFormat的对象实例。

三 ThreadLocal中的重要方法

initialValue()

    protected T initialValue() {return null;}

initialValue()方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get方法的时候才会触发,当线程第一次使用get方法访问变量时,将调用此方法,当线程先调用了set方法的情况下,不会为线程调用本身的initalValue()方法。

如果不重写该方法默认情况下会返回null。一般如场景一同样使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。

get()

    public T get() {//获取当前线程Thread t = Thread.currentThread();//获取ThreadLocalMap,每个线程都拥有一个ThreadLocalMap类的成员变量ThreadLocalMap map = getMap(t);if (map != null) {//this表示将当前的ThreadLocal对象作为key获取对应的value对象ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {//result为我们的目标对象,如场景一中的SimpleDateFormat对象和场景二中的User对象@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}

get()是先取出当前线程的ThreadLocalMap实例,然后调用map.getEntry方法,将该ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value,而且这个map中的key和value都是保存在当前线程中。

ThreadLocalMap类似于HashMap,不同于HashMap的是处理hash碰撞的方式,前者是采用线性探测法,即当发生hash冲突的时候就继续找下一个空位置,而后者是采用拉链法,当发生hash冲突之后就会采用链表存储,在Java8开始当链表长度超过8之后就使用红黑树进行存储。

四 ThreadLocal中的内存泄露问题

      static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;//可能导致内存泄漏Entry(ThreadLocal<?> k, Object v) {//弱引用super(k);//强引用value = v;}}

由上述代码可知,ThreadLocalMap中的value和Thread之间存在强引用链路(JVM中只要强引用关系存在,垃圾收集器就永远不会回收掉被引用的对象),所以会导致value对象无法被正常回收,可能会出现OOM。基于这种情况,JDK进行了相应的处理,即在使用set,remove,rehash方法的时候扫描key为null的entry,并把对应的value设置成null,这样value对象就可以被回收。

问题是如果一个ThreadLocal对象不再被使用了,那么set,remove,rehash方法也不会被调用,如果同时线程又停止了,那么强引用链就会一致存在,就会导致内存泄漏。

阿里规约中写到,调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal之后,应该调用remove方法。

五 ThreadLocal注意点

ThreadLocal存在其好处,但是并不需要强行使用,如在任务数很少的时候,在局部变量中可以新建对象解决问题,就不需要使用ThreadLocal来解决问题。

如果每个线程中ThreadLocal.set()的对象本身就是多线程共享,如static对象,那么多线程的ThreadLocal.get()取得的还是这个共享对象的本身,就会出现并发访问的问题。

我们应该善于使用框架中成熟的ThreadLocal方案,如Spring中的RequestContestHolder,DateTimeContextHolder,这样可以减少我们的维护工作。

RequestContextHolder

public abstract class RequestContextHolder  {private static final ThreadLocal<RequestAttributes> requestAttributesHolder =new NamedThreadLocal<>("Request attributes");private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =new NamedInheritableThreadLocal<>("Request context");
}

DateTimeContextHolder

public final class DateTimeContextHolder {private static final ThreadLocal<DateTimeContext> dateTimeContextHolder =new NamedThreadLocal<>("DateTimeContext");
}

每一个Http请求都对应一个线程,线程之间是相互隔离的,这种情况就是ThreadLocal的典型应用场景。

六 父子进程可共享的ThreadLocal实现

ThreadLocal是一个父子进程不能共享的线程独享实现方式,如果想要在父子线程之间进行共享可以使用InheritableThreadLocal类来实现此功能。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {//父线程创建子线程时,向子线程复制InheritableThreadLocal变量时用protected T childValue(T parentValue) {return parentValue;}//重写getMap,操作InheritableThreadLocal时,将于threadLocals变量无关,只会影响Thread类中的inheritableThreadLocals变量ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}//类似getMapvoid createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}
}

Thread类中的inheritableThreadLocals变量

/*Thread类中的变量inheritableThreadLocals继承了父线程的ThreadLocalMap,
用于父子进程之间ThreadLocal变量的传递,即inheritableThreadLocals主要存储
可自动向子进程传递的ThreadLocal.ThreadLocalMap.*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

Thread的初始化方法init(...)

public class Thread implements Runnable {private void init(ThreadGroup g, Runnable target, String name,long stackSize) {init(g, target, name, stackSize, null, true);} private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {if (name == null) {throw new NullPointerException("name cannot be null");}this.name = name;Thread parent = currentThread();SecurityManager security = System.getSecurityManager();if (g == null) {/* Determine if it's an applet or not *//* If there is a security manager, ask the security managerwhat to do. */if (security != null) {g = security.getThreadGroup();}/* If the security doesn't have a strong opinion of the matteruse the parent thread group. */if (g == null) {g = parent.getThreadGroup();}}/* checkAccess regardless of whether or not threadgroup isexplicitly passed in. */g.checkAccess();/** Do we have the required permissions?*/if (security != null) {if (isCCLOverridden(getClass())) {security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);}}g.addUnstarted();this.group = g;this.daemon = parent.isDaemon();this.priority = parent.getPriority();if (security == null || isCCLOverridden(parent.getClass()))this.contextClassLoader = parent.getContextClassLoader();elsethis.contextClassLoader = parent.contextClassLoader;this.inheritedAccessControlContext =acc != null ? acc : AccessController.getContext();this.target = target;setPriority(priority);if (inheritThreadLocals && parent.inheritableThreadLocals != null)this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);/* Stash the specified stack size in case the VM cares */this.stackSize = stackSize;/* Set thread ID */tid = nextThreadID();}
}

Thread类中的init(...)方法有两个实现,差别为init(ThreadGroup g, Runnable target, String name,long stackSize) 未传入参数AccessControlContext和inheritThreadLocals默认为true,这种情况下父线程inheritableThreadLocals不为空时就会将父线程的inheritablethreadLocals传递至子线程。而init(ThreadGroup g, Runnable target,String name,long stackSize, AccessControlContext acc, boolean inheritThreadLocals)传入了AccessControlContext而且inheritThreadLocals变量默认为false。

ThreadLocal的createInheritedMap()方法

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {return new ThreadLocalMap(parentMap);}/*构建一个包含所有parentMap中Inheritable ThreadLocalsd ThreadLocals的ThreadLocalMap*/
private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);//使用Entry数组存放ThreadLocalMap中的ThreadLocaltable = new Entry[len];//逐一复制parentMap中的记录for (int j = 0; j < len; j++) {Entry e = parentTable[j];if (e != null) {@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null) {//从子线程中的ThreadLocalMap中获取指定的变量Object value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode & (len - 1);while (table[h] != null)h = nextIndex(h, len);table[h] = c;size++;}}}
}

根据ThreadLocalMap(ThreadLocalMap parentMap)方法可知,子线程将父线程的ThreadLocalMap中的值逐一复制到本身。

更多推荐

Java是如何通过ThreadLocal类来实现变量的线程独享

本文发布于:2024-02-12 17:43:03,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1688767.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:来实现   线程   独享   变量   Java

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!