threadLocal

1. ThreadLocal辨析

ThreadLocal与Synchronize的比较

ThreadLocal和Synchronize都用于解决多线程并发访问,但是ThreadLocal与Synchronize有本质的区别。

  • Synchronized是利用锁的机制,使变量或代码块在某一时刻仅仅能被一个线程访问。

  • ThreadLocal则是为每个线程都提供了一个变量的副本,使得每个线程在某一时刻访问到的并非是同一个对象,这样就隔离了多个线程对数据的共享。

2. ThreadLocal的应用场景

Spring的事务就借助了ThreadLocal类。

Spring会从数据库连接池中获得一个Connection,然后会把Connection放到ThreadLocal中,也就和线程绑定了,事务需要提交或回滚的时候,只需要从ThreadLocal中拿到Connection进行操作

3.ThreadLocal的简单使用

ThreadLocal有四个方法

  1. void set(Object value)

    设置当前线程的线程局部变量的值

  2. public Object get()

    该方法返回当前线程所对应的线程局部变量

  3. public void remove()

    将当前线程句柄变量的值删除,目的是为了减少内存的占用。该方法是JDK5.0新增的方法。需要指出的是,当线程结束后,对应线程的局部变量将自动被垃圾回收,所以显示调用该方法清楚线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

  4. protected Object initialValue()

    返回该线程句柄变量的初始值,该方法是一个Protected方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第一次调用get()或set(Object)时才执行,并且只执行一次。ThreadLocal中的缺省实现是直接返回一个null。

public final static ThreadLocal<String> resource = new ThreadLocal<String>;

​ resource代表一个能存放String类型的ThreadLocal对象。此时不论什么一个线程都能够并发访问这个变量,对它进行写入,读取操作,都是线程安全的。

4. 实现解析

1666621127980


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();
}


private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    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);
    else
        createMap(t, value);
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

  • 调用ThreadLocal的set方法的时候,会获取当前线程,根据当前线程调用getMap(t)方法,第一次是为空的,调用createMap(t,value)方法;创建一个ThreadLocalMap对象,以ThreadLocal为key,传入进来的value为值。再次set的时候会覆盖
  • 调用ThreadLocal的get方法的时候。会根据当前的线程找到对应的ThreadLocalMap,获取对应key为ThreadLocal的Map
  • ThreadLocalMap里面是一个Entry对象,和Map的Entry类似,是一个K,V键值对。key为ThreadLocal对象,V为传进来的value,但是key为弱引用,当发生垃圾回收的时候,如果内存不足的话,key会被回收

所以,我们调用ThreadLocal的get方法,其实就是拿到当前线程对应的ThreadLocalMap对象,根据ThreadLocal为key找到对应的值

5. ThreadLocal内存泄漏分析

内存泄漏:

不再使用的对象无法被垃圾回收器回收

package com.wxc.thread.threadlocal;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * ThreadLocal造成内存泄漏演示
 */
@Slf4j
public class ThreadLocalOOM {
    private static final int TASK_LOOP_SIZE = 500;

    final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,
                                                                          5,
                                                                          1,
                                                                          TimeUnit.MINUTES,
                                                                          new LinkedBlockingDeque<>());

    static class LocalVariable {
        private byte[] bytes = new byte[1024 * 1024 * 5];
    }

    static ThreadLocal<LocalVariable> threadLocal = new ThreadLocal<LocalVariable>();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < TASK_LOOP_SIZE; i++) {
            poolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    threadLocal.set(new LocalVariable());
                    //                    new LocalVariable();
                    log.info("use local variable");
                    //                    threadLocal.remove();
                }
            });

            Thread.sleep(1000);
        }

        log.info("pool execute over...");
    }
}

1666623455299

每个Thread都维护一个ThreadLocalMap,这个映射表的key就是ThreadLocal本身,value是真正要存储的值,也就是说ThreadLocal本身并不存储值。它只是作为一个key来让线程从ThreadLocalMap获取Value。仔细观察ThreadLocalMap,这个Map是使用ThreadLocal的弱引用作为key的,弱引用的对象在GC时会被回收。因此,使用了ThreadLocal之后,引用链如下:

1666623515649

图中的虚线表示弱引用。

这样,当把threadLocal变量置为null以后,没有任何强引用指向ThreadLocal实例,所以ThreadLocal将会被gc回收。这样一来,ThreadLocalMap就会出现key为null的Entry,就没有办法访问这些key为null的value,如果当前线程迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Current Thread Ref -> Current Thread->ThreadLocalMap->Entry->Value,而这块value永远不会被访问到了,所以存在内存泄漏。

只有当前Thrad结束后,强引用断开,Current Thread,Map Value将全部被GC回收。最好的做法就是在不需要使用ThreadLocal变量后,都调用它的remove()方法,清除数据。

所以在代码中,虽然线程池里面的任务执行完毕了,但是线程池里面的5个线程会一直存在直到JVM退出,我们set了线程的localVariable变量后没有调用localVariable.remove()方法,导致线程池里面的5个线程的threadLocals变量里面的new LocalVariable()实例没有被释放。

其实考察ThreadLocal的实现,我们可以看见,无论是get(),set()在某些时候,都会调用expungeStaleEntry()方法来清除Entry中key为null的value,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄漏。只有remove()方法中显示调用了expungeStaleEntry方法

6. ThreadLocal为什么使用弱引用而不是使用强引用?

1.key 使用强引用

引用ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal对象的实例不会被回收,导致Entry内存泄漏

2.Key使用弱引用

引用ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal实例也会被回收。Value在下一次ThreadLocalMap调用set,get,remove()方法的时候都有机会被回收

比较两种情况,我们发现,由于ThreadLocalMap的生命周期和Thread的一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是弱引用可以多一层保障。

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

当使用线程池+ThreadLocal时要小心,因为这种情况下,线程是不断的重复运行的,从而也就造就了value可能造成积累的情况。

7.错误使用ThreadLocal导致线程不安全

package com.wxc.thread.threadlocal;

import com.wxc.thread.tools.SleepTools;

public class ThreadLocalUnSafe implements Runnable{

    public static Number number = new Number(0);

    public void run() {
        //每个线程计数加一
        number.setNum(number.getNum()+1);
        //将其存储到ThreadLocal中
        value.set(number);
        SleepTools.ms(2);
        //输出num值
        System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
    }

    public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new ThreadLocalUnSafe()).start();
        }
    }

    private static class Number {
        public Number(int num) {
            this.num = num;
        }

        private int num;

        public int getNum() {
            return num;
        }

        public void setNum(int num) {
            this.num = num;
        }

        @Override
        public String toString() {
            return "Number [num=" + num + "]";
        }
    }
}

因为Number是static,不是线程私有的,是每个线程所共享的。ThreadLocalMap保存的是一个对象的引用,每次线程修改的时候都是修改同一个引用对应的对象的值,所有线程的值值最终都会变成最后一个线程所修改的值。

posted on 2022-10-25 01:48  帅哥川  阅读(151)  评论(0编辑  收藏  举报

导航