Java并发编程之 ThreadLocal

x33g5p2x  于2021-10-06 转载在 Java  
字(8.0k)|赞(0)|评价(0)|浏览(672)

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的另一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。

ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

ThreadLocal的基本用法

一个线程不安全的例子

public class Main {
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        Main main = new Main();
        ThreadPoolExecutor pool = new ThreadPoolExecutor(10, 10, 0, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 10; i++) {
            pool.execute(() -> {
                main.setContent(Thread.currentThread().getName() + "的数据");
                System.out.println(Thread.currentThread().getName() + "--->" + main.getContent());
            });
        }
        pool.shutdown();
    }
}

发生这样的情况是因为在setContent()和getContent()方法之间有可能会被其他线程插队

synchronized保证线程安全

public class Main {
    private String content;
    public final Object lock = new Object();

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        Main main = new Main();
        ThreadPoolExecutor pool = new ThreadPoolExecutor(10, 10, 0, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 10; i++) {
            pool.execute(() -> {
                synchronized (main.lock) {
                    main.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println(Thread.currentThread().getName() + "--->" + main.getContent());
                }
            });
        }
        pool.shutdown();
    }
}

ReentrantLock保证线程安全

public class Main {
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        Main main = new Main();
        ThreadPoolExecutor pool = new ThreadPoolExecutor(10, 10, 0, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 10; i++) {
            pool.execute(() -> {
                ReentrantLock lock = new ReentrantLock();
                lock.lock();
                try {
                    main.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println(Thread.currentThread().getName() + "--->" + main.getContent());
                } finally {
                    lock.unlock();
                }
            });
        }
        pool.shutdown();
    }
}

ThreadLocal保证线程安全

public class Main {
    private String content;
    ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public String getContent() {
        // return this.content;
        return threadLocal.get();
    }

    public void setContent(String content) {
        // this.content = content;
        threadLocal.set(content);
    }

    public void removeThreadLocal() {
        threadLocal.remove();
    }

    public static void main(String[] args) {
        Main main = new Main();
        ThreadPoolExecutor pool = new ThreadPoolExecutor(10, 10, 0, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 10; i++) {
            pool.execute(() -> {
                main.setContent(Thread.currentThread().getName() + "的数据");
                System.out.println(Thread.currentThread().getName() + "--->" + main.getContent());
            });
        }
        pool.shutdown();
        main.removeThreadLocal();
    }
}

ThreadLocal同步和加锁同步的区别

二者处理问题的角度不同。

  • **加锁:**以时间换空间,只有一份变量,加上锁让其他线程排队访问。
  • **ThreadLocal:**以空间换时间,给每一个线程都提供一份变量的副本,各个线程可以同时操作自己的副本变量。

ThreadLocal的内部结构

  • 每个Thread线程内部都有一个Map(ThreadLocalMap)
  • Map里面存储的是ThreadLocal对象(key)和线程的变量副本(value)
  • Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向Map获取和设置线程的变量值
  • 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰

ThreadLocal的变迁

这样的改变有什么好处?

  • 每个 Map 存储的 Entry 数量变少,因为原来的 Entry 数量是由 Thread 决定,而现在是由 ThreadLocal 决定的。真实开发中,Thread 的数量远远大于 ThreadLocal 的数量
  • 当 Thread 销毁的时候,ThreadLocalMap 也会随之销毁,因为 ThreadLocal 是存放在 Thread 中的,随着 Thread 销毁而消失,能降低开销

核心方法

set()

  1. 获取当前线程对象及它的ThreadLocalMap
  2. 如果ThreadLocalMap不为空,将Entry<ThreadLocal对象, 参数value>添加到ThreadLocalMap
  3. 如果ThreadLocalMap为空,给该线程对象创建一个ThreadLocalMap并将Entry<ThreadLocal对象, 参数value>设置进去
public void set(T value) {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取此线程对象中维护的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // map不为空,设置 <K, V>
        map.set(this, value);
    else
        // map为空,创建一个ThreadLocalMap并将 K, V 放入map中
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

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

get()

  1. 获取当前线程对象及它的ThreadLocalMap
  2. 如果ThreadLocalMap不为空,则以当前ThreadLocal对象作为key获取Entry
  3. 如果获取到的Entry不为空,则返回它的value
  4. 如果ThreadLocalMap为空或Entry为空,则通过initialValue()函数获取初始值value,然后用当前ThreadLocal对象的引用作为key,自定义初始值value作为value创建一个新的ThreadLocalMap

获取当前线程的ThreadLcoalMap进而获取Entry,如果存在则返回值,不存在则返回initialValue()指定的初始值

public T get() {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取此线程对象中维护的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // map不为空,尝试获取ThreadLocalMap中当前ThreadLocal对象做key的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果获取到了,返回value
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 初始化
    // 1.ThreadLocalMap不存在,表示此线程没有维护的ThreadLocalMap对象
    // 2.ThreadLocalMap存在,但是没有找到以当前ThreadLocal对象做key的Entry
    return setInitialValue();
}

private T setInitialValue() {
    // 调用initialValue()获取初始化的值
    // 子类不重写,默认为null
    T value = initialValue();
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取此线程对象中维护的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 存在,设置Entry
        map.set(this, value);
    else
        // 不存在,创建一个ThreadLocalMap并设置Entry
        createMap(t, value);
    return value;
}

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

remove()

删除当前线程中保存ThreadLcoal的Entry

  1. 首先获取当前线程,并根据当前线程获取一个ThreadLocalMap
  2. 如果ThreadLocalMap不为空,则移除当前ThreadLocal对象对应的Entry
public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }

initialValue()

此方法的作用是返回该线程局部变量的初始值

  • 这个方法是一个延迟调用方法,从上面的代码我们得知,在set()方法还未调用而先调用了get()方法时才执行,并且仅执行1次
  • 这个方法缺省实现直接返回一个null
  • 如果想要一个除null之外的初始值,可以重写此方法。(备注∶该方法是一个protected的方法,显然是为了让子类覆盖而设计的)
protected T initialValue() {
    return null;
}

ThreadLocalMap

ThreadLocalMap是ThreadLocal的静态内部类,没有实现Map接口,它的Map功能是独立实现的,这个Map解决哈希冲突用的线性探测法(将Entry数组看作一个环形数组,循环探测)而非JDK的Map用的拉链法,内部的Entry也是独立实现的。

核心参数

// 初始容量,必须是2^n
private static final int INITIAL_CAPACITY = 16;

// 存放数据的table,数组长度必须为2^n
private Entry[] table;

// 数组里面Entry的个数,用于判断table当前使用量是否超过阈值
private int size = 0;

// 进行扩容的阈值,默认为0,数组中的Entry个数大于它的时候需要扩容
private int threshold;

// Entry是ThreadLocalMap的静态内部类
// key是弱引用WeakReference,目的是让ThreadLocal对象的生命周期与线程(Thread对象)生命周期进行解绑
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    // key必须为ThreadLocal对象
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

弱引用和内存泄漏

弱引用
  • 强引用:传统引用,只要对象存在强引用,永不回收
  • 软引用(SoftReference):堆内存不满时,不会回收软引用的对象;堆内存已满需要腾出空间时,软引用的对象会被清理
  • 弱引用(WeakReference):弱引用的对象只要发生垃圾回收就会被清理
  • 虚引用(PhantomReference):是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知
内存泄漏
  • 内存溢出(Memory Overflow):内存溢出,没有足够的内存提供给申请者使用
  • 内存泄漏(Memory Leak):内存泄漏是指程序中已动态分配的堆内存由于某种原因导致程序内有释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至崩溃。内存泄漏最终会导致内存溢出。
为什么会发生内存泄漏

假设在业务中使用完ThreadLocal对象了,ThreadLcoal Ref被回收了,但是由于Entry仍然具有对堆中ThreadLocal对象的引用:

如果Entry的key对ThreadLocal对象的引用是强引用,在没有调用ThreadLocal对象的remove()方法以及当前线程仍然还在运行的前提下,始终有着强引用链Thread Ref -> Thread Obj -> ThreadLocalMap Obj -> Entry Obj,导致Entry无法被回收,进而导致内存泄漏。

如果Entry的key对ThreadLocal对象的引用是弱引用(绿色虚线),ThreadLcoal Ref被回收后,由于没有任何强引用连接,只有一个弱引用连接,那么堆中的ThreadLocal对象就可以被顺利回收,Entry中的key变为null。但是在没有调用ThreadLocal对象的remove()方法以及当前线程仍然还在运行的前提下,依然有着强引用链Thread Ref -> Thread Obj -> ThreadLocalMap Obj -> Entry Obj -> value Obj,导致Entry的value对象无法被回收,进而导致内存泄漏。

可见,即便Entry的key为弱引用,还是有可能会发生内存泄漏的。因此,内存泄露产生的原因不在于Entry的key是强引用还是弱引用。

ThreadLcoal内存泄漏的根本原因是ThreadLocalMap对象的生命周期跟Thread对象一样长,导致ThreadLocalMap底层的Entry数组中的Entry对象的value(在不调用remove()方法和线程运行完成之前)无法被回收。

为什么使用弱引用

使用弱引用会将不用的ThreadLocal对象及时回收,对应的Entry的key会值为null,而在ThreadLocalMap的set()replaceStaleEntry())、getEntry()expungeStaleEntry())、remove()expungeStaleEntry())方法中对会对key为null而value不为null的Entry进行处理(replaceStaleEntry()方法中,如果key为null,则将value置为null,在将这个Entry数组的对应位置替换为一个新的Entry;expungeStaleEntry()方法中,如果key为null,则将value置为null,再将这个Entry所在的数组的这个格子置为null)。

为什么set()remove()需要成对执行

因为ThreadLocal一般都是配合线程池来用的,线程池资源不释放,导致这引用链一直存在没法释放value资源,进而导致内存溢出

相关文章