(七)全面剖析Java并发编程之线程变量副本ThreadLocal原理分析

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 在之前的文章:彻底理解Java并发编程之Synchronized关键字实现原理剖析中我们曾初次谈到线程安全问题引发的"三要素":多线程、共享资源/临界资源、非原子性操作,简而言之:在同一时刻,多条线程同时对临界资源进行非原子性操作则有可能产生线程安全问题。

引言

在之前的文章:彻底理解Java并发编程之Synchronized关键字实现原理剖析中我们曾初次谈到线程安全问题引发的"三要素":多线程、共享资源/临界资源、非原子性操作,简而言之:在同一时刻,多条线程同时对临界资源进行非原子性操作则有可能产生线程安全问题。而如果想要解决线程安全问题,我们只需要破坏掉三要素中的任意条件即可,如下:

  • ①破坏多线程条件:同一时刻,一条线程对共享资源进行非原子性操作,不会产生线程安全问题
  • ②破坏共享资源条件:同一时刻多条线程对局部资源进行非原子性操作,也不会产生线程安全问题
  • ③破坏非原子性条件:同一时刻多条线程对共享资源进行原子性操作,也不会产生线程安全问题

“三要素”说法仅是个人理解,如有疑义可纠正

而在前面的文章中,我们曾谈到过CAS无锁机制、Synchronized隐式锁、ReetrantLock显式锁等都可以解决线程安全问题。而在这些方案当中,CAS机制是利用上面第三点:破坏非原子性条件,保证原子性来解决线程安全问题;Synchronized与ReetrantLock则是利用上述第一点:破坏多线程条件,在同一时刻只允许一条线程访问临界资源解决此问题。而本文谈到的ThreadLocal则是通过如上第二点:破坏共享资源条件解决线程安全问题。

一、ThreadLocal概念及使用浅析

ThreadLocal线程本地副本,在很多地方也被称为线程本地变量、线程局部存储等叫法,但总归来说都是形容ThreadLocal这一个东西。在执行时,ThreadLocal会为变量在每一条线程创建一个副本,这个副本只有每条线程自己可以访问。下面我们可以先看看ThreadLocal类以及它提供的一些方法:

// 省略方法体(后面源码再详细分析)
public class ThreadLocal<T> {
   
   
    // 构造函数
    public ThreadLocal() {
   
   }

    // 初始化方法:在创建ThreadLocal对象时可以使用该方法进行初始化设值
    protected T initialValue()

    // 获取ThreadLocal在当前线程中保存的变量副本
    public T get() 

    // 设置当前线程中变量的副本
    public void set(T value)

    // 移除当前线程中变量的副本
    public void remove()

    // 内部子类:扩展了ThreadLocal的初始化值的方法,支持Lambda表达式赋值
    static final class SuppliedThreadLocal<T> extends ThreadLocal<T>

    // 内部类:定制的hashMap,仅用于维护当前线程的本地变量值。
    // 仅ThreadLocal类对其有操作权限,是Thread的私有属性。
    // 为避免占用空间较大或生命周期较长的数据常驻于内存引发一系列问题,
    // hashtable的key是弱引用WeakReferences。
    // 当堆空间不足时,会清理未被引用的entry。
    static class ThreadLocalMap

    // 省略其他代码.......
}

如上便是ThreadLocal提供的一些主要方法,在创建ThreadLocal对象时可以initialValue()对变量副本进行初始化,也可以使用set()方法更改值或者设置线程变量副本,使用get()方法获取变量副本,而remove()则可以移除当前线程中变量的副本。我们先来看一个例子:

public class DBUtils {
   
   
    private static Connection connection = null;

    public static Connection getConnection() throws SQLException {
   
   
        if (connection == null)
            connection = DriverManager.getConnection(
                    "jdbc:mysql:127.0.0.1:3306/test?user=root&password=root");
        return connection;
    }

    public static void closeConnection() throws SQLException {
   
   
        if (connection != null)
            connection.close();
    }
}

假设有上面这么一个数据库连接工具类DBUtils,如上代码在单线程的环境下运行是没有问题的,但是如果把这个工具类丢在多线程的情况下则会出现问题。很显然,在获取连接getConnection()方法中,同一时刻如果有多条线程同时执行if (connection == null)判断则很有可能会导致创建多个连接对象。而因为connection是共享资源,所以在操作时也应该保证线程安全问题,不然在多线程情况下可能会造成:一条线程还在执行SQL,另外一条线程则调用closeConnection()方法关闭了连接对象。

所以如上这个例子我们该怎么解决遇到的问题?简单~

public class DBUtils {
   
   
    private static volatile Connection connection = null;

    public synchronized static Connection getConnection() throws SQLException {
   
   
        if (connection == null)
            connection = DriverManager.getConnection(
                    "jdbc:mysql:127.0.0.1:3306/test?user=root&password=root");
        return connection;
    }

    public synchronized static void closeConnection() throws SQLException {
   
   
        if (connection != null)
            connection.close();
    }
}

我们在共享变量connection加上volatile关键字修饰以及在操作临界资源的方法上添加synchronized关键字修饰,这样就能保证线程安全。或者我们也可以这样:

public class DBUtils {
   
   
    private static volatile Connection connection = null;
    private static ReentrantLock lock = new ReentrantLock();

    public static Connection getConnection() throws SQLException {
   
   
        lock.lock(); //获取锁
        if (connection == null)
            connection = DriverManager.getConnection(
                    "jdbc:mysql:127.0.0.1:3306/test?user=root&password=root");
        lock.unlock(); // 释放锁
        return connection;
    }

    public static void closeConnection() throws SQLException {
   
   
        lock.lock(); //获取锁
        if (connection != null)
            connection.close();
        lock.unlock(); // 释放锁
    }
}

但是上面的两种方式确实可以保证线程安全,但是带来的弊端也很明显:

当一条线程在执行SQL时,其他线程只能等待当前线程先处理完成之后才可以获取连接,这样会大大的影响程序的效率。

我们可以思考一下,此处到底是否需要将connection对象变成共享资源?结果显而易见,其实是不需要的,因为每条线程可以持有一个connection对象进行DB操作,每条线程之间对connection对象的操作是不存在任何依赖关系的。那我们能不能这样?

public class DBUtils {
   
   
    public static Connection getConnection() throws SQLException {
   
   
        return DriverManager.getConnection(
                    "jdbc:mysql:127.0.0.1:3306/test?user=root&password=root");;
    }

    public static void closeConnection(Connection connection) throws SQLException {
   
   
        if (connection != null)
            connection.close();
    }
}

理论上是可行的,因为由于每次线程操作DB时创建的都是不同的连接对象,自然也就不存在线程安全问题。但是由于线程每次访问DB都需要创建一个新的连接对象,用完之后再次关闭,在执行过程中会频繁的获取/关闭数据库连接,这样不但影响系统整体效率,还会导致给DB服务器造成巨大的压力,严重的情况下甚至会直接导致系统崩溃。

那么在这种情况下时,我们就可以使用ThreadLocal来解决此类问题,如下:

public class DBUtils {
   
   
    private static ThreadLocal<Connection> connectionHolder =
    new ThreadLocal<Connection>(){
   
   
        @SneakyThrows
        public Connection initialValue(){
   
   
            return DriverManager.getConnection(
                    "jdbc:mysql:127.0.0.1:3306/test?user=root&password=root");
        }
    };

    public static Connection getConnection() throws SQLException {
   
   
        return connectionHolder.get();
    }
}

在如上例子中,我们可以使用ThreadLocal为每个线程创建一个Connection变量副本,从而达到我们最开始所说的:ThreadLocal通过破坏共享资源条件解决线程安全问题,每条执行的线程操作的都是自己本地的副本变量,自然也就不构成“三要素”。

ThreadLocal使用场景

  • ①上下文(context)传递。一个对象需要在多个方法中层次传递使用,比如用户身份、任务信息、调用链ID、关联ID(如日志的uniqueID,方便串起多个日志)等,如果此时使用责任链模式给每个方法添加一个context参数会比较麻烦,而此时就可以使用ThreadLocal设置参数,需要使用时get一下即可。
  • ②线程间的数据隔离。如spring事务管理机制实现则使用到ThreadLocal来保证单个线程中的数据库操作使用的是同一个数据库连接。同时,采用这种方式可以使业务层使用事务时不需要感知并管理Connection连接对象,通过传播级别,能够巧妙地管理多个事务配置之间的切换,挂起和恢复。
  • ③ThreadLocal一般情况下,我们在项目开发过程中很少使用,而它更多应用则是在框架源码中应用,如Spring框架的事务隔离机制中的TransactionSynchronizationManager类,也包括Netty框架中的二次封装类FastThreadLocal等。
  • ④上个ThreadLocal的应用案例:
// 日期工具类
private static ThreadLocal<DateFormat> threadLocal = 
        ThreadLocal.withInitial(()-> 
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

public static Date parse(String dateStr) {
   
   
    Date date = null;
    try {
   
   
        date = threadLocal.get().parse(dateStr);
    } catch (ParseException e) {
   
   
        e.printStackTrace();
    }
    return date;
}

二、ThreadLocal原理分析

在前面我们对ThreadLocal进行了简单的讲解,而ThreadLocal作为一个存储类型的类,重点就是读写get()set()。现在我们则可以深入源码去一探ThreadLocal的神秘面纱。

2.1、ThreadLocal创建变量副本原理分析

先从ThreadLocal.set()方法开始:

// ThreadLocal类 → set()方法
public void set(T value) {
   
   
    // 获取当前执行线程
    Thread t = Thread.currentThread();
    // 获取当前线程的threadlocals成员变量
    ThreadLocalMap map = getMap(t);
    // 如果map不为空,则将value添加进map
    if (map != null)
        map.set(this, value);
    // 如果map为空则先为当前线程创建一个map再将value加入map
    else
        createMap(t, value);
}

ThreadLocal.set()方法中总归来说分为三步:

  • 调用getMap()获取当前线程的ThreadLocalMap
  • 如果map不为空则将传入的value值添加进map
  • 如果map为空则先为当前线程创建一个map再将value加入map

首先来看看getMap(Thread)方法:

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

是不是有些意外?在getMap(Thread)方法中是调用当前线程对象的成员变量threadLocals并返回的:

Thread类:ThreadLocal.ThreadLocalMap threadLocals = null;

可以看到,Thread类的成员变量threadLocals实则就是ThreadLocalMap,而ThreadLocalMap则是一个给ThreadLocal定制版的HashMap,也是ThreadLocal的内部类,如下:

// ThreadLocal类
public class ThreadLocal<T> {
   
   
    // ThreadLocal内部类:ThreadLocalMap
    static class ThreadLocalMap {
   
   
        // ThreadLocalMap内部类:Entry
        static class Entry extends WeakReference<ThreadLocal<?>> {
   
   
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
   
   
                super(k);
                value = v;
            }
        }
    }
}

ThreadLocalMap类中还存在一个内部类Entry,继承自WeakReference弱引用类型,结构如下:

1.png

再来看看createMap()方法:

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

至此,我们应该已经明白了ThreadLocal是如何为每个线程创建变量副本的:

在每条线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是每条线程用来存储变量副本的,key值为当前ThreadLocal对象,value为变量副本(即T类型的变量)。每个Thread线程对象最开始的threadLocals都为空,当线程调用ThreadLocal.set()或ThreadLocal.get()方法时(get方法待会而会分析到),都会调用createMap()方法对threadLocals进行初始化。然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

原理如下:

2.png

2.2、ThreadLocal获取变量副本原理分析

在上述过程中,我们已经分析了ThreadLocal创建变量副本原理,接下来我们再看看ThreadLocal.get()方法:

// ThreadLocal类 → get()方法
public T get() {
   
   
    // 获取当前执行线程
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
   
   
        // 如果map不为空,将当前ThreadLocal对象作为key获取对应值
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果获取的值不为空则返回获取到的value
        if (e != null) {
   
   
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果map为空则调用setInitialValue方法
    return setInitialValue();
}

ThreadLocal.get()方法中,总归来说分为三步:

  • 获取到当前执行线程,通过getMap(Thread)方法获取ThreadLocalMap类型的map
  • 将当前ThreadLocal对象this作为key尝试获取map中的<key,value>键值对,获取成功返回value
  • 如果第一步获取的map为空则调用setInitialValue()方法返回value

调用get()方法之后首先会获取当前线程的threadLocals成员变量(即ThreadLocalMap),如map不为空则以为this作为key获取ThreadLocal中存储的变量副本,如果为空则调用setInitialValue()方法:

// ThreadLocal类 → setInitialValue()方法
private T setInitialValue() {
   
   
    // 获取ThreadLocal初始化值
    T value = initialValue();
    Thread t = Thread.currentThread();
    // 获取当前线程的map
    ThreadLocalMap map = getMap(t);
    // 如果map不为空则将初始化值添加进map容器
    if (map != null)
        map.set(this, value);
    // 如果map为空则创建一个ThreadLocalMap容器
    else
        createMap(t, value);
    return value;
}

// ThreadLocal类 → initialValue()()方法
protected T initialValue() {
   
   
    return null;
}

setInitialValue()与前面分析的ThreadLocal.set(value)方法有些类似,在setInitialValue()方法中首先会调用initialValue()方法获取初始化值,而initialValue()方法默认是返回空的,但是initialValue()方法可以在创建ThreadLocal对象时进行重写,如下:

private static ThreadLocal<Object> threadlocal =
new ThreadLocal<Object>(){
   
   
    @SneakyThrows
    public Object initialValue(){
   
   
        return new Object();
    }
};

获取到初始化的值之后,再次获取当前线程的threadLocals,如果不为空则以this为key,初始值为value添加进map。如果当前线程的threadLocals为空,则先调用createMap(t, value);为当前线程创建一个ThreadLocalMap并将this和初始值以k-v形式加入map中,然后并将value返回,如果没有创建ThreadLocal对象时吗没有初始化值则返回null,至此整个ThreadLocal.get()方法结束。如下:

3.png

三、InheritableThreadLocal详解

通过上述的分析,不难得知ThreadLocal设计的目的就是为每条线程都开辟一块自己的局部变量存储区域(并不是为了解决线程安全问题设计的,不过使用ThreadLocal可以避免一定的线程安全问题产生),所以如果你想要将ThreadLocal中的数据共享给子线程时,实现起来将额外的困难。而InheritableThreadLocal则应运而生,InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,ok~。上个例子:

private static InheritableThreadLocal<String> itl = 
new InheritableThreadLocal<String>();
public static void main(String[] args) throws InterruptedException {
   
   
    System.out.println(Thread.currentThread().getName()
    + "......线程执行......");
    itl.set("竹子....");
    System.out.println("父线程:main线程赋值:竹子....");
    new Thread(()->{
   
   
        System.out.println(Thread.currentThread().getName()
        + "......线程执行......");
        System.out.println("子线程:T1线程读值:"+itl.get());
    },"T1").start();
    System.out.println("执行结束.....");
}

如上代码所示,创建一个InheritableThreadLocal类型变量itl,在父线程main中进行赋值操作,然后开启一条子线程T1进行读值操作,执行结果如下:

/*
 执行结果:
    main......线程执行......
    父线程:main线程赋值:竹子....
    执行结束.....
    T1......线程执行......
    子线程:T1线程读值:竹子....
*/

从结果中不难看出,子线程T1读取的值竟然是main父线程设置的值,这是为什么呢?下面我们看看InheritableThreadLocal的源码:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
   
   
    // 在父线程向子线程复制InheritableThreadLocal变量时使用
    protected T childValue(T parentValue) {
   
   
        return parentValue;
    }
    // 返回线程的inheritableThreadLocals成员变量
    ThreadLocalMap getMap(Thread t) {
   
   
       return t.inheritableThreadLocals;
    }
    // 为线程的成员变量inheritableThreadLocals进行初始化
    void createMap(Thread t, T firstValue) {
   
   
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

InheritableThreadLocal中重写了父类ThreadLocalgetMap()以及createMap()方法,在我们前面分析ThreadLocal时,曾提到过线程类Thread中存在一个成员变量threadlocals,而实则Thread中除开threadlocals成员之外,还存在另外一个成员变量inheritableThreadLocals,如下:

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

所以当操作InheritableThreadLocal变量时只影响线程的inheritableThreadLocals成员,而并不影响`threadlocals``成员。

3.1、InheritableThreadLocal父子线程传值原理

搞清楚InheritableThreadLocal构成之后,我们接着来分析一下父子线程传值究竟是如何实现的。我们一般在创建子线程时,都是直接选择new Thread()创建:

Thread t1 = new Thread();

接着会调用Thread类的构造函数创建线程对象:

// Thread类 → 构造函数
public Thread() {
   
   
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

// Thread类 → init()方法重载
private void init(ThreadGroup g, Runnable target, String name,
                long stackSize) {
   
   
    // 调用全参的init方法完成线程初始化
    init(g, target, name, stackSize, null, true);
}

// Thread类 → init()方法
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
   
   
    if (name == null) {
   
   
        throw new NullPointerException("name cannot be null");
    }
    this.name = name;
    // 获取当前执行线程作为父线程
    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    if (g == null) {
   
   
        // 确认创建出的线程是否为子线程
        // 如果SecurityManager不为空则获取SecurityManager的线程分组
        if (security != null) {
   
   
            g = security.getThreadGroup();
        }

        // 如果SecurityManager中没有为创建出的线程设置线程分组,
        // 则使用当前执行的线程parent的父线程组
        if (g == null) {
   
   
            g = parent.getThreadGroup();
        }
    }

    // 无论是否显式传入threadgroup,都要检查访问
    g.checkAccess();

    // 如果SecurityManager不为空则检查权限是否
    // 为SUBCLASS_IMPLEMENTATION_PERMISSION
    if (security != null) {
   
   
        if (isCCLOverridden(getClass())) {
   
   
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }
    g.addUnstarted();
    // 将当前执行线程设置为创建出的线程的父线程
    this.group = g;
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    // 获取线程上下文类加载器
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    // 为当前创建出的线程设置线程上下文类加载器
    this.inheritedAccessControlContext =
            acc != null ? acc : AccessController.getContext();
    this.target = target;
    setPriority(priority);
    // 重点!!!后面详细分析
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    // 为创建出的线程分配默认线程栈大小
    this.stackSize = stackSize;
    // 设置线程ID
    tid = nextThreadID();
}

如上便是线程创建时的初始化过程,在init()方法中有这么一段代码:

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

当采用默认方式创建子线程时,一条线程执行new指令创建Thread对象的方式被称为默认方式,而这种方式会将当前执行创建逻辑的线程设置为创建出来的线程的父线程。如果父线程的inheritableThreadLocals成员变量不为空,那么则会执行this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);,将父线程inheritableThreadLocals传递至子线程。接着可以再看看ThreadLocal.createInheritedMap()方法:

// ThreadLocal类 -> createInheritedMap()方法
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
   
   
    return new ThreadLocalMap(parentMap);
}

//  ThreadLocalMap类 -> 私有构造函数
// 构建一个包含所有parentMap中Inheritable ThreadLocals的ThreadLocalMap
// 该函数只被createInheritedMap()调用.
private ThreadLocalMap(ThreadLocalMap parentMap) {
   
   
    // 获取父线程的所有Entry
    Entry[] parentTable = parentMap.table;
    // 获取父线程的Entry数量
    int len = parentTable.length;
    setThreshold(len);
    // ThreadLocalMap使用Entry[] table存储ThreadLocal
    table = new Entry[len];

    // 挨个复制父线程中map的Entry
    for (int j = 0; j < len; j++) {
   
   
        Entry e = parentTable[j];
        if (e != null) {
   
   
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
   
   
                // 为什么这里不是直接赋值而是使用childValue方法?
                // 因为childValue内部是直接将e.value返回的,
                // 这样实现的目的可能是为了保证代码最大程度上的拓展性
                // 因为可以重写childValue()覆盖
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

当调用ThreadLocal.createInheritedMap()方法后会将父线程中inheritableThreadLocals成员的所有Entry全部复制一遍给子线程的inheritableThreadLocals成员,至此,整个创建过程完成。从这个流程中我们可以得知:父子线程传值的实现是通过创建线程时复制inheritableThreadLocals的所有Entry实现的。

四、ThreadLocalMap原理剖析

ThreadLocal的原理是涉及三个核心类:ThreadLocalThread以及ThreadLocalMap类。在Thread类中存在两个成员变量:threadLocalsinheritableThreadLocals,这两个成员变量的类型都为ThreadLocalMap,经过一系列分析后我们可以得知,这两个成员变量是存储线程变量副本的最终容器,而前面也曾提到过:ThreadLocalMapThreadLocal中定制版的HashMap,但是它并没有实现Map接口,而是自己内部通过数组类型存储Entry实现。而Entry只是简单的继承了WeakReference弱引用,并没有没有实现类似HashMapNode.next的后继节点指向,所以ThreadLocalMap并不是链表形式的实现。哪没有了链表结构之后,ThreadLocalMap是如何解决哈希冲突的呢?下面可以从源码角度分析得知:

// ThreadLocalMap类 → Entry静态内部类
static class Entry extends WeakReference<ThreadLocal<?>> {
   
   
    // value:存储的变量副本
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
   
   
           super(k);
            value = v;
    }
}

// ThreadLocalMap类 → 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
   
   
    // 成员变量table(数组结构),INITIAL_CAPACITY值为16的常量
    table = new Entry[INITIAL_CAPACITY];
    // 位运算,类似于取模算法,计算出需要存放的位置
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

从如上代码不难得知,在调用createMap()方法创建ThreadLocalMap示例时,在ThreadLocalMap的构造方法中,会为成员变量table初始化一个长度为16的Entry数组,通过hashCodelength位运算确定出一个下标索引值i,这个i就是被存储在table数组中的下标位置。那么现在可以来个简单的例子理解一下:

ThreadLocal<Zero> tl0 = new ThreadLocal<Zero>();
ThreadLocal<One> tl1 = new ThreadLocal<One>();
ThreadLocal<Two> tl2 = new ThreadLocal<Two>();

new Thread(()->{
   
   
    tl0.set(new Zero());
    tl1.set(new One());
    tl2.set(new Two());
},"T1").start();

new Thread(()->{
   
   
    tl0.set(new Zero());
    tl1.set(new One());
    tl2.set(new Two());
},"T2").start();

在案例中,创建了三个ThreadLocal对象:tl0、tl1、tl2以及两个线程对象:T1、T2,经过前面分析我们知道,在每个Thread对象中都维护着一个ThreadLocalMap类型的成员变量threadlocals存储每条线程的副本变量。所以,T1、T2内部分别都维护着一个ThreadLocalMap,当T1、T2操作tl0、tl1、tl2时,Zero、One、Two都会以key-value的形式存储在数组的不同位置,这个数组就是前面提到的ThreadLocalMap类中的成员Entry[] table。哪又是怎么确定tl0-Zero、tl1-One、tl2-Two这三组K-Vtable中的存储位置呢?如下:

  //ThreadLocalMap类 → set()方法
  private void set(ThreadLocal<?> key, Object value) {
   
   
    // 获取table及其长度
    Entry[] tab = table;
    int len = tab.length;
    // 使用key的哈希值和数组长度计算获取索引值
    int i = key.threadLocalHashCode & (len-1);

    // 遍历table如果已经存在则更新值,不存在则创建
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
   
   
        ThreadLocal<?> k = e.get();
        // 如果key相同,则使用新value替换老value
        if (k == key) {
   
   
            e.value = value;
            return;
        }
        // 如果table[i]为空则创建新的Entry存储
        if (k == null) {
   
   
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // table[i]不为null且key不相同的情况下,
    // 如果遍历完数组也没有找到为null的位置,
    // 则代表数组需要扩容,则将数组扩容两倍
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 如果清理过期的数据之后,数组内的可用数据还占
    // 3/4的情况下,直接扩容两倍
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

我们可以从源码中不难发现,在set()方法开始后,会首先获取table的长度和ThreadLocal对象的哈希值用于计算出一个下标索引值iint i = key.threadLocalHashCode & (len-1);

// ThreadLocal中threadLocalHashCode相关代码
private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode =
    new AtomicInteger();

// 0x61c88647为斐波那契散列乘数,哈希得到的结果会比较分散
private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
   
   
    // 原子计数器自增
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

因为ThreadLocal中哈希码相关的成员都是静态static关键字修饰的原因,每次创建ThreadLocal对象时,都会在对象初始化的时候调用一次自增方法为ThreadLocal对象生成一个哈希值:

private final int threadLocalHashCode = nextHashCode();

HASH_INCREMENT=0x61c88647是因为0x61c88647为斐波那契散列乘数,通过它散列(hash)出来的结果分布会比较均匀,可以很大程度上避免hash冲突。\
经过如上分析我们能够得到一个结论:每条线程的threadlocals都会在内部维护独立table数组,而每个ThreadLocal对象在不同的线程table中位置都是相同的。对于同一条线程而言,不同的ThreadLocal变量副本都会被封装成一个个的Entry对象存储在自己内部的table中。

ok~,接着往下说,经过int i = key.threadLocalHashCode & (len-1);计算出索引下标值之后,会开始遍历table,然后会开始判断,如果table[i]位置不为空,但是原本的key值和现在新的key值是相同的情况下,则使用现在的新值替换掉之前的老值,刷新value值并返回:

if (k == key) {
   
   
    e.value = value;
    return;
}

如果table[i]位置为空,则创建一个的Entry对象封装K-V值并将该对象放在table[i]位置:

if (k == null) {
   
   
    replaceStaleEntry(key, value, i);
    return;
}

如果table[i]位置不为空并且Key不相同时,哪就调用nextIndex(i,len)获取下一个位置信息并判断下一个位置是否为空,直到找到为空的位置为止:

e = tab[i = nextIndex(i, len)] // 在for循环的末尾循环体

table[i]位置不为空并且Key不相同的情况下,如果遍历完整个table数组也没有找到为空的下标位置时,代表数组已经存满了需要扩容,则调用rehash()对数组扩容两倍:

// 满足条件table数组扩容两倍
if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();

至此整个ThreadLocalMap存储过程结束,如下:

4.png

接下来再看看ThreadLocalMap的get原理:

// ThreadLocal类 -> ThreadLocalMap内部类 -> getEntry()方法
private Entry getEntry(ThreadLocal<?> key) {
   
   
    // 通过`ThreadLocal`对象的哈希值跟`table`数组长度
    // 进行计算获取下标索引值`i`
    int i = key.threadLocalHashCode & (table.length - 1);
    // 获取table[i]位置的元素,如果不为空并且key相同则返回
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    // 如果key不相同则遍历整个table[i]之后的元素获取对应key的值
    else
        return getEntryAfterMiss(key, i, e);
}

// ThreadLocal类 -> ThreadLocalMap内部类 -> getEntryAfterMiss()方法
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
   
   
    Entry[] tab = table;
    int len = tab.length;
    // 遍历整个table[i]之后的元素
    while (e != null) {
   
   
        ThreadLocal<?> k = e.get();
        // 如果key相同则返回对应的元素
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

与前面分析的set同理,在get时,也会根据ThreadLocal对象的哈希值跟table数组长度进行计算获取下标索引值i,然后判断该位置Entry对象的key值与get(key)的key是否相同,如果相同则直接获取该位置的值并返回。如果不相同则遍历整个数组中table[i]之后的所有元素,循环判断下一个位置的key是否与传入进来的key一致,如果一致则获取返回。

五、ThreadLocal注意事项

5.1、ThreadLocal线程安全问题

ThreadLocal虽然能够在一定程度上解决线程安全问题,但ThreadLocal设计的初衷是为每条线程开辟一块自己的存储空间。所以如果ThreadLocal.set()的对象如果是共享的,多线程情况下也会造成线程安全问题的出现。

5.2、ThreadLocal副本变量的产生

ThreadLocal的变量并不是每条线程拷贝克隆一个对象,而是每个线程新建一个。

5.3、ThreadLocal在线程池情况下可能会产生脏数据

因为线程池会复用线程,而线程上一个执行的任务对ThreadLocal进行set()操作后,在线程run()结束后没有调用remove()移除变量副本,下个Runnable任务如果直接对ThreadLocal进行get()操作则可能读到脏数据。

5.4、ThreadLocal可能会造成内存泄露

ThreadLocalMap中存储变量副本时,Entry对象使用ThreadLocal的弱引用作为key,如果一个ThreadLocal对象没有外部强引用来指向它,在堆内存不足时GC机制会回收掉这些弱引用类型的key,则会造成ThreadLocalMap<null,Object>的情况,同时线程也迟迟不结束(比如线程池中的常驻线程),那么这些key=null的value值则会一直存在一条强引用链:Thread.threadlocals(Reference)成员变量 -> ThreadLocalMap对象 -> Entry对象 -> Object value对象导致GC无法回收造成内存泄露,这个Object就是泄露的对象。至于为什么要将key设置成弱引用类型的原因:

因为key如果不设计成弱引用类型的情况下,会造成entry中value出现内存泄漏的场景

解决方案

关于5.3和5.4的两个问题,我们可以在使用完ThreadLocal手动调用ThreadLocal.remove()方法清空ThreadLocal变量副本即可解决。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
1月前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
16天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
21 0
|
21天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
19天前
|
缓存 Java 调度
多线程编程核心:上下文切换深度解析
在现代计算机系统中,多线程编程已成为提高程序性能和响应速度的关键技术。然而,多线程编程中一个不可避免的概念就是上下文切换(Context Switching)。本文将深入探讨上下文切换的概念、原因、影响以及优化策略,帮助你在工作和学习中深入理解这一技术干货。
37 10
|
21天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
15天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
15天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
40 3
|
19天前
|
调度 开发者
核心概念解析:进程与线程的对比分析
在操作系统和计算机编程领域,进程和线程是两个基本而核心的概念。它们是程序执行和资源管理的基础,但它们之间存在显著的差异。本文将深入探讨进程与线程的区别,并分析它们在现代软件开发中的应用和重要性。
38 4
|
19天前
|
算法 调度 开发者
多线程编程核心:上下文切换深度解析
在多线程编程中,上下文切换是一个至关重要的概念,它直接影响到程序的性能和响应速度。本文将深入探讨上下文切换的含义、原因、影响以及如何优化,帮助你在工作和学习中更好地理解和应用多线程技术。
30 4
|
21天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
77 6
下一篇
DataWorks