Java线程基础-Thread ThreadGroup ThreadLocal ThreadGroupContext 之ThreadLocal(三)

讲完了Thread以及ThreadGroup,接下来进行对ThreadLocal的认识。

1. 认识ThreadLocal

基于JDK8查看ThreadLocal的解释:

/** * This class provides thread-local variables. These variables
differ from their normal counterparts in that each thread that
accesses one (via its * {@code get} or {@code set} method) has its
own, independently initialized * copy of the variable. {@code
ThreadLocal} instances are typically private * static fields in
classes that wish to associate state with a thread (
e.g., > a user ID > or Transaction ID).

For example, the class below generates unique identifiers local to each * thread. * A thread's id is assigned the first time it invokes {@code ThreadId.get()} * and remains unchanged on subsequent calls. */

对此我的理解是,ThreadLocal用于对线程的局部变量进行读写,每个线程都可以通过set()和get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。

我们通过一个小demo去体会ThreadLocal是怎么一回事。

编写一个用户上下文类,用于获取/写入当前用户

public class UserContext {
    static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();

    public User getThreadLocalUser(String name){
        return  threadLocalUser.get();
    }

    public  void setThreadLocalUser(User user){
        threadLocalUser.set(user);
    }

}

编写线程类,实现线程当前用户读写

    static class MyThread extends Thread {
       static  UserContext userContext = new UserContext();

        public void run() {
          User user = new User();
          //使用线程名作为用户名称
          user.setName(this.getName());
          userContext.setThreadLocalUser(user);
          System.out.println("获取"+this.getName()+"线程用户:"+this.getCurUser().getName());
        }

        private User getCurUser(){
            if( userContext.getThreadLocalUser()==null)
                return null;
            return userContext.getThreadLocalUser();
        }
    }

main调用实现:

扫描二维码关注公众号,回复: 9778618 查看本文章
    public static void main(String[] args) throws Exception {
        // 创建2个MyThread A,B
        MyThread mtA = new MyThread();
        mtA.setName("A");
        mtA.start();

        MyThread mtB =  new MyThread();
        mtB.setName("B");
        mtB.start();
    }

控制台输出结果如下:
在这里插入图片描述

这里可以看到A线程的用户是A,B线程用户是B。

到这里可能说,你的UserContext是每个线程里面的局部变量,怎么也说明不了隔离。那我们把UserContext放出去,通过线程构造方法设置进去。
具体实现

        UserContext userContext = new UserContext();
        MyThread mtA = new MyThread(userContext);
        mtA.setName("A");
        mtA.start();
        System.out.println(userContext.getThreadLocalUser());
        MyThread mtB =  new MyThread(userContext);
        mtB.setName("B");
        mtB.start();
        System.out.println(userContext.getThreadLocalUser());

控制台输出结果:
在这里插入图片描述

这里说明UserContext并不是同一个UserContext,在main方法里面的UserContext是属于主线程的ThreadLocal,同理AB,因此这里说明了ThreadLocal是属于线程的局部变量并不共享,且相互隔离。

2. ThreadLocal原理

在##1说了,ThreadLocal是实现对线程局部变量的读写,使得存在ThreadLocal的局部变量每个线程之间是互相隔离,通过get/set读写,通过ThreadLocal的get/set探究ThreadLocal的原理。

2.1 ThreadLocal.set

具体实现

  public void set(T var1) {
        Thread var2 = Thread.currentThread();
        ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
        if (var3 != null) {
            var3.set(this, var1);
        } else {
            this.createMap(var2, var1);
        }

    }

可以看到ThreadLocal的实现实际是一下这么一个链路:

Thread --> ThreadLocal --> ThreadMap --> map.set(ThreadLocal,val)

Thread里面都有一个变量 ThreadLocal.ThreadLocalMap ,用于存储线程的局部变量Map

  /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

而ThreadLocalMap是ThreadLocal的一个内部类。用Entry类来进行存储,我们的值都是存储到这个Map上的,key是当前ThreadLocal对象

 static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

其中Entry是一个弱引用对象,这里只需要知道就行,我们下面再详细说。

2.2 ThreadLocal.get

有了set的基础,就能知道get实际上是调用Thread中ThreadLocalMap.get

链路如下:

Thread --> ThreadLocal --> ThreadMap --> map.get(ThreadLocal)

2.3 原理总结

1.ThreadLocal是用于维护线程局部变量,线程间互不影响。
2.实际实现线程局部变量的读写的是ThreadLocalMap,ThreadLocalMap被Thread所维护。
3.ThreadLocal是作为ThreadLocalMap的存储对象的Key值。

3. ThreadLocal OOM 解决

大家都说 :

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

想要避免内存泄露就要手动remove()掉

我也想直接这样提示一下就行了,但是我还是试一下,到底怎么回事。
首先看一下Thread ThreadLocal在JVM的存储以及引用关系,可以结合Java线程基础-认识并区分Thread ThreadGroup ThreadLocal ThreadGroupContext(一)

在这里插入图片描述
这个图说明了ThreadLocalMap是归Thread管理,而ThreadLocal是ThreadLocalMap的key,他自己管理自己。也就是跟上面说的如果没有及时删除key,有可能导致内存溢出OOM。

为什么有可能呢?一般来说我们创建线程并不会无限制的创建,因此由于过量线程创建导致ThreadLocal OOM的情况是很少的,但是我们会使用线程池,他可比我们自己手动创建线程用得要多,在线程池的应用下,是否会造成ThreadLocal OOM呢?我们通过一个小demo实验一下。

创建一个大小为5的线程池,每次运行线程先输出线程里面读取的用户,然后再设置新值。

      UserContext userContext = new UserContext();
        ExecutorService exec = Executors.newFixedThreadPool(5);
        int loop= 100;
        for (int i = 0; i < 100; i++) {
            exec.execute(()->{
                System.out.println("当前线程:"+Thread.currentThread()+"==="+"用户:"+userContext.getThreadLocalUser().getName());
                userContext.setThreadLocalUser(new User(Thread.currentThread().getName()));
            });
        }

控制台输出如下结果
在这里插入图片描述
这个说明在线程池中线程的复用并不会销毁线程里面的ThreadLocal,当ThreadLocal积累得差不多就可以OOM了,为此我们设置jvm参数-Xms100m -Xmx100m,增大线程池大小以及创建次数,同时增大ThreadLocal存储对象大小。


  ExecutorService exec = Executors.newFixedThreadPool(99);
        for (int i = 0; i < 1000; i++) {
            exec.execute(() -> {
                threadLocal.set(new byte[1024 * 1024]);
                try {
                    TimeUnit.MILLISECONDS.sleep(50);
                } catch (InterruptedException e) {
                    System.out.println("当前线程:"+Thread.currentThread());
                    e.printStackTrace();
                }
        
            });
        }

很快控制台就出现OOM报错
java.lang.OutOfMemoryError: Java heap space

在这里插入图片描述

怎么避免OOM,用完把key也就是threadLocal处理就行了。
添加如下代码:

    finally {
                    threadLocal.remove();
                }

线程执行结束后,手动remove掉。
但是不方便啊,我们可以通过实现AutoCloseable 接口自动remove

public class MyThreadLocal  implements AutoCloseable {

    static ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
    public byte[] get(){
        return  threadLocal.get();
    }

    public  void set(byte[] user){
        threadLocal.set(user);
    }
    @Override
    public void close() {
        System.out.println("自动关闭");
        threadLocal.remove();
    }
}

调用方式

 ExecutorService exec = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            exec.execute(() -> {
                try ( MyThreadLocal threadLocal = new MyThreadLocal();){
                    threadLocal.set(new byte[1024 * 1024]);
                    TimeUnit.MILLISECONDS.sleep(50);
                } catch (InterruptedException e) {
                    System.out.println("当前线程:"+Thread.currentThread());
                    e.printStackTrace();
                }

            });
        }

这样就会执行一次循环后remove。
当然过大的loop还是会内存溢出,这时候适当控制线程池大小。

发布了32 篇原创文章 · 获赞 26 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_28540443/article/details/104675252