【JUC剖析】ThreadLocal类 详解

shadowLogo

我们在阅读 Mybatis框架源码 时,会看到一个 ThreadLocal类
而这个类,常常作为面试考点,
可以看出:

我们若是想要 了解底层实现,学习高妙的编程思维,就不得不来学习 ThreadLocal类

骄傲
那么,在本篇博文中,本人就来讲解下 ThreadLocal类

基本概念:

官方简介:

此类提供 线程局部变量
这些变量与普通变量不同,每个线程(通过其get或set方法)的线程都有其 自己的独立初始化的 变量副本
ThreadLocal实例 通常是希望将 状态线程 关联的类 中的 私有静态字段
(例如,用户ID或交易ID)

例如,下面的类生成每个线程本地的唯一标识符。
线程的ID是在第一次调用ThreadId.get()时分配的,并且在后续调用中保持不变

import java.util.concurrent.atomic.AtomicInteger;
 
  public class ThreadId {
      // Atomic integer containing the next thread ID to be assigned
      private static final AtomicInteger nextId = new AtomicInteger(0);
 
      // Thread local variable containing each thread's ID
      private static final ThreadLocal<Integer> threadId =
          new ThreadLocal<Integer>() {
              @Override protected Integer initialValue() {
                  return nextId.getAndIncrement();
          }
      };
 
      // Returns the current thread's unique ID, assigning it if necessary
      public static int get() {
          return threadId.get();
      }
  }

只要线程是 active 并且 ThreadLocal实例是可访问的,则 每个线程 都对其 线程局部变量的副本 持有 隐式引用
线程 失活 后,其线程 本地实例的所有副本 都将进行 垃圾回收
除非存在对这些副本的其他引用

JDK官方文档 中的描述:

ThreadLocal类 用来 提供 线程内部的局部变量
这种变量在 多线程环境 下访问(通过 get和set方法 访问)时能保证 各个线程的变量 相对 独立其他线程内的变量
ThreadLocal实例 通常来说都是 private static权限 的,用于关联 线程线程上下文

我们可以得知 ThreadLocal 的 作用 是:

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


那么,本人来总结官方介绍 中的 重要内容

总结:

  1. 线程并发:
    多线程并发 的场景下
  2. 传递数据:
    我们可以通过ThreadLocal在 同一线程不同组件 中传递 公共变量
  3. 线程隔离:
    每个线程的变量都是 独立 的,不会相互影响

首先,我们来认识几个ThreadLocal的常用方法

常用方法:

方法声明 描述
new ThreadLocal() 创建ThreadLocal对象
public void set(T value) 设置 当前线程绑定的局部变量
public T get() 获取 当前线程绑定的局部变量
public void remove() 移除 当前线程绑定的局部变量

那么,本人现在来通过一个案例来展示下 ThreadLocal类使用

案例:

首先,我们来故意制造一个有 线程安全问题 的代码:

问题出现:

package edu.youzg.threadlocal;

public class MyDemo {
    private String content;

    private String getContent() {
        return content;
    }

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

    public static void main(String[] args) {
        MyDemo demo = new MyDemo();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                demo.setContent(Thread.currentThread().getName() + "的数据");
                System.out.println(Thread.currentThread().getName() + "->" + demo.getContent());
            }, "线程" + i).start();
        }
    }

}

那么,现在本人来展示下 运行结果
打印结果
从上面的运行结果,我们可以看出:

多个线程 在访问 同一变量 的时候出现的异常,线程间的数据 没有隔离

下面我们来看下采用 ThreadLocal 的方式来解决这个例子的问题:

问题解决:

package edu.youzg.threadlocal;

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

    private String getContent() {
        return content.get();
    }

    private void setContent(String content) {
        this.content.set(content);
    }

    public static void main(String[] args) {
        MyDemo demo = new MyDemo();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                demo.setContent(Thread.currentThread().getName() + "的数据");
                System.out.println(Thread.currentThread().getName() + "->" + demo.getContent());
            }, "线程" + i).start();
        }
    }
}

本人来展示下 运行结果
运行结果
从上面的使用案例中,我们能够看出:

ThreadLocal 很好地 解决了 多线程之间数据隔离 问题


谈及 并发同步,我们就不得不来讲解下 synchronized关键字

ThreadLocal 与 synchronized:

​可能会有同学会觉得在上述例子中,我们完全可以通过加锁来实现这个功能。
那么,本人首先来用 synchronized关键字,来解决上述 线程安全问题

synchronized 解决方案:

package edu.youzg.threadlocal;

public class MyDemo {
    private String content;

    public String getContent() {
        return content;
    }

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

    public static void main(String[] args) {
        MyDemo demo = new MyDemo();

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (MyDemo.class) {
                    demo.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println(Thread.currentThread().getName() + "->" + demo.getContent());
                }
            }, "线程" + i).start();
        }
    }
}

运行结果 如下:
运行结果
从上述运行结果,可以发现:

加锁确实可以解决这个问题,
但是在这里我们强调的是 线程数据隔离问题
不是 多线程共享数据的问题
因此,在这个案例中使用 synchronized关键字 是不合适的,有点 “杀鸡用牛刀” 的感觉... ...

黄鸡


那么,根据上述例子,本人来总结下 两者的区别

区别:

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

那么,依据上文原理,本人来总结下 ThreadLocal和synchronized:

总结:

虽然使用 ThreadLocalsynchronized 都能解决问题,
但是使用 ThreadLocal 更为合适
因为这样可以使程序拥有 更高的并发性


相信认真看完了上文的讲解,同学们都大致明白了 ThreadLocal的 概念及用法
那么,在讲解完 “是什么” 问题后,本人来讲解下 代表 “怎么做” 的 运用场景 问题:

适用场景:

说明:

关于 ThreadLocal类,有一个非常重要的 使用场景 —— 事务

案例:

场景构建:

这里我们先构建一个简单的转账场景: 有一个数据表account,里面有两个用户Jack和Rose,用户Jack 给用户Rose 转账。

案例的实现就简单的用mysql数据库,JDBC 和 C3P0 框架实现。以下是详细代码 :

(1) 项目结构
项目结构

(2) 数据准备

-- 使用数据库
use test;
-- 创建一张账户表
create table account(
	id int primary key auto_increment,
	name varchar(20),
	money double
);
-- 初始化数据
insert into account values(null, 'Jack', 1000);
insert into account values(null, 'Rose', 1000);

​ (3) C3P0配置文件和工具类

<c3p0-config>
  <!-- 使用默认的配置读取连接池对象 -->
  <default-config>
  	<!--  连接参数 -->
    <property name="driverClass">com.mysql.jdbc.Driver</property>
    <property name="jdbcUrl">jdbc:mysql://localhost:3306/test</property>
    <property name="user">root</property>
    <property name="password">1234</property>
    
    <!-- 连接池参数 -->
    <property name="initialPoolSize">5</property>
    <property name="maxPoolSize">10</property>
    <property name="checkoutTimeout">3000</property>
  </default-config>

</c3p0-config>

​ (4) 工具类 : JdbcUtils

package com.itheima.transfer.utils;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;

public class JdbcUtils {
    // c3p0 数据库连接池对象属性
    private static final ComboPooledDataSource ds = new ComboPooledDataSource();
    // 获取连接
    public static Connection getConnection() throws SQLException {
        return ds.getConnection();
    }
    //释放资源
    public static void release(AutoCloseable... ios){
        for (AutoCloseable io : ios) {
            if(io != null){
                try {
                    io.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void commitAndClose(Connection conn) {
        try {
            if(conn != null){
                //提交事务
                conn.commit();
                //释放连接
                conn.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void rollbackAndClose(Connection conn) {
        try {
            if(conn != null){
                //回滚事务
                conn.rollback();
                //释放连接
                conn.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

​ (5) dao层代码 : AccountDao

package com.itheima.transfer.dao;

import com.itheima.transfer.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class AccountDao {

    public void out(String outUser, int money) throws SQLException {
        String sql = "update account set money = money - ? where name = ?";

        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,outUser);
        pstm.executeUpdate();

        JdbcUtils.release(pstm, conn);
    }

    public void in(String inUser, int money) throws SQLException {
        String sql = "update account set money = money + ? where name = ?";

        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,inUser);
        pstm.executeUpdate();

        JdbcUtils.release(pstm,conn);
    }
}

​ (6) service层代码 : AccountService

package com.itheima.transfer.service;

import com.itheima.transfer.dao.AccountDao;
import java.sql.SQLException;

public class AccountService {

    public boolean transfer(String outUser, String inUser, int money) {
        AccountDao ad = new AccountDao();
        try {
            // 转出
            ad.out(outUser, money);
            // 转入
            ad.in(inUser, money);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

​ (7) web层代码 : AccountWeb

package com.itheima.transfer.web;

import com.itheima.transfer.service.AccountService;

public class AccountWeb {

    public static void main(String[] args) {
        // 模拟数据 : Jack 给 Rose 转账 100
        String outUser = "Jack";
        String inUser = "Rose";
        int money = 100;

        AccountService as = new AccountService();
        boolean result = as.transfer(outUser, inUser, money);

        if (result == false) {
            System.out.println("转账失败!");
        } else {
            System.out.println("转账成功!");
        }
    }
}

引入事务:

案例中的转账涉及两个DML操作: 一个转出,一个转入。

这些操作是需要具备原子性的,不可分割
不然就有可能出现数据修改异常情况。

public class AccountService {
    public boolean transfer(String outUser, String inUser, int money) {
        AccountDao ad = new AccountDao();
        try {
            // 转出
            ad.out(outUser, money);
            // 模拟转账过程中的异常
            int i = 1/0;
            // 转入
            ad.in(inUser, money);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

​ 所以这里就需要操作事务,来保证转出和转入操作具备原子性,要么同时成功,要么同时失败。

(1) JDBC中关于事务的操作的api

Connection接口的方法 作用
void setAutoCommit(false) 禁用事务自动提交(改为手动)
void commit(); 提交事务
void rollback(); 回滚事务

(2) 开启事务的注意点:

  • 为了保证所有的操作在一个事务中,案例中使用的连接必须是同一个: service层开启事务的connection需要跟dao层访问数据库的connection保持一致

  • 线程并发情况下, 每个线程只能操作各自的 connection

常规解决方案

常规方案的实现

基于上面给出的前提, 大家通常想到的解决方案是 :

  • 从service层将connection对象向dao层传递
  • 加锁

以下是代码实现修改的部分:

​ (1 ) AccountService 类

package com.itheima.transfer.service;

import com.itheima.transfer.dao.AccountDao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;

public class AccountService {

    public boolean transfer(String outUser, String inUser, int money) {
        AccountDao ad = new AccountDao();
        //线程并发情况下,为了保证每个线程使用各自的connection,故加锁
        synchronized (AccountService.class) {

            Connection conn = null;
            try {
                conn = JdbcUtils.getConnection();
                //开启事务
                conn.setAutoCommit(false);
                // 转出
                ad.out(conn, outUser, money);
                // 模拟转账过程中的异常
//            int i = 1/0;
                // 转入
                ad.in(conn, inUser, money);
                //事务提交
                JdbcUtils.commitAndClose(conn);
            } catch (Exception e) {
                e.printStackTrace();
                //事务回滚
                JdbcUtils.rollbackAndClose(conn);
                return false;
            }
            return true;
        }
    }
}

​ (2) AccountDao 类 (这里需要注意的是: connection不能在dao层释放,要在service层,不然在dao层释放,service层就无法使用了)

package com.itheima.transfer.dao;

import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class AccountDao {

    public void out(Connection conn, String outUser, int money) throws SQLException{
        String sql = "update account set money = money - ? where name = ?";
        //注释从连接池获取连接的代码,使用从service中传递过来的connection
//        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,outUser);
        pstm.executeUpdate();
        //连接不能在这里释放,service层中还需要使用
//        JdbcUtils.release(pstm,conn);
        JdbcUtils.release(pstm);
    }

    public void in(Connection conn, String inUser, int money) throws SQLException {
        String sql = "update account set money = money + ? where name = ?";
//        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,inUser);
        pstm.executeUpdate();
//        JdbcUtils.release(pstm,conn);
        JdbcUtils.release(pstm);
    }
}

常规方案的弊端

上述方式我们看到的确按要求解决了问题,但是仔细观察,会发现这样实现的弊端:

  1. 直接从service层传递connection到dao层, 造成代码耦合度提高

  2. 加锁会造成线程失去并发性,程序性能降低

ThreadLocal解决方案:

ThreadLocal方案的实现

像这种需要在项目中进行数据传递线程隔离的场景,我们不妨用ThreadLocal来解决:

​ (1) 工具类的修改: 加入ThreadLocal

package com.itheima.transfer.utils;

import com.mchange.v2.c3p0.ComboPooledDataSource;
import java.sql.Connection;
import java.sql.SQLException;

public class JdbcUtils {
    //ThreadLocal对象 : 将connection绑定在当前线程中
    private static final ThreadLocal<Connection> tl = new ThreadLocal();

    // c3p0 数据库连接池对象属性
    private static final ComboPooledDataSource ds = new ComboPooledDataSource();

    // 获取连接
    public static Connection getConnection() throws SQLException {
        //取出当前线程绑定的connection对象
        Connection conn = tl.get();
        if (conn == null) {
            //如果没有,则从连接池中取出
            conn = ds.getConnection();
            //再将connection对象绑定到当前线程中
            tl.set(conn);
        }
        return conn;
    }

    //释放资源
    public static void release(AutoCloseable... ios) {
        for (AutoCloseable io : ios) {
            if (io != null) {
                try {
                    io.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void commitAndClose() {
        try {
            Connection conn = getConnection();
            //提交事务
            conn.commit();
            //解除绑定
            tl.remove();
            //释放连接
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void rollbackAndClose() {
        try {
            Connection conn = getConnection();
            //回滚事务
            conn.rollback();
            //解除绑定
            tl.remove();
            //释放连接
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

​ (2) AccountService类的修改:不需要传递connection对象

package com.itheima.transfer.service;

import com.itheima.transfer.dao.AccountDao;
import com.itheima.transfer.utils.JdbcUtils;
import java.sql.Connection;

public class AccountService {

    public boolean transfer(String outUser, String inUser, int money) {
        AccountDao ad = new AccountDao();

        try {
            Connection conn = JdbcUtils.getConnection();
            //开启事务
            conn.setAutoCommit(false);
            // 转出 : 这里不需要传参了 !
            ad.out(outUser, money);
            // 模拟转账过程中的异常
//            int i = 1 / 0;
            // 转入
            ad.in(inUser, money);
            //事务提交
            JdbcUtils.commitAndClose();
        } catch (Exception e) {
            e.printStackTrace();
            //事务回滚
           JdbcUtils.rollbackAndClose();
            return false;
        }
        return true;
    }
}

​ (3) AccountDao类的修改:照常使用

package com.itheima.transfer.dao;

import com.itheima.transfer.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class AccountDao {

    public void out(String outUser, int money) throws SQLException {
        String sql = "update account set money = money - ? where name = ?";
        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,outUser);
        pstm.executeUpdate();
        //照常使用
//        JdbcUtils.release(pstm,conn);
        JdbcUtils.release(pstm);
    }

    public void in(String inUser, int money) throws SQLException {
        String sql = "update account set money = money + ? where name = ?";
        Connection conn = JdbcUtils.getConnection();
        PreparedStatement pstm = conn.prepareStatement(sql);
        pstm.setInt(1,money);
        pstm.setString(2,inUser);
        pstm.executeUpdate();
//        JdbcUtils.release(pstm,conn);
        JdbcUtils.release(pstm);
    }
}

ThreadLocal方案的好处

从上述的案例中我们可以看到, 在一些特定场景下,ThreadLocal方案有两个突出的优势:

  1. 传递数据 : 保存每个线程绑定的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合问题

  2. 线程隔离 : 各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失


通过以上的学习,相信同学们对ThreadLocal的作用有了一定的认识
那么,现在我们来看一下ThreadLocal的内部结构
以便后文 探究 它能够实现 线程数据隔离 的原理:

内部结构:

对于 ThreadLocal类,在JDK发展改进的过程中,出现过 两种设计方式

JDK1.8 前:

每个 ThreadLocal类 都创建一个 Map
然后用 线程的ID threadID 作为 Mapkey要存储的局部变量 作为 Mapvalue
这样就能达到 各个线程局部变量隔离 的效果


JDK1.8 后:

每个Thread 维护一个 ThreadLocalMap哈希表
这个哈希表的 keyThreadLocal实例本身value 才是 真正要存储的值

图示:

核心结构
那么,本人对于上图做些说明:

说明:

  1. 每个 Thread线程 内部都有一个Map
    (准确来说是两个ThreadLocalMap,一个是本类,一个是用于继承子类)
    示意图
  2. Map里面存储 ThreadLocal对象(key)属于该线程的目标变量的副本(value)
  3. Thread内部的Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置 线程的变量值
  4. 对于 不同的线程
    每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离互不干扰

或许有同学这时候就有疑问了:

jdk1.8之前的设计看起来蛮好的,而且由线程自己来维护本地变量副本,也比较方便些
那jdk1.8之后版本的设计 的 好处 是什么?

那么,本人现在来讲解下这样改进的好处:

好处:

  1. 因为之前的 存储数量Thread的数量 决定,现在是由 ThreadLocal的数量 决定
    这样设计之后 每个Map存储的Entry数量 就会 变少
  2. Thread销毁 之后,对应的 ThreadLocalMap 也会随之 被回收,能 减少内存的使用

那么,知道了ThreadLocal的 概念用法适用场景,本人现在来带同学们解析下 ThreadLocal类 的 核心源码

核心源码 解析:


除了 构造方法 之外, ThreadLocal对外暴露的方法有以下4个:

方法声明 描述
public void set( T value) 设置 当前线程绑定的局部变量
public T get() 获取 当前线程绑定的局部变量
public void remove() 移除 当前线程绑定的局部变量

那么,本人先来展示下 构造方法源码

构造方法:

/**
 * Creates a thread local variable.
 * @see #withInitial(java.util.function.Supplier)
 */
public ThreadLocal() {
}

在讲解其它方法前,本人要先来讲解一个protected方法 —— 方法:

initialValue() 方法

源码:

protected T initialValue() {
    return null;
}

作用:

此方法的作用是:

返回 该线程 局部变量的 初始值

说明:

  1. 这个方法是一个 延迟调用 方法,
    从之后的代码我们能看到

在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。

  1. 这个方法缺省实现,直接返回一个null
  2. 如果想要一个 除null之外初始值,可以重写此方法
    (备注: 该方法是一个protected的方法,显然是为了 让子类覆盖 而设计的)

get() 方法:

源码:

/**
 * 返回当前线程中保存ThreadLocal的值
 * 如果当前线程没有此ThreadLocal变量,
 * 则它会通过调用 initialValue()方法 进行初始化值
 *
 * @return 返回当前线程对应此ThreadLocal的值
 */
public T get() {
    Thread t = Thread.currentThread();
    /*
        获取此线程中 维护的ThreadLocalMap对象
     */
    ThreadLocalMap map = getMap(t);
    
    /*
        若该map存在:
            以当前的ThreadLocal对象为 key,调用getEntry获取对应的 存储实体e
            若该对象存在:
                则转换类型后 返回该对象
        若该map不存在:
            则说明 目标线程 没有 维护的ThreadLocalMap对象,调用setInitialValue进行初始化
     */
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;
            return result;
        }
    }
    return setInitialValue();
}

那么,现在本人来总结下 get()方法执行流程

执行流程:

  1. 获取 当前线程对象
  2. 根据 当前线程,获取 一个Map
  3. 如果 获取的Map不为空,则在Map中以ThreadLocal的引用 作为 key 来在Map中获取对应的value e,否则转到 步骤5
  4. 如果 e不为null,则 返回e.value,否则转到 步骤5
  5. Map为空 或者 e为空
    则通过 initialValue()方法 获取 初始值value
    然后用 ThreadLocal的引用和value 作为 firstKey和firstValue 创建一个 新的Map

总结:

先获取 当前线程 的 ThreadLocalMap 变量,
如果 存在 则返回 恰当类型的值不存在 则创建并返回 初始值


那么,本人来展示并讲解下 get中所调用的 两个方法

getMap 方法:

/**
 * 获取当前线程Thread对应维护的ThreadLocalMap
 *
 * @param t the current thread 当前线程
 * @return the map 对应维护的ThreadLocalMap
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

setInitialValue 方法:

/**
 * set的变样实现,用于初始化值initialValue,
 * 用于代替防止用户重写set()方法
 *
 * @return the initial value 初始化后的值
 */
private T setInitialValue() {
    // 调用initialValue获取初始化的值
    T value = initialValue();
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取此线程对象中维护的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    // 如果此map存在
    if (map != null)
        // 存在则调用map.set设置此实体entry
        map.set(this, value);
    else
        // 1)当前线程Thread 不存在ThreadLocalMap对象
        // 2)则调用createMap进行ThreadLocalMap对象的初始化
        // 3)并将此实体entry作为第一个值存放至ThreadLocalMap中
        createMap(t, value);
    // 返回设置的值value
    return value;
}

/**
 * 创建当前线程Thread对应维护的ThreadLocalMap
 *
 * @param t          当前线程
 * @param firstValue 存放到map中第一个entry的值
 */
void createMap(Thread t, T firstValue) {
    //这里的this是调用此方法的threadLocal
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

set() 方法:

源码:

/**
 * 设置当前线程对应的ThreadLocal的值
 *
 * @param value 将要保存在当前线程对应的ThreadLocal的值
 */
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // map存在,则调用map.set设置此实体entry
        map.set(this, value);
    else
        /*
            1)当前线程Thread 不存在ThreadLocalMap对象
            2)则调用createMap进行ThreadLocalMap对象的初始化
            3)并将此实体entry作为第一个值存放至ThreadLocalMap中
         */
        createMap(t, value);
}

相信看了上述源码的同学,已经对执行流程了然于心了
尬笑
为了排版整齐,本人还是来总结下这个方法的执行流程吧:

执行流程:

  1. 获取当前线程,并根据 当前线程 获取 对应的Map
  2. 如果获取的 Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
  3. 如果 Map为空,则给该线程 创建 Map,并 设置初始值

remove() 方法

源码:

/**
 * 删除 当前线程所拥有的ThreadLocal副本键值对
 */
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

执行流程:

  1. 获取当前线程,并根据 当前线程 获取 一个Map
  2. 如果获取的 Map不为空,则移除 当前ThreadLocal对象 对应的 entry

那么,相信很多同学对于 ThreadLocal类 的理解已经很透彻了
但是,这还是远远不够的!
在上文中,我们都能看得出:

对于 多线程 下的 ThreadLocal类的对象,会根据线程的创建而构造一个Map
而我们也知道,若是想要 提升性能,就得从 底层的数据结构 入手
那么,这个“Map”的学习,就变得不得不了解了

欠揍

那么,为了想要真正搞懂ThreadLocal,就必须了解 这个“Map” —— ThreadLocalMap

ThreadLocalMap类:

首先,本人来展示下 ThreadLocalMap类继承体系

继承体系:

继承关系
从上图中,我们可以看出:

ThreadLocalMap类 中的键值对,是 Entry类
但是,这个Entry,并不是HashMap中的内部类Entry,而是 ThreadLocal类的内部类ThreadLocalMap类的内部类
所处位置
而这个Entry类 继承自 WeakReference类便于垃圾回收


接下来,本人来带同学们解读下 源码

核心源码 解读:

首先,本人来展示下 ThreadLocal类 的成员变量

成员变量:

/**
 * 初始容量 —— 必须是2的整次幂
 * 默认为16
 */
private static final int INITIAL_CAPACITY = 16;

/**
 * 存放数据的table,Entry类的定义
 * 数组长度必须是2的幂
 */
private Entry[] table;

/**
 * 数组里面entrys的个数,
 * 可以用于判断table当前使用量是否超过负因子。
 */
private int size = 0;

/**
 * 扩容阈值,
 * 表使用量大于它的时候进行扩容
 */
private int threshold; // Default to 0

/**
 * 阈值设置为长度的2/3
 */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

从上文中,我们能够看出:

  1. 初始默认容量16
  2. ThreadLocalMap扩容阈值容量的2/3

接下来,本人来展示下 ThreadLocalMap类构造方法

构造方法:

/**
 * Construct a new map initially containing (firstKey, firstValue).
 * ThreadLocalMaps are constructed lazily, so we only create
 * one when we have at least one entry to put in it.
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    /*
		计算所传firstKey所在table中的下标
		并将 firstValue 作为值,存储并作为 table当前单元的首元素
	*/
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);	// 计算阈值(初始容量的2/3)
}

/**
 * Construct a new map including all Inheritable ThreadLocals
 * from given parent map. Called only by createInheritedMap.
 *
 * @param parentMap the map associated with parent thread.
 */
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

	/*
		将 parentMap中的键值对,转存到 新的table中
	*/
    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) {
                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++;
            }
        }
    }
}

这时候可能就有同学联想到本人之前博文中所讲解的HasdMap了,于是提出如下问题:

这个会发生安全问题吧?
没有加锁,无论是 拉链 还是 红黑树 的转存,都是有线程安全的!

不不不,同学,ThreadLocal是 线程隔离 的,是 不会存在线程安全问题 的!
笑看


那么,本人还是从 增删查 角度 来分析源码:
首先是代表 set方法

set 方法:

/**
 * Set the value associated with key.
 *
 * @param key the thread local object
 * @param value the value to be set
 */
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    // 根据 table长度 和 所传key的哈希值,
    // 计算在当前table中的下标
    int i = key.threadLocalHashCode & (len-1);

	/*
		提供 “线性探测法”,查找当前key是否存在map中:
			1、若 存在,则 将原值覆盖
			2、若 不存在,则 
	*/
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

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

        if (k == null) {
            replaceStaleEntry(key, value, i);	// 存储当前键值对
            return;
        }
    }

	// 判断 当前长度是否到达阈值,若到达,则扩容
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();	// 将原长度 扩容为 2倍
}

关于set方法,有一个非常重要的问题 —— hash冲突
由于这个问题十分重要,在面试中常常作为面试常考点出现
那么,本人就在下文中的 总结区 进行详细的讲解!


接下来,是代表 “” 的 remove方法

remove 方法:

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

/**
 * Remove the entry for key.
 */
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);	// 删除过期的entry
            return;
        }
    }
}

从源码中,我们可以看得出:

ThreadLocalMap 中的 remove方法实现逻辑 很简单:
找到 目标ThreadLocal位置,并进行 清理


那么,键值对ThreadLocalMap类 中,是如何存储的呢?
答曰:

Entry类

键值对 存储结构 —— Entry类:

源码:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

那么,接下来,本人来对这个类做些说明

说明:

  1. 在ThreadLocalMap中,也是用 Entry类 来保存 K-V结构 数据的
    但是Entry中 key 只能是 ThreadLocal对象
  2. Entry继承 WeakReference,使用弱引用
    可以将ThreadLocal对象生命周期线程生命周期 解绑,
    持有对 ThreadLocal 的 弱引用,可以使得ThreadLocal在 没有其他强引用 的时候 被回收掉
    这样可以避免因为 线程得不到销毁,导致 ThreadLocal对象无法被回收

接下来,本人来讲解下代表 getEntry 方法

getEntry 方法:

private final int threadLocalHashCode = nextHashCode();

/**
 * 从0开始,获取下一个要给出的哈希码。
 * 自动更新。
 */
private static AtomicInteger nextHashCode =
    new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

/**
 * 获取 与键关联 的 Entry。
 * 此方法本身仅处理快速路径:
 * 直接命中现有密钥。
 * 否则,它将中继到getEntryAfterMiss。
 * 旨在通过部分使此方法易于操作来最大程度地提高直接打击的性能。
 * @param  key the thread local object
 * @return the entry associated with key, or null if no such
 */
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);	// 计算当前key应在下标
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
    	// 若是没找到,则 继续查找
        return getEntryAfterMiss(key, i, e);
}

关于getEntry方法,没有什么需要细讲之处
本人来讲解下其中 常量HASH_INCREMENT取值原因

HASH_INCREMENT 小故事:

说法一

与斐波那契散列有关,
0x61c88647对应的十进制为1640531527。
斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,
如果把这个值给转为带符号的int,则会得到-1640531527。

还有一种说法 是:

0x61c88647如果单位是秒的话,那么化成年约为51年,
如果按照第一台计算机诞生1946年算,这是数字恰好是ThreadLocal上写的年份 —— 1997年
证实
(此处故事摘自《jdk源码之ThreadLocal》)


我们都知道:

作为存储 K-V键值对 的数据结构,
最重要得到一点就是 hash冲突

那么,本人现在来讲解下 在 ThreadLocalMap类 中,hash冲突 是如何解决的:

hash冲突 解决:

说到hash冲突,根据本人之前博文中所讲解的HashMap来看,一般只会出现在 添加新元素 的方法中

那么,我们再来回顾下 ThreadLocal类set方法源码

set方法 源码再现:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocal.ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else	// 懒加载模式
        createMap(t, value);
}

那么,我们再来看看其中的 createMap方法

createMap 方法:

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

继续跟踪,接下来是 ThreadLocalMap类构造方法

ThreadLocalMap类 构造方法:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
   // 初始化 table
   table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
   // 计算索引
   int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
   
   // 设置 值
   table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
   size = 1;
   
   // 设置 阈值
   setThreshold(INITIAL_CAPACITY);
}

对于上述代码,本人来讲解下 计算索引原理

上文方法中的 计算索引 的那行代码

int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

而上行代码中的 & (INITIAL_CAPACITY - 1)
不用本人多说,就是 约束 计算出的索引的范围

那么,本人就针对 firstKey.threadLocalHashCode 来讲解下 原理

threadLocalHashCode 属性:

首先,来看一下 相关源码

private static final int HASH_INCREMENT = 0x61c88647;

private final int threadLocalHashCode = nextHashCode();

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

关于这里的代码,在上文中本人已经讲解地很详细了
而这里的 HASH_INCREMENT 保证了

哈希码均匀分布2的n次方 的数组里
也就是Entry[] table 中,这样做可以尽量 避免hash冲突


然后就是 ThreadLocalMap类set方法 中的下方 代码块

for (Entry e = tab[i];
     e != null;
     e = tab[i = nextIndex(i, len)]) {
    ThreadLocal<?> k = e.get();

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

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

如上的循环中,使用了 线性探测法 来解决哈希冲突

线性探测法·具体流程:

线性探测法 的地址增量di = 1, 2, ... 其中,i 为 探测次数
该方法一次探测下一个地址,直到有空的地址后插入,
若整个空间都找不到空余的地址,则产生 溢出
假设当前table长度为16,也就是说如果计算出来key的hash值为14,
如果table[14]上已经有值,并且其key与当前key不一致,
那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,
这个时候如果还是冲突会回到0,取table[0]
以此类推,直到可以插入
... ...


最后,本人稍作总结:

总结:

ThreadLocal 通过 如下方式处理 哈希冲突

  1. 元素的插入是根据 线性探测法 计算 数组下标
  2. 扩容后的 容量阈值 是 扩容前的 2倍
    (相关源码未展示,有兴趣的同学请自行解析)

那么,至此,ThreadLocal类 讲解完毕!
拽

posted @ 2020-12-06 21:28  在下右转,有何贵干  阅读(120)  评论(0编辑  收藏  举报