写点什么

JUC 并发—ThreadLocal 源码分析

  • 2025-02-25
    福建
  • 本文字数:22277 字

    阅读完需:约 73 分钟

1.ThreadLocal 的特点介绍


(1)ThreadLocal 的注释说明


//This class provides thread-local variables.  //These variables differ from their normal counterparts in that //each thread that accesses one (via its get or set method) has its own, //independently initialized copy of the variable. //ThreadLocal instances are typically private static fields in classes that //wish to associate state with a thread (e.g., a user ID or Transaction ID).public class ThreadLocal<T> {    ...    ...}
复制代码


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

 

由此可知,ThreadLocal 的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰。由于这种变量只会在线程的生命周期内起作用,所以可以减少同一个线程内,在多个函数或者组件之间传递数据的复杂度。

 

ThreadLocal 的总结如下:

一.线程并发:适用于多线程并发的场景

二.传递数据:可通过 ThreadLocal 在同一线程,不同组件中传递公共变量

三.线程隔离:每个线程的变量都是独立的,互相之间不会影响

 

(2)ThreadLocal 的常用方法


ThreadLocal 的常用方法如下:



(3)ThreadLocal 的使用案例


一.没用 ThreadLocal 时共享变量在线程间不隔离


//需求:线程隔离//在多线程并发的场景下, 每个线程中的变量都是相互独立//线程A:设置(变量1) 获取(变量1)//线程B:设置(变量2) 获取(变量2)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++) {            Thread thread = new Thread(new Runnable() {                @Override                public void run() {                    //每个线程: 先存一个变量, 过一会再取出这个变量                    demo.setContent(Thread.currentThread().getName() + "的数据");                    System.out.println("-----------------------");                    System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());                }            });            thread.setName("线程" + i);            thread.start();//启动线程        }    }}
复制代码


运行后打印结果如下,从结果可以看出多个线程在访问同一个变量的时候出现了异常,有些线程取出了其他线程的数据,线程之间的数据没有实现隔离。


----------------------------------------------线程0--->线程3的数据----------------------------------------------线程2--->线程4的数据-----------------------线程4--->线程4的数据线程3--->线程3的数据线程1--->线程4的数据
复制代码


二.使用 ThreadLocal 实现共享变量在线程间隔离


//需求:线程隔离//在多线程并发的场景下, 每个线程中的变量都是相互独立//线程A:设置(变量1) 获取(变量1)//线程B:设置(变量2) 获取(变量2)//ThreadLocal://1.set(): 将变量绑定到当前线程中//2.get(): 获取当前线程绑定的变量public class MyDemo {    private static ThreadLocal<String> tl = new ThreadLocal<>();
//变量 private String content;
private String getContent() { return tl.get(); }
private void setContent(String content) { //变量content绑定到当前线程 tl.set(content); }
public static void main(String[] args) { MyDemo demo = new MyDemo(); for (int i = 0; i < 5; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { demo.setContent(Thread.currentThread().getName() + "的数据"); System.out.println("-----------------------"); System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent()); } }); thread.setName("线程" + i); thread.start();//启动线程 } }}
复制代码


运行后打印结果如下,从结果看,很好地解决了多线程之间数据隔离的问题。


-----------------------线程0--->线程0的数据-----------------------线程1--->线程1的数据-----------------------线程2--->线程2的数据-----------------------线程3--->线程3的数据-----------------------线程4--->线程4的数据
复制代码


(4)ThreadLocal 类与 synchronized 关键字对比


一.synchronized 同步方式


上述线程隔离的效果完全可以通过加 synchronized 锁来实现。


//需求:线程隔离//在多线程并发的场景下, 每个线程中的变量都是相互独立//线程A:设置(变量1) 获取(变量1)//线程B:设置(变量2) 获取(变量2)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++) {            Thread thread = new Thread(new Runnable() {                @Override                public void run() {                    //每个线程: 存一个变量, 过一会再取出这个变量                    synchronized (MyDemo.class){                        demo.setContent(Thread.currentThread().getName() + "的数据");                        System.out.println("-----------------------");                        System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());                    }                }            });            thread.setName("线程" + i); //线程0~4            thread.start();        }    }}
复制代码


运行后打印结果如下:


-----------------------线程0--->线程0的数据-----------------------线程1--->线程1的数据-----------------------线程2--->线程2的数据-----------------------线程3--->线程3的数据-----------------------线程4--->线程4的数据
复制代码


从结果可以发现,加锁确实可以解决这个问题。但是这里强调的是多线程数据隔离的问题,并不是多线程共享数据的问题。在这个案例中使用 synchronized 关键字是不合适的,降低了并发性。

 

二.ThreadLocal 与 synchronized 的区别


虽然 ThreadLocal 模式与 synchronized 关键字,都可以用于处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不同。



三.总结


虽然使用 ThreadLocal 和 synchronized 都能解决线程间数据隔离的问题,但是 ThreadLocal 更为合适,因为它可以使程序拥有更高的并发性。

 

2.ThreadLocal 的使用案例


(1)转账案例的场景


有一个数据表 account,里面有两个用户 Jack 和 Rose,Jack 给 Rose 转账。案例的实现使用了 MySQL 数据库、JDBC 和 C3P0 框架,以下是详细的代码。

 

一.数据准备


--使用数据库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', 0);
复制代码


二.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">123456</property>                <!-- 连接池参数 -->        <property name="initialPoolSize">5</property>        <property name="maxPoolSize">10</property>        <property name="checkoutTimeout">3000</property>    </default-config></c3p0-config>
复制代码


三.JdbcUtils 工具类


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();        }    }}
复制代码


四.Dao 层代码:AccountDao


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); }}
复制代码


五.Service 层代码:AccountService


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;    }}
复制代码


六.Web 层代码:AccountWeb


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("转账成功!"); } }}
复制代码


(2)转账案例引入事务


案例中的转账涉及两个 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;    }}
复制代码


引入事务改造如下:


public class AccountService {    public boolean transfer(String outUser, String inUser, int money) {        AccountDao ad = new AccountDao();        Connection conn = null;        try {            //1.开启事务            conn = JdbcUtils.getConnection();            conn.setAutoCommit(false);//禁用事务自动提交(改为手动)                        //转出            ad.out(conn, outUser, money);            //模拟转账过程中的异常:转出成功,转入失败            int i = 1/0;            //转入            ad.in(conn, inUser, money);                        //2.事务提交            JdbcUtils.commitAndClose(conn);        } catch (Exception e) {            e.printStackTrace();            //3.失败时事务回滚            JdbcUtils.rollbackAndClose(conn);            return false;        }        return true;    }}
复制代码


JDBC 开启事务的注意点:


为了保证所有操作在一个事务中,转账时使用的 Connection 必须是同一个。Service 层开启事务的 Connection 要和 Dao 层访问数据库的 Connection 一致。线程并发情况下,每个线程只能操作各自从 JdbcUtils 中获取的 Connection。

 

(3)常规方案解决引入事务后的问题


一.常规方案的实现


基于上面给出的前提, 常规方案是:传参 + 加锁。


传参:从 Service 层将 Connection 对象向 Dao 层传递


加锁:防止多个线程并发操作从 JdbcUtils 中获取的是同一个 Connection

 

AccountService 类修改如下:


//事务的使用注意点://1.Service层和Dao层的连接对象保持一致//2.每个线程的Connection对象必须前后一致, 线程隔离//常规的解决方案//1.传参: 将Service层的Connection对象直接传递到Dao层//2.加锁: 防止多个线程并发操作从JdbcUtils中获取的同一个Connection//常规解决方案的弊端://1.代码耦合度高//2.降低程序性能public class AccountService {    public boolean transfer(String outUser, String inUser, int money) {        AccountDao ad = new AccountDao();        //线程并发情况下,为了保证每个线程使用各自的Connection,避免不同的线程修改同一个Connection,故加锁        synchronized (AccountService.class) {            Connection conn = null;            try {                //1.开启事务                conn = JdbcUtils.getConnection();//从线程池中获取一个Connection对象                conn.setAutoCommit(false);//禁用事务自动提交(改为手动)                                //转出                ad.out(conn, outUser, money);                //模拟转账过程中的异常:转出成功,转入失败                int i = 1/0;                //转入                ad.in(conn, inUser, money);                                //2.事务提交                JdbcUtils.commitAndClose(conn);            } catch (Exception e) {                e.printStackTrace();                //3.失败时事务回滚                JdbcUtils.rollbackAndClose(conn);                return false;            }            return true;        }    }}
复制代码


AccountDao 类修改如下:


注意:Connection 不能在 Dao 层释放,否则 Service 层就无法使用了。


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(); //连接不能在这里释放,Service层中还需要使用 //JdbcUtils.release(pstm,conn); JdbcUtils.release(pstm); }}
复制代码


二.常规方案的弊端


上述的确按要求解决了问题,但是存在如下弊端:


第一.直接从 Service 层传递 Connection 到 Dao 层,代码耦合度比较高

第二.加锁会降低程序并发性,程序性能下降

 

(4)ThreadLocal 方案解决引入事务后的问题


一.ThreadLocal 方案的实现


像这种需要进行数据传递和线程隔离的场景,可以使用 ThreadLocal 来解决。

 

JdbcUtils 工具类加入 ThreadLocal:


public class JdbcUtils {    //ThreadLocal对象: 将Connection绑定在当前线程中    private static final ThreadLocal<Connection> tl = new ThreadLocal();
//c3p0数据库连接池对象属性 private static final ComboPooledDataSource ds = new ComboPooledDataSource();
//获取连接 //原来: 直接从连接池中获取连接 //现在: //1.直接获取当前线程绑定的连接对象 //2.如果连接对象是空的,再去连接池中获取连接,并将此连接对象跟当前线程进行绑定 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(); } }}
复制代码


AccountService 类不需要传递 Connection 对象:


public class AccountService {    public boolean transfer(String outUser, String inUser, int money) {        AccountDao ad = new AccountDao();        try {            //1.开启事务            conn = JdbcUtils.getConnection();            conn.setAutoCommit(false);//禁用事务自动提交(改为手动)                        //转出:这里不需要传参conn了            ad.out(outUser, money);            //模拟转账过程中的异常:转出成功,转入失败            int i = 1/0;            //转入            ad.in(inUser, money);                        //2.事务提交            JdbcUtils.commitAndClose(conn);        } catch (Exception e) {            e.printStackTrace();            //3.失败时事务回滚            JdbcUtils.rollbackAndClose(conn);            return false;        }        return true;    }}
复制代码


AccountDao 类去掉 Connection 参数:


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);    }
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); }}
复制代码


二.ThreadLocal 方案的好处


从上述可以看到,ThreadLocal 方案有两个优势:

 

优势一:传递数据

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

 

优势二:线程隔离

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

 

(5)ThreadLocal 的典型应用场景


场景一:

在 TransactionSynchronizationManager 类中(这是 Spring-JDBC 的类),会通过 ThreadLocal 来保证数据库连接和事务资源的隔离性,从而避免了不同线程之间事务和连接混乱的问题。

 

场景二:

在实际开发中,当用户登录之后,拦截器会获得用户的基本信息。这些信息在后续的方法中会用到,如果设置到 HttpServletRequest 中,则不是很灵活,而且还依赖服务器对象,这时就可以用 ThreadLocal。

 

3.ThreadLocal 的内部结构


(1)早期的设计


如果不去看源码,可能会猜测 ThreadLocal 是如下这样设计的:

 

首先每个 ThreadLocal 都创建一个 Map,然后用线程作为 Map 的 key,线程的变量副本作为 Map 的 value,这样就能让各个线程的变量副本实现数据隔离的效果。

 

这是最简单的设计方法,JDK 最早期的 ThreadLocal 确实是这样设计的。



(2)现在的设计


JDK 后面优化了设计方案,在 JDK8 中 ThreadLocal 的设计是:

 

首先让每个 Thread 创建一个 ThreadLocalMap,这个 Map 的 key 是 ThreadLocal 实例本身,value 才是真正要存储的局部变量。

 

具体设计如下:

一.每个 Thread 线程内部都有一个 Map(ThreadLocalMap)

二.Map 里面存储 ThreadLocal 对象(key)和线程的变量副本(value)

三.Thread 内部的 Map 由 ThreadLocal 维护,ThreadLocal 会向 Map 获取和设置线程的变量副本

四.其他线程不能获取当前线程的变量副本,从而实现了数据隔离



(3)现在的设计的优势


优势一:

每个 Map 存储的 Entry 数量变少,可以降低 Hash 冲突发生的概率。因为之前的存储数量由 Thread 的数量决定,现在由 ThreadLocal 的数量决定。在实际中,ThreadLocal 的数量往往要少于 Thread 的数量。

 

优势二:

当 Thread 销毁后,对应的 ThreadLocalMap 也随之销毁。让线程变量副本的生命周期跟随着线程的生命周期,可以减少内存的占用。


 

4. ThreadLocal 的核心方法源码


(1)ThreadLocal 的整体设计原理


ThreadLocal 为实现多个线程对同一个共享变量进行 set 操作时线程隔离,每个线程都有一个与 ThreadLocal 关联的容器来存储共享变量的初始化副本。当线程对变量副本更新时,只更新存储在当前线程关联容器中的数据副本。

 

如下图示,在每个线程中都会维护一个成员变量 ThreadLocalMap。其中 key 是一个指向 ThreadLocal 实例的弱引用,而 value 表示 ThreadLocal 的初始化值或者当前线程执行 set()方法设置的值。

 

假设定义了 3 个不同功能的 ThreadLocal 共享变量,而在 Thread1 中分别用到了这 3 个 ThreadLocal 进行操作,那么这 3 个 ThreadLocal 都会存储到 Thread1 的 ThreadLocalMap 中。

 

如果 Thread2 也想用这 3 个 ThreadLocal 共享变量,那么在 Thread2 中也会维护一个 ThreadLocalMap,把这 3 个 ThreadLocal 共享变量保存到该 ThreadLocalMap 中。

 

如果 Thread1 想要对 local1 进行运算,则将 local1 实例作为 key,从 Thread1 的 ThreadLocalMap 中获取对应的 value 值,进行运算即可。



(2)ThreadLocal 的 set()方法源码


该方法会在当前线程的成员变量 ThreadLocalMap 中设置一个值。

 

具体的执行流程如下:

一.首先通过 Thread 的 currentThread()方法获取当前线程。

二.然后通过 ThreadLocal 的 getMap()方法获取当前线程的成员变量 ThreadLocalMap。

三.如果获取的 ThreadLocalMap 为空,于是调用 createMap()方法初始化当前线程的成员变量 ThreadLocalMap。

四.如果获取的 ThreadLocalMap 不为空,则更新 ThreadLocalMap 中 key 为当前 ThreadLocal 对象所对应的 value。


public class ThreadLocal<T> {    ...    //在当前线程中设置一个值,并保存在该线程的ThreadLocalMap中    public void set(T value) {        //首先通过Thread.currentThread()获取当前线程        Thread t = Thread.currentThread();        //然后获取当前线程的成员变量ThreadLocalMap        ThreadLocalMap map = getMap(t);        //判断当前线程的成员变量ThreadLocalMap是否为空        if (map != null) {            //如果不为空,则调用map.set()更新ThreadLocalMap中key为当前ThreadLocal对象所对应的value            map.set(this, value);        } else {            //如果为空,则调用createMap()方法初始化当前线程的成员变量ThreadLocalMap            createMap(t, value);        }    }
//获取线程Thread的成员变量ThreadLocalMap ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
//初始化线程Thread的成员变量ThreadLocalMap void createMap(Thread t, T firstValue) { //这里的this是调用此方法的threadLocal对象 //初始化ThreadLocalMap的第一个元素,key为调用此方法的threadLocal对象,value为传入的firstValue t.threadLocals = new ThreadLocalMap(this, firstValue); } ...}
public class Thread implements Runnable { ... //每个线程都有一个ThreadLocalMap类型的成员变量,叫threadLocals ThreadLocal.ThreadLocalMap threadLocals = null; ...}
复制代码


(3)ThreadLocal 的 get()方法源码


该方法会从当前线程的成员变量 ThreadLocalMap 中获取一个值。

 

具体的执行流程如下:

一.首先通过 Thread 的 currentThread()方法获取当前线程。

二.然后通过 ThreadLocal 的 getMap()方法获取当前线程的成员变量 ThreadLocalMap。

三.如果获取的 ThreadLocalMap 不为空,而且 key 为当前 ThreadLocal 对象所对应的 value 值也不为空,则返回该 value。

四.否则就调用 setInitialValue()方法,初始化当前线程的成员变量 ThreadLocalMap,或者初始化 ThreadLocalMap 中 key 为当前 ThreadLocal 对象所对应的 value 值。


public class ThreadLocal<T> {    ...    //从当前线程的成员变量ThreadLocalMap中获取一个值    //如果当前线程的成员变量ThreadLocalMap为空,则调用setInitialValue()方法进行初始化    public T get() {        //获取当前线程        Thread t = Thread.currentThread();        //获取当前线程的成员变量ThreadLocalMap        ThreadLocalMap map = getMap(t);        //如果当前线程的成员变量ThreadLocalMap不为空        if (map != null) {            //以当前ThreadLocal对象为key,            //调用当前线程的成员变量ThreadLocalMap的getEntry()方法,来获取对应的Entry对象            ThreadLocalMap.Entry e = map.getEntry(this);            if (e != null) {                @SuppressWarnings("unchecked")                T result = (T)e.value;                return result;            }        }              //如果当前线程的成员变量ThreadLocalMap为空,        //或者ThreadLocalMap中key为当前ThreadLocal对象所对应的value为空        //那么就需要进行初始化        return setInitialValue();    }
//进行初始化并返回key为当前ThreadLocal对象所对应的value //该方法通过initialValue()方法获取初始值来初始化当前线程的成员变量ThreadLocalMap并赋值 //如下两种情况需要进行初始化: //第一种情况: map不存在,表示当前线程的成员变量ThreadLocalMap还没初始化 //第二种情况: map存在, 但是key为当前ThreadLocal对象所对应的value为空 private T setInitialValue() { //调用initialValue()方法获取初始化的值 T value = initialValue(); //获取当前线程对象 Thread t = Thread.currentThread(); //获取当前线程的成员变量ThreadLocalMap ThreadLocalMap map = getMap(t); //判断当前线程的成员变量ThreadLocalMap是否为空 if (map != null) { //如果不为空,则调用map.set()更新ThreadLocalMap中key为当前ThreadLocal对象所对应的value map.set(this, value); } else { //如果为空,则调用createMap()方法初始化当前线程的成员变量ThreadLocalMap createMap(t, value); } //返回初始化的value return value; } ...}
复制代码


(4)ThreadLocal 的 initialValue()方法源码


在 set()方法还未调用而先调用 get()方法时,就会执行 initialValue()方法。该方法会返回当前线程的成员变量 ThreadLocalMap 中,key 为当前 ThreadLocal 对象所对应的 value 的初始值。initialValue()方法默认情况下会返回一个 null,但可以重写覆盖此方法。


public class ThreadLocal<T> {    ...    //返回当前线程的成员变量ThreadLocalMap中,key为当前ThreadLocal对象所对应的value的初始值    protected T initialValue() {        return null;    }    ...}
复制代码


5.ThreadLocalMap 的核心方法源码


(1)ThreadLocalMap 的初始化方法


ThreadLocal 的 createMap()方法会初始化一个 ThreadLocalMap 集合。

 

ThreadLocalMap 的构造方法主要会进行如下处理:首先初始化一个长度为 16 的 Entry 数组,然后通过对 firstKey 的 hashCode 进行位运算取模来得到一个数组下标 i,接着根据 firstKey 和 firstValue 封装一个 Entry 对象,最后将这个 Entry 对象保存到 Entry 数组的下标为 i 的位置中。


public class ThreadLocal<T> {    ...    //初始化线程Thread的成员变量ThreadLocalMap    void createMap(Thread t, T firstValue) {        //这里的this是调用此方法的threadLocal对象        //初始化ThreadLocalMap的第一个元素,key为调用此方法的threadLocal对象,value为传入的firstValue        t.threadLocals = new ThreadLocalMap(this, firstValue);    }        //ThreadLocalMap is a customized hash map suitable only for maintaining thread local values.    //No operations are exported outside of the ThreadLocal class.     //The class is package private to allow declaration of fields in class Thread.    //To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.     //However, since reference queues are not used,     //stale entries are guaranteed to be removed only when the table starts running out of space.    static class ThreadLocalMap {        //The entries in this hash map extend WeakReference,         //using its main ref field as the key (which is always a ThreadLocal object).        //Note that null keys (i.e. entry.get() == null) mean that the key is no longer referenced,         //so the entry can be expunged from table.        //Such entries are referred to as "stale entries" in the code that follows.        static class Entry extends WeakReference<ThreadLocal<?>> {            //The value associated with this ThreadLocal.            Object value;            Entry(ThreadLocal<?> k, Object v) {                super(k);                value = v;            }        }              //The initial capacity -- MUST be a power of two.        private static final int INITIAL_CAPACITY = 16;
//The table, resized as necessary. //table.length MUST always be a power of two. private Entry[] table;
//The number of entries in the table. private int size = 0;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //初始化一个长度为16的Entry数组 table = new Entry[INITIAL_CAPACITY]; //通过对firstKey这个ThreadLocal实例对象的hashCode,进行位运算取模,来得到一个数组下标i int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //将firstKey和firstValue封装成一个Entry对象,保存到数组的下标为i的位置 table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } ... } ...}
public class Thread implements Runnable { ... //每个线程都有一个ThreadLocalMap类型的成员变量,叫threadLocals ThreadLocal.ThreadLocalMap threadLocals = null; ...}
复制代码


(2)ThreadLocalMap 的 set()方法


在 ThreadLocal 的 set()方法中,如果发现当前线程的成员变量 ThreadLocalMap 已经初始化了,那么就会调用 ThreadLocalMap 的 set()方法来保存要设置的值。

 

在 ThreadLocalMap 的 set(key, value)方法中,首先根据 ThreadLocal 对象的 hashCode 和数组长度进行位与运算(即取模),来获取 set()方法要设置的元素,应该放置在数组的哪个位置(即数组下标 i)。

 

如果数组下标 i 的位置不存在 Entry 对象,则直接将 key 和 value 封装成一个新的 Entry 对象然后存储到数组的下标为 i 的这个位置。

 

如果数组下标 i 的位置存在 Entry 对象,则使用 for 循环从数组下标 i 位置开始往后遍历(线性探索解决 Hash 冲突)。

 

如果根据 key 计算出来的数组下标 i 已经存在其他的 value,且该位置的 key 和要设置的 key 不同,则继续寻找 i + 1 的位置进行存储。

 

如果根据要设置的 key 找出的数组对应位置的 Entry 元素的 key 为 null,则调用 replaceStaleEntry()方法来进行替换和清理。

 

因为 Entry 元素中的 key 是弱引用,有可能 ThreadLocal 实例被回收了导致 Entry 元素中的 key 为 null。

 

最后统计数组的元素个数,如果元素个数超出阈值则进行扩容。


public class ThreadLocal<T> {    ...    static class ThreadLocalMap {        //The entries in this hash map extend WeakReference,         //using its main ref field as the key (which is always a ThreadLocal object).        //Note that null keys (i.e. entry.get() == null) mean that the key is no longer referenced,         //so the entry can be expunged from table.        //Such entries are referred to as "stale entries" in the code that follows.        static class Entry extends WeakReference<ThreadLocal<?>> {            //The value associated with this ThreadLocal.            Object value;            Entry(ThreadLocal<?> k, Object v) {                super(k);                value = v;            }        }              //The initial capacity -- MUST be a power of two.        private static final int INITIAL_CAPACITY = 16;
//The table, resized as necessary. //table.length MUST always be a power of two. private Entry[] table;
//The number of entries in the table. private int size = 0; ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //初始化一个长度为16的Entry数组 table = new Entry[INITIAL_CAPACITY]; //通过对firstKey这个ThreadLocal实例对象的hashCode,进行位运算取模,来得到一个数组下标i int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //将firstKey和firstValue封装成一个Entry对象,保存到数组的下标为i的位置 table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } //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) { Entry[] tab = table; int len = tab.length; //首先根据ThreadLocal对象的hashCode和数组长度进行位与运算(即取模),来获取元素放置的位置(即数组下标) int i = key.threadLocalHashCode & (len-1); //然后从i开始往后遍历到数组最后一个Entry(线性探索) for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { //获取Entry元素中的key ThreadLocal<?> k = e.get(); //如果key相等,则覆盖value if (k == key) { e.value = value; return; } //如果key为null,则用新key、value覆盖 //同时清理key = null的陈旧数据(弱引用) if (k == null) { replaceStaleEntry(key, value, i); return; } } //如果数组下标i的位置不存在数据,则直接将key和value封装成Entry对象存储到该位置 tab[i] = new Entry(key, value); int sz = ++size; //如果超过阈值,就需要扩容了,cleanSomeSlots()方法会清理数组中的无效的key if (!cleanSomeSlots(i, sz) && sz >= threshold) { rehash();//扩容 } } private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } ... } ...}
复制代码


(3)ThreadLocalMap 的弱引用


ThreadLocalMap 的数组 table 中的 Entry 元素的 key 什么时候会为空?

 

当线程已经退出 + ThreadLocal 已失去了线程的引用 + 垃圾回收时,key 便会为空。也就是如果 ThreadLocal 对象被回收了,那么 ThreadLocalMap 中的 key 就会为空。


public class ThreadLocal<T> {    ...    static class ThreadLocalMap {        private Entry[] table;        static class Entry extends WeakReference<ThreadLocal<?>> {            //The value associated with this ThreadLocal.            Object value;            Entry(ThreadLocal<?> k, Object v) {                super(k);                value = v;            }        }        ...    }}
复制代码


在 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。如下代码中,定义了一个 Object 对象和一个引用了 Object 对象的弱引用对象。当把 object 变量设置为 null 后,也就是 object 变量不再引用这个 Object 对象了,那么 gc()方法便会对这个 Object 对象进行回收,尽管它被弱引用对象引用着。


public class WeakReferneceExample {    //object变量引用了新创建的Object对象    static Object object = new Object();    public static void main(String[] args) {        //objectWeakReference变量引用了新创建的WeakReference对象        //但WeakReference对象是通过传入object变量创建的,所以objectWeakReference变量其实引用了Object对象        WeakReference<Object> objectWeakReference = new WeakReference<>(object);        object = null;//object变量不再引用Object对象,但此时objectWeakReference还在引用Object对象        System.gc();//此时垃圾回收,会回收还在被弱引用引用着的Object对象        System.out.println("gc之后" + objectWeakReference.get());//会输出null    }}
复制代码


如果在强引用的情况下,执行 gc()方法时,Object 对象是不会被回收的。


public class StrongReferenceExample {    static Object object = new Object();//object变量引用了新创建的Object对象    public static void main(String[] args) {        Object strongRef = object;//strongRef变量也引用新创建的Object对象        object = null;//object变量不再引用Object对象,但此时strongRef变量还在引用新创建的Object对象        System.gc();//此时垃圾回收,不会回收还在被强引用引用着的Object对象        System.out.println("gc之后" + strongRef);//会输出Object对象    }}
复制代码


6.ThreadLocalMap 的原理总结

 

(1)ThreadLocalMap 的基本结构


ThreadLocalMap 是 ThreadLocal 的内部类,它没有实现 Map 接口,而是用独立的方式实现了 Map 的功能,它内部的 Entry 也是用独立的方式实现的。



(2)ThreadLocalMap 的成员变量


table 是一个 Entry 类型的数组,用于存储数据。


size 代表 table 数组中的元素个数。


threshold 代表需要扩容时 size 的阈值。


public class ThreadLocal<T> {    ...    static class ThreadLocalMap {        static class Entry extends WeakReference<ThreadLocal<?>> {            Entry(ThreadLocal<?> k, Object v) {                super(k);                value = v;            }        }        //初始容量 —— 必须是2的整次幂        private static final int INITIAL_CAPACITY = 16;        //存放数据的table,同样,数组长度必须是2的整次幂        private Entry[] table;        //数组里面Entry的个数,可以用于判断table当前使用量是否超过阈值        private int size = 0;        //进行数组扩容的阈值        private int threshold; // Default to 0        ...    }    ...}
复制代码


(3)ThreadLocalMap 的存储结构 Entry


在 ThreadLocalMap 中,也是用 Entry 来保存 K-V 结构数据的,不过 Entry 中的 key 只能是 ThreadLocal 类型的对象。

 

另外 Entry 继承自 WeakReference,也就是 key(ThreadLocal 对象)是弱引用,使用弱引用的目的是将 ThreadLocal 对象的生命周期和线程的生命周期进行解绑。


public class ThreadLocal<T> {    ...    static class ThreadLocalMap {        //Entry继承WeakReference,并且用ThreadLocal作为key.        //如果key为null(entry.get() == null),意味着key不再被引用,此时Entry也可以从table中清除        static class Entry extends WeakReference<ThreadLocal<?>> {            Object value;            Entry(ThreadLocal<?> k, Object v) {                super(k);                value = v;            }        }        ...    }    ...}
复制代码


(4)弱引用和内存泄漏


有人在使用 ThreadLocal 的过程中会发现有内存泄漏的情况,就猜测内存泄漏与 Entry 中使用了弱引用有关,这个理解其实是不对的。

 

一.内存泄漏和内存溢出


Memory Overflow 内存溢出:指的是没有足够的内存提供申请者使用。

 

Memory Leak 内存泄漏:指的是程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等后果,内存泄漏的堆积终将导致内存溢出。

 

二.弱引用和强引用


强引用:就是最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还活着,垃圾回收器就不会回收这种对象。

 

弱引用:GC 垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间是否足够,都会将其回收。

 

(5)如果 ThreadLocalMap 的 key 使用强引用


假设 ThreadLocalMap 中的数组元素 Entry 对象的 key 使用了强引用,此时 ThreadLocal 的内存图(实线表示强引用)如下:



假设在业务代码中使用完 ThreadLocal ,栈中的 ThreadLocalRef 被回收了。但是因为 ThreadLocalMap 对象的 Entry 元素强引用了 ThreadLocal 对象,那么就可能会造成 ThreadLocal 对象无法被回收。

 

在没有手动删除这个 Entry 对象以及 CurrentThread 依然运行的情况下,存在这样的强引用链:CurrentThreadRef -> CurrentThread -> ThreadLocalMap -> Entry

 

于是 Entry 对象就不会被回收,从而导致 Entry 的 key 和 value 都内存泄露。由此可见:即使 Entry 对象的 key 使用强引用, 也无法避免内存泄漏。

 

(6)如果 ThreadLocalMap 的 key 使用弱引用


假设 ThreadLocalMap 中的数组元素 Entry 对象的 key 使用了强引用,此时 ThreadLocal 的内存图(实线表示强引用,虚线表示弱引用)如下:



假设在业务代码中使用完 ThreadLocal ,栈中的 ThreadLocalRef 被回收了。由于 ThreadLocalMap 只持有 ThreadLocal 对象的弱引用,此时没有任何强引用指向 Heap 堆中的 Threadlocal 对象,所以 ThreadLocal 对象就可以顺利被 GC 回收,此时 Entry 对象中的 key = null。

 

在没有手动删除这个 Entry 对象以及 CurrentThread 依然运行的情况下,存在这样的强引用链:CurrentThreadRef -> CurrentThread -> ThreadLocalMap -> Entry -> Value

 

于是即使被 key 弱引用的 ThreadLocal 对象被 GC 回收了(key 变为 null),但被 value 强引用的对象不会被回收,从而导致 Entry 的 value 存在内存泄漏。由此可见:即使 Entry 对象的 key 使用了弱引用, 也有可能内存泄漏。

 

Entry 对象的 key 可使用弱引用,是因为栈中有变量强引用 ThreadLocal 对象。Entry 对象的 value 就不能使用弱引用了,因为 Value 对象只有 value 引用。否则一旦 GC 回收 Value 对象后,而 ThreadLocal 对象没被回收就会有问题。

 

(7)使用 ThreadLocal 出现内存泄漏的原因


发生内存泄漏与 ThreadLocalMap 中的 key 是否使用弱引用是没有关系的。

 

发生内存泄漏的的真正原因是:

原因一:没有手动删除 Entry 对象;

原因二:CurrentThread 依然运行;

 

一.手动删除 Entry 元素


只要在使用完 ThreadLocal 后,调用其 remove()方法删除对应的 Entry 元素,则可避免内存泄漏。

 

二.使用完 ThreadLocal 后当前线程随之结束


由于 ThreadLocalMap 是 Thread 的一个属性,被当前线程所引用,所以 ThreadLocalMap 的生命周期和 Thread 线程一样长。那么在使用完 ThreadLocal,如果当前线程 Thread 随之执行结束,ThreadLocalMap 自然也会被 GC 回收,这样就能从根源上避免了内存泄漏。

 

综上可知,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期和 Thread 线程一样长,如果没有手动删除对应的 Entry 对象,那么就会导致内存泄漏。

 

(8)ThreadLocalMap 为什么使用弱引用


ThreadLocalMap 的数组中的 Entry 元素的 key,无论使用强引用还是弱引用,都无法完全避免内存泄漏,出现内存泄露与使用弱引用是没有关系的。

 

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

一.使用完 ThreadLocal 后,调用其 remove()方法删除对应的 Entry 元素

二.使用完 ThreadLocal 后,当前线程也结束运行

 

相对第一种方式,第二种方式显然更不好控制。特别是在使用线程池的时候,核心线程一般不会随便结束的。也就是说,只要记得在使用完 ThreadLocal 后及时调用 remove()方法删除 Entry,那么无论 Entry 元素的 key 是强引用还是弱引用都不会出现内存泄露的问题。

 

那么为什么 Entry 元素的 key 要用弱引用呢?

因为在调用 ThreadLocal 的 set()、get()、remove()方法中,会触发调用 ThreadLocalMap 的 set()、getEntry()、remove()方法。这些方法会判断 key 是否为 null,如果为 null 就设置对应的 value 也为 null。

 

这就意味着使用完 ThreadLocal,CurrentThread 依然运行的前提下,就算忘记调用 remove()方法删除 Entry 元素,弱引用也比强引用多一层保障。

 

弱引用的 ThreadLocal 对象会被回收,那么对应的 value 在下一次调用 set()、getEntry()、remove()方法时就会被清除,从而避免内存泄漏。但如果没有下一次调用 set()、getEntry()、remove()中的任一方法,那么还是会存在内存泄露的问题。


public class ThreadLocal<T> {    ...    static class ThreadLocalMap {        static class Entry extends WeakReference<ThreadLocal<?>> {            Entry(ThreadLocal<?> k, Object v) {                super(k);                value = v;            }        }        //初始容量 —— 必须是2的整次幂        private static final int INITIAL_CAPACITY = 16;        //存放数据的table,同样,数组长度必须是2的整次幂        private Entry[] table;        //数组里面Entry的个数,可以用于判断table当前使用量是否超过阈值        private int size = 0;        //进行数组扩容的阈值        private int threshold; // Default to 0               private void set(ThreadLocal<?> key, Object value) {            Entry[] tab = table;            int len = tab.length;            //首先根据ThreadLocal对象的hashCode和数组长度进行位与运算(即取模),来获取元素放置的位置(即数组下标)            int i = key.threadLocalHashCode & (len-1);            //然后从i开始往后遍历到数组最后一个Entry(线性探索)            for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {                //获取Entry元素中的key                ThreadLocal<?> k = e.get();                //如果key相等,则覆盖value                if (k == key) {                    e.value = value;                    return;                }                //如果key为null,则用新key、value覆盖                //同时清理key = null的陈旧数据(弱引用)                if (k == null) {                    replaceStaleEntry(key, value, i);                    return;                }            }                    //如果数组下标i的位置不存在数据,则直接将key和value封装成Entry对象存储到该位置            tab[i] = new Entry(key, value);            int sz = ++size;            //如果超过阈值,就需要扩容了,cleanSomeSlots()方法会清理数组中的无效的key            if (!cleanSomeSlots(i, sz) && sz >= threshold) {                rehash();//扩容            }        }               private Entry getEntry(ThreadLocal<?> key) {            int i = key.threadLocalHashCode & (table.length - 1);            Entry e = table[i];            if (e != null && e.get() == key) {                return e;            } else {                return getEntryAfterMiss(key, i, e);            }        }               private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {            Entry[] tab = table;            int len = tab.length;
while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) { return e; } if (k == null) { expungeStaleEntry(i);//清理数组中的无效的key } else { i = nextIndex(i, len); } e = tab[i]; } return null; } 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);//清理数组中的无效的key return; } } } ... } ...}
复制代码


(9)使用线性探测法解决 Hash 冲突


ThreadLocalMap 是使用线性探测法(开放寻址法)来解决 Hash 冲突的,该方法一次探测下一个位置,直到有空的位置后插入。若整个空间都找不到有空的位置,则产生溢出。

 

假设当前 table 长度为 16,如果根据当前 key 计算出来的 Hash 值为 14。此时 table[14]上已经有值,且其 key 与当前 key 不一致,则发生了 Hash 冲突。这时就会将 14 加 1 得到 15,取 table[15]进行判断。如果判断 table[15]时还是 Hash 冲突,那么就会回到 0,取 table[0]继续判断,直到可以插入为止。


文章转载自:东阳马生架构

原文链接:https://www.cnblogs.com/mjunz/p/18735205

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
JUC并发—ThreadLocal源码分析_Java_不在线第一只蜗牛_InfoQ写作社区