ThreadLocal从入门到放弃

1、ThreadLocal

1.1 定义

用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get或者set方法访问)时能保证各个线程的变量对应相对独立于其他线程内的变量。

作用是:提供了线程内部的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

1.2 示例

需求:线程隔离

在多线程并发的场景下,每个线程中变量都是相互隔离的

  • 线程A:设置变量1,获取变量1
  • 线程B:设置变量2,获取变量2

1.2.1 错误实现

public class ThreadLockDemo {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) {

        ThreadLockDemo demo = new ThreadLockDemo();

        for (int i = 1; i <= 10; i++) {

            new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.setName(Thread.currentThread().getName() + "的数据");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "--->" + demo.getName());
                }
            }, "线程" + i).start();
        }
    }
}

运行结果

线程4--->线程10的数据
线程3--->线程10的数据
线程8--->线程10的数据
线程10--->线程10的数据
线程2--->线程10的数据
线程9--->线程10的数据
线程7--->线程10的数据
线程5--->线程10的数据
线程6--->线程10的数据
线程1--->线程10的数据

1.2.2 使用 ThreadLocal 解决

ThreadLocal
1、set() 将变量绑定到当前线程中
2、get() 获取当前线程绑定的变量

public class ThreadLockDemo {

    ThreadLocal<String> threadLocal = new ThreadLocal<>();

    private String name;

//    public String getName() {
//        return name;
//    }
//
//    public void setName(String name) {
//        this.name = name;
//    }

    public String getName() {
        return threadLocal.get();
    }

    public void setName(String name) {
        threadLocal.set(name);
    }

    public static void main(String[] args) {

        ThreadLockDemo demo = new ThreadLockDemo();

        for (int i = 1; i <= 10; i++) {

            new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.setName(Thread.currentThread().getName() + "的数据");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "--->" + demo.getName());
                }
            }, "线程" + i).start();
        }
    }
}

运行结果

线程8--->线程8的数据
线程1--->线程1的数据
线程5--->线程5的数据
线程6--->线程6的数据
线程7--->线程7的数据
线程10--->线程10的数据
线程3--->线程3的数据
线程9--->线程9的数据
线程2--->线程2的数据
线程4--->线程4的数据

1.3 使用 synchronize 来处理

1.3.1 使用synchronize 加锁来实现

使用 synchronize 加锁也能解决这个问题

public class ThreadLockDemo {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) {

        ThreadLockDemo demo = new ThreadLockDemo();

        for (int i = 1; i <= 10; i++) {

            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (ThreadLockDemo.class){
                        demo.setName(Thread.currentThread().getName() + "的数据");
                        try {
                            TimeUnit.SECONDS.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "--->" + demo.getName());
                    }
                }
            }, "线程" + i).start();
        }
    }
}

运行也没有问题,但是加锁后,各个线程需要排队进入运行,性能会降低,造成该操作不是并发运行。

1.3.2 ThreadLocal 与 synchronize 的区别

​ 都用于处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不同。

synchronize ThreadLocal
原理 同步机制采用 以时间换空间的方式,只提供了一份变量,让不容的线程排队访问 ThreadLocal采用以空间换时间的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰
侧重点 多个线程之间访问资源的同步 多线程中让每个线程之间的数据互相隔离

2 在数据库连接案例中使用到了 ThreadLocal

  • 传递数据:保存每个线程绑定的数据,在需要的地方直接获取,避免参数直接传递带来的代码耦合问题
  • 线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失

3 ThreadLocal 的内部结构

JDK8 中 ThreadLocal 的设计是:

  • 每一个Thread线程内部都有一个Map(ThreadLocalMap)
  • Map 里边存储 ThreadLocal 对象(key)和线程的变量副本(value)
  • Thread 内部的Map是由ThreadLocal维护的,由ThreadLocal负责向 map 获取和设置线程的变量值
  • 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰
public void set(T value) {
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null)
  	map.set(this, value);
  else
 		createMap(t, value);
}
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();
}


/**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
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 remove() {
  ThreadLocalMap m = getMap(Thread.currentThread());
  if (m != null)
    m.remove(this);
}

4 为什么要使用弱引用

无论 ThreadLocalMap 中的 key 使用哪种类型引用都无法完全避免出现内存泄漏。

要避免内存泄漏有两种方式:

  • 使用完 ThreadLocal ,调用其 remove 方法删除对应的 Entry
  • 使用完 ThreadLocal,当前Thread也随之运行结束

第二种方式是不好控制,特别是使用线程池的时候,线程结束不会销毁的。

也就说,在使用完后,记得调用 remove,无论是强引用还是弱引用都不会有问题,那为什么要使用弱引用呢?

在 ThreadLocalMap 中的 set/getEntry 的方法中,会对 key 为 null(即 ThreadLocal 为 null)进行判断,如果为 null 的话,那么会对 value 设置为 null。

即在使用完 ThreadLocal ,CurrentThread 依然运行的情况下,就算忘记调用 remove 方法,弱引用比强引用多一层保障:弱引用的 ThreadLocal 会被回收,对应的 value 在下一次 ThreadLocalMap 调用 set/get/remove中的任意方法时都会被清除,从而避免内存泄漏。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章