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调用实现:

    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还是会内存溢出,这时候适当控制线程池大小。

Java中的线程是通过Thread类来实现的,Thread类封装了所有线程相关的方法和属性。下面是Thread类的部分源码: ```java public class Thread implements Runnable { //线程状态 private volatile int threadStatus = 0; private static final int RUNNING = 1; private static final int SHUTDOWN = -1; private static final int STOP = -2; private static final int TIDYING = 2; private static final int TERMINATED = 3; //线程优先级 public final static int MIN_PRIORITY = 1; public final static int NORM_PRIORITY = 5; public final static int MAX_PRIORITY = 10; //线程组 private ThreadGroup group; private Runnable target; private String name; private long stackSize; private long eetop; //线程ID private long tid; //线程本地存储 ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; //线程中断标志 private volatile boolean interrupted = false; private static final int HIBERNATE = 0; private static final int WAITING = 1; private static final int TIMED_WAITING = 2; private static final int BLOCKED = 3; private static final int NEW = 0; //线程锁 private Object parkBlocker; //线程中断处理 private void handleInterrupt() { if (this != Thread.currentThread()) throw new RuntimeException("Only the original thread can be interrupted"); if (this.interrupted) { park(); } } //线程休眠 public static void sleep(long millis) throws InterruptedException { Thread.sleep(millis, 0); } //中断线程 public void interrupt() { if (this != Thread.currentThread()) checkAccess(); synchronized (this) { interrupted = true; notifyAll(); } } //线程运行方法 @Override public void run() { if (target != null) { target.run(); } } } ``` 在Thread类中,我们可以看到一些重要的属性和方法,比如: - threadStatus: 线程状态,用整数表示,包含RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED五种状态。 - group: 线程所属的线程组。 - target: 线程要执行的任务。 - name: 线程名称。 - stackSize: 线程堆栈大小。 - tid: 线程ID。 - threadLocals: 线程本地存储。 - interrupted: 线程中断标志。 - parkBlocker: 线程锁。 - sleep(): 线程休眠方法。 - interrupt(): 中断线程方法。 - run(): 线程运行方法。 通过这些属性和方法,我们可以使用Java中的线程实现多线程编程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值