Java多线程 ThreadLocal

ThreadLocal

ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存。

ThreadLocal的作用(有什么用)?

多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步,同步的措施一般是加锁,这就需要使用者对锁有一定的了解,这显然加重了使用者的负担。那么有没有一种方式可以做到,当创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量呢?其实ThreadLocal就可以做这件事情,虽然ThreadLocal并不是为了解决这个问题而出现的。

ThreadLocal的原理

首先看一下ThreadLocal相关类的类图结构:
在这里插入图片描述由该图可知,Thread类中有一个threadLocals和一个inheritableThreadLocals,它们都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的Hashmap。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocal的 set或者get方法时才会创建它们。其实每个线程的本地变量不是存放在ThreadLocal 实例里面,而是存放在调用线程的 threadLocals变量里面。也就是说,ThreadLocal类型的本地变量存放在具体的线程内存空间中。ThreadLocal就是一个工具壳,它通过set方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用。如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的threadLocals变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal变量的remove方法,从当前线程的threadLocals里面删除该本地变量。另外,Thread里面的threadLocals为何被设计为map结构?很明显是因为每个线程可以关联多个ThreadLocal变量

在这里插入图片描述

下面简单分析ThreadLocal的 set、get 及 remove方法的实现逻辑。

set方法

代码(1)首先获取调用线程,然后使用当前线程作为参数调用getMap(t)方法,可以看到,getMap(t)的作用是获取线程自己的变量threadLocals,threadlocal变量被绑定到了线程的成员变量上。
如果getMap(t)的返回值不为空,则把value值设置到 threadLocals中,也就是把当前变量值放入当前线程的内存变量threadLocals 中。threadLocals是一个HashMap结构,其中key就是当前ThreadLocal的实例对象引用,value是通过set方法传递的值。 如果getMap(t)返回空值则说明是第一次调用set方法,这时创建当前线程的threadLocals变量。
// set()方法
public void set(T value) {
    
    
	// (1)获取当前线程
    Thread t = Thread.currentThread();
    // (2)将当前线程作为key,去查找对应的线程变量,找到则设置
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
    	// (3)第一次调用就创建当前线程对应的HashMap
        createMap(t, value);
}

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

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

get()方法

public T get() {
    
    
	// (4)获取当前线程
   Thread t = Thread.currentThread();
   //(5)获取当前线程的threadLocals变量
   ThreadLocalMap map = getMap(t);
   //(6)如果threadLocals不为null,则返回对应本地变量的值
   if (map != null) {
    
    
       ThreadLocalMap.Entry e = map.getEntry(this);
       if (e != null) {
    
    
           @SuppressWarnings("unchecked")
           T result = (T)e.value;
           return result;
       }
   }
   // ( 7) threadLocals为空则初始化当前线程的threadLocals成员变量
   return setInitialValue();
}

// setInitialValue()
private T setInitialValue() {
    
    
	// (8)初始化为null
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // (9)如果当前线程的threadLocals变量不为空
    if (map != null)
        map.set(this, value);
    else
    	//(10)如果当前线程的threadLocals变量为空
        createMap(t, value);
    return value;
}

// initialValue()
protected T initialValue() {
    
    
   return null;
}

remove()方法

public void remove() {
    
    
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

InheritableThreadLocal

ThreadLocal不支持继承性,也就是说,同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。为了解决这个问题,InheritableThreadLocal应运而生。InheritableThreadLocal继承自ThreadLocal,其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量。下面看一下InheritableThreadLocal的代码。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    
    
	//(1)
	protected T childValue(T parentValue) {
    
    
        return parentValue;
    }
	//(2)
	ThreadLocalMap getMap(Thread t) {
    
    
       return t.inheritableThreadLocals;
    }
	//(3)
	void createMap(Thread t, T firstValue) {
    
    
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }

由如上代码可知,InheritableThreadLocal继承了ThreadLocal,并重写了三个方法。由代码(3)可知,InheritableThreadLocal重写了createMap方法,那么现在当第一次调用set方法时,创建的是当前线程的inheritableThreadLocals变量的实例而不再是threadLocals。由代码(2)可知,当调用get方法获取当前线程内部的map变量时,获取的是inheritableThreadLocals而不再是threadLocals。

ThreadLocal内存泄漏问题

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。

ThreadLocalRandom类

我们知道生成随机数有Random这个类帮助我们,比如生成多个int类型的整数,我们会用nextInt()方法,在单线程下,没有任何问题。那如果在多线程条件下呢?我们先看一下nextInt()的源码:

protected int next(int bits) {
    
    
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
    
    
            oldseed = seed.get();
            nextseed = (oldseed * multiplier + addend) & mask;
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
    }

由此可见,新的随机数的生成需要两个步骤:·

  • 首先根据老的种子生成新的种子。
  • 然后根据新的种子来计算新的随机数。

由于oldseed = seed.get();的原子性问题,高并发情况下可能导致多个线程获取的旧的种子都是同一个,然后生成的新的种子也是相同的,最后的结果就是生成的随机数也是相同的。

为了弥补多线程高并发情况下Random的缺陷,在JUC包下新增了ThreadLocalRandom类。

首先看下ThreadLocalRandom 的类图结构:

![在这里插入图片描述](https://img-blog.csdnimg.cn/20210125192430649.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDUzMzEyOQ==,size_16,color_FFFFFF,t_70

从图中可以看出 ThreadLocalRandom类继承了Random类并重写了nextInt方法,在 ThreadLocalRandom类中并没有使用继承自Random类的原子性种子变量。在ThreadLocalRandom中并没有存放具体的种子,具体的种子存放在具体的调用线程的threadLocalRandomSeed变量里面。ThreadLocalRandom类似于ThreadLocal类,就是个工具类。当线程调用ThreadLocalRandom 的current方法时,ThreadLocalRandom负责初始化调用线程的threadLocalRandomSeed变量,也就是初始化种子。

当调用ThreadLocalRandom的 nextInt方法时,实际上是获取当前线程的threadLocalRandomSeed变量作为当前种子来计算新的种子,然后更新新的种子到当前线程的threadLocalRandomSeed变量,而后再根据新种子并使用具体算法计算随机数。这里需要注意的是,threadLocalRandomSeed变量就是Thread类里面的一个普通long变量,
它并不是原子性变量。其实道理很简单,因为这个变量是线程级别的,所以根本不需要使用原子性变量,如果你还是不理解可以思考下ThreadLocal的原理。

猜你喜欢

转载自blog.csdn.net/weixin_44533129/article/details/113111127